diff --git a/README-sam2-cpu.md b/README-sam2-cpu.md new file mode 100644 index 0000000..590509e --- /dev/null +++ b/README-sam2-cpu.md @@ -0,0 +1,225 @@ +# SAM2 to YOLOv9t Pipeline + +Automated video annotation pipeline using **SAM2** (Segment Anything Model 2) to create YOLO format datasets for **YOLOv9t** training on Kaggle. + +## Overview +``` +Video → SAM2 (auto-segment) → Bounding Boxes → YOLO Dataset → Train YOLOv9t +``` + +## Features + +- **SAM2 Auto-Annotation**: Automatically segment any object in video frames +- **YOLO Format Export**: Convert masks to YOLO bounding box format +- **Kaggle-Ready**: All notebooks optimized for Kaggle GPU environment +- **YOLOv9t Training**: Train efficient tiny YOLO model on custom data + +## Project Structure + +``` +sam2-yolo-pipeline/ +├── notebooks/ +│ ├── 01_sam2_video_annotation.ipynb # SAM2 setup + video annotation +│ ├── 02_create_yolo_dataset.ipynb # Convert to YOLO format +│ └── 03_train_yolov9t.ipynb # Train YOLOv9t model +├── utils/ +│ ├── __init__.py +│ ├── video_utils.py # Video processing utilities +│ ├── sam2_utils.py # SAM2 annotation utilities +│ └── yolo_utils.py # YOLO dataset utilities +└── README.md +``` + +## Quick Start + +py scripts/frigate_mini.py --config configs/frigate_mini_cpu_pt.yaml +py scripts/frigate_mini.py --model models/krg_masuk_yolov9t_best.pt --video input/karung_masuk.mp4 + + +### On Kaggle + +1. **Upload Video** + - Create a new Kaggle Dataset with your video file(s) + +2. **Run Notebook 1: SAM2 Annotation** + - Upload `01_sam2_video_annotation.ipynb` to Kaggle + - Enable GPU (Settings → Accelerator → GPU) + - Update `VIDEO_PATH` to your video + - Run all cells + +3. **Run Notebook 2: Create YOLO Dataset** + - Upload `02_create_yolo_dataset.ipynb` + - Point to annotations from step 2 + - Run all cells + - Download `yolo_dataset.zip` or create Kaggle Dataset + +4. **Run Notebook 3: Train YOLOv9t** + - Upload `03_train_yolov9t.ipynb` + - Enable GPU + - Point to your YOLO dataset + - Run training + - Download trained weights + +## Configuration + +### SAM2 Model Variants + +| Model | Size | Speed | Accuracy | +|-------|------|-------|----------| +| `tiny` | 39MB | Fastest | Good | +| `small` | 46MB | Fast | Better | +| `base_plus` | 81MB | Medium | High | +| `large` | 224MB | Slow | Best | + +### Frame Extraction Settings + +```python +SAMPLE_FPS = 2 # Frames per second to extract +MAX_FRAMES = 500 # Maximum frames (None for all) +MIN_MASK_AREA = 500 # Minimum object area in pixels +``` + +### Training Settings + +```python +CONFIG = { + 'epochs': 100, + 'batch': 16, + 'imgsz': 640, + 'patience': 20, + 'lr0': 0.001, +} +``` + +## YOLO Dataset Format + +``` +yolo_dataset/ +├── data.yaml # Dataset configuration +├── images/ +│ ├── train/ # Training images +│ └── val/ # Validation images +└── labels/ + ├── train/ # Training labels (.txt) + └── val/ # Validation labels (.txt) +``` + +### Label Format (YOLO) + +``` +# class x_center y_center width height (normalized 0-1) +0 0.45 0.32 0.12 0.25 +0 0.78 0.61 0.08 0.15 +``` + +## Requirements + +Automatically installed in notebooks: + +``` +torch>=2.0 +ultralytics +segment-anything-2 +opencv-python +supervision +tqdm +pyyaml +``` + +## Usage Examples + +### After Training + +```python +from ultralytics import YOLO + +# Load trained model +model = YOLO('best.pt') + +# Inference on image +results = model.predict('image.jpg', conf=0.25) + +# Inference on video +results = model.predict('video.mp4', conf=0.25, save=True) + +# Access detections +for result in results: + for box in result.boxes: + x1, y1, x2, y2 = box.xyxy[0].tolist() + confidence = box.conf[0].item() + class_id = int(box.cls[0].item()) + print(f"Class {class_id}: {confidence:.2f} at [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}]") +``` + +### Export Formats + +```python +# ONNX +model.export(format='onnx') + +# TensorRT +model.export(format='engine') + +# OpenVINO +model.export(format='openvino') + +# CoreML +model.export(format='coreml') +``` + +## Tips + +### Improve Annotation Quality + +- Adjust `MIN_MASK_AREA` to filter small/noisy detections +- Use `MAX_MASK_AREA` to exclude large background regions +- Lower `SAMPLE_FPS` if video frames are very similar +- Use SAM2 `large` model for better segmentation accuracy + +### Improve Training Results + +- More diverse training data +- Experiment with augmentation settings +- Try different learning rates +- Use larger image size (imgsz=1280) +- Train for more epochs with patience + +### Kaggle GPU Tips + +- P100 GPU: ~16GB VRAM, use batch=16-32 +- T4 GPU: ~16GB VRAM, use batch=16-32 +- Enable mixed precision for faster training +- Save checkpoints to avoid losing progress + +## Troubleshooting + +### CUDA Out of Memory + +```python +# Reduce batch size +CONFIG['batch'] = 8 + +# Or reduce image size +CONFIG['imgsz'] = 416 +``` + +### SAM2 Installation Issues + +```bash +# Install from source +pip install git+https://github.com/facebookresearch/segment-anything-2.git +``` + +### Missing Labels Warning + +This is normal - some frames may have no detectable objects. The dataset is still valid. + +## License + +MIT License - Feel free to use and modify for your projects. + +## Acknowledgments + +- [SAM2](https://github.com/facebookresearch/segment-anything-2) by Meta AI +- [YOLOv9](https://github.com/WongKinYiu/yolov9) by WongKinYiu +- [Ultralytics](https://github.com/ultralytics/ultralytics) for YOLO implementation diff --git a/sam2-cpu/PLAN.md b/sam2-cpu/PLAN.md new file mode 100644 index 0000000..1a8911a --- /dev/null +++ b/sam2-cpu/PLAN.md @@ -0,0 +1,800 @@ +# Feature Plan: YOLO-Assisted Auto-Annotation + Mini Frigate RKNN + +## Overview + +Create two integrated components: +1. **YOLO-Assisted Annotator** - Use pretrained YOLOv9t to auto-annotate video frames +2. **Frigate-Mini-RKNN** - Standalone mini fork of Frigate for RKNN inference with MP4 input + +## Goals + +- Auto-annotate videos using YOLOv9t pretrained model (replaces manual SAM2 prompts) +- Minimal Frigate fork with multiple detector backends: + - **RKNN** - Rockchip NPU acceleration (RK3588, RK3568, etc.) + - **ONNX** - CPU-only inference (cross-platform, no special hardware) + - **YOLO** - Ultralytics backend (CPU/CUDA) +- MP4 file as camera feed source +- Output: Clean snapshot + YOLO format label pairs +- Simple text-based configuration +- Debug mode with object list visualization + +## Detector Backends Comparison + +| Backend | Hardware | Performance | Platform | Use Case | +|---------|----------|-------------|----------|----------| +| **RKNN** | Rockchip NPU | Fast (30+ FPS) | ARM (RK3588/3568) | Production on Rockchip SBC | +| **ONNX** | CPU | Medium (5-15 FPS) | Any (x86/ARM) | Development, testing, no GPU | +| **YOLO** | CPU/CUDA | Fast with GPU | Any | Development, CUDA systems | + +### Recommended Workflow + +1. **Development/Testing**: Use ONNX backend on any CPU +2. **Production on Rockchip**: Convert to RKNN, deploy on NPU +3. **Production on x86/CUDA**: Use YOLO backend with GPU + +--- + +## Project Structure + +``` +sam2-yolo-pipeline/ +├── notebooks/ # Existing Kaggle notebooks +├── utils/ # Existing utilities +├── yolo_annotator/ # NEW: YOLO-assisted annotation +│ ├── __init__.py +│ ├── annotator.py # Core YOLOv9t annotator +│ ├── video_source.py # MP4/RTSP video source handler +│ ├── export.py # Snapshot + label export +│ └── visualizer.py # Debug visualization +├── frigate_mini/ # NEW: Mini Frigate fork +│ ├── __init__.py +│ ├── app.py # Main application entry +│ ├── config/ +│ │ ├── __init__.py +│ │ ├── schema.py # Config validation +│ │ └── loader.py # YAML config loader +│ ├── detector/ +│ │ ├── __init__.py +│ │ ├── base.py # Base detector interface +│ │ ├── rknn_detector.py # RKNN backend +│ │ ├── onnx_detector.py # ONNX fallback +│ │ └── yolo_detector.py # Ultralytics YOLO fallback +│ ├── video/ +│ │ ├── __init__.py +│ │ ├── mp4_source.py # MP4 file source +│ │ └── frame_processor.py # Frame processing pipeline +│ ├── output/ +│ │ ├── __init__.py +│ │ ├── snapshot.py # Snapshot capture +│ │ └── annotation.py # YOLO label writer +│ └── debug/ +│ ├── __init__.py +│ ├── object_list.py # Detected objects display +│ └── visualizer.py # Bounding box overlay +├── configs/ # NEW: Configuration files +│ ├── annotator.yaml # Annotator settings +│ └── frigate_mini.yaml # Frigate-mini settings +├── models/ # NEW: Model weights storage +│ └── .gitkeep +├── output/ # NEW: Default output directory +│ ├── snapshots/ +│ ├── labels/ +│ └── debug/ +├── scripts/ # NEW: CLI scripts +│ ├── annotate.py # Run annotation pipeline +│ ├── frigate_mini.py # Run mini frigate +│ └── convert_to_rknn.py # Convert ONNX to RKNN +└── requirements.txt # Updated dependencies +``` + +--- + +## Component 1: YOLO-Assisted Annotator + +### Purpose +Replace SAM2 auto-annotation with faster YOLOv9t-based detection for creating training datasets. + +### Workflow +``` +MP4 Video → Frame Extraction → YOLOv9t Detection → Filter/NMS → YOLO Labels + Snapshots +``` + +### Features + +1. **Model Loading** + - Load pretrained YOLOv9t (.pt file) + - Support custom trained models + - Configurable confidence threshold + - Configurable NMS threshold + +2. **Video Processing** + - MP4 file input + - Configurable FPS sampling + - Frame skip / time range selection + - Resolution scaling + +3. **Detection Filtering** + - Filter by class IDs + - Filter by confidence score + - Filter by bbox size (min/max area) + - Filter by aspect ratio + +4. **Output Generation** + - Clean snapshot images (no annotations drawn) + - YOLO format label files (.txt) + - Optional debug images with boxes drawn + - JSON manifest of all detections + +### Configuration (annotator.yaml) + +```yaml +# YOLO-Assisted Annotator Configuration + +model: + path: "models/yolov9t.pt" # Path to YOLO model + device: "cuda" # cuda, cpu, or rknn + conf_threshold: 0.25 # Confidence threshold + iou_threshold: 0.45 # NMS IoU threshold + +video: + source: "input/video.mp4" # Video file path + sample_fps: 2 # Frames per second to extract + max_frames: null # Max frames (null = all) + start_time: 0 # Start time in seconds + end_time: null # End time (null = end of video) + resize: null # [width, height] or null + +detection: + classes: null # Class IDs to keep (null = all) + min_confidence: 0.3 # Minimum confidence to save + min_area: 100 # Minimum bbox area in pixels + max_area: null # Maximum bbox area (null = no limit) + min_size: 0.01 # Minimum bbox dimension (normalized) + +output: + directory: "output/annotations" # Output directory + save_snapshots: true # Save clean images + save_labels: true # Save YOLO labels + save_debug: true # Save debug visualizations + save_manifest: true # Save JSON manifest + image_format: "jpg" # jpg or png + image_quality: 95 # JPEG quality (1-100) + +classes: + # Class name mapping (for display/filtering) + 0: "person" + 1: "bicycle" + 2: "car" + # ... etc +``` + +--- + +## Component 2: Frigate-Mini-RKNN + +### Purpose +Minimal standalone Frigate-like system for RKNN inference on Rockchip devices, outputting annotation pairs. + +### Workflow +``` +MP4 Feed → Frame Decode → RKNN Inference → Object Tracking → Snapshot + Label Export +``` + +### Features + +1. **Video Input** + - MP4 file as "camera" source + - Loop playback option + - Configurable FPS limit + - Multiple video sources support + +2. **RKNN Detector** + - Load RKNN model (.rknn file) + - NPU acceleration on Rockchip SoCs + - Fallback to ONNX/CPU if RKNN unavailable + - Batch inference support + +3. **Object Detection** + - YOLOv9t architecture support + - Configurable input resolution + - Post-processing (NMS, filtering) + - Class filtering + +4. **Snapshot System** + - Capture on detection trigger + - Configurable cooldown period + - Clean snapshots (no overlays) + - Crop to detected object (optional) + +5. **Annotation Export** + - YOLO format labels + - Synchronized snapshot-label pairs + - Auto-naming with timestamps + - Dataset structure output + +6. **Debug Mode** + - Real-time object list display + - Bounding box visualization + - FPS counter + - Detection statistics + - Save debug frames + +### Configuration (frigate_mini.yaml) + +#### Option A: ONNX CPU-Only (Recommended for development/testing) + +```yaml +# Frigate-Mini Configuration - ONNX CPU Mode +# Works on any system without special hardware + +debug: true +log_level: "info" + +detector: + type: "onnx" # Use ONNX Runtime + model_path: "models/yolov9t.onnx" # ONNX model file + input_size: [640, 640] # Model input resolution + conf_threshold: 0.25 # Detection confidence + nms_threshold: 0.45 # NMS threshold + + # ONNX specific settings + onnx: + device: "cpu" # cpu or cuda + num_threads: 4 # CPU threads (0 = auto) + optimization_level: "all" # none, basic, extended, all +``` + +#### Option B: RKNN NPU (For Rockchip devices) + +```yaml +# Frigate-Mini Configuration - RKNN NPU Mode +# For Rockchip SBCs (RK3588, RK3568, etc.) + +debug: true +log_level: "info" + +detector: + type: "rknn" # Use RKNN Runtime + model_path: "models/yolov9t.rknn" # RKNN model file + input_size: [640, 640] # Model input resolution + conf_threshold: 0.25 # Detection confidence + nms_threshold: 0.45 # NMS threshold + + # RKNN specific + rknn: + target_platform: "rk3588" # rk3588, rk3568, rk3566, etc. + core_mask: 7 # NPU core mask (7 = all 3 cores on RK3588) + + # Fallback to ONNX if RKNN fails + fallback: + enabled: true + type: "onnx" + device: "cpu" +``` + +#### Option C: Ultralytics YOLO (For CUDA systems) + +```yaml +# Frigate-Mini Configuration - Ultralytics YOLO Mode +# For systems with NVIDIA GPU + +debug: true +log_level: "info" + +detector: + type: "yolo" # Use Ultralytics + model_path: "models/yolov9t.pt" # PyTorch model file + conf_threshold: 0.25 + nms_threshold: 0.45 + + # YOLO specific + yolo: + device: "cuda" # cpu, cuda, cuda:0, etc. + half: true # FP16 inference (faster on GPU) +``` + +#### Full Configuration Example (with all options) + +# Video sources (cameras) +cameras: + front_door: + enabled: true + source: "input/front_door.mp4" # MP4 file path + fps: 5 # Processing FPS limit + loop: true # Loop video playback + + # Detection zones (optional) + detect: + enabled: true + width: 1280 # Detection resolution + height: 720 + + # Object filtering + objects: + track: + - person + - car + - dog + filters: + person: + min_area: 1000 # Minimum area in pixels + max_area: 500000 + min_score: 0.4 + + backyard: + enabled: true + source: "input/backyard.mp4" + fps: 5 + loop: true + +# Snapshot settings +snapshots: + enabled: true + output_dir: "output/snapshots" + + # Trigger settings + trigger: + objects: # Objects that trigger snapshot + - person + - car + min_score: 0.5 # Minimum score to trigger + cooldown: 2.0 # Seconds between snapshots per object + + # Output settings + format: "jpg" # jpg or png + quality: 95 # JPEG quality + clean: true # No annotations on snapshot + crop: false # Crop to object bbox + retain_days: 7 # Days to keep snapshots + +# Annotation export +annotations: + enabled: true + output_dir: "output/labels" + format: "yolo" # YOLO format + + # Pairing + pair_with_snapshots: true # Create snapshot-label pairs + + # Filtering + min_score: 0.3 + classes: null # null = all classes + +# Debug settings +debug_output: + enabled: true + output_dir: "output/debug" + + # Object list display + object_list: + enabled: true + show_confidence: true + show_class: true + show_bbox: true + + # Visualization + visualization: + enabled: true + draw_boxes: true + draw_labels: true + draw_confidence: true + box_thickness: 2 + font_scale: 0.5 + + # Statistics + stats: + show_fps: true + show_detection_count: true + log_interval: 100 # Log stats every N frames + +# Class definitions +class_names: + 0: person + 1: bicycle + 2: car + 3: motorcycle + 4: airplane + 5: bus + 6: train + 7: truck + 8: boat + 9: traffic light + 10: fire hydrant + # ... COCO classes continue +``` + +--- + +## Module Specifications + +### 1. yolo_annotator/annotator.py + +```python +class YOLOAnnotator: + """YOLO-based automatic video annotator.""" + + def __init__(self, config_path: str): + """Load configuration and initialize model.""" + + def load_model(self, model_path: str, device: str) -> None: + """Load YOLOv9t model.""" + + def process_video(self, video_path: str) -> AnnotationResult: + """Process entire video and generate annotations.""" + + def process_frame(self, frame: np.ndarray) -> List[Detection]: + """Process single frame and return detections.""" + + def filter_detections(self, detections: List[Detection]) -> List[Detection]: + """Apply filtering rules to detections.""" + + def export_annotations(self, output_dir: str) -> None: + """Export all annotations to YOLO format.""" +``` + +### 2. frigate_mini/detector/rknn_detector.py + +```python +class RKNNDetector: + """RKNN-based YOLO detector for Rockchip NPU.""" + + def __init__(self, model_path: str, target_platform: str): + """Initialize RKNN runtime.""" + + def load_model(self) -> bool: + """Load RKNN model to NPU.""" + + def preprocess(self, frame: np.ndarray) -> np.ndarray: + """Preprocess frame for inference.""" + + def inference(self, input_data: np.ndarray) -> np.ndarray: + """Run inference on NPU.""" + + def postprocess(self, outputs: np.ndarray) -> List[Detection]: + """Parse YOLO outputs and apply NMS.""" + + def detect(self, frame: np.ndarray) -> List[Detection]: + """Full detection pipeline.""" + + def release(self) -> None: + """Release RKNN resources.""" +``` + +### 3. frigate_mini/output/annotation.py + +```python +class AnnotationWriter: + """Write YOLO format annotation files.""" + + def __init__(self, output_dir: str, class_names: Dict[int, str]): + """Initialize annotation writer.""" + + def write_label(self, + image_name: str, + detections: List[Detection], + image_size: Tuple[int, int]) -> str: + """Write YOLO label file for image.""" + + def detection_to_yolo(self, + detection: Detection, + image_width: int, + image_height: int) -> str: + """Convert detection to YOLO format string.""" + + def create_dataset_structure(self) -> None: + """Create YOLO dataset directory structure.""" + + def write_data_yaml(self, train_path: str, val_path: str) -> str: + """Generate data.yaml for training.""" +``` + +### 4. frigate_mini/debug/object_list.py + +```python +class ObjectListDisplay: + """Display detected objects in debug mode.""" + + def __init__(self, config: Dict): + """Initialize display settings.""" + + def update(self, detections: List[Detection]) -> None: + """Update object list with new detections.""" + + def format_detection(self, detection: Detection) -> str: + """Format single detection for display.""" + + def print_list(self) -> None: + """Print current object list to console.""" + + def save_snapshot_with_labels(self, + frame: np.ndarray, + detections: List[Detection], + output_path: str) -> None: + """Save debug image with annotations.""" +``` + +--- + +## Data Structures + +### Detection + +```python +@dataclass +class Detection: + class_id: int # Class index + class_name: str # Class name + confidence: float # Detection confidence (0-1) + bbox: BBox # Bounding box + track_id: Optional[int] # Tracking ID (if tracked) + timestamp: float # Frame timestamp + frame_id: int # Frame number + +@dataclass +class BBox: + x1: float # Top-left x (pixels) + y1: float # Top-left y (pixels) + x2: float # Bottom-right x (pixels) + y2: float # Bottom-right y (pixels) + + def to_yolo(self, img_w: int, img_h: int) -> Tuple[float, float, float, float]: + """Convert to YOLO format (x_center, y_center, width, height) normalized.""" + + def area(self) -> float: + """Calculate bbox area in pixels.""" +``` + +### AnnotationPair + +```python +@dataclass +class AnnotationPair: + image_path: str # Path to snapshot image + label_path: str # Path to YOLO label file + detections: List[Detection] + timestamp: datetime + camera_name: str + frame_id: int +``` + +--- + +## Output Format + +### Directory Structure + +``` +output/ +├── snapshots/ +│ ├── front_door/ +│ │ ├── 20240115_143022_001.jpg +│ │ ├── 20240115_143025_002.jpg +│ │ └── ... +│ └── backyard/ +│ └── ... +├── labels/ +│ ├── front_door/ +│ │ ├── 20240115_143022_001.txt +│ │ ├── 20240115_143025_002.txt +│ │ └── ... +│ └── backyard/ +│ └── ... +├── debug/ +│ ├── front_door/ +│ │ ├── 20240115_143022_001_debug.jpg +│ │ └── ... +│ └── object_log.txt +└── manifest.json +``` + +### YOLO Label Format + +``` +# {class_id} {x_center} {y_center} {width} {height} +0 0.456789 0.321456 0.123456 0.234567 +2 0.789012 0.654321 0.098765 0.176543 +``` + +### Manifest JSON + +```json +{ + "created": "2024-01-15T14:30:22", + "model": "yolov9t.rknn", + "total_frames": 1500, + "total_detections": 3420, + "pairs": [ + { + "image": "snapshots/front_door/20240115_143022_001.jpg", + "label": "labels/front_door/20240115_143022_001.txt", + "camera": "front_door", + "frame_id": 150, + "timestamp": "2024-01-15T14:30:22.500", + "detections": [ + {"class": "person", "confidence": 0.87}, + {"class": "car", "confidence": 0.92} + ] + } + ] +} +``` + +--- + +## Implementation Phases + +### Phase 1: Core YOLO Annotator (Week 1) +- [ ] Create `yolo_annotator/` module structure +- [ ] Implement `YOLOAnnotator` class with Ultralytics backend +- [ ] Implement video source handling +- [ ] Implement YOLO label export +- [ ] Create `annotator.yaml` config loader +- [ ] Add CLI script `scripts/annotate.py` +- [ ] Test with sample video + +### Phase 2: Frigate-Mini Base (Week 2) +- [ ] Create `frigate_mini/` module structure +- [ ] Implement config schema and loader +- [ ] Implement base detector interface +- [ ] Implement ONNX detector (for testing) +- [ ] Implement MP4 video source +- [ ] Implement basic frame processing loop +- [ ] Test basic detection pipeline + +### Phase 3: RKNN Integration (Week 3) +- [ ] Implement RKNN detector backend +- [ ] Create ONNX to RKNN conversion script +- [ ] Test on Rockchip hardware (RK3588/RK3568) +- [ ] Optimize for NPU performance +- [ ] Add fallback mechanism + +### Phase 4: Snapshot & Annotation System (Week 4) +- [ ] Implement snapshot capture system +- [ ] Implement annotation writer +- [ ] Implement snapshot-label pairing +- [ ] Add trigger-based capture logic +- [ ] Create manifest generator + +### Phase 5: Debug System (Week 5) +- [ ] Implement object list display +- [ ] Implement debug visualization +- [ ] Add statistics tracking +- [ ] Create debug frame saver +- [ ] Add console and file logging + +### Phase 6: Integration & Testing (Week 6) +- [ ] Integration testing +- [ ] Performance optimization +- [ ] Documentation +- [ ] Example configs for common use cases +- [ ] Package for distribution + +--- + +## Dependencies + +### New Requirements + +``` +# requirements.txt additions + +# YOLO +ultralytics>=8.0.0 + +# RKNN (install separately based on platform) +# rknn-toolkit2 # For conversion (x86) +# rknnlite2 # For inference (ARM) + +# Video processing +opencv-python>=4.8.0 +av>=10.0.0 # PyAV for efficient video decoding + +# Configuration +pyyaml>=6.0 +pydantic>=2.0 # Config validation + +# Utilities +tqdm>=4.65.0 +numpy>=1.24.0 +``` + +### RKNN Installation Notes + +```bash +# On x86 host (for model conversion): +pip install rknn-toolkit2 + +# On Rockchip device (for inference): +pip install rknnlite2 + +# Or install from Rockchip GitHub releases +``` + +--- + +## Usage Examples + +### 1. CPU-Only Workflow (ONNX) - Recommended for Development + +```bash +# Step 1: Download pretrained YOLOv9t +wget https://github.com/ultralytics/assets/releases/download/v8.1.0/yolov9t.pt -O models/yolov9t.pt + +# Step 2: Convert to ONNX +python scripts/convert_to_onnx.py --input models/yolov9t.pt --output models/yolov9t.onnx + +# Step 3a: Auto-annotate video (CPU) +python scripts/annotate.py --config configs/annotator_cpu.yaml +# Or with CLI args: +python scripts/annotate.py \ + --model models/yolov9t.onnx \ + --video input/video.mp4 \ + --device cpu + +# Step 3b: Run Frigate-Mini (CPU) +python scripts/frigate_mini.py --config configs/frigate_mini_cpu.yaml +# Or with CLI args: +python scripts/frigate_mini.py \ + --model models/yolov9t.onnx \ + --video input/video.mp4 \ + --output output/ \ + --debug +``` + +### 2. RKNN Workflow (Rockchip NPU) + +```bash +# Step 1: Convert ONNX to RKNN (on x86 host) +python scripts/convert_to_rknn.py \ + --input models/yolov9t.onnx \ + --output models/yolov9t.rknn \ + --platform rk3588 + +# Step 2: Copy to Rockchip device and run +python scripts/frigate_mini.py --config configs/frigate_mini.yaml +# Or: +python scripts/frigate_mini.py \ + --model models/yolov9t.rknn \ + --video input/video.mp4 \ + --platform rk3588 +``` + +### 3. GPU Workflow (CUDA) + +```bash +# Using Ultralytics directly with GPU +python scripts/annotate.py \ + --model models/yolov9t.pt \ + --video input/video.mp4 \ + --device cuda +``` + +### Quick Reference + +| Task | CPU (ONNX) | RKNN (NPU) | GPU (CUDA) | +|------|------------|------------|------------| +| Model file | `.onnx` | `.rknn` | `.pt` | +| Config | `*_cpu.yaml` | `frigate_mini.yaml` | Use `--device cuda` | +| Speed | 5-15 FPS | 30+ FPS | 50+ FPS | +| Hardware | Any CPU | Rockchip SBC | NVIDIA GPU | + +--- + +## Future Enhancements + +1. **RTSP Support** - Add real camera stream input +2. **Object Tracking** - Add ByteTrack/BoT-SORT for consistent IDs +3. **Web UI** - Simple web interface for monitoring +4. **Multi-model** - Support different models per camera +5. **Event System** - Webhooks for detection events +6. **Auto-labeling Refinement** - Use SAM2 to refine YOLO boxes +7. **Active Learning** - Flag low-confidence detections for review + +--- + +## References + +- [Ultralytics YOLOv9](https://github.com/ultralytics/ultralytics) +- [RKNN-Toolkit2](https://github.com/rockchip-linux/rknn-toolkit2) +- [Frigate NVR](https://github.com/blakeblackshear/frigate) +- [YOLO Label Format](https://docs.ultralytics.com/datasets/detect/) diff --git a/sam2-cpu/configs/annotator.yaml b/sam2-cpu/configs/annotator.yaml new file mode 100644 index 0000000..534cc78 --- /dev/null +++ b/sam2-cpu/configs/annotator.yaml @@ -0,0 +1,115 @@ +# YOLO-Assisted Annotator Configuration +# Use pretrained YOLOv9t to auto-annotate video frames + +model: + path: "models/yolov9t.pt" # Path to YOLO model (.pt, .onnx, or .rknn) + device: "cuda" # cuda, cpu, or rknn + conf_threshold: 0.25 # Confidence threshold + iou_threshold: 0.45 # NMS IoU threshold + +video: + source: "input/video.mp4" # Video file path + sample_fps: 2 # Frames per second to extract + max_frames: null # Max frames (null = all) + start_time: 0 # Start time in seconds + end_time: null # End time (null = end of video) + resize: null # [width, height] or null + +detection: + classes: null # Class IDs to keep (null = all), e.g. [0, 2, 5] + min_confidence: 0.3 # Minimum confidence to save + min_area: 100 # Minimum bbox area in pixels + max_area: null # Maximum bbox area (null = no limit) + min_size: 0.01 # Minimum bbox dimension (normalized 0-1) + +output: + directory: "output/annotations" # Output directory + save_snapshots: true # Save clean images (no boxes) + save_labels: true # Save YOLO format labels + save_debug: true # Save debug visualizations (with boxes) + save_manifest: true # Save JSON manifest + image_format: "jpg" # jpg or png + image_quality: 95 # JPEG quality (1-100) + +# Class name mapping (COCO classes for pretrained model) +class_names: + 0: person + 1: bicycle + 2: car + 3: motorcycle + 4: airplane + 5: bus + 6: train + 7: truck + 8: boat + 9: traffic light + 10: fire hydrant + 11: stop sign + 12: parking meter + 13: bench + 14: bird + 15: cat + 16: dog + 17: horse + 18: sheep + 19: cow + 20: elephant + 21: bear + 22: zebra + 23: giraffe + 24: backpack + 25: umbrella + 26: handbag + 27: tie + 28: suitcase + 29: frisbee + 30: skis + 31: snowboard + 32: sports ball + 33: kite + 34: baseball bat + 35: baseball glove + 36: skateboard + 37: surfboard + 38: tennis racket + 39: bottle + 40: wine glass + 41: cup + 42: fork + 43: knife + 44: spoon + 45: bowl + 46: banana + 47: apple + 48: sandwich + 49: orange + 50: broccoli + 51: carrot + 52: hot dog + 53: pizza + 54: donut + 55: cake + 56: chair + 57: couch + 58: potted plant + 59: bed + 60: dining table + 61: toilet + 62: tv + 63: laptop + 64: mouse + 65: remote + 66: keyboard + 67: cell phone + 68: microwave + 69: oven + 70: toaster + 71: sink + 72: refrigerator + 73: book + 74: clock + 75: vase + 76: scissors + 77: teddy bear + 78: hair drier + 79: toothbrush diff --git a/sam2-cpu/configs/annotator_cpu.yaml b/sam2-cpu/configs/annotator_cpu.yaml new file mode 100644 index 0000000..8512835 --- /dev/null +++ b/sam2-cpu/configs/annotator_cpu.yaml @@ -0,0 +1,61 @@ +# YOLO Annotator Configuration - CPU Only (ONNX) +# +# This configuration uses ONNX Runtime for CPU-only inference. +# No GPU required - works on any system. +# +# Usage: +# python scripts/annotate.py --config configs/annotator_cpu.yaml + +model: + path: "models/yolov9t.onnx" # ONNX model file + device: "cpu" # cpu (ONNX uses CPU by default) + backend: "onnx" # Force ONNX backend + conf_threshold: 0.25 # Confidence threshold + iou_threshold: 0.45 # NMS IoU threshold + + # ONNX specific options + onnx: + num_threads: 0 # CPU threads (0 = auto) + optimization_level: "all" # Graph optimization level + +video: + source: "input/video.mp4" # Video file path + sample_fps: 2 # Frames per second to extract + max_frames: null # Max frames (null = all) + start_time: 0 # Start time in seconds + end_time: null # End time (null = end of video) + resize: null # [width, height] or null + +detection: + classes: null # Class IDs to keep (null = all) + min_confidence: 0.3 # Minimum confidence to save + min_area: 100 # Minimum bbox area in pixels + max_area: null # Maximum bbox area (null = no limit) + min_size: 0.01 # Minimum bbox dimension (normalized) + +output: + directory: "output/annotations" # Output directory + save_snapshots: true # Save clean images + save_labels: true # Save YOLO labels + save_debug: true # Save debug visualizations + save_manifest: true # Save JSON manifest + image_format: "jpg" # jpg or png + image_quality: 95 # JPEG quality (1-100) + +# Class names (COCO subset - common objects) +class_names: + 0: person + 1: bicycle + 2: car + 3: motorcycle + 4: airplane + 5: bus + 6: train + 7: truck + 8: boat + 14: bird + 15: cat + 16: dog + 17: horse + 18: sheep + 19: cow diff --git a/sam2-cpu/configs/annotator_cpu_pt.yaml b/sam2-cpu/configs/annotator_cpu_pt.yaml new file mode 100644 index 0000000..d2068d5 --- /dev/null +++ b/sam2-cpu/configs/annotator_cpu_pt.yaml @@ -0,0 +1,51 @@ +# YOLO Annotator Configuration - CPU Only (ONNX) +# +# This configuration uses ONNX Runtime for CPU-only inference. +# No GPU required - works on any system. +# +# Usage: +# python scripts/annotate.py --config configs/annotator_cpu.yaml + +model: + #path: "models/krg_tuang_yolov9t_best.pt" # ONNX model file + #path: "models/tuangatas.pt" # ONNX model file + path: "models/krg_masuk_yolov9t_best.pt" # ONNX model file + device: "cpu" # cpu (ONNX uses CPU by default) + backend: "pt" # Force ONNX backend + conf_threshold: 0.25 # Confidence threshold + iou_threshold: 0.45 # NMS IoU threshold + + # ONNX specific options + onnx: + num_threads: 0 # CPU threads (0 = auto) + optimization_level: "all" # Graph optimization level + +video: + source: "input/karung_masuk.mp4" # Video file path + #source: "input/tuang_train_20260120-1.mp4" # Video file path + #source: "input/atas.mp4" # Video file path + sample_fps: 3 # Frames per second to extract + max_frames: null # Max frames (null = all) + start_time: 0 # Start time in seconds + end_time: null # End time (null = end of video) + resize: null # [width, height] or null + +detection: + classes: 0 # Class IDs to keep (null = all) + min_confidence: 0.3 # Minimum confidence to save + min_area: 100 # Minimum bbox area in pixels + max_area: null # Maximum bbox area (null = no limit) + min_size: 0.01 # Minimum bbox dimension (normalized) + +output: + directory: "output/annotations" # Output directory + save_snapshots: true # Save clean images + save_labels: true # Save YOLO labels + save_debug: true # Save debug visualizations + save_manifest: true # Save JSON manifest + image_format: "jpg" # jpg or png + image_quality: 95 # JPEG quality (1-100) + +# Class names (COCO subset - common objects) +class_names: + 0: karung diff --git a/sam2-cpu/configs/frigate_mini.yaml b/sam2-cpu/configs/frigate_mini.yaml new file mode 100644 index 0000000..29855fe --- /dev/null +++ b/sam2-cpu/configs/frigate_mini.yaml @@ -0,0 +1,220 @@ +# Frigate-Mini-RKNN Configuration +# Minimal Frigate fork for RKNN inference with MP4 input + +# Global settings +debug: true # Enable debug mode +log_level: "info" # debug, info, warning, error + +# Model / Detector configuration +detector: + type: "rknn" # rknn, onnx, or yolo + model_path: "models/yolov9t.rknn" # Path to model file + input_size: [640, 640] # Model input resolution [width, height] + conf_threshold: 0.25 # Detection confidence threshold + nms_threshold: 0.45 # NMS IoU threshold + + # RKNN specific settings + rknn: + target_platform: "rk3588" # rk3588, rk3568, rk3566, rk3562, rv1106, rv1103 + core_mask: 7 # NPU core mask (RK3588: 7=all 3 cores, 1/2/4=single core) + async_mode: false # Async inference mode + + # Fallback settings (when RKNN not available) + fallback: + enabled: true # Fall back to ONNX/YOLO if RKNN fails + type: "yolo" # onnx or yolo + device: "cpu" # cpu or cuda + +# Video sources (cameras) +cameras: + # Example camera 1: Front door + front_door: + enabled: true + source: "input/front_door.mp4" # MP4 file path (acts as camera feed) + fps: 5 # Max processing FPS + loop: true # Loop video when finished + + # Detection settings + detect: + enabled: true + width: 1280 # Processing resolution width + height: 720 # Processing resolution height + + # Object filtering per camera + objects: + track: # Objects to detect + - person + - car + - dog + - cat + filters: + person: + min_area: 1000 # Minimum bbox area in pixels + max_area: 500000 # Maximum bbox area + min_score: 0.4 # Minimum confidence + car: + min_area: 2000 + min_score: 0.35 + + # Example camera 2: Backyard + backyard: + enabled: false # Disabled by default + source: "input/backyard.mp4" + fps: 5 + loop: true + detect: + enabled: true + width: 1280 + height: 720 + objects: + track: + - person + - dog + - cat + - bird + +# Snapshot settings +snapshots: + enabled: true + output_dir: "output/snapshots" + + # Trigger settings + trigger: + objects: # Objects that trigger snapshot + - person + - car + min_score: 0.5 # Minimum score to trigger snapshot + cooldown: 2.0 # Seconds between snapshots per object type + + # Output settings + format: "jpg" # jpg or png + quality: 95 # JPEG quality (1-100) + clean: true # Save clean snapshots (no bboxes drawn) + crop: false # Crop to detected object bbox + retain_days: 7 # Days to keep snapshots (0 = forever) + +# Annotation export settings +annotations: + enabled: true + output_dir: "output/labels" + format: "yolo" # YOLO format (class x_center y_center w h) + + # Pairing with snapshots + pair_with_snapshots: true # Create snapshot-label pairs + + # Filtering + min_score: 0.3 # Minimum score to include in annotation + classes: null # null = all classes, or list like [0, 2] + +# Debug output settings +debug_output: + enabled: true + output_dir: "output/debug" + + # Object list display (console output) + object_list: + enabled: true + show_confidence: true # Show confidence scores + show_class: true # Show class names + show_bbox: true # Show bbox coordinates + show_track_id: false # Show tracking ID (if tracking enabled) + + # Visualization (debug images) + visualization: + enabled: true + draw_boxes: true # Draw bounding boxes + draw_labels: true # Draw class labels + draw_confidence: true # Draw confidence scores + box_color: [0, 255, 0] # BGR color for boxes + box_thickness: 2 # Line thickness + font_scale: 0.5 # Font scale for labels + save_interval: 10 # Save every N frames (0 = save all) + + # Statistics + stats: + show_fps: true # Show FPS counter + show_detection_count: true # Show total detections + log_interval: 100 # Log stats every N frames + +# Class name mapping (same as annotator for consistency) +class_names: + 0: person + 1: bicycle + 2: car + 3: motorcycle + 4: airplane + 5: bus + 6: train + 7: truck + 8: boat + 9: traffic light + 10: fire hydrant + 11: stop sign + 12: parking meter + 13: bench + 14: bird + 15: cat + 16: dog + 17: horse + 18: sheep + 19: cow + 20: elephant + 21: bear + 22: zebra + 23: giraffe + 24: backpack + 25: umbrella + 26: handbag + 27: tie + 28: suitcase + 29: frisbee + 30: skis + 31: snowboard + 32: sports ball + 33: kite + 34: baseball bat + 35: baseball glove + 36: skateboard + 37: surfboard + 38: tennis racket + 39: bottle + 40: wine glass + 41: cup + 42: fork + 43: knife + 44: spoon + 45: bowl + 46: banana + 47: apple + 48: sandwich + 49: orange + 50: broccoli + 51: carrot + 52: hot dog + 53: pizza + 54: donut + 55: cake + 56: chair + 57: couch + 58: potted plant + 59: bed + 60: dining table + 61: toilet + 62: tv + 63: laptop + 64: mouse + 65: remote + 66: keyboard + 67: cell phone + 68: microwave + 69: oven + 70: toaster + 71: sink + 72: refrigerator + 73: book + 74: clock + 75: vase + 76: scissors + 77: teddy bear + 78: hair drier + 79: toothbrush diff --git a/sam2-cpu/configs/frigate_mini_cpu.yaml b/sam2-cpu/configs/frigate_mini_cpu.yaml new file mode 100644 index 0000000..9c26664 --- /dev/null +++ b/sam2-cpu/configs/frigate_mini_cpu.yaml @@ -0,0 +1,105 @@ +# Frigate-Mini Configuration - CPU Only (ONNX) +# +# This configuration uses ONNX Runtime for CPU-only inference. +# No special hardware required - works on any x86 or ARM system. +# +# Usage: +# python scripts/frigate_mini.py --config configs/frigate_mini_cpu.yaml + +# Global settings +debug: true # Enable debug mode +log_level: "info" # debug, info, warning, error + +# Detector configuration - ONNX CPU Mode +detector: + type: "pt" # Use PYTORCH Runtime (CPU) + model_path: "models/krg_masuk_yolov9t_best.pt" # PT model file + input_size: [640, 640] # Model input resolution [width, height] + conf_threshold: 0.25 # Detection confidence threshold + nms_threshold: 0.45 # NMS IoU threshold + + # ONNX specific settings + onnx: + device: "cpu" # cpu or cuda + num_threads: 0 # CPU threads (0 = auto, uses all cores) + optimization_level: "all" # none, basic, extended, all + + # No fallback needed for ONNX (it's already the most compatible) + fallback: + enabled: false + +# Video sources (cameras) +cameras: + default: + enabled: true + source: "input/video.mp4" # MP4 file path + fps: 5 # Processing FPS (adjust based on CPU speed) + loop: true # Loop video playback + + detect: + enabled: true + width: 1280 # Processing resolution + height: 720 + + objects: + track: + - person + - car + - dog + - cat + filters: + person: + min_area: 1000 + min_score: 0.4 + car: + min_area: 2000 + min_score: 0.35 + +# Snapshot settings +snapshots: + enabled: true + output_dir: "output/snapshots" + + trigger: + objects: + - person + - car + min_score: 0.5 + cooldown: 2.0 + + format: "jpg" + quality: 95 + clean: true # No annotations on snapshot + +# Annotation export +annotations: + enabled: true + output_dir: "output/labels" + format: "yolo" + pair_with_snapshots: true + min_score: 0.3 + +# Debug output +debug_output: + enabled: true + output_dir: "output/debug" + + object_list: + enabled: true + show_confidence: true + show_class: true + show_bbox: true + + visualization: + enabled: true + draw_boxes: true + draw_labels: true + save_interval: 10 # Save every 10 frames + + stats: + show_fps: true + log_interval: 100 + +# Class names (COCO) +class_names: + 0: karung diff --git a/sam2-cpu/configs/frigate_mini_cpu_pt.yaml b/sam2-cpu/configs/frigate_mini_cpu_pt.yaml new file mode 100644 index 0000000..285f6f7 --- /dev/null +++ b/sam2-cpu/configs/frigate_mini_cpu_pt.yaml @@ -0,0 +1,103 @@ +# Frigate-Mini Configuration - CPU Only (ONNX) +# +# This configuration uses ONNX Runtime for CPU-only inference. +# No special hardware required - works on any x86 or ARM system. +# +# Usage: +# python scripts/frigate_mini.py --config configs/frigate_mini_cpu.yaml + +# Global settings +debug: true # Enable debug mode +log_level: "info" # debug, info, warning, error + +# Detector configuration - ONNX CPU Mode +detector: + type: "cpu" # Use ONNX Runtime (CPU) + model_path: "models/krg_masuk_yolov9t_best.pt" # ONNX model file + #model_path: "models/tuangatas.pt" # ONNX model file + input_size: [320, 320] # Model input resolution [width, height] + conf_threshold: 0.25 # Detection confidence threshold + nms_threshold: 0.45 # NMS IoU threshold + + # ONNX specific settings + onnx: + device: "cpu" # cpu or cuda + num_threads: 0 # CPU threads (0 = auto, uses all cores) + optimization_level: "all" # none, basic, extended, all + + # No fallback needed for ONNX (it's already the most compatible) + fallback: + enabled: false + +# Video sources (cameras) +cameras: + default: + enabled: true + #source: "input/tuang_train_20260120-1.mp4" # Video file path + source: "input/karung_masuk.mp4" # Video file path + fps: 2 # Processing FPS (adjust based on CPU speed) + loop: true # Loop video playback + + detect: + enabled: true + width: 1280 # Processing resolution + height: 720 + + objects: + track: + - karung + filters: + karung: + min_area: 1000 + min_score: 0.4 + car: + min_area: 2000 + min_score: 0.35 + +# Snapshot settings +snapshots: + enabled: true + output_dir: "output/snapshots" + + trigger: + objects: + - karung + min_score: 0.4 + cooldown: 0.0 + + format: "jpg" + quality: 95 + clean: true # No annotations on snapshot + +# Annotation export +annotations: + enabled: true + output_dir: "output/labels" + format: "yolo" + pair_with_snapshots: true + min_score: 0.3 + +# Debug output +debug_output: + enabled: true + output_dir: "output/debug" + + object_list: + enabled: true + show_confidence: true + show_class: true + show_bbox: true + + visualization: + enabled: true + draw_boxes: true + draw_labels: true + save_interval: 10 # Save every 10 frames + + stats: + show_fps: false + log_interval: 10 + +# Class names (COCO) +class_names: + 0: karung diff --git a/sam2-cpu/frigate-dev/.cspell/frigate-dictionary.txt b/sam2-cpu/frigate-dev/.cspell/frigate-dictionary.txt new file mode 100644 index 0000000..f2bcf41 --- /dev/null +++ b/sam2-cpu/frigate-dev/.cspell/frigate-dictionary.txt @@ -0,0 +1,320 @@ +aarch +absdiff +airockchip +Alloc +alpr +Amcrest +amdgpu +analyzeduration +Annke +apexcharts +arange +argmax +argmin +argpartition +ascontiguousarray +astype +authelia +authentik +autodetected +automations +autotrack +autotracked +autotracker +autotracking +backchannel +balena +Beelink +BGRA +BHWC +blackshear +blakeblackshear +bottombar +buildx +castable +cdist +Celeron +cgroups +chipset +chromadb +Chromecast +cmdline +codeowner +CODEOWNERS +codeproject +colormap +colorspace +comms +cooldown +coro +ctypeslib +CUDA +Cuvid +Dahua +datasheet +debconf +deci +deepstack +defragment +devcontainer +DEVICEMAP +discardcorrupt +dpkg +dsize +dtype +ECONNRESET +edgetpu +facenet +fastapi +faststart +fflags +ffprobe +fillna +flac +foscam +fourcc +framebuffer +fregate +frégate +fromarray +frombuffer +frontdoor +fstype +fullchain +fullscreen +genai +generativeai +genpts +getpid +gpuload +HACS +Hailo +hass +hconcat +healthcheck +hideable +Hikvision +homeassistant +homekit +homography +hsize +hstack +httpx +hwaccel +hwdownload +hwmap +hwupload +iloc +imagestream +imdecode +imencode +imread +imwrite +inpoint +interp +iostat +iotop +itemsize +Jellyfin +jetson +jetsons +jina +jinaai +joserfc +jsmpeg +jsonify +Kalman +keepalive +keepdims +labelmap +letsencrypt +levelname +LIBAVFORMAT +libedgetpu +libnvinfer +libva +libwebp +libx +libyolo +linalg +localzone +logpipe +Loryta +lstsq +lsusb +markupsafe +maxsplit +MEMHOSTALLOC +memlimit +meshgrid +metadatas +migraphx +minilm +mjpeg +mkfifo +mobiledet +mobilenet +modelpath +mosquitto +mountpoint +movflags +mpegts +mqtt +mse +msenc +namedtuples +nbytes +nchw +ndarray +ndimage +nethogs +newaxis +nhwc +NOBLOCK +nobuffer +nokey +NONBLOCK +noninteractive +noprint +Norfair +nptype +NTSC +numpy +nvenc +nvhost +nvml +nvmpi +ollama +onnx +onnxruntime +onvif +ONVIF +openai +opencv +openvino +overfitting +OWASP +paddleocr +paho +passwordless +popleft +posthog +postprocess +poweroff +preexec +probesize +protobuf +pstate +psutil +pubkey +putenv +pycache +pydantic +pyobj +pysqlite +pytz +pywebpush +qnap +quantisation +Radeon +radeonsi +radeontop +rawvideo +rcond +RDONLY +rebranded +referer +reindex +Reolink +restream +restreamed +restreaming +rkmpp +rknn +rkrga +rockchip +rocm +rocminfo +rootfs +rtmp +RTSP +ruamel +scroller +setproctitle +setpts +shms +SIGUSR +skylake +sleeptime +SNDMORE +socs +sqliteq +sqlitevecq +ssdlite +statm +stimeout +stylelint +subclassing +substream +superfast +surveillance +svscan +Swipeable +sysconf +tailscale +Tapo +tensorrt +tflite +thresholded +timelapse +titlecase +tmpfs +tobytes +toggleable +traefik +tzlocal +Ubiquiti +udev +udevadm +ultrafast +unichip +unidecode +Unifi +unixepoch +unraid +unreviewed +userdata +usermod +uvicorn +vaapi +vainfo +variations +vbios +vconcat +vitb +vstream +vsync +wallclock +webp +webpush +webrtc +websockets +webui +werkzeug +workdir +WRONLY +wsgirefserver +wsgiutils +wsize +xaddr +xmaxs +xmins +XPUB +XSUB +ymaxs +ymins +yolo +yolonas +yolox +zeep +zerolatency diff --git a/sam2-cpu/frigate-dev/.cursor/rules/frontend-always-use-translation-files.mdc b/sam2-cpu/frigate-dev/.cursor/rules/frontend-always-use-translation-files.mdc new file mode 100644 index 0000000..3503406 --- /dev/null +++ b/sam2-cpu/frigate-dev/.cursor/rules/frontend-always-use-translation-files.mdc @@ -0,0 +1,6 @@ +--- +globs: ["**/*.ts", "**/*.tsx"] +alwaysApply: false +--- + +Never write strings in the frontend directly, always write to and reference the relevant translations file. \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/.devcontainer/devcontainer.json b/sam2-cpu/frigate-dev/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c782fb3 --- /dev/null +++ b/sam2-cpu/frigate-dev/.devcontainer/devcontainer.json @@ -0,0 +1,125 @@ +{ + "name": "Frigate Devcontainer", + "dockerComposeFile": "../docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspace/frigate", + "initializeCommand": ".devcontainer/initialize.sh", + "postCreateCommand": ".devcontainer/post_create.sh", + "overrideCommand": false, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {} + // Uncomment the following lines to use ONNX Runtime with CUDA support + // "ghcr.io/devcontainers/features/nvidia-cuda:1": { + // "installCudnn": true, + // "installNvtx": true, + // "installToolkit": true, + // "cudaVersion": "12.5", + // "cudnnVersion": "9.4.0.58" + // }, + // "./features/onnxruntime-gpu": {} + }, + "forwardPorts": [ + 8971, + 5000, + 5001, + 5173, + 8554, + 8555 + ], + "portsAttributes": { + "8971": { + "label": "External NGINX", + "onAutoForward": "silent" + }, + "5000": { + "label": "Internal NGINX", + "onAutoForward": "silent" + }, + "5001": { + "label": "Frigate API", + "onAutoForward": "silent" + }, + "5173": { + "label": "Vite Server", + "onAutoForward": "silent" + }, + "8554": { + "label": "gortc RTSP", + "onAutoForward": "silent" + }, + "8555": { + "label": "go2rtc WebRTC", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "mhutchie.git-graph", + "ms-azuretools.vscode-docker", + "streetsidesoftware.code-spell-checker", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "mikestead.dotenv", + "csstools.postcss", + "blanu.vscode-styled-jsx", + "bradlc.vscode-tailwindcss", + "charliermarsh.ruff", + "eamodio.gitlens" + ], + "settings": { + "remote.autoForwardPorts": false, + "python.formatting.provider": "none", + "python.languageServer": "Pylance", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./frigate/test" + ], + "files.trimTrailingWhitespace": true, + "eslint.workingDirectories": [ + "./web" + ], + "isort.args": [ + "--settings-path=./pyproject.toml" + ], + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "[json][jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsx][js][tsx][ts]": { + "editor.codeActionsOnSave": [ + "source.addMissingImports", + "source.fixAll" + ], + "editor.tabSize": 2 + }, + "cSpell.ignoreWords": [ + "rtmp" + ], + "cSpell.words": [ + "preact", + "astype", + "hwaccel", + "mqtt" + ] + } + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json b/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json new file mode 100644 index 0000000..3051444 --- /dev/null +++ b/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/devcontainer-feature.json @@ -0,0 +1,22 @@ +{ + "id": "onnxruntime-gpu", + "version": "0.0.1", + "name": "ONNX Runtime GPU (Nvidia)", + "description": "Installs ONNX Runtime for Nvidia GPUs.", + "documentationURL": "", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "1.20.1", + "1.20.0" + ], + "default": "latest", + "description": "Version of ONNX Runtime to install" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/nvidia-cuda" + ] +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/install.sh b/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/install.sh new file mode 100644 index 0000000..0c090be --- /dev/null +++ b/sam2-cpu/frigate-dev/.devcontainer/features/onnxruntime-gpu/install.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -e + +VERSION=${VERSION} + +python3 -m pip config set global.break-system-packages true +# if VERSION == "latest" or VERSION is empty, install the latest version +if [ "$VERSION" == "latest" ] || [ -z "$VERSION" ]; then + python3 -m pip install onnxruntime-gpu +else + python3 -m pip install onnxruntime-gpu==$VERSION +fi + +echo "Done!" \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/.devcontainer/initialize.sh b/sam2-cpu/frigate-dev/.devcontainer/initialize.sh new file mode 100755 index 0000000..2300df1 --- /dev/null +++ b/sam2-cpu/frigate-dev/.devcontainer/initialize.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +# These folders needs to be created and owned by the host user +mkdir -p debug web/dist + +if [[ -f "config/config.yml" ]]; then + echo "config/config.yml already exists, skipping initialization" >&2 +else + echo "initializing config/config.yml" >&2 + cp -fv config/config.yml.example config/config.yml +fi diff --git a/sam2-cpu/frigate-dev/.devcontainer/post_create.sh b/sam2-cpu/frigate-dev/.devcontainer/post_create.sh new file mode 100755 index 0000000..fcf7ca6 --- /dev/null +++ b/sam2-cpu/frigate-dev/.devcontainer/post_create.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euxo pipefail + +# Cleanup the old github host key +if [[ -f ~/.ssh/known_hosts ]]; then + # Add new github host key + sed -i -e '/AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31\/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi\/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==/d' ~/.ssh/known_hosts + curl -L https://api.github.com/meta | jq -r '.ssh_keys | .[]' | \ + sed -e 's/^/github.com /' >> ~/.ssh/known_hosts +fi + +# Frigate normal container runs as root, so it have permission to create +# the folders. But the devcontainer runs as the host user, so we need to +# create the folders and give the host user permission to write to them. +sudo mkdir -p /media/frigate +sudo chown -R "$(id -u):$(id -g)" /media/frigate + +# When started as a service, LIBAVFORMAT_VERSION_MAJOR is defined in the +# s6 service file. For dev, where frigate is started from an interactive +# shell, we define it in .bashrc instead. +echo 'export LIBAVFORMAT_VERSION_MAJOR=$("$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py)" -version | grep -Po "libavformat\W+\K\d+")' >> "$HOME/.bashrc" + +make version + +cd web + +npm install + +npm run build diff --git a/sam2-cpu/frigate-dev/.dockerignore b/sam2-cpu/frigate-dev/.dockerignore new file mode 100644 index 0000000..b22b1b5 --- /dev/null +++ b/sam2-cpu/frigate-dev/.dockerignore @@ -0,0 +1,16 @@ +README.md +docs/ +.gitignore +debug +config/ +*.pyc +.git +core +*.mp4 +*.jpg +*.db +*.ts + +web/dist/ +web/node_modules/ +web/.npm diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/beta-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/beta-support.yml new file mode 100644 index 0000000..e342127 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/beta-support.yml @@ -0,0 +1,129 @@ +title: "[Beta Support]: " +labels: ["support", "triage", "beta"] +body: + - type: markdown + attributes: + value: | + Thank you for testing Frigate beta versions! Use this form for support with beta releases. + + **Note:** Beta versions may have incomplete features, known issues, or unexpected behavior. Please check the [release notes](https://github.com/blakeblackshear/frigate/releases) and [recent discussions][discussions] for known beta issues before submitting. + + Before submitting, read the [beta documentation][docs]. + + [docs]: https://deploy-preview-19787--frigate-docs.netlify.app/ + - type: textarea + id: description + attributes: + label: Describe the problem you are having + description: Please be as detailed as possible. Include what you expected to happen vs what actually happened. + validations: + required: true + - type: input + id: version + attributes: + label: Beta Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1) + placeholder: "0.17.0-beta1" + validations: + required: true + - type: dropdown + id: issue-category + attributes: + label: Issue Category + description: What area is your issue related to? This helps us understand the context. + options: + - Object Detection / Detectors + - Hardware Acceleration + - Configuration / Setup + - WebUI / Frontend + - Recordings / Storage + - Notifications / Events + - Integration (Home Assistant, etc) + - Performance / Stability + - Installation / Updates + - Other + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. Remove any sensitive information like passwords or URLs. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output (if applicable) + description: If your issue involves cameras, streams, or playback, please include go2rtc logs. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. Include relevant environment variables and device mappings. + render: yaml + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Ubuntu + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: input + id: hardware + attributes: + label: CPU / GPU / Hardware + description: Provide details about your hardware (e.g., Intel i5-9400, NVIDIA RTX 3060, Raspberry Pi 4, etc) + placeholder: "Intel i7-10700, NVIDIA GTX 1660" + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Screenshots of the issue, System metrics pages, or any relevant UI. Drag and drop or paste images directly. + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: If applicable, provide detailed steps to reproduce the issue + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + - type: textarea + id: other + attributes: + label: Any other information that may be helpful + description: Additional context, related issues, when the problem started appearing, etc. diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/camera-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/camera-support.yml new file mode 100644 index 0000000..521d65d --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/camera-support.yml @@ -0,0 +1,138 @@ +title: "[Camera Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions for an issue with your cameras. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: What browser(s) are you using? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/config-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/config-support.yml new file mode 100644 index 0000000..575f7f6 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/config-support.yml @@ -0,0 +1,113 @@ +title: "[Config Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions related to Frigate's configuration and config file. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop or simple cut/paste is possible in this field + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/detector-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/detector-support.yml new file mode 100644 index 0000000..fb99450 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/detector-support.yml @@ -0,0 +1,87 @@ +title: "[Detector Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support or questions related to Frigate's object detectors. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/general-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/general-support.yml new file mode 100644 index 0000000..0b9f225 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/general-support.yml @@ -0,0 +1,130 @@ +title: "[Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form for support for issues that don't fall into any specific category. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: What browser(s) are you using? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: textarea + id: stats + attributes: + label: Frigate stats + description: Output from frigate's /api/stats endpoint + render: json + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml new file mode 100644 index 0000000..8611566 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml @@ -0,0 +1,120 @@ +title: "[HW Accel Support]: " +labels: ["support", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form to submit a support request for hardware acceleration issues. + + Before submitting your support request, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: Describe the problem you are having + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: ffprobe + attributes: + label: FFprobe output from your camera + description: Run `ffprobe ` from within the Frigate container if possible, and provide output below + render: shell + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + - Proxmox via Docker + - Proxmox via TTeck Script + - Windows WSL2 + validations: + required: true + - type: dropdown + id: object-detector + attributes: + label: Object Detector + options: + - Coral + - OpenVino + - TensorRT + - RKNN + - Other + - CPU (no coral) + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of at least General and Cameras tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/question.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/question.yml new file mode 100644 index 0000000..6a4789c --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/question.yml @@ -0,0 +1,21 @@ +title: "[Question]: " +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Use this form for questions you have about Frigate. + + Before submitting your question, please [search the discussions][discussions], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your question has already been answered by the community. + + **If you are looking for support, start a new discussion and use a support category.** + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + - type: textarea + id: description + attributes: + label: "What is your question?" + validations: + required: true diff --git a/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/report-a-bug.yml new file mode 100644 index 0000000..de870ac --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -0,0 +1,151 @@ +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Use this form to submit a reproducible bug in Frigate or Frigate's UI. + + **⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.** + + Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. + + **If you are unsure if your issue is actually a bug or not, please submit a support request first.** + + [discussions]: https://www.github.com/blakeblackshear/frigate/discussions + [prs]: https://www.github.com/blakeblackshear/frigate/pulls + [docs]: https://docs.frigate.video + [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + [ai]: https://docs.frigate.video + - type: checkboxes + attributes: + label: Checklist + description: Please verify that you've followed these steps + options: + - label: I have updated to the latest available Frigate version. + required: true + - label: I have cleared the cache of my browser. + required: true + - label: I have tried a different browser to see if it is related to my browser. + required: true + - label: I have tried reproducing the issue in [incognito mode](https://www.computerworld.com/article/1719851/how-to-go-incognito-in-chrome-firefox-safari-and-edge.html) to rule out problems with any third party extensions or plugins I have installed. + - label: I have asked the AI at https://docs.frigate.video about my issue. + required: true + - type: textarea + id: description + attributes: + label: Describe the problem you are having + description: Provide a clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: | + Please tell us exactly how to reproduce your issue. + Provide clear and concise step by step instructions and add code snippets if needed. + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.14.0-ea36ds1) + validations: + required: true + - type: input + attributes: + label: In which browser(s) are you experiencing the issue with? + placeholder: Google Chrome 88.0.4324.150 + description: > + Provide the full name and don't forget to add the version! + - type: textarea + id: config + attributes: + label: Frigate config file + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: docker + attributes: + label: docker-compose file or Docker CLI command + description: This will be automatically formatted into code, so no need for backticks. + render: yaml + validations: + required: true + - type: textarea + id: frigatelogs + attributes: + label: Relevant Frigate log output + description: Please copy and paste any relevant Frigate log output. Include logs before and after your exact error when possible. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: textarea + id: go2rtclogs + attributes: + label: Relevant go2rtc log output + description: Please copy and paste any relevant go2rtc log output. Include logs before and after your exact error when possible. Logs can be viewed via the Frigate UI, Docker, or the go2rtc dashboard. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Home Assistant OS + - Debian + - Other Linux + - Proxmox + - UNRAID + - Windows + - Other + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Install method + options: + - Home Assistant Add-on + - Docker Compose + - Docker CLI + validations: + required: true + - type: dropdown + id: network + attributes: + label: Network connection + options: + - Wired + - Wireless + - Mixed + validations: + required: true + - type: input + id: camera + attributes: + label: Camera make and model + description: Dahua, hikvision, amcrest, reolink, etc and model number + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots of the Frigate UI's System metrics pages + description: Drag and drop for images is possible in this field. Please post screenshots of all tabs. + validations: + required: true + - type: textarea + id: other + attributes: + label: Any other information that may be helpful diff --git a/sam2-cpu/frigate-dev/.github/FUNDING.yml b/sam2-cpu/frigate-dev/.github/FUNDING.yml new file mode 100644 index 0000000..b8892ce --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: + - blakeblackshear + - NickM-27 + - hawkeye217 diff --git a/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/config.yml b/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a7474bc --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Frigate Support + url: https://github.com/blakeblackshear/frigate/discussions/new/choose + about: Get support for setting up or troubleshooting Frigate. + - name: Frigate Bug Report + url: https://github.com/blakeblackshear/frigate/discussions/new/choose + about: Report a specific UI or backend bug. diff --git a/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/feature_request.md b/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..57f76d3 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe what you are trying to accomplish and why in non technical terms** +I want to be able to ... so that I can ... + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/sam2-cpu/frigate-dev/.github/actions/setup/action.yml b/sam2-cpu/frigate-dev/.github/actions/setup/action.yml new file mode 100644 index 0000000..724af45 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/actions/setup/action.yml @@ -0,0 +1,50 @@ +name: 'Setup' +description: 'Set up QEMU and Buildx' +inputs: + GITHUB_TOKEN: + required: true +outputs: + image-name: + value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ steps.create-short-sha.outputs.SHORT_SHA }} + cache-name: + value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:cache +runs: + using: "composite" + steps: + # Stop docker so we can mount more space at /var/lib/docker + - name: Stop docker + run: sudo systemctl stop docker + shell: bash + # This creates a virtual volume at /var/lib/docker to maximize the size + # As of 2/14/2024, this results in 97G for docker images + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + build-mount-path: '/var/lib/docker' + - name: Start docker + run: sudo systemctl start docker + shell: bash + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v5 + with: + string: ${{ github.repository }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.GITHUB_TOKEN }} + - name: Create version file + run: make version + shell: bash + - id: create-short-sha + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + shell: bash diff --git a/sam2-cpu/frigate-dev/.github/copilot-instructions.md b/sam2-cpu/frigate-dev/.github/copilot-instructions.md new file mode 100644 index 0000000..6c14bb1 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/copilot-instructions.md @@ -0,0 +1,2 @@ +Never write strings in the frontend directly, always write to and reference the relevant translations file. +Always conform new and refactored code to the existing coding style in the project. diff --git a/sam2-cpu/frigate-dev/.github/dependabot.yml b/sam2-cpu/frigate-dev/.github/dependabot.yml new file mode 100644 index 0000000..db67aa2 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/dependabot.yml @@ -0,0 +1,40 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "pip" + directory: "/docker/main" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "pip" + directory: "/docker/tensorrt" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "npm" + directory: "/web" + schedule: + interval: daily + open-pull-requests-limit: 10 + target-branch: dev + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: daily + allow: + - dependency-name: "@docusaurus/*" + open-pull-requests-limit: 10 + target-branch: dev diff --git a/sam2-cpu/frigate-dev/.github/pull_request_template.md b/sam2-cpu/frigate-dev/.github/pull_request_template.md new file mode 100644 index 0000000..3204244 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/pull_request_template.md @@ -0,0 +1,39 @@ +## Proposed change + + + +## Type of change + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code +- [ ] Documentation Update + +## Additional information + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: + +## Checklist + + + +- [ ] The code change is tested and works locally. +- [ ] Local tests pass. **Your PR cannot be merged unless tests pass** +- [ ] There is no commented out code in this PR. +- [ ] UI changes including text have used i18n keys and have been added to the `en` locale. +- [ ] The code has been formatted using Ruff (`ruff format frigate`) diff --git a/sam2-cpu/frigate-dev/.github/workflows/ci.yml b/sam2-cpu/frigate-dev/.github/workflows/ci.yml new file mode 100644 index 0000000..54df536 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/workflows/ci.yml @@ -0,0 +1,226 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - dev + - master + paths-ignore: + - "docs/**" + +# only run the latest commit to avoid cache overwrites +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: 3.11 + +jobs: + amd64_build: + runs-on: ubuntu-22.04 + name: AMD64 Build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push amd64 standard build + uses: docker/build-push-action@v5 + with: + context: . + file: docker/main/Dockerfile + push: true + platforms: linux/amd64 + target: frigate + tags: ${{ steps.setup.outputs.image-name }}-amd64 + cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64 + cache-to: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64,mode=max + arm64_build: + runs-on: ubuntu-22.04-arm + name: ARM Build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push arm64 standard build + uses: docker/build-push-action@v5 + with: + context: . + file: docker/main/Dockerfile + push: true + platforms: linux/arm64 + target: frigate + tags: | + ${{ steps.setup.outputs.image-name }}-standard-arm64 + cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 + - name: Build and push RPi build + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: rpi + files: docker/rpi/rpi.hcl + set: | + rpi.tags=${{ steps.setup.outputs.image-name }}-rpi + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max + jetson_jp6_build: + runs-on: ubuntu-22.04-arm + name: Jetson Jetpack 6 + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push TensorRT (Jetson, Jetpack 6) + env: + ARCH: arm64 + BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu + SLIM_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu + TRT_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: tensorrt + files: docker/tensorrt/trt.hcl + set: | + tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp6 + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6 + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6,mode=max + amd64_extra_builds: + runs-on: ubuntu-22.04 + name: AMD64 Extra Build + needs: + - amd64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push TensorRT (x86 GPU) + env: + COMPUTE_LEVEL: "50 60 70 80 90" + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: tensorrt + files: docker/tensorrt/trt.hcl + set: | + tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max + - name: AMD/ROCm general build + env: + HSA_OVERRIDE: 0 + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: rocm + files: docker/rocm/rocm.hcl + set: | + rocm.tags=${{ steps.setup.outputs.image-name }}-rocm + *.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm,mode=max + *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm + arm64_extra_builds: + runs-on: ubuntu-22.04-arm + name: ARM Extra Build + needs: + - arm64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Rockchip build + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: rk + files: docker/rockchip/rk.hcl + set: | + rk.tags=${{ steps.setup.outputs.image-name }}-rk + *.cache-from=type=gha + synaptics_build: + runs-on: ubuntu-22.04-arm + name: Synaptics Build + needs: + - arm64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push Synaptics build + uses: docker/bake-action@v6 + with: + source: . + push: true + targets: synaptics + files: docker/synaptics/synaptics.hcl + set: | + synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics + *.cache-from=type=gha + # The majority of users running arm64 are rpi users, so the rpi + # build should be the primary arm64 image + assemble_default_build: + runs-on: ubuntu-22.04 + name: Assemble and push default build + needs: + - amd64_build + - arm64_build + steps: + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + - name: Log in to the Container registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create short sha + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + - uses: int128/docker-manifest-create-action@v2 + with: + tags: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }} + sources: | + ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64 + ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi diff --git a/sam2-cpu/frigate-dev/.github/workflows/pull_request.yml b/sam2-cpu/frigate-dev/.github/workflows/pull_request.yml new file mode 100644 index 0000000..c4d8aa7 --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/workflows/pull_request.yml @@ -0,0 +1,95 @@ +name: On pull request + +on: + pull_request: + paths-ignore: + - "docs/**" + - ".github/*.yml" + - ".github/DISCUSSION_TEMPLATE/**" + - ".github/ISSUE_TEMPLATE/**" + +env: + DEFAULT_PYTHON: 3.11 + +jobs: + web_lint: + name: Web - Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: npm install + working-directory: ./web + - name: Lint + run: npm run lint + working-directory: ./web + + web_test: + name: Web - Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - run: npm install + working-directory: ./web + - name: Build web + run: npm run build + working-directory: ./web + # - name: Test + # run: npm run test + # working-directory: ./web + + python_checks: + runs-on: ubuntu-latest + name: Python Checks + steps: + - name: Check out the repository + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v5.4.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Install requirements + run: | + python3 -m pip install -U pip + python3 -m pip install -r docker/main/requirements-dev.txt + - name: Check formatting + run: | + ruff format --check --diff frigate migrations docker *.py + - name: Check lint + run: | + ruff check frigate migrations docker *.py + + python_tests: + runs-on: ubuntu-latest + name: Python Tests + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Install devcontainer cli + run: npm install --global @devcontainers/cli + - name: Build devcontainer + env: + DOCKER_BUILDKIT: "1" + run: devcontainer build --workspace-folder . + - name: Start devcontainer + run: devcontainer up --workspace-folder . + - name: Run mypy in devcontainer + run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m mypy --config-file frigate/mypy.ini frigate" + - name: Run unit tests in devcontainer + run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest" diff --git a/sam2-cpu/frigate-dev/.github/workflows/release.yml b/sam2-cpu/frigate-dev/.github/workflows/release.yml new file mode 100644 index 0000000..6961fcf --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: On release + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - id: lowercaseRepo + uses: ASzc/change-string-case-action@v6 + with: + string: ${{ github.repository }} + - name: Log in to the Container registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create tag variables + env: + TAG: ${{ github.ref_name }} + LOWERCASE_REPO: ${{ steps.lowercaseRepo.outputs.lowercase }} + run: | + BUILD_TYPE=$([[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "stable" || echo "beta") + echo "BUILD_TYPE=${BUILD_TYPE}" >> $GITHUB_ENV + echo "BASE=ghcr.io/${LOWERCASE_REPO}" >> $GITHUB_ENV + echo "BUILD_TAG=${GITHUB_SHA::7}" >> $GITHUB_ENV + echo "CLEAN_VERSION=$(echo ${GITHUB_REF##*/} | tr '[:upper:]' '[:lower:]' | sed 's/^[v]//')" >> $GITHUB_ENV + - name: Tag and push the main image + run: | + VERSION_TAG=${BASE}:${CLEAN_VERSION} + STABLE_TAG=${BASE}:stable + PULL_TAG=${BASE}:${BUILD_TAG} + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG} + for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant} + done + + # stable tag + if [[ "${BUILD_TYPE}" == "stable" ]]; then + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${STABLE_TAG} + for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do + docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${STABLE_TAG}-${variant} + done + fi diff --git a/sam2-cpu/frigate-dev/.github/workflows/stale.yml b/sam2-cpu/frigate-dev/.github/workflows/stale.yml new file mode 100644 index 0000000..011f70a --- /dev/null +++ b/sam2-cpu/frigate-dev/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +# Close Stale Issues +# Warns and then closes issues and PRs that have had no activity for a specified amount of time. +# https://github.com/actions/stale + +name: "Stalebot" +on: + schedule: + - cron: "0 0 * * *" # run stalebot once a day + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@main + id: stale + with: + stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions." + close-issue-message: "" + days-before-stale: 30 + days-before-close: 3 + exempt-draft-pr: true + exempt-issue-labels: "pinned,security" + exempt-pr-labels: "pinned,security,dependencies" + operations-per-run: 120 + - name: Print outputs + env: + STALE_OUTPUT: ${{ join(steps.stale.outputs.*, ',') }} + run: echo "$STALE_OUTPUT" + + # clean_ghcr: + # name: Delete outdated dev container images + # runs-on: ubuntu-latest + # steps: + # - name: Delete old images + # uses: snok/container-retention-policy@v2 + # with: + # image-names: dev-* + # cut-off: 60 days ago UTC + # keep-at-least: 5 + # account-type: personal + # token: ${{ secrets.GITHUB_TOKEN }} + # token-type: github-token diff --git a/sam2-cpu/frigate-dev/.gitignore b/sam2-cpu/frigate-dev/.gitignore new file mode 100644 index 0000000..660a378 --- /dev/null +++ b/sam2-cpu/frigate-dev/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +__pycache__ +.mypy_cache +*.swp +debug +.vscode/* +!.vscode/launch.json +config/* +!config/*.example +models +*.mp4 +*.db +*.csv +frigate/version.py +web/build +web/node_modules +web/coverage +web/.env +core +!/web/**/*.ts +.idea/* +.ipynb_checkpoints \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/.pylintrc b/sam2-cpu/frigate-dev/.pylintrc new file mode 100644 index 0000000..bf205da --- /dev/null +++ b/sam2-cpu/frigate-dev/.pylintrc @@ -0,0 +1,588 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=fstr + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/sam2-cpu/frigate-dev/.vscode/launch.json b/sam2-cpu/frigate-dev/.vscode/launch.json new file mode 100644 index 0000000..5c85826 --- /dev/null +++ b/sam2-cpu/frigate-dev/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Launch Frigate", + "type": "debugpy", + "request": "launch", + "module": "frigate" + } + ] +} diff --git a/sam2-cpu/frigate-dev/CODEOWNERS b/sam2-cpu/frigate-dev/CODEOWNERS new file mode 100644 index 0000000..c37041c --- /dev/null +++ b/sam2-cpu/frigate-dev/CODEOWNERS @@ -0,0 +1,7 @@ +# Community-supported boards +/docker/tensorrt/ @madsciencetist @NateMeyer +/docker/tensorrt/*arm64* @madsciencetist +/docker/tensorrt/*jetson* @madsciencetist +/docker/rockchip/ @MarcA711 +/docker/rocm/ @harakas +/docker/hailo8l/ @spanner3003 diff --git a/sam2-cpu/frigate-dev/LICENSE b/sam2-cpu/frigate-dev/LICENSE new file mode 100644 index 0000000..0c1fc1f --- /dev/null +++ b/sam2-cpu/frigate-dev/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2025 Frigate LLC (Frigate™) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sam2-cpu/frigate-dev/Makefile b/sam2-cpu/frigate-dev/Makefile new file mode 100644 index 0000000..d1427b6 --- /dev/null +++ b/sam2-cpu/frigate-dev/Makefile @@ -0,0 +1,60 @@ +default_target: local + +COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) +VERSION = 0.17.0 +IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate +GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +BOARDS= #Initialized empty + +include docker/*/*.mk + +build-boards: $(BOARDS:%=build-%) + +push-boards: $(BOARDS:%=push-%) + +version: + echo 'VERSION = "$(VERSION)-$(COMMIT_HASH)"' > frigate/version.py + echo 'VITE_GIT_COMMIT_HASH=$(COMMIT_HASH)' > web/.env + +local: version + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag frigate:latest \ + --load + +debug: version + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --build-arg DEBUG=true \ + --tag frigate:latest \ + --load + +amd64: + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/amd64 + +arm64: + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/arm64 + +build: version amd64 arm64 + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):$(VERSION)-$(COMMIT_HASH) \ + --platform linux/arm64/v8,linux/amd64 + +push: push-boards + docker buildx build --target=frigate --file docker/main/Dockerfile . \ + --tag $(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH) \ + --platform linux/arm64/v8,linux/amd64 \ + --push + +run: local + docker run --rm --publish=5000:5000 --volume=${PWD}/config:/config frigate:latest + +run_tests: local + docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \ + python3 -u -m unittest + docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \ + python3 -u -m mypy --config-file frigate/mypy.ini frigate + +.PHONY: run_tests diff --git a/sam2-cpu/frigate-dev/README.md b/sam2-cpu/frigate-dev/README.md new file mode 100644 index 0000000..b1eab6c --- /dev/null +++ b/sam2-cpu/frigate-dev/README.md @@ -0,0 +1,83 @@ +

+ logo +

+ +# Frigate NVR™ - Realtime Object Detection for IP Cameras + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +Translation status + + +\[English\] | [简体中文](https://github.com/blakeblackshear/frigate/blob/dev/README_CN.md) + +A complete and local NVR designed for [Home Assistant](https://www.home-assistant.io) with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras. + +Use of a GPU or AI accelerator is highly recommended. AI accelerators will outperform even the best CPUs with very little overhead. See Frigate's supported [object detectors](https://docs.frigate.video/configuration/object_detectors/). + +- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration) +- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary +- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame +- Uses a very low overhead motion detection to determine where to run object detection +- Object detection with TensorFlow runs in separate processes for maximum FPS +- Communicates over MQTT for easy integration into other systems +- Records video with retention settings based on detected objects +- 24/7 recording +- Re-streaming via RTSP to reduce the number of connections to your camera +- WebRTC & MSE support for low-latency live view + +## Documentation + +View the documentation at https://docs.frigate.video + +## Donations + +If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear). + +## License + +This project is licensed under the **MIT License**. + +- **Code:** The source code, configuration files, and documentation in this repository are available under the [MIT License](LICENSE). You are free to use, modify, and distribute the code as long as you include the original copyright notice. +- **Trademarks:** The "Frigate" name, the "Frigate NVR" brand, and the Frigate logo are **trademarks of Frigate LLC** and are **not** covered by the MIT License. + +Please see our [Trademark Policy](TRADEMARK.md) for details on acceptable use of our brand assets. + +## Screenshots + +### Live dashboard + +
+Live dashboard +
+ +### Streamlined review workflow + +
+Streamlined review workflow +
+ +### Multi-camera scrubbing + +
+Multi-camera scrubbing +
+ +### Built-in mask and zone editor + +
+Multi-camera scrubbing +
+ +## Translations + +We use [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) to support language translations. Contributions are always welcome. + + +Translation status + + +--- + +**Copyright © 2025 Frigate LLC.** diff --git a/sam2-cpu/frigate-dev/README_CN.md b/sam2-cpu/frigate-dev/README_CN.md new file mode 100644 index 0000000..3c37f64 --- /dev/null +++ b/sam2-cpu/frigate-dev/README_CN.md @@ -0,0 +1,89 @@ +

+ logo +

+ +# Frigate NVR™ - 一个具有实时目标检测的本地 NVR + +[English](https://github.com/blakeblackshear/frigate) | \[简体中文\] + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +翻译状态 + + +一个完整的本地网络视频录像机(NVR),专为[Home Assistant](https://www.home-assistant.io)设计,具备 AI 目标/物体检测功能。使用 OpenCV 和 TensorFlow 在本地为 IP 摄像头执行实时物体检测。 + +强烈推荐使用 GPU 或者 AI 加速器(例如[Google Coral 加速器](https://coral.ai/products/) 或者 [Hailo](https://hailo.ai/)等)。它们的运行效率远远高于现在的顶级 CPU,并且功耗也极低。 + +- 通过[自定义组件](https://github.com/blakeblackshear/frigate-hass-integration)与 Home Assistant 紧密集成 +- 设计上通过仅在必要时和必要地点寻找目标,最大限度地减少资源使用并最大化性能 +- 大量利用多进程处理,强调实时性而非处理每一帧 +- 使用非常低开销的画面变动检测(也叫运动检测)来确定运行目标检测的位置 +- 使用 TensorFlow 进行目标检测,并运行在单独的进程中以达到最大 FPS +- 通过 MQTT 进行通信,便于集成到其他系统中 +- 根据检测到的物体设置保留时间进行视频录制 +- 24/7 全天候录制 +- 通过 RTSP 重新流传输以减少摄像头的连接数 +- 支持 WebRTC 和 MSE,实现低延迟的实时观看 + +## 社区中文翻译文档 + +你可以在这里查看文档 https://docs.frigate-cn.video + +## 赞助 + +如果您想通过捐赠支持开发,请使用 [Github Sponsors](https://github.com/sponsors/blakeblackshear)。 + +## 协议 + +本项目采用 **MIT 许可证**授权。 +**代码部分**:本代码库中的源代码、配置文件和文档均遵循 [MIT 许可证](LICENSE)。您可以自由使用、修改和分发这些代码,但必须保留原始版权声明。 + +**商标部分**:“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate LLC 的商标**,**不在** MIT 许可证覆盖范围内。 +有关品牌资产的规范使用详情,请参阅我们的[《商标政策》](TRADEMARK.md)。 + +## 截图 + +### 实时监控面板 + +
+实时监控面板 +
+ +### 简单的核查工作流程 + +
+简单的审查工作流程 +
+ +### 多摄像头可按时间轴查看 + +
+多摄像头可按时间轴查看 +
+ +### 内置遮罩和区域编辑器 + +
+内置遮罩和区域编辑器 +
+ +## 翻译 + +我们使用 [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) 平台提供翻译支持,欢迎参与进来一起完善。 + +## 非官方中文讨论社区 + +欢迎加入中文讨论 QQ 群:[1043861059](https://qm.qq.com/q/7vQKsTmSz) + +Bilibili:https://space.bilibili.com/3546894915602564 + +## 中文社区赞助商 + +[![EdgeOne](https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png)](https://edgeone.ai/zh?from=github) +本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 + +--- + +**Copyright © 2025 Frigate LLC.** diff --git a/sam2-cpu/frigate-dev/TRADEMARK.md b/sam2-cpu/frigate-dev/TRADEMARK.md new file mode 100644 index 0000000..ef3681c --- /dev/null +++ b/sam2-cpu/frigate-dev/TRADEMARK.md @@ -0,0 +1,58 @@ +# Trademark Policy + +**Last Updated:** November 2025 + +This document outlines the policy regarding the use of the trademarks associated with the Frigate NVR project. + +## 1. Our Trademarks + +The following terms and visual assets are trademarks (the "Marks") of **Frigate LLC**: + +- **Frigate™** +- **Frigate NVR™** +- **Frigate+™** +- **The Frigate Logo** + +**Note on Common Law Rights:** +Frigate LLC asserts all common law rights in these Marks. The absence of a federal registration symbol (®) does not constitute a waiver of our intellectual property rights. + +## 2. Interaction with the MIT License + +The software in this repository is licensed under the [MIT License](LICENSE). + +**Crucial Distinction:** + +- The **Code** is free to use, modify, and distribute under the MIT terms. +- The **Brand (Trademarks)** is **NOT** licensed under MIT. + +You may not use the Marks in any way that is not explicitly permitted by this policy or by written agreement with Frigate LLC. + +## 3. Acceptable Use + +You may use the Marks without prior written permission in the following specific contexts: + +- **Referential Use:** To truthfully refer to the software (e.g., _"I use Frigate NVR for my home security"_). +- **Compatibility:** To indicate that your product or project works with the software (e.g., _"MyPlugin for Frigate NVR"_ or _"Compatible with Frigate"_). +- **Commentary:** In news articles, blog posts, or tutorials discussing the software. + +## 4. Prohibited Use + +You may **NOT** use the Marks in the following ways: + +- **Commercial Products:** You may not use "Frigate" in the name of a commercial product, service, or app (e.g., selling an app named _"Frigate Viewer"_ is prohibited). +- **Implying Affiliation:** You may not use the Marks in a way that suggests your project is official, sponsored by, or endorsed by Frigate LLC. +- **Confusing Forks:** If you fork this repository to create a derivative work, you **must** remove the Frigate logo and rename your project to avoid user confusion. You cannot distribute a modified version of the software under the name "Frigate". +- **Domain Names:** You may not register domain names containing "Frigate" that are likely to confuse users (e.g., `frigate-official-support.com`). + +## 5. The Logo + +The Frigate logo (the bird icon) is a visual trademark. + +- You generally **cannot** use the logo on your own website or product packaging without permission. +- If you are building a dashboard or integration that interfaces with Frigate, you may use the logo only to represent the Frigate node/service, provided it does not imply you _are_ Frigate. + +## 6. Questions & Permissions + +If you are unsure if your intended use violates this policy, or if you wish to request a specific license to use the Marks (e.g., for a partnership), please contact us at: + +**help@frigate.video** diff --git a/sam2-cpu/frigate-dev/audio-labelmap.txt b/sam2-cpu/frigate-dev/audio-labelmap.txt new file mode 100644 index 0000000..4a38b5f --- /dev/null +++ b/sam2-cpu/frigate-dev/audio-labelmap.txt @@ -0,0 +1,521 @@ +speech +speech +speech +speech +babbling +speech +yell +bellow +whoop +yell +yell +yell +whispering +laughter +laughter +laughter +snicker +laughter +laughter +crying +crying +crying +yell +sigh +singing +choir +sodeling +chant +mantra +child_singing +synthetic_singing +rapping +humming +groan +grunt +whistling +breathing +wheeze +snoring +gasp +pant +snort +cough +throat_clearing +sneeze +sniff +run +shuffle +footsteps +chewing +biting +gargling +stomach_rumble +burping +hiccup +fart +hands +finger_snapping +clapping +heartbeat +heart_murmur +cheering +applause +chatter +crowd +speech +children_playing +animal +pets +dog +bark +yip +howl +bow-wow +growling +whimper_dog +cat +purr +meow +hiss +caterwaul +livestock +horse +clip-clop +neigh +cattle +moo +cowbell +pig +oink +goat +bleat +sheep +fowl +chicken +cluck +cock-a-doodle-doo +turkey +gobble +duck +quack +goose +honk +wild_animals +roaring_cats +roar +bird +chird +chirp +squawk +pigeon +coo +crow +caw +owl +hoot +flapping_wings +dogs +rats +mouse +patter +insect +cricket +mosquito +fly +buzz +buzz +frog +croak +snake +rattle +whale_vocalization +music +musical_instrument +plucked_string_instrument +guitar +electric_guitar +bass_guitar +acoustic_guitar +steel_guitar +tapping +strum +banjo +sitar +mandolin +zither +ukulele +keyboard +piano +electric_piano +organ +electronic_organ +hammond_organ +synthesizer +sampler +harpsichord +percussion +drum_kit +drum_machine +drum +snare_drum +rimshot +drum_roll +bass_drum +timpani +tabla +cymbal +hi-hat +wood_block +tambourine +rattle +maraca +gong +tubular_bells +mallet_percussion +marimba +glockenspiel +vibraphone +steelpan +orchestra +brass_instrument +french_horn +trumpet +trombone +bowed_string_instrument +string_section +violin +pizzicato +cello +double_bass +wind_instrument +flute +saxophone +clarinet +harp +bell +church_bell +jingle_bell +bicycle_bell +tuning_fork +chime +wind_chime +change_ringing +harmonica +accordion +bagpipes +didgeridoo +shofar +theremin +singing_bowl +scratching +pop_music +hip_hop_music +beatboxing +rock_music +heavy_metal +punk_rock +grunge +progressive_rock +rock_and_roll +psychedelic_rock +rhythm_and_blues +soul_music +reggae +country +swing_music +bluegrass +funk +folk_music +middle_eastern_music +jazz +disco +classical_music +opera +electronic_music +house_music +techno +dubstep +drum_and_bass +electronica +electronic_dance_music +ambient_music +trance_music +music_of_latin_america +salsa_music +flamenco +blues +music_for_children +new-age_music +vocal_music +a_capella +music_of_africa +afrobeat +christian_music +gospel_music +music_of_asia +carnatic_music +music_of_bollywood +ska +traditional_music +independent_music +song +background_music +theme_music +jingle +soundtrack_music +lullaby +video_game_music +christmas_music +dance_music +wedding_music +happy_music +sad_music +tender_music +exciting_music +angry_music +scary_music +wind +rustling_leaves +wind_noise +thunderstorm +thunder +water +rain +raindrop +rain_on_surface +stream +waterfall +ocean +waves +steam +gurgling +fire +crackle +vehicle +boat +sailboat +rowboat +motorboat +ship +motor_vehicle +car +honk +toot +car_alarm +power_windows +skidding +tire_squeal +car_passing_by +race_car +truck +air_brake +air_horn +reversing_beeps +ice_cream_truck +bus +emergency_vehicle +police_car +ambulance +fire_engine +motorcycle +traffic_noise +rail_transport +train +train_whistle +train_horn +railroad_car +train_wheels_squealing +subway +aircraft +aircraft_engine +jet_engine +propeller +helicopter +fixed-wing_aircraft +bicycle +skateboard +engine +light_engine +dental_drill's_drill +lawn_mower +chainsaw +medium_engine +heavy_engine +engine_knocking +engine_starting +idling +accelerating +door +doorbell +ding-dong +sliding_door +slam +knock +tap +squeak +cupboard_open_or_close +drawer_open_or_close +dishes +cutlery +chopping +frying +microwave_oven +blender +water_tap +sink +bathtub +hair_dryer +toilet_flush +toothbrush +electric_toothbrush +vacuum_cleaner +zipper +keys_jangling +coin +scissors +electric_shaver +shuffling_cards +typing +typewriter +computer_keyboard +writing +alarm +telephone +telephone_bell_ringing +ringtone +telephone_dialing +dial_tone +busy_signal +alarm_clock +siren +civil_defense_siren +buzzer +smoke_detector +fire_alarm +foghorn +whistle +steam_whistle +mechanisms +ratchet +clock +tick +tick-tock +gears +pulleys +sewing_machine +mechanical_fan +air_conditioning +cash_register +printer +camera +single-lens_reflex_camera +tools +hammer +jackhammer +sawing +filing +sanding +power_tool +drill +explosion +gunshot +machine_gun +fusillade +artillery_fire +cap_gun +fireworks +firecracker +burst +eruption +boom +wood +chop +splinter +crack +glass +chink +shatter +liquid +splash +slosh +squish +drip +pour +trickle +gush +fill +spray +pump +stir +boiling +sonar +arrow +whoosh +thump +thunk +electronic_tuner +effects_unit +chorus_effect +basketball_bounce +bang +slap +whack +smash +breaking +bouncing +whip +flap +scratch +scrape +rub +roll +crushing +crumpling +tearing +beep +ping +ding +clang +squeal +creak +rustle +whir +clatter +sizzle +clicking +clickety-clack +rumble +plop +jingle +hum +zing +boing +crunch +silence +sine_wave +harmonic +chirp_tone +sound_effect +pulse +inside +inside +inside +outside +outside +reverberation +echo +noise +environmental_noise +static +mains_hum +distortion +sidetone +cacophony +white_noise +pink_noise +throbbing +vibration +television +radio +field_recording diff --git a/sam2-cpu/frigate-dev/benchmark.py b/sam2-cpu/frigate-dev/benchmark.py new file mode 100755 index 0000000..46adc59 --- /dev/null +++ b/sam2-cpu/frigate-dev/benchmark.py @@ -0,0 +1,109 @@ +import datetime +import multiprocessing as mp +from statistics import mean + +import numpy as np + +from frigate.config import DetectorTypeEnum +from frigate.object_detection.base import ( + ObjectDetectProcess, + RemoteObjectDetector, + load_labels, +) +from frigate.util.process import FrigateProcess + +my_frame = np.expand_dims(np.full((300, 300, 3), 1, np.uint8), axis=0) +labels = load_labels("/labelmap.txt") + +###### +# Minimal same process runner +###### +# object_detector = LocalObjectDetector() +# tensor_input = np.expand_dims(np.full((300,300,3), 0, np.uint8), axis=0) + +# start = datetime.datetime.now().timestamp() + +# frame_times = [] +# for x in range(0, 1000): +# start_frame = datetime.datetime.now().timestamp() + +# tensor_input[:] = my_frame +# detections = object_detector.detect_raw(tensor_input) +# parsed_detections = [] +# for d in detections: +# if d[1] < 0.4: +# break +# parsed_detections.append(( +# labels[int(d[0])], +# float(d[1]), +# (d[2], d[3], d[4], d[5]) +# )) +# frame_times.append(datetime.datetime.now().timestamp()-start_frame) + +# duration = datetime.datetime.now().timestamp()-start +# print(f"Processed for {duration:.2f} seconds.") +# print(f"Average frame processing time: {mean(frame_times)*1000:.2f}ms") + + +def start(id, num_detections, detection_queue, event): + object_detector = RemoteObjectDetector( + str(id), "/labelmap.txt", detection_queue, event + ) + start = datetime.datetime.now().timestamp() + + frame_times = [] + for x in range(0, num_detections): + start_frame = datetime.datetime.now().timestamp() + object_detector.detect(my_frame) + frame_times.append(datetime.datetime.now().timestamp() - start_frame) + + duration = datetime.datetime.now().timestamp() - start + object_detector.cleanup() + print(f"{id} - Processed for {duration:.2f} seconds.") + print(f"{id} - FPS: {object_detector.fps.eps():.2f}") + print(f"{id} - Average frame processing time: {mean(frame_times) * 1000:.2f}ms") + + +###### +# Separate process runner +###### +# event = mp.Event() +# detection_queue = mp.Queue() +# edgetpu_process = EdgeTPUProcess(detection_queue, {'1': event}, 'usb:0') + +# start(1, 1000, edgetpu_process.detection_queue, event) +# print(f"Average raw inference speed: {edgetpu_process.avg_inference_speed.value*1000:.2f}ms") + +#### +# Multiple camera processes +#### +camera_processes = [] + +events = {} +for x in range(0, 10): + events[str(x)] = mp.Event() +detection_queue = mp.Queue() +edgetpu_process_1 = ObjectDetectProcess( + detection_queue, events, DetectorTypeEnum.edgetpu, "usb:0" +) +edgetpu_process_2 = ObjectDetectProcess( + detection_queue, events, DetectorTypeEnum.edgetpu, "usb:1" +) + +for x in range(0, 10): + camera_process = FrigateProcess( + target=start, args=(x, 300, detection_queue, events[str(x)]) + ) + camera_process.daemon = True + camera_processes.append(camera_process) + +start_time = datetime.datetime.now().timestamp() + +for p in camera_processes: + p.start() + +for p in camera_processes: + p.join() + +duration = datetime.datetime.now().timestamp() - start_time +print(f"Total - Processed for {duration:.2f} seconds.") diff --git a/sam2-cpu/frigate-dev/benchmark_motion.py b/sam2-cpu/frigate-dev/benchmark_motion.py new file mode 100644 index 0000000..431398f --- /dev/null +++ b/sam2-cpu/frigate-dev/benchmark_motion.py @@ -0,0 +1,118 @@ +import datetime +import multiprocessing as mp +import os + +import cv2 +import numpy as np + +from frigate.config import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector +from frigate.util import create_mask + +# get info on the video +# cap = cv2.VideoCapture("debug/front_cam_2023_05_23_08_41__2023_05_23_08_43.mp4") +# cap = cv2.VideoCapture("debug/motion_test_clips/rain_1.mp4") +cap = cv2.VideoCapture("debug/motion_test_clips/lawn_mower_night_1.mp4") +# cap = cv2.VideoCapture("airport.mp4") +width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) +height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) +fps = cap.get(cv2.CAP_PROP_FPS) +frame_shape = (height, width, 3) +# Nick back: +# "1280,0,1280,316,1170,216,1146,126,1016,127,979,82,839,0", +# "310,350,300,402,224,405,241,354", +# "378,0,375,26,0,23,0,0", +# Front door: +# "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", +# "336,833,438,1024,346,1093,103,1052,24,814", +# Back +# "1855,0,1851,100,1289,96,1105,161,1045,119,890,121,890,0", +# "505,95,506,138,388,153,384,114", +# "689,72,689,122,549,134,547,89", +# "261,134,264,176,169,195,167,158", +# "145,159,146,202,70,220,65,183", + +mask = create_mask( + (height, width), + [ + "1080,0,1080,339,1010,280,1020,169,777,163,452,170,318,299,191,365,186,417,139,470,108,516,40,530,0,514,0,0", + "336,833,438,1024,346,1093,103,1052,24,814", + ], +) + +# create the motion config +motion_config_1 = MotionConfig() +motion_config_1.mask = np.zeros((height, width), np.uint8) +motion_config_1.mask[:] = mask +# motion_config_1.improve_contrast = 1 +motion_config_1.frame_height = 150 +# motion_config_1.frame_alpha = 0.02 +# motion_config_1.threshold = 30 +# motion_config_1.contour_area = 10 + +motion_config_2 = MotionConfig() +motion_config_2.mask = np.zeros((height, width), np.uint8) +motion_config_2.mask[:] = mask +# motion_config_2.improve_contrast = 1 +motion_config_2.frame_height = 150 +# motion_config_2.frame_alpha = 0.01 +motion_config_2.threshold = 20 +# motion_config.contour_area = 10 + +save_images = True + +improved_motion_detector_1 = ImprovedMotionDetector( + frame_shape=frame_shape, + config=motion_config_1, + fps=fps, + improve_contrast=mp.Value("i", motion_config_1.improve_contrast), + threshold=mp.Value("i", motion_config_1.threshold), + contour_area=mp.Value("i", motion_config_1.contour_area), + name="default", +) +improved_motion_detector_1.save_images = save_images + +improved_motion_detector_2 = ImprovedMotionDetector( + frame_shape=frame_shape, + config=motion_config_2, + fps=fps, + improve_contrast=mp.Value("i", motion_config_2.improve_contrast), + threshold=mp.Value("i", motion_config_2.threshold), + contour_area=mp.Value("i", motion_config_2.contour_area), + name="compare", +) +improved_motion_detector_2.save_images = save_images + +# read and process frames +ret, frame = cap.read() +frame_counter = 1 +while ret: + yuv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2YUV_I420) + + start_frame = datetime.datetime.now().timestamp() + improved_motion_detector_1.detect(yuv_frame) + + start_frame = datetime.datetime.now().timestamp() + improved_motion_detector_2.detect(yuv_frame) + + default_frame = f"debug/frames/default-{frame_counter}.jpg" + compare_frame = f"debug/frames/compare-{frame_counter}.jpg" + if os.path.exists(default_frame) and os.path.exists(compare_frame): + images = [ + cv2.imread(default_frame), + cv2.imread(compare_frame), + ] + + cv2.imwrite( + f"debug/frames/all-{frame_counter}.jpg", + cv2.vconcat(images) + if frame_shape[0] > frame_shape[1] + else cv2.hconcat(images), + ) + os.unlink(default_frame) + os.unlink(compare_frame) + frame_counter += 1 + + ret, frame = cap.read() + +cap.release() diff --git a/sam2-cpu/frigate-dev/config/config.yml.example b/sam2-cpu/frigate-dev/config/config.yml.example new file mode 100644 index 0000000..87deab1 --- /dev/null +++ b/sam2-cpu/frigate-dev/config/config.yml.example @@ -0,0 +1,16 @@ +mqtt: + host: mqtt + +cameras: + test: + ffmpeg: + inputs: + - path: /media/frigate/car-stopping.mp4 + input_args: -re -stream_loop -1 -fflags +genpts + roles: + - detect + - rtmp + detect: + height: 1080 + width: 1920 + fps: 5 diff --git a/sam2-cpu/frigate-dev/cspell.json b/sam2-cpu/frigate-dev/cspell.json new file mode 100644 index 0000000..132e515 --- /dev/null +++ b/sam2-cpu/frigate-dev/cspell.json @@ -0,0 +1,22 @@ +{ + "version": "0.2", + "ignorePaths": [ + "Dockerfile", + "Dockerfile.*", + "CMakeLists.txt", + "*.db", + "node_modules", + "__pycache__", + "dist", + "/audio-labelmap.txt" + ], + "language": "en", + "dictionaryDefinitions": [ + { + "name": "frigate-dictionary", + "path": "./.cspell/frigate-dictionary.txt", + "addWords": true + } + ], + "dictionaries": ["frigate-dictionary"] +} diff --git a/sam2-cpu/frigate-dev/docker-compose.yml b/sam2-cpu/frigate-dev/docker-compose.yml new file mode 100644 index 0000000..db63297 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker-compose.yml @@ -0,0 +1,42 @@ +services: + devcontainer: + container_name: frigate-devcontainer + # Check host system's actual render/video/plugdev group IDs with 'getent group render', 'getent group video', and 'getent group plugdev' + # Must add these exact IDs in container's group_add section or OpenVINO GPU acceleration will fail + group_add: + - "109" # render + - "110" # render + - "44" # video + - "46" # plugdev + shm_size: "256mb" + build: + context: . + dockerfile: docker/main/Dockerfile + # Use target devcontainer-trt for TensorRT dev + target: devcontainer + ## Uncomment this block for nvidia gpu support + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + environment: + YOLO_MODELS: "" + # devices: + # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB + # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware + volumes: + - .:/workspace/frigate:cached + - ./web/dist:/opt/frigate/web:cached + - /etc/localtime:/etc/localtime:ro + - ./config:/config + - ./debug:/media/frigate + # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB + mqtt: + container_name: mqtt + image: eclipse-mosquitto:2.0 + command: mosquitto -c /mosquitto-no-auth.conf # enable no-auth mode + ports: + - "1883:1883" diff --git a/sam2-cpu/frigate-dev/docker/hailo8l/user_installation.sh b/sam2-cpu/frigate-dev/docker/hailo8l/user_installation.sh new file mode 100644 index 0000000..239f81b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/hailo8l/user_installation.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Update package list and install dependencies +sudo apt-get update +sudo apt-get install -y build-essential cmake git wget + +hailo_version="4.21.0" +arch=$(uname -m) + +if [[ $arch == "x86_64" ]]; then + sudo apt install -y linux-headers-$(uname -r); +else + sudo apt install -y linux-modules-extra-$(uname -r); +fi + +# Clone the HailoRT driver repository +git clone --depth 1 --branch v${hailo_version} https://github.com/hailo-ai/hailort-drivers.git + +# Build and install the HailoRT driver +cd hailort-drivers/linux/pcie +sudo make all +sudo make install + +# Load the Hailo PCI driver +sudo modprobe hailo_pci + +if [ $? -ne 0 ]; then + echo "Unable to load hailo_pci module, common reasons for this are:" + echo "- Key was rejected by service: Secure Boot is enabling disallowing install." + echo "- Permissions are not setup correctly." + exit 1 +fi + +# Download and install the firmware +cd ../../ +./download_firmware.sh + +# verify the firmware folder is present +if [ ! -d /lib/firmware/hailo ]; then + sudo mkdir /lib/firmware/hailo +fi +sudo mv hailo8_fw.*.bin /lib/firmware/hailo/hailo8_fw.bin + +# Install udev rules +sudo cp ./linux/pcie/51-hailo-udev.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger + +echo "HailoRT driver installation complete." +echo "reboot your system to load the firmware!" diff --git a/sam2-cpu/frigate-dev/docker/main/Dockerfile b/sam2-cpu/frigate-dev/docker/main/Dockerfile new file mode 100644 index 0000000..8dbf5ff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/Dockerfile @@ -0,0 +1,340 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +ARG BASE_IMAGE=debian:12 +ARG SLIM_BASE=debian:12-slim + +# A hook that allows us to inject commands right after the base images +ARG BASE_HOOK= + +FROM ${BASE_IMAGE} AS base +ARG PIP_BREAK_SYSTEM_PACKAGES +ARG BASE_HOOK + +RUN sh -c "$BASE_HOOK" + +FROM --platform=${BUILDPLATFORM} debian:12 AS base_host +ARG PIP_BREAK_SYSTEM_PACKAGES + +FROM ${SLIM_BASE} AS slim-base +ARG PIP_BREAK_SYSTEM_PACKAGES +ARG BASE_HOOK + +RUN sh -c "$BASE_HOOK" + +FROM slim-base AS wget +ARG DEBIAN_FRONTEND +RUN apt-get update \ + && apt-get install -y wget xz-utils \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /rootfs + +FROM base AS nginx +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G + +RUN --mount=type=bind,source=docker/main/build_nginx.sh,target=/deps/build_nginx.sh \ + /deps/build_nginx.sh + +FROM wget AS sqlite-vec +ARG DEBIAN_FRONTEND + +# Build sqlite_vec from source +COPY docker/main/build_sqlite_vec.sh /deps/build_sqlite_vec.sh +RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \ + --mount=type=bind,source=docker/main/build_sqlite_vec.sh,target=/deps/build_sqlite_vec.sh \ + --mount=type=cache,target=/root/.ccache \ + /deps/build_sqlite_vec.sh + +FROM scratch AS go2rtc +ARG TARGETARCH +WORKDIR /rootfs/usr/local/go2rtc/bin +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.10/go2rtc_linux_${TARGETARCH}" go2rtc + +FROM wget AS tempio +ARG TARGETARCH +RUN --mount=type=bind,source=docker/main/install_tempio.sh,target=/deps/install_tempio.sh \ + /deps/install_tempio.sh + +#### +# +# OpenVino Support +# +# 1. Download and convert a model from Intel's Public Open Model Zoo +# +#### +# Download and Convert OpenVino model +FROM base_host AS ov-converter +ARG DEBIAN_FRONTEND + +# Install OpenVino Runtime and Dev library +COPY docker/main/requirements-ov.txt /requirements-ov.txt +RUN apt-get -qq update \ + && apt-get -qq install -y wget python3 python3-dev python3-distutils gcc pkg-config libhdf5-dev \ + && wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" \ + && pip3 install -r /requirements-ov.txt + +# Get OpenVino Model +RUN --mount=type=bind,source=docker/main/build_ov_model.py,target=/build_ov_model.py \ + mkdir /models && cd /models \ + && wget http://download.tensorflow.org/models/object_detection/ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz \ + && tar -xvf ssdlite_mobilenet_v2_coco_2018_05_09.tar.gz \ + && python3 /build_ov_model.py + +#### +# +# Coral Compatibility +# +# Builds libusb without udev. Needed for synology and other devices with USB coral +#### +# libUSB - No Udev +FROM wget as libusb-build +ARG TARGETARCH +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G + +# Build libUSB without udev. Needed for Openvino NCS2 support +WORKDIR /opt +RUN apt-get update && apt-get install -y unzip build-essential automake libtool ccache pkg-config +RUN --mount=type=cache,target=/root/.ccache wget -q https://github.com/libusb/libusb/archive/v1.0.26.zip -O v1.0.26.zip && \ + unzip v1.0.26.zip && cd libusb-1.0.26 && \ + ./bootstrap.sh && \ + ./configure CC='ccache gcc' CCX='ccache g++' --disable-udev --enable-shared && \ + make -j $(nproc --all) +RUN apt-get update && \ + apt-get install -y --no-install-recommends libusb-1.0-0-dev && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /opt/libusb-1.0.26/libusb +RUN /bin/mkdir -p '/usr/local/lib' && \ + /bin/bash ../libtool --mode=install /usr/bin/install -c libusb-1.0.la '/usr/local/lib' && \ + /bin/mkdir -p '/usr/local/include/libusb-1.0' && \ + /usr/bin/install -c -m 644 libusb.h '/usr/local/include/libusb-1.0' && \ + /bin/mkdir -p '/usr/local/lib/pkgconfig' && \ + cd /opt/libusb-1.0.26/ && \ + /usr/bin/install -c -m 644 libusb-1.0.pc '/usr/local/lib/pkgconfig' && \ + ldconfig + +FROM wget AS models + +# Get model and labels +RUN wget -qO edgetpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite +RUN wget -qO cpu_model.tflite https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite +COPY labelmap.txt . +# Copy OpenVino model +COPY --from=ov-converter /models/ssdlite_mobilenet_v2.xml openvino-model/ +COPY --from=ov-converter /models/ssdlite_mobilenet_v2.bin openvino-model/ +RUN wget -q https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt -O openvino-model/coco_91cl_bkgr.txt && \ + sed -i 's/truck/car/g' openvino-model/coco_91cl_bkgr.txt +# Get Audio Model and labels +RUN wget -qO - https://www.kaggle.com/api/v1/models/google/yamnet/tfLite/classification-tflite/1/download | tar xvz && mv 1.tflite cpu_audio_model.tflite +COPY audio-labelmap.txt . + + +FROM wget AS s6-overlay +ARG TARGETARCH +RUN --mount=type=bind,source=docker/main/install_s6_overlay.sh,target=/deps/install_s6_overlay.sh \ + /deps/install_s6_overlay.sh + + +FROM base AS wheels +ARG DEBIAN_FRONTEND +ARG TARGETARCH +ARG DEBUG=false + +# Use a separate container to build wheels to prevent build dependencies in final image +RUN apt-get -qq update \ + && apt-get -qq install -y \ + apt-transport-https wget unzip \ + && apt-get -qq update \ + && apt-get -qq install -y \ + python3.11 \ + python3.11-dev \ + # opencv dependencies + build-essential cmake git pkg-config libgtk-3-dev \ + libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \ + libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \ + gfortran openexr libatlas-base-dev libssl-dev\ + libtbbmalloc2 libtbb-dev libdc1394-dev libopenexr-dev \ + libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \ + # sqlite3 dependencies + tclsh \ + # scipy dependencies + gcc gfortran libopenblas-dev liblapack-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +COPY docker/main/requirements.txt /requirements.txt +COPY docker/main/requirements-dev.txt /requirements-dev.txt + +RUN pip3 install -r /requirements.txt + +# Build pysqlite3 from source +COPY docker/main/build_pysqlite3.sh /build_pysqlite3.sh +RUN /build_pysqlite3.sh + +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt +RUN pip3 wheel --wheel-dir=/wheels -r /requirements-wheels.txt && \ + if [ "$DEBUG" = "true" ]; then \ + pip3 wheel --wheel-dir=/wheels -r /requirements-dev.txt; \ + fi + +# Install HailoRT & Wheels +RUN --mount=type=bind,source=docker/main/install_hailort.sh,target=/deps/install_hailort.sh \ + /deps/install_hailort.sh + +# Collect deps in a single layer +FROM scratch AS deps-rootfs +COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/ +COPY --from=sqlite-vec /usr/local/lib/ /usr/local/lib/ +COPY --from=go2rtc /rootfs/ / +COPY --from=libusb-build /usr/local/lib /usr/local/lib +COPY --from=tempio /rootfs/ / +COPY --from=s6-overlay /rootfs/ / +COPY --from=models /rootfs/ / +COPY --from=wheels /rootfs/ / +COPY docker/main/rootfs/ / + + +# Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc) +FROM slim-base AS deps +ARG TARGETARCH +ARG BASE_IMAGE + +ARG DEBIAN_FRONTEND +# http://stackoverflow.com/questions/48162574/ddg#49462622 +ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn + +# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" + +# Disable tokenizer parallelism warning +# https://stackoverflow.com/questions/62691279/how-to-disable-tokenizers-parallelism-true-false-warning/72926996#72926996 +ENV TOKENIZERS_PARALLELISM=true +# https://github.com/huggingface/transformers/issues/27214 +ENV TRANSFORMERS_NO_ADVISORY_WARNINGS=1 + +# Set OpenCV ffmpeg loglevel to fatal: https://ffmpeg.org/doxygen/trunk/log_8h.html +ENV OPENCV_FFMPEG_LOGLEVEL=8 + +# Set NumPy to ignore getlimits warning +ENV PYTHONWARNINGS="ignore:::numpy.core.getlimits" + +# Set HailoRT to disable logging +ENV HAILORT_LOGGER_PATH=NONE + +# TensorFlow error only +ENV TF_CPP_MIN_LOG_LEVEL=3 + +ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PATH}" + +# Install dependencies +RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \ + /deps/install_deps.sh + +ENV DEFAULT_FFMPEG_VERSION="7.0" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:5.0" + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \ + pip3 install -U /deps/wheels/*.whl + +# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image) +RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \ + bash -c "bash /deps/install_memryx.sh" + +COPY --from=deps-rootfs / / + +RUN ldconfig + +EXPOSE 5000 +EXPOSE 8554 +EXPOSE 8555/tcp 8555/udp + +# Configure logging to prepend timestamps, log to stdout, keep 0 archives and rotate on 10MB +ENV S6_LOGGING_SCRIPT="T 1 n0 s10000000 T" +# Do not fail on long-running download scripts +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + +ENTRYPOINT ["/init"] +CMD [] + +HEALTHCHECK --start-period=300s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ + CMD test -f /dev/shm/.frigate-is-stopping && exit 0; curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1 + +# Frigate deps with Node.js and NPM for devcontainer +FROM deps AS devcontainer + +# Do not start the actual Frigate service on devcontainer as it will be started by VS Code +# But start a fake service for simulating the logs +COPY docker/main/fake_frigate_run /etc/s6-overlay/s6-rc.d/frigate/run + +# Create symbolic link to the frigate source code, as go2rtc's create_config.sh uses it +RUN mkdir -p /opt/frigate \ + && ln -svf /workspace/frigate/frigate /opt/frigate/frigate + +# Install Node 20 +RUN curl -SLO https://deb.nodesource.com/nsolid_setup_deb.sh && \ + chmod 500 nsolid_setup_deb.sh && \ + ./nsolid_setup_deb.sh 20 && \ + apt-get install nodejs -y \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g npm@10 + +WORKDIR /workspace/frigate + +RUN apt-get update \ + && apt-get install make -y \ + && rm -rf /var/lib/apt/lists/* + +RUN --mount=type=bind,source=./docker/main/requirements-dev.txt,target=/workspace/frigate/requirements-dev.txt \ + pip3 install -r requirements-dev.txt + +HEALTHCHECK NONE + +CMD ["sleep", "infinity"] + + +# Frigate web build +# This should be architecture agnostic, so speed up the build on multiarch by not using QEMU. +FROM --platform=$BUILDPLATFORM node:20 AS web-build + +WORKDIR /work +COPY web/package.json web/package-lock.json ./ +RUN npm install + +COPY web/ ./ +RUN npm run build \ + && mv dist/BASE_PATH/monacoeditorwork/* dist/assets/ \ + && rm -rf dist/BASE_PATH + +# Collect final files in a single layer +FROM scratch AS rootfs + +WORKDIR /opt/frigate/ +COPY frigate frigate/ +COPY migrations migrations/ +COPY --from=web-build /work/dist/ web/ + +# Frigate final container +FROM deps AS frigate + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / diff --git a/sam2-cpu/frigate-dev/docker/main/build_nginx.sh b/sam2-cpu/frigate-dev/docker/main/build_nginx.sh new file mode 100755 index 0000000..6066826 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/build_nginx.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +set -euxo pipefail + +NGINX_VERSION="1.27.4" +VOD_MODULE_VERSION="1.31" +SECURE_TOKEN_MODULE_VERSION="1.5" +SET_MISC_MODULE_VERSION="v0.33" +NGX_DEVEL_KIT_VERSION="v0.3.3" + +source /etc/os-release + +if [[ "$VERSION_ID" == "12" ]]; then + sed -i '/^Types:/s/deb/& deb-src/' /etc/apt/sources.list.d/debian.sources +else + cp /etc/apt/sources.list /etc/apt/sources.list.d/sources-src.list + sed -i 's|deb http|deb-src http|g' /etc/apt/sources.list.d/sources-src.list +fi + +apt-get update +apt-get -yqq build-dep nginx + +apt-get -yqq install --no-install-recommends ca-certificates wget +update-ca-certificates -f +apt install -y ccache + +export PATH="/usr/lib/ccache:$PATH" + +mkdir /tmp/nginx +wget -nv https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz +tar -zxf nginx-${NGINX_VERSION}.tar.gz -C /tmp/nginx --strip-components=1 +rm nginx-${NGINX_VERSION}.tar.gz +mkdir /tmp/nginx-vod-module +wget -nv https://github.com/kaltura/nginx-vod-module/archive/refs/tags/${VOD_MODULE_VERSION}.tar.gz +tar -zxf ${VOD_MODULE_VERSION}.tar.gz -C /tmp/nginx-vod-module --strip-components=1 +rm ${VOD_MODULE_VERSION}.tar.gz + # Patch MAX_CLIPS to allow more clips to be added than the default 128 +sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h +patch -d /tmp/nginx-vod-module/ -p1 << 'EOF' +--- a/vod/avc_hevc_parser.c 2022-06-27 11:38:10.000000000 +0000 ++++ b/vod/avc_hevc_parser.c 2023-01-16 11:25:10.900521298 +0000 +@@ -3,6 +3,9 @@ + bool_t + avc_hevc_parser_rbsp_trailing_bits(bit_reader_state_t* reader) + { ++ // https://github.com/blakeblackshear/frigate/issues/4572 ++ return TRUE; ++ + uint32_t one_bit; + + if (reader->stream.eof_reached) +EOF + + +mkdir /tmp/nginx-secure-token-module +wget https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz +tar -zxf ${SECURE_TOKEN_MODULE_VERSION}.tar.gz -C /tmp/nginx-secure-token-module --strip-components=1 +rm ${SECURE_TOKEN_MODULE_VERSION}.tar.gz + +mkdir /tmp/ngx_devel_kit +wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/${NGX_DEVEL_KIT_VERSION}.tar.gz +tar -zxf ${NGX_DEVEL_KIT_VERSION}.tar.gz -C /tmp/ngx_devel_kit --strip-components=1 +rm ${NGX_DEVEL_KIT_VERSION}.tar.gz + +mkdir /tmp/nginx-set-misc-module +wget https://github.com/openresty/set-misc-nginx-module/archive/refs/tags/${SET_MISC_MODULE_VERSION}.tar.gz +tar -zxf ${SET_MISC_MODULE_VERSION}.tar.gz -C /tmp/nginx-set-misc-module --strip-components=1 +rm ${SET_MISC_MODULE_VERSION}.tar.gz + +cd /tmp/nginx + +./configure --prefix=/usr/local/nginx \ + --with-file-aio \ + --with-http_sub_module \ + --with-http_ssl_module \ + --with-http_auth_request_module \ + --with-http_realip_module \ + --with-threads \ + --add-module=../ngx_devel_kit \ + --add-module=../nginx-set-misc-module \ + --add-module=../nginx-vod-module \ + --add-module=../nginx-secure-token-module \ + --with-cc-opt="-O3 -Wno-error=implicit-fallthrough" + +make CC="ccache gcc" -j$(nproc) && make install +rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default diff --git a/sam2-cpu/frigate-dev/docker/main/build_ov_model.py b/sam2-cpu/frigate-dev/docker/main/build_ov_model.py new file mode 100644 index 0000000..2888d87 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/build_ov_model.py @@ -0,0 +1,11 @@ +import openvino as ov +from openvino.tools import mo + +ov_model = mo.convert_model( + "/models/ssdlite_mobilenet_v2_coco_2018_05_09/frozen_inference_graph.pb", + compress_to_fp16=True, + transformations_config="/usr/local/lib/python3.11/dist-packages/openvino/tools/mo/front/tf/ssd_v2_support.json", + tensorflow_object_detection_api_pipeline_config="/models/ssdlite_mobilenet_v2_coco_2018_05_09/pipeline.config", + reverse_input_channels=True, +) +ov.save_model(ov_model, "/models/ssdlite_mobilenet_v2.xml") diff --git a/sam2-cpu/frigate-dev/docker/main/build_pysqlite3.sh b/sam2-cpu/frigate-dev/docker/main/build_pysqlite3.sh new file mode 100755 index 0000000..14d0cde --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/build_pysqlite3.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euxo pipefail + +SQLITE3_VERSION="3.46.1" +PYSQLITE3_VERSION="0.5.3" + +# Install libsqlite3-dev if not present (needed for some base images like NVIDIA TensorRT) +if ! dpkg -l | grep -q libsqlite3-dev; then + echo "Installing libsqlite3-dev for compilation..." + apt-get update && apt-get install -y libsqlite3-dev && rm -rf /var/lib/apt/lists/* +fi + +# Fetch the pre-built sqlite amalgamation instead of building from source +if [[ ! -d "sqlite" ]]; then + mkdir sqlite + cd sqlite + + # Download the pre-built amalgamation from sqlite.org + # For SQLite 3.46.1, the amalgamation version is 3460100 + SQLITE_AMALGAMATION_VERSION="3460100" + + wget https://www.sqlite.org/2024/sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}.zip -O sqlite-amalgamation.zip + unzip sqlite-amalgamation.zip + mv sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION}/* . + rmdir sqlite-amalgamation-${SQLITE_AMALGAMATION_VERSION} + rm sqlite-amalgamation.zip + + cd ../ +fi + +# Grab the pysqlite3 source code. +if [[ ! -d "./pysqlite3" ]]; then + git clone https://github.com/coleifer/pysqlite3.git +fi + +cd pysqlite3/ +git checkout ${PYSQLITE3_VERSION} + +# Copy the sqlite3 source amalgamation into the pysqlite3 directory so we can +# create a self-contained extension module. +cp "../sqlite/sqlite3.c" ./ +cp "../sqlite/sqlite3.h" ./ + +# Create the wheel and put it in the /wheels dir. +sed -i "s|name='pysqlite3-binary'|name=PACKAGE_NAME|g" setup.py +python3 setup.py build_static +pip3 wheel . -w /wheels diff --git a/sam2-cpu/frigate-dev/docker/main/build_sqlite_vec.sh b/sam2-cpu/frigate-dev/docker/main/build_sqlite_vec.sh new file mode 100755 index 0000000..b41f338 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/build_sqlite_vec.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -euxo pipefail + +SQLITE_VEC_VERSION="0.1.3" + +source /etc/os-release + +if [[ "$VERSION_ID" == "12" ]]; then + sed -i '/^Types:/s/deb/& deb-src/' /etc/apt/sources.list.d/debian.sources +else + cp /etc/apt/sources.list /etc/apt/sources.list.d/sources-src.list + sed -i 's|deb http|deb-src http|g' /etc/apt/sources.list.d/sources-src.list +fi + +apt-get update +apt-get -yqq build-dep sqlite3 gettext git + +mkdir /tmp/sqlite_vec +# Grab the sqlite_vec source code. +wget -nv https://github.com/asg017/sqlite-vec/archive/refs/tags/v${SQLITE_VEC_VERSION}.tar.gz +tar -zxf v${SQLITE_VEC_VERSION}.tar.gz -C /tmp/sqlite_vec + +cd /tmp/sqlite_vec/sqlite-vec-${SQLITE_VEC_VERSION} + +mkdir -p vendor +wget -O sqlite-amalgamation.zip https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip +unzip sqlite-amalgamation.zip +mv sqlite-amalgamation-3450300/* vendor/ +rmdir sqlite-amalgamation-3450300 +rm sqlite-amalgamation.zip + +# build loadable module +make loadable + +# install it +cp dist/vec0.* /usr/local/lib + diff --git a/sam2-cpu/frigate-dev/docker/main/fake_frigate_run b/sam2-cpu/frigate-dev/docker/main/fake_frigate_run new file mode 100755 index 0000000..7344f62 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/fake_frigate_run @@ -0,0 +1,13 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the fake Frigate service + +set -o errexit -o nounset -o pipefail + +# Tell S6-Overlay not to restart this service +s6-svc -O . + +while true; do + echo "[INFO] The fake Frigate service is running..." + sleep 5s +done diff --git a/sam2-cpu/frigate-dev/docker/main/install_deps.sh b/sam2-cpu/frigate-dev/docker/main/install_deps.sh new file mode 100755 index 0000000..330caff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/install_deps.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +set -euxo pipefail + +apt-get -qq update + +apt-get -qq install --no-install-recommends -y \ + apt-transport-https \ + ca-certificates \ + gnupg \ + wget \ + lbzip2 \ + procps vainfo \ + unzip locales tzdata libxml2 xz-utils \ + python3.11 \ + curl \ + lsof \ + jq \ + nethogs \ + libgl1 \ + libglib2.0-0 \ + libusb-1.0.0 \ + python3-h2 \ + libgomp1 # memryx detector + +update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +mkdir -p -m 600 /root/.gnupg + +# install coral runtime +wget -q -O /tmp/libedgetpu1-max.deb "https://github.com/feranick/libedgetpu/releases/download/16.0TF2.17.1-1/libedgetpu1-max_16.0tf2.17.1-1.bookworm_${TARGETARCH}.deb" +unset DEBIAN_FRONTEND +yes | dpkg -i /tmp/libedgetpu1-max.deb && export DEBIAN_FRONTEND=noninteractive +rm /tmp/libedgetpu1-max.deb + +# install mesa-teflon-delegate from bookworm-backports +# Only available for arm64 at the moment +if [[ "${TARGETARCH}" == "arm64" ]]; then + if [[ "${BASE_IMAGE}" == *"nvcr.io/nvidia/tensorrt"* ]]; then + echo "Info: Skipping apt-get commands because BASE_IMAGE includes 'nvcr.io/nvidia/tensorrt' for arm64." + else + echo "deb http://deb.debian.org/debian bookworm-backports main" | tee /etc/apt/sources.list.d/bookworm-backbacks.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y mesa-teflon-delegate/bookworm-backports + fi +fi + +# ffmpeg -> amd64 +if [[ "${TARGETARCH}" == "amd64" ]]; then + mkdir -p /usr/lib/ffmpeg/5.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe + rm -rf ffmpeg.tar.xz +fi + +# ffmpeg -> arm64 +if [[ "${TARGETARCH}" == "arm64" ]]; then + mkdir -p /usr/lib/ffmpeg/5.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linuxarm64-gpl-5.1.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz + mkdir -p /usr/lib/ffmpeg/7.0 + wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz" + tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe + rm -f ffmpeg.tar.xz +fi + +# arch specific packages +if [[ "${TARGETARCH}" == "amd64" ]]; then + # Install non-free version of i965 driver + sed -i -E "/^Components: main$/s/main/main contrib non-free non-free-firmware/" "/etc/apt/sources.list.d/debian.sources" \ + && apt-get -qq update \ + && apt-get install --no-install-recommends --no-install-suggests -y i965-va-driver-shaders \ + && sed -i -E "/^Components: main contrib non-free non-free-firmware$/s/main contrib non-free non-free-firmware/main/" "/etc/apt/sources.list.d/debian.sources" \ + && apt-get update + + # install amd / intel-i965 driver packages + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + intel-gpu-tools onevpl-tools \ + libva-drm2 \ + mesa-va-drivers radeontop + + # intel packages use zst compression so we need to update dpkg + apt-get install -y dpkg + + # use intel apt intel packages + wget -qO - https://repositories.intel.com/gpu/intel-graphics.key | gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu jammy client" | tee /etc/apt/sources.list.d/intel-gpu-jammy.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + intel-media-va-driver-non-free libmfx1 libmfxgen1 libvpl2 + + apt-get -qq install -y ocl-icd-libopencl1 + + # install libtbb12 for NPU support + apt-get -qq install -y libtbb12 + + rm -f /usr/share/keyrings/intel-graphics.gpg + rm -f /etc/apt/sources.list.d/intel-gpu-jammy.list + + # install legacy and standard intel icd and level-zero-gpu + # see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info + # needed core package + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/libigdgmm12_22.5.5_amd64.deb + dpkg -i libigdgmm12_22.5.5_amd64.deb + rm libigdgmm12_22.5.5_amd64.deb + + # legacy packages + wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb + wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb + # standard packages + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-opencl-icd_24.52.32224.5_amd64.deb + wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-level-zero-gpu_1.6.32224.5_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-opencl-2_2.5.6+18417_amd64.deb + wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-core-2_2.5.6+18417_amd64.deb + # npu packages + wget https://github.com/oneapi-src/level-zero/releases/download/v1.21.9/level-zero_1.21.9+u22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-driver-compiler-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-fw-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-level-zero-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb + + dpkg -i *.deb + rm *.deb +fi + +if [[ "${TARGETARCH}" == "arm64" ]]; then + apt-get -qq install --no-install-recommends --no-install-suggests -y \ + libva-drm2 mesa-va-drivers radeontop +fi + +# install vulkan +apt-get -qq install --no-install-recommends --no-install-suggests -y \ + libvulkan1 mesa-vulkan-drivers + +apt-get purge gnupg apt-transport-https xz-utils -y +apt-get clean autoclean -y +apt-get autoremove --purge -y +rm -rf /var/lib/apt/lists/* + +# Install yq, for frigate-prepare and go2rtc echo source +curl -fsSL \ + "https://github.com/mikefarah/yq/releases/download/v4.48.2/yq_linux_$(dpkg --print-architecture)" \ + --output /usr/local/bin/yq +chmod +x /usr/local/bin/yq diff --git a/sam2-cpu/frigate-dev/docker/main/install_hailort.sh b/sam2-cpu/frigate-dev/docker/main/install_hailort.sh new file mode 100755 index 0000000..2e568a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/install_hailort.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euxo pipefail + +hailo_version="4.21.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + arch="x86_64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + arch="aarch64" +fi + +wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - +wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" diff --git a/sam2-cpu/frigate-dev/docker/main/install_memryx.sh b/sam2-cpu/frigate-dev/docker/main/install_memryx.sh new file mode 100644 index 0000000..676e06d --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/install_memryx.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e + +# Download the MxAccl for Frigate github release +wget https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip -O /tmp/mxaccl.zip +unzip /tmp/mxaccl.zip -d /tmp +mv /tmp/mx_accl_frigate-2.1.0 /opt/mx_accl_frigate +rm /tmp/mxaccl.zip + +# Install Python dependencies +pip3 install -r /opt/mx_accl_frigate/freeze + +# Link the Python package dynamically +SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])") +ln -s /opt/mx_accl_frigate/memryx "$SITE_PACKAGES/memryx" + +# Copy architecture-specific shared libraries +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + cp /opt/mx_accl_frigate/memryx/x86/libmemx.so* /usr/lib/x86_64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/x86/libmx_accl.so* /usr/lib/x86_64-linux-gnu/ +elif [[ "$ARCH" == "aarch64" ]]; then + cp /opt/mx_accl_frigate/memryx/arm/libmemx.so* /usr/lib/aarch64-linux-gnu/ + cp /opt/mx_accl_frigate/memryx/arm/libmx_accl.so* /usr/lib/aarch64-linux-gnu/ +else + echo "Unsupported architecture: $ARCH" + exit 1 +fi + +# Refresh linker cache +ldconfig diff --git a/sam2-cpu/frigate-dev/docker/main/install_s6_overlay.sh b/sam2-cpu/frigate-dev/docker/main/install_s6_overlay.sh new file mode 100755 index 0000000..3ea387c --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/install_s6_overlay.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euxo pipefail + +s6_version="3.2.1.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + s6_arch="x86_64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + s6_arch="aarch64" +fi + +mkdir -p /rootfs/ + +wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-noarch.tar.xz" | + tar -C /rootfs/ -Jxpf - + +wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-${s6_arch}.tar.xz" | + tar -C /rootfs/ -Jxpf - diff --git a/sam2-cpu/frigate-dev/docker/main/install_tempio.sh b/sam2-cpu/frigate-dev/docker/main/install_tempio.sh new file mode 100755 index 0000000..743a122 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/install_tempio.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euxo pipefail + +tempio_version="2021.09.0" + +if [[ "${TARGETARCH}" == "amd64" ]]; then + arch="amd64" +elif [[ "${TARGETARCH}" == "arm64" ]]; then + arch="aarch64" +fi + +mkdir -p /rootfs/usr/local/tempio/bin + +wget -q -O /rootfs/usr/local/tempio/bin/tempio "https://github.com/home-assistant/tempio/releases/download/${tempio_version}/tempio_${arch}" +chmod 755 /rootfs/usr/local/tempio/bin/tempio diff --git a/sam2-cpu/frigate-dev/docker/main/requirements-dev.txt b/sam2-cpu/frigate-dev/docker/main/requirements-dev.txt new file mode 100644 index 0000000..ac9d357 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/requirements-dev.txt @@ -0,0 +1,4 @@ +ruff + +# types +types-peewee == 3.17.* diff --git a/sam2-cpu/frigate-dev/docker/main/requirements-ov.txt b/sam2-cpu/frigate-dev/docker/main/requirements-ov.txt new file mode 100644 index 0000000..6fd1ca5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/requirements-ov.txt @@ -0,0 +1,3 @@ +numpy +tensorflow +openvino-dev>=2024.0.0 \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/requirements-wheels.txt b/sam2-cpu/frigate-dev/docker/main/requirements-wheels.txt new file mode 100644 index 0000000..1a1043b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/requirements-wheels.txt @@ -0,0 +1,85 @@ +aiofiles == 24.1.* +click == 8.1.* +# FastAPI +aiohttp == 3.12.* +starlette == 0.47.* +starlette-context == 0.4.* +fastapi[standard-no-fastapi-cloud-cli] == 0.116.* +uvicorn == 0.35.* +slowapi == 0.1.* +joserfc == 1.2.* +cryptography == 44.0.* +pathvalidate == 3.3.* +markupsafe == 3.0.* +python-multipart == 0.0.20 +# Classification Model Training +tensorflow == 2.19.* ; platform_machine == 'aarch64' +tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64' +# General +mypy == 1.6.1 +onvif-zeep-async == 4.0.* +paho-mqtt == 2.1.* +pandas == 2.2.* +peewee == 3.17.* +peewee_migrate == 1.14.* +psutil == 7.1.* +pydantic == 2.10.* +git+https://github.com/fbcotter/py3nvml#egg=py3nvml +pytz == 2025.* +pyzmq == 26.2.* +ruamel.yaml == 0.18.* +tzlocal == 5.2 +requests == 2.32.* +types-requests == 2.32.* +norfair == 2.3.* +setproctitle == 1.3.* +ws4py == 0.5.* +unidecode == 1.3.* +titlecase == 2.4.* +# Image Manipulation +numpy == 1.26.* +opencv-python-headless == 4.11.0.* +opencv-contrib-python == 4.11.0.* +scipy == 1.16.* +# OpenVino & ONNX +openvino == 2025.3.* +onnxruntime == 1.22.* +# Embeddings +transformers == 4.45.* +# Generative AI +google-generativeai == 0.8.* +ollama == 0.5.* +openai == 1.65.* +# push notifications +py-vapid == 1.9.* +pywebpush == 2.0.* +# alpr +pyclipper == 1.3.* +shapely == 2.0.* +rapidfuzz==3.12.* +# HailoRT Wheels +appdirs==1.4.* +argcomplete==2.0.* +contextlib2==0.6.* +distlib==0.3.* +filelock==3.8.* +future==0.18.* +importlib-metadata==5.1.* +importlib-resources==5.1.* +netaddr==0.8.* +netifaces==0.10.* +verboselogs==1.7.* +virtualenv==20.17.* +prometheus-client == 0.21.* +# TFLite +tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64' +tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64' +# audio transcription +sherpa-onnx==1.12.* +faster-whisper==1.1.* +librosa==0.11.* +soundfile==0.13.* +# DeGirum detector +degirum == 0.16.* +# Memory profiling +memray == 1.15.* diff --git a/sam2-cpu/frigate-dev/docker/main/requirements.txt b/sam2-cpu/frigate-dev/docker/main/requirements.txt new file mode 100644 index 0000000..f1ba7d9 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/requirements.txt @@ -0,0 +1 @@ +scikit-build == 0.18.* diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for new file mode 100644 index 0000000..09a147a --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/consumer-for @@ -0,0 +1 @@ +certsync diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/dependencies.d/log-prepare b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name new file mode 100644 index 0000000..204da27 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/pipeline-name @@ -0,0 +1 @@ +certsync-pipeline diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run new file mode 100755 index 0000000..7d66e2c --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/certsync diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync-log/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/dependencies.d/nginx b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/dependencies.d/nginx new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish new file mode 100755 index 0000000..3450034 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/finish @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service fails + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="CERTSYNC" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi + if [[ "${exit_code_signal}" -eq 15 ]]; then + exec /run/s6/basedir/bin/halt + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for new file mode 100644 index 0000000..886683f --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/producer-for @@ -0,0 +1 @@ +certsync-log diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run new file mode 100755 index 0000000..4ce1c13 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/run @@ -0,0 +1,58 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the CERTSYNC service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +echo "[INFO] Starting certsync..." + +lefile="/etc/letsencrypt/live/frigate/fullchain.pem" + +tls_enabled=`python3 /usr/local/nginx/get_listen_settings.py | jq -r .tls.enabled` + +while true +do + if [[ "$tls_enabled" == 'false' ]]; then + sleep 9999 + continue + fi + + if [ ! -e $lefile ] + then + echo "[ERROR] TLS certificate does not exist: $lefile" + fi + + leprint=`openssl x509 -in $lefile -fingerprint -noout 2>&1 || echo 'failed'` + + case "$leprint" in + *Fingerprint*) + ;; + *) + echo "[ERROR] Missing fingerprint from $lefile" + ;; + esac + + liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:8971 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'` + + case "$liveprint" in + *Fingerprint*) + ;; + *) + echo "[ERROR] Missing fingerprint from current nginx TLS cert" + ;; + esac + + if [[ "$leprint" != "failed" && "$liveprint" != "failed" && "$leprint" != "$liveprint" ]] + then + echo "[INFO] Reloading nginx to refresh TLS certificate" + echo "$lefile: $leprint" + /usr/local/nginx/sbin/nginx -s reload + fi + + sleep 60 + +done + +exit 0 \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/certsync/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for new file mode 100644 index 0000000..5e93017 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/consumer-for @@ -0,0 +1 @@ +frigate diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/dependencies.d/log-prepare b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name new file mode 100644 index 0000000..01f465e --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/pipeline-name @@ -0,0 +1 @@ +frigate-pipeline diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run new file mode 100755 index 0000000..c102848 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/frigate diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate-log/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/go2rtc b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/go2rtc new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish new file mode 100755 index 0000000..75869b5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/finish @@ -0,0 +1,28 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service exits + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="Frigate" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi +fi + +exec /run/s6/basedir/bin/halt diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for new file mode 100644 index 0000000..65f1316 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/producer-for @@ -0,0 +1 @@ +frigate-log diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run new file mode 100755 index 0000000..9c84c20 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/run @@ -0,0 +1,33 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the Frigate service + +set -o errexit -o nounset -o pipefail + +# opt out of openvino telemetry +if [ -e /usr/local/bin/opt_in_out ]; then + /usr/local/bin/opt_in_out --opt_out > /dev/null 2>&1 +fi + +# Logs should be sent to stdout so that s6 can collect them + +# Tell S6-Overlay not to restart this service +s6-svc -O . + +function set_libva_version() { + local ffmpeg_path + ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py) + LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+") + export LIBAVFORMAT_VERSION_MAJOR +} + +echo "[INFO] Preparing Frigate..." +set_libva_version + +echo "[INFO] Starting Frigate..." + +cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate" + +# Replace the bash process with the Frigate process, redirecting stderr to stdout +exec 2>&1 +exec python3 -u -m frigate diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill new file mode 100644 index 0000000..6f4f418 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/timeout-kill @@ -0,0 +1 @@ +120000 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/frigate/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/dependencies.d/go2rtc b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/dependencies.d/go2rtc new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish new file mode 100755 index 0000000..f834216 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/finish @@ -0,0 +1,12 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="go2rtc-healthcheck" + +echo "[INFO] The ${service} service exited with code ${exit_code_service} (by signal ${exit_code_signal})" diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for new file mode 100644 index 0000000..20fbc45 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/producer-for @@ -0,0 +1 @@ +go2rtc-log diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run new file mode 100755 index 0000000..3a6e423 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/run @@ -0,0 +1,22 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the go2rtc-healthcheck service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +# Give some additional time for go2rtc to start before start pinging +sleep 10s +echo "[INFO] Starting go2rtc healthcheck service..." + +while sleep 30s; do + # Check if the service is running + if ! curl --connect-timeout 10 --fail --silent --show-error --output /dev/null http://127.0.0.1:1984/api/streams 2>&1; then + echo "[ERROR] The go2rtc service is not responding to ping, restarting..." + # We can also use -r instead of -t to send kill signal rather than term + s6-svc -t /var/run/service/go2rtc 2>&1 + # Give some additional time to go2rtc to restart before start pinging again + sleep 10s + fi +done diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill new file mode 100644 index 0000000..e9c02da --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/timeout-kill @@ -0,0 +1 @@ +5000 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-healthcheck/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for new file mode 100644 index 0000000..bdd482e --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/consumer-for @@ -0,0 +1,2 @@ +go2rtc +go2rtc-healthcheck diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/dependencies.d/log-prepare b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name new file mode 100644 index 0000000..1fe5452 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/pipeline-name @@ -0,0 +1 @@ +go2rtc-pipeline diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run new file mode 100755 index 0000000..96a204b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/go2rtc diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc-log/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/dependencies.d/prepare b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/dependencies.d/prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish new file mode 100755 index 0000000..e95ba75 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/finish @@ -0,0 +1,12 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="go2rtc" + +echo "[INFO] The ${service} service exited with code ${exit_code_service} (by signal ${exit_code_signal})" diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for new file mode 100644 index 0000000..20fbc45 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/producer-for @@ -0,0 +1 @@ +go2rtc-log diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run new file mode 100755 index 0000000..8f5b1c2 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/run @@ -0,0 +1,124 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the go2rtc service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +function get_ip_and_port_from_supervisor() { + local ip_address + # Example: 192.168.1.10/24 + local ip_regex='^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/[0-9]{1,2}$' + if ip_address=$( + curl -fsSL \ + -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" \ + -H "Content-Type: application/json" \ + http://supervisor/network/interface/default/info | + jq --exit-status --raw-output '.data.ipv4.address[0]' + ) && [[ "${ip_address}" =~ ${ip_regex} ]]; then + ip_address="${BASH_REMATCH[1]}" + echo "[INFO] Got IP address from supervisor: ${ip_address}" + else + echo "[WARN] Failed to get IP address from supervisor" + return 0 + fi + + local webrtc_port + local port_regex='^([0-9]{1,5})$' + if webrtc_port=$( + curl -fsSL \ + -H "Authorization: Bearer ${SUPERVISOR_TOKEN}" \ + -H "Content-Type: application/json" \ + http://supervisor/addons/self/info | + jq --exit-status --raw-output '.data.network["8555/tcp"]' + ) && [[ "${webrtc_port}" =~ ${port_regex} ]]; then + webrtc_port="${BASH_REMATCH[1]}" + echo "[INFO] Got WebRTC port from supervisor: ${webrtc_port}" + else + echo "[WARN] Failed to get WebRTC port from supervisor" + return 0 + fi + + export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}" +} + +function set_libva_version() { + local ffmpeg_path + ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py) + LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+") + export LIBAVFORMAT_VERSION_MAJOR +} + +function setup_homekit_config() { + local config_path="$1" + + if [[ ! -f "${config_path}" ]]; then + echo "[INFO] Creating empty HomeKit config file..." + echo '{}' > "${config_path}" + fi + + # Convert YAML to JSON for jq processing + local temp_json="/tmp/cache/homekit_config.json" + yq eval -o=json "${config_path}" > "${temp_json}" 2>/dev/null || { + echo "[WARNING] Failed to convert HomeKit config to JSON, skipping cleanup" + return 0 + } + + # Use jq to filter and keep only the homekit section + local cleaned_json="/tmp/cache/homekit_cleaned.json" + jq ' + # Keep only the homekit section if it exists, otherwise empty object + if has("homekit") then {homekit: .homekit} else {homekit: {}} end + ' "${temp_json}" > "${cleaned_json}" 2>/dev/null || echo '{"homekit": {}}' > "${cleaned_json}" + + # Convert back to YAML and write to the config file + yq eval -P "${cleaned_json}" > "${config_path}" 2>/dev/null || { + echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config" + echo '{"homekit": {}}' > "${config_path}" + } + + # Clean up temp files + rm -f "${temp_json}" "${cleaned_json}" +} + +set_libva_version + +if [[ -f "/dev/shm/go2rtc.yaml" ]]; then + echo "[INFO] Removing stale config from last run..." + rm /dev/shm/go2rtc.yaml +fi + +if [[ ! -f "/dev/shm/go2rtc.yaml" ]]; then + echo "[INFO] Preparing new go2rtc config..." + + if [[ -n "${SUPERVISOR_TOKEN:-}" ]]; then + # Running as a Home Assistant Add-on, infer the IP address and port + get_ip_and_port_from_supervisor + fi + + python3 /usr/local/go2rtc/create_config.py +else + echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually." +fi + +# HomeKit configuration persistence setup +readonly homekit_config_path="/config/go2rtc_homekit.yml" +setup_homekit_config "${homekit_config_path}" + +readonly config_path="/config" + +if [[ -x "${config_path}/go2rtc" ]]; then + readonly binary_path="${config_path}/go2rtc" + echo "[WARN] Using go2rtc binary from '${binary_path}' instead of the embedded one" +else + readonly binary_path="/usr/local/go2rtc/bin/go2rtc" +fi + +echo "[INFO] Starting go2rtc..." + +# Replace the bash process with the go2rtc process, redirecting stderr to stdout +# Use HomeKit config as the primary config so writebacks go there +# The main config from Frigate will be loaded as a secondary config +exec 2>&1 +exec "${binary_path}" -config="${homekit_config_path}" -config=/dev/shm/go2rtc.yaml diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/go2rtc/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/dependencies.d/base b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run new file mode 100755 index 0000000..c493e32 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/run @@ -0,0 +1,11 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Prepare the logs folder for s6-log + +set -o errexit -o nounset -o pipefail + +dirs=(/dev/shm/logs/frigate /dev/shm/logs/go2rtc /dev/shm/logs/nginx /dev/shm/logs/certsync) + +mkdir -p "${dirs[@]}" +chown nobody:nogroup "${dirs[@]}" +chmod 02755 "${dirs[@]}" diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up new file mode 100644 index 0000000..f90be02 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/log-prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/log-prepare/run diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for new file mode 100644 index 0000000..68b7d12 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/consumer-for @@ -0,0 +1 @@ +nginx diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/dependencies.d/log-prepare b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/dependencies.d/log-prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name new file mode 100644 index 0000000..e22259a --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/pipeline-name @@ -0,0 +1 @@ +nginx-pipeline diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run new file mode 100755 index 0000000..50057d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/run @@ -0,0 +1,4 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +exec logutil-service /dev/shm/logs/nginx diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx-log/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check new file mode 100755 index 0000000..8307a79 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/data/check @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e + +# Wait for PID file to exist. +while ! test -f /run/nginx.pid; do sleep 1; done \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/frigate b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/frigate new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish new file mode 100755 index 0000000..d147d74 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/finish @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Take down the S6 supervision tree when the service fails + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +declare exit_code_container +exit_code_container=$(cat /run/s6-linux-init-container-results/exitcode) +readonly exit_code_container +readonly exit_code_service="${1}" +readonly exit_code_signal="${2}" +readonly service="NGINX" + +echo "[INFO] Service ${service} exited with code ${exit_code_service} (by signal ${exit_code_signal})" + +if [[ "${exit_code_service}" -eq 256 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo $((128 + exit_code_signal)) >/run/s6-linux-init-container-results/exitcode + fi + if [[ "${exit_code_signal}" -eq 15 ]]; then + exec /run/s6/basedir/bin/halt + fi +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" >/run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/notification-fd @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for new file mode 100644 index 0000000..307d740 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/producer-for @@ -0,0 +1 @@ +nginx-log diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run new file mode 100755 index 0000000..8bd9b52 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -0,0 +1,96 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Start the NGINX service + +set -o errexit -o nounset -o pipefail + +# Logs should be sent to stdout so that s6 can collect them + +echo "[INFO] Starting NGINX..." + +# Taken from https://github.com/felipecrs/cgroup-scripts/commits/master/get_cpus.sh +function get_cpus() { + local quota="" + local period="" + + if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + if [ -f /sys/fs/cgroup/cpu.max ]; then + read -r quota period &2 + fi + else + if [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ] && [ -f /sys/fs/cgroup/cpu/cpu.cfs_period_us ]; then + quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us) + period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us) + + if [ "$quota" = "-1" ]; then + quota="" + period="" + fi + else + echo "[WARN] /sys/fs/cgroup/cpu/cpu.cfs_quota_us or /sys/fs/cgroup/cpu/cpu.cfs_period_us not found. Falling back to /proc/cpuinfo." >&2 + fi + fi + + local cpus + if [ "${period}" != "0" ] && [ -n "${quota}" ] && [ -n "${period}" ]; then + cpus=$((quota / period)) + if [ "$cpus" -eq 0 ]; then + cpus=1 + fi + else + cpus=$(grep -c ^processor /proc/cpuinfo) + fi + + printf '%s' "$cpus" +} + +function set_worker_processes() { + # Capture number of assigned CPUs to calculate worker processes + local cpus + + cpus=$(get_cpus) + if [[ "${cpus}" -gt 4 ]]; then + cpus=4 + fi + + # we need to catch any errors because sed will fail if user has bind mounted a custom nginx file + sed -i "s/worker_processes auto;/worker_processes ${cpus};/" /usr/local/nginx/conf/nginx.conf || true +} + +set_worker_processes + +# ensure the directory for ACME challenges exists +mkdir -p /etc/letsencrypt/www + +# Create self signed certs if needed +letsencrypt_path=/etc/letsencrypt/live/frigate +mkdir -p $letsencrypt_path + +if [ ! \( -f "$letsencrypt_path/privkey.pem" -a -f "$letsencrypt_path/fullchain.pem" \) ]; then + echo "[INFO] No TLS certificate found. Generating a self signed certificate..." + openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ + -subj "/O=FRIGATE DEFAULT CERT/CN=*" \ + -keyout "$letsencrypt_path/privkey.pem" -out "$letsencrypt_path/fullchain.pem" 2>/dev/null +fi + +# build templates for optional FRIGATE_BASE_PATH environment variable +python3 /usr/local/nginx/get_base_path.py | \ + tempio -template /usr/local/nginx/templates/base_path.gotmpl \ + -out /usr/local/nginx/conf/base_path.conf + +# build templates for optional TLS support +python3 /usr/local/nginx/get_listen_settings.py | \ + tempio -template /usr/local/nginx/templates/listen.gotmpl \ + -out /usr/local/nginx/conf/listen.conf + +# Replace the bash process with the NGINX process, redirecting stderr to stdout +exec 2>&1 +exec \ + s6-notifyoncheck -t 30000 -n 1 \ + nginx diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill new file mode 100644 index 0000000..3a05c8b --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/timeout-kill @@ -0,0 +1 @@ +30000 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/type @@ -0,0 +1 @@ +longrun diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run new file mode 100755 index 0000000..27b1d63 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/run @@ -0,0 +1,146 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Do preparation tasks before starting the main services + +set -o errexit -o nounset -o pipefail + +function migrate_addon_config_dir() { + local home_assistant_config_dir="/homeassistant" + + if ! mountpoint --quiet "${home_assistant_config_dir}"; then + # Not running as a Home Assistant Add-on + return 0 + fi + + local config_dir="/config" + local new_config_file="${config_dir}/config.yml" + local new_config_file_yaml="${new_config_file//.yml/.yaml}" + if [[ -f "${new_config_file_yaml}" || -f "${new_config_file}" ]]; then + # Already migrated + return 0 + fi + + local old_config_file="${home_assistant_config_dir}/frigate.yml" + local old_config_file_yaml="${old_config_file//.yml/.yaml}" + if [[ -f "${old_config_file}" ]]; then + : + elif [[ -f "${old_config_file_yaml}" ]]; then + old_config_file="${old_config_file_yaml}" + new_config_file="${new_config_file_yaml}" + else + # Nothing to migrate + return 0 + fi + unset old_config_file_yaml new_config_file_yaml + + echo "[INFO] Starting migration from Home Assistant config dir to Add-on config dir..." >&2 + + local db_path + db_path=$(yq -r '.database.path' "${old_config_file}") + if [[ "${db_path}" == "null" ]]; then + db_path="${config_dir}/frigate.db" + fi + if [[ "${db_path}" == "${config_dir}/"* ]]; then + # replace /config/ prefix with /homeassistant/ + local old_db_path="${home_assistant_config_dir}/${db_path:8}" + + if [[ -f "${old_db_path}" ]]; then + local new_db_dir + new_db_dir="$(dirname "${db_path}")" + echo "[INFO] Migrating database from '${old_db_path}' to '${new_db_dir}' dir..." >&2 + mkdir -vp "${new_db_dir}" + mv -vf "${old_db_path}" "${new_db_dir}" + local db_file + for db_file in "${old_db_path}"-shm "${old_db_path}"-wal; do + if [[ -f "${db_file}" ]]; then + mv -vf "${db_file}" "${new_db_dir}" + fi + done + unset db_file + fi + fi + + local config_entry + for config_entry in .model.path .model.labelmap_path .ffmpeg.path .mqtt.tls_ca_certs .mqtt.tls_client_cert .mqtt.tls_client_key; do + local config_entry_path + config_entry_path=$(yq -r "${config_entry}" "${old_config_file}") + if [[ "${config_entry_path}" == "${config_dir}/"* ]]; then + # replace /config/ prefix with /homeassistant/ + local old_config_entry_path="${home_assistant_config_dir}/${config_entry_path:8}" + + if [[ -f "${old_config_entry_path}" ]]; then + local new_config_entry_entry + new_config_entry_entry="$(dirname "${config_entry_path}")" + echo "[INFO] Migrating ${config_entry} from '${old_config_entry_path}' to '${config_entry_path}'..." >&2 + mkdir -vp "${new_config_entry_entry}" + mv -vf "${old_config_entry_path}" "${config_entry_path}" + fi + fi + done + + local old_model_cache_path="${home_assistant_config_dir}/model_cache" + if [[ -d "${old_model_cache_path}" ]]; then + echo "[INFO] Migrating '${old_model_cache_path}' to '${config_dir}'..." >&2 + mv -f "${old_model_cache_path}" "${config_dir}" + fi + + echo "[INFO] Migrating other files from '${home_assistant_config_dir}' to '${config_dir}'..." >&2 + local file + for file in .exports .jwt_secret .timeline .vacuum go2rtc; do + file="${home_assistant_config_dir}/${file}" + if [[ -f "${file}" ]]; then + mv -vf "${file}" "${config_dir}" + fi + done + + echo "[INFO] Migrating config file from '${old_config_file}' to '${new_config_file}'..." >&2 + mv -vf "${old_config_file}" "${new_config_file}" + + echo "[INFO] Migration from Home Assistant config dir to Add-on config dir completed." >&2 +} + +function migrate_db_from_media_to_config() { + # Find config file in yml or yaml, but prefer yml + local config_file="${CONFIG_FILE:-"/config/config.yml"}" + local config_file_yaml="${config_file//.yml/.yaml}" + if [[ -f "${config_file}" ]]; then + : + elif [[ -f "${config_file_yaml}" ]]; then + config_file="${config_file_yaml}" + else + # Frigate will create the config file on startup + return 0 + fi + unset config_file_yaml + + local user_db_path + user_db_path=$(yq -r '.database.path' "${config_file}") + if [[ "${user_db_path}" == "null" ]]; then + local old_db_path="/media/frigate/frigate.db" + local new_db_dir="/config" + if [[ -f "${old_db_path}" ]]; then + echo "[INFO] Migrating database from '${old_db_path}' to '${new_db_dir}' dir..." >&2 + if mountpoint --quiet "${new_db_dir}"; then + # /config is a mount point, move the db + mv -vf "${old_db_path}" "${new_db_dir}" + local db_file + for db_file in "${old_db_path}"-shm "${old_db_path}"-wal; do + if [[ -f "${db_file}" ]]; then + mv -vf "${db_file}" "${new_db_dir}" + fi + done + unset db_file + else + echo "[ERROR] Trying to migrate the database path from '${old_db_path}' to '${new_db_dir}' dir, but '${new_db_dir}' is not a mountpoint, please mount the '${new_db_dir}' dir" >&2 + return 1 + fi + fi + fi +} + +# remove leftover from last run, not normally needed, but just in case +# used by the docker healthcheck +rm -f /dev/shm/.frigate-is-stopping + +migrate_addon_config_dir +migrate_db_from_media_to_config diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up new file mode 100644 index 0000000..ea17af5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/prepare/run diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certsync-pipeline b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certsync-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frigate-pipeline b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frigate-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/go2rtc-pipeline b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/go2rtc-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx-pipeline b/sam2-cpu/frigate-dev/docker/main/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx-pipeline new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco-80.txt b/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco-80.txt new file mode 100644 index 0000000..79e0171 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco-80.txt @@ -0,0 +1,80 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 car +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 stop sign +12 parking meter +13 bench +14 bird +15 cat +16 dog +17 horse +18 sheep +19 cow +20 elephant +21 bear +22 zebra +23 giraffe +24 backpack +25 umbrella +26 handbag +27 tie +28 suitcase +29 frisbee +30 skis +31 snowboard +32 sports ball +33 kite +34 baseball bat +35 baseball glove +36 skateboard +37 surfboard +38 tennis racket +39 bottle +40 wine glass +41 cup +42 fork +43 knife +44 spoon +45 bowl +46 banana +47 apple +48 sandwich +49 orange +50 broccoli +51 carrot +52 hot dog +53 pizza +54 donut +55 cake +56 chair +57 couch +58 potted plant +59 bed +60 dining table +61 toilet +62 tv +63 laptop +64 mouse +65 remote +66 keyboard +67 cell phone +68 microwave +69 oven +70 toaster +71 sink +72 refrigerator +73 book +74 clock +75 vase +76 scissors +77 teddy bear +78 hair drier +79 toothbrush \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco.txt b/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco.txt new file mode 100644 index 0000000..79fff17 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/labelmap/coco.txt @@ -0,0 +1,91 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 bus +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 street sign +12 stop sign +13 parking meter +14 bench +15 bird +16 cat +17 dog +18 horse +19 sheep +20 cow +21 elephant +22 bear +23 zebra +24 giraffe +25 hat +26 backpack +27 umbrella +28 shoe +29 eye glasses +30 handbag +31 tie +32 suitcase +33 frisbee +34 skis +35 snowboard +36 sports ball +37 kite +38 baseball bat +39 baseball glove +40 skateboard +41 surfboard +42 tennis racket +43 bottle +44 plate +45 wine glass +46 cup +47 fork +48 knife +49 spoon +50 bowl +51 banana +52 apple +53 sandwich +54 orange +55 broccoli +56 carrot +57 hot dog +58 pizza +59 donut +60 cake +61 chair +62 couch +63 potted plant +64 bed +65 mirror +66 dining table +67 window +68 desk +69 toilet +70 door +71 tv +72 laptop +73 mouse +74 remote +75 keyboard +76 cell phone +77 microwave +78 oven +79 toaster +80 sink +81 refrigerator +82 blender +83 book +84 clock +85 vase +86 scissors +87 teddy bear +88 hair drier +89 toothbrush +90 hair brush \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py new file mode 100644 index 0000000..0f492cc --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py @@ -0,0 +1,37 @@ +import json +import sys +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.const import ( + DEFAULT_FFMPEG_VERSION, + INCLUDED_FFMPEG_VERSIONS, +) +from frigate.util.config import find_config_file + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +path = config.get("ffmpeg", {}).get("path", "default") +if path == "default": + print(f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg") +elif path in INCLUDED_FFMPEG_VERSIONS: + print(f"/usr/lib/ffmpeg/{path}/bin/ffmpeg") +else: + print(f"{path}/bin/ffmpeg") diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/go2rtc/create_config.py b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/go2rtc/create_config.py new file mode 100644 index 0000000..1b44a80 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -0,0 +1,150 @@ +"""Creates a go2rtc config file.""" + +import json +import os +import sys +from pathlib import Path +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.const import ( + BIRDSEYE_PIPE, + DEFAULT_FFMPEG_VERSION, + INCLUDED_FFMPEG_VERSIONS, + LIBAVFORMAT_VERSION_MAJOR, +) +from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode +from frigate.util.config import find_config_file + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} +# read docker secret files as env vars too +if os.path.isdir("/run/secrets"): + for secret_file in os.listdir("/run/secrets"): + if secret_file.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[secret_file] = ( + Path(os.path.join("/run/secrets", secret_file)).read_text().strip() + ) + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +go2rtc_config: dict[str, Any] = config.get("go2rtc", {}) + +# Need to enable CORS for go2rtc so the frigate integration / card work automatically +if go2rtc_config.get("api") is None: + go2rtc_config["api"] = {"origin": "*"} +elif go2rtc_config["api"].get("origin") is None: + go2rtc_config["api"]["origin"] = "*" + +# Need to set default location for HA config +if go2rtc_config.get("hass") is None: + go2rtc_config["hass"] = {"config": "/homeassistant"} + +# we want to ensure that logs are easy to read +if go2rtc_config.get("log") is None: + go2rtc_config["log"] = {"format": "text"} +elif go2rtc_config["log"].get("format") is None: + go2rtc_config["log"]["format"] = "text" + +# ensure there is a default webrtc config +if go2rtc_config.get("webrtc") is None: + go2rtc_config["webrtc"] = {} + +if go2rtc_config["webrtc"].get("candidates") is None: + default_candidates = [] + # use internal candidate if it was discovered when running through the add-on + internal_candidate = os.environ.get("FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL") + if internal_candidate is not None: + default_candidates.append(internal_candidate) + # should set default stun server so webrtc can work + default_candidates.append("stun:8555") + + go2rtc_config["webrtc"]["candidates"] = default_candidates + +if go2rtc_config.get("rtsp", {}).get("username") is not None: + go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format( + **FRIGATE_ENV_VARS + ) + +if go2rtc_config.get("rtsp", {}).get("password") is not None: + go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format( + **FRIGATE_ENV_VARS + ) + +# ensure ffmpeg path is set correctly +path = config.get("ffmpeg", {}).get("path", "default") +if path == "default": + ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" +elif path in INCLUDED_FFMPEG_VERSIONS: + ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg" +else: + ffmpeg_path = f"{path}/bin/ffmpeg" + +if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path} +elif go2rtc_config["ffmpeg"].get("bin") is None: + go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path + +# need to replace ffmpeg command when using ffmpeg4 +if LIBAVFORMAT_VERSION_MAJOR < 59: + rtsp_args = "-fflags nobuffer -flags low_delay -stimeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}" + if go2rtc_config.get("ffmpeg") is None: + go2rtc_config["ffmpeg"] = {"rtsp": rtsp_args} + elif go2rtc_config["ffmpeg"].get("rtsp") is None: + go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args + +for name in go2rtc_config.get("streams", {}): + stream = go2rtc_config["streams"][name] + + if isinstance(stream, str): + try: + go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format( + **FRIGATE_ENV_VARS + ) + except KeyError as e: + print( + "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." + ) + sys.exit(e) + + elif isinstance(stream, list): + for i, stream in enumerate(stream): + try: + go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS) + except KeyError as e: + print( + "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." + ) + sys.exit(e) + +# add birdseye restream stream if enabled +if config.get("birdseye", {}).get("restream", False): + birdseye: dict[str, Any] = config.get("birdseye") + + input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}" + ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}" + + if go2rtc_config.get("streams"): + go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd + else: + go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd} + +# Write go2rtc_config to /dev/shm/go2rtc.yaml +with open("/dev/shm/go2rtc.yaml", "w") as f: + yaml.dump(go2rtc_config, f) diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf new file mode 100644 index 0000000..285a3d8 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf @@ -0,0 +1,43 @@ +set $upstream_auth http://127.0.0.1:5001/auth; + +## Virtual endpoint created by nginx to forward auth requests. +location /auth { + ## Essential Proxy Configuration + internal; + proxy_pass $upstream_auth; + + ## Headers + + # First strip out all the request headers + # Note: This is important to ensure that upgrade requests for secure + # websockets dont cause the backend to fail + proxy_pass_request_headers off; + # Pass info about the request + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Server-Port $server_port; + proxy_set_header Content-Length ""; + # Pass along auth related info + proxy_set_header Authorization $http_authorization; + proxy_set_header Cookie $http_cookie; + proxy_set_header X-CSRF-TOKEN "1"; + + # include headers from common auth proxies + include proxy_trusted_headers.conf; + + ## Basic Proxy Configuration + proxy_pass_request_body off; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead + proxy_redirect http:// $scheme://; + proxy_http_version 1.1; + proxy_cache_bypass $cookie_session; + proxy_no_cache $cookie_session; + proxy_buffers 4 32k; + client_body_buffer_size 128k; + + ## Advanced Proxy Configuration + send_timeout 5m; + proxy_read_timeout 240; + proxy_send_timeout 240; + proxy_connect_timeout 240; +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf new file mode 100644 index 0000000..9e745b6 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/auth_request.conf @@ -0,0 +1,24 @@ +## Send a subrequest to verify if the user is authenticated and has permission to access the resource. +auth_request /auth; + +## Save the upstream metadata response headers from the auth request to variables +auth_request_set $user $upstream_http_remote_user; +auth_request_set $role $upstream_http_remote_role; +auth_request_set $groups $upstream_http_remote_groups; +auth_request_set $name $upstream_http_remote_name; +auth_request_set $email $upstream_http_remote_email; + +## Inject the metadata response headers from the variables into the request made to the backend. +proxy_set_header Remote-User $user; +proxy_set_header Remote-Role $role; +proxy_set_header Remote-Groups $groups; +proxy_set_header Remote-Email $email; +proxy_set_header Remote-Name $name; + +## Refresh the cookie as needed +auth_request_set $auth_cookie $upstream_http_set_cookie; +add_header Set-Cookie $auth_cookie; + +## Pass the location header back up if it exists +auth_request_set $redirection_url $upstream_http_location; +add_header Location $redirection_url; diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf new file mode 100644 index 0000000..811bb94 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/go2rtc_upstream.conf @@ -0,0 +1,4 @@ +upstream go2rtc { + server 127.0.0.1:1984; + keepalive 1024; +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/nginx.conf new file mode 100644 index 0000000..46241c5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -0,0 +1,362 @@ +daemon off; +user root; +worker_processes auto; + +error_log /dev/stdout warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + map_hash_bucket_size 256; + + include mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'request_time="$request_time" upstream_response_time="$upstream_response_time"'; + + + access_log /dev/stdout main; + + # send headers in one piece, it is better than sending them one by one + tcp_nopush on; + + sendfile on; + + keepalive_timeout 65; + + gzip on; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript image/svg+xml image/x-icon image/bmp; + gzip_proxied no-cache no-store private expired auth; + gzip_vary on; + + proxy_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=api_cache:10m max_size=10m inactive=1m use_temp_path=off; + + map $sent_http_content_type $should_not_cache { + 'application/json' 0; + default 1; + } + + upstream frigate_api { + server 127.0.0.1:5001; + keepalive 1024; + } + + upstream mqtt_ws { + server 127.0.0.1:5002; + keepalive 1024; + } + + upstream jsmpeg { + server 127.0.0.1:8082; + keepalive 1024; + } + + include go2rtc_upstream.conf; + + server { + include listen.conf; + + # vod settings + vod_base_url ''; + vod_segments_base_url ''; + vod_mode mapped; + vod_max_mapping_response_size 1m; + vod_upstream_location /api; + vod_align_segments_to_key_frames on; + vod_manifest_segment_durations_mode accurate; + vod_ignore_edit_list on; + vod_segment_duration 10000; + + # MPEG-TS settings (not used when fMP4 is enabled, kept for reference) + vod_hls_mpegts_align_frames off; + vod_hls_mpegts_interleave_frames on; + + # file handle caching / aio + open_file_cache max=1000 inactive=5m; + open_file_cache_valid 2m; + open_file_cache_min_uses 1; + open_file_cache_errors on; + aio on; + + # file upload size + client_max_body_size 20M; + + # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool + vod_open_file_thread_pool default; + + # vod caches + vod_metadata_cache metadata_cache 512m; + vod_mapping_cache mapping_cache 5m 10m; + + # gzip manifests + gzip on; + gzip_types application/vnd.apple.mpegurl; + + include auth_location.conf; + include base_path.conf; + + location /vod/ { + include auth_request.conf; + aio threads; + vod hls; + + # Use fMP4 (fragmented MP4) instead of MPEG-TS for better performance + # Smaller segments, faster generation, better browser compatibility + vod_hls_container_format fmp4; + + secure_token $args; + secure_token_types application/vnd.apple.mpegurl; + + add_header Cache-Control "no-store"; + expires off; + + keepalive_disable safari; + + # vod module returns 502 for non-existent media + # https://github.com/kaltura/nginx-vod-module/issues/468 + error_page 502 =404 /vod-not-found; + } + + location = /vod-not-found { + return 404; + } + + location /stream/ { + include auth_request.conf; + add_header Cache-Control "no-store"; + expires off; + + types { + application/dash+xml mpd; + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + image/jpeg jpg; + } + + root /tmp; + } + + location /clips/ { + include auth_request.conf; + types { + video/mp4 mp4; + image/jpeg jpg; + } + + expires 7d; + add_header Cache-Control "public"; + autoindex on; + root /media/frigate; + } + + location /cache/ { + internal; # This tells nginx it's not accessible from the outside + alias /tmp/cache/; + } + + location /recordings/ { + include auth_request.conf; + types { + video/mp4 mp4; + } + + autoindex on; + autoindex_format json; + root /media/frigate; + } + + location /exports/ { + include auth_request.conf; + types { + video/mp4 mp4; + } + + autoindex on; + autoindex_format json; + root /media/frigate; + } + + location /ws { + include auth_request.conf; + proxy_pass http://mqtt_ws/; + include proxy.conf; + } + + location /live/jsmpeg/ { + include auth_request.conf; + proxy_pass http://jsmpeg/; + include proxy.conf; + } + + # frigate lovelace card uses this path + location /live/mse/api/ws { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/api/ws; + include proxy.conf; + } + + location /live/webrtc/api/ws { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/api/ws; + include proxy.conf; + } + + # pass through go2rtc player + location /live/webrtc/webrtc.html { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/webrtc.html; + include proxy.conf; + } + + # frontend uses this to fetch the version + location /api/go2rtc/api { + include auth_request.conf; + limit_except GET { + deny all; + } + proxy_pass http://go2rtc/api; + include proxy.conf; + } + + # integration uses this to add webrtc candidate + location /api/go2rtc/webrtc { + include auth_request.conf; + limit_except POST { + deny all; + } + proxy_pass http://go2rtc/api/webrtc; + include proxy.conf; + } + + location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ { + include auth_request.conf; + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/ { + include auth_request.conf; + add_header Cache-Control "no-store"; + expires off; + proxy_pass http://frigate_api/; + include proxy.conf; + + proxy_cache api_cache; + proxy_cache_lock on; + proxy_cache_use_stale updating; + proxy_cache_valid 200 5s; + proxy_cache_bypass $http_x_cache_bypass; + proxy_no_cache $should_not_cache; + add_header X-Cache-Status $upstream_cache_status; + + location /api/vod/ { + include auth_request.conf; + proxy_pass http://frigate_api/vod/; + include proxy.conf; + proxy_cache off; + } + + location /api/login { + auth_request off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + # Allow unauthenticated access to the first_time_login endpoint + # so the login page can load help text before authentication. + location /api/auth/first_time_login { + auth_request off; + limit_except GET { + deny all; + } + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/stats { + include auth_request.conf; + access_log off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + + location /api/version { + include auth_request.conf; + access_log off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + } + + location / { + # do not require auth for static assets + add_header Cache-Control "no-store"; + expires off; + + location /assets/ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + } + + location /fonts/ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + } + + location /locales/ { + access_log off; + add_header Cache-Control "public"; + } + + location ~ ^/.*-([A-Za-z0-9]+)\.webmanifest$ { + access_log off; + expires 1y; + add_header Cache-Control "public"; + default_type application/json; + proxy_set_header Accept-Encoding ""; + sub_filter_once off; + sub_filter_types application/json; + sub_filter '"start_url": "/BASE_PATH/"' '"start_url" : "$http_x_ingress_path/"'; + sub_filter '"src": "/BASE_PATH/' '"src": "$http_x_ingress_path/'; + } + + sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/'; + sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/'; + sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/'; + sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/'; + sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/'; + sub_filter '"/BASE_PATH/locales/' '"$http_x_ingress_path/locales/'; + sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/'; + sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl'; + sub_filter '' ''; + sub_filter_types text/css application/javascript; + sub_filter_once off; + + root /opt/frigate/web; + try_files $uri $uri.html $uri/ /index.html; + } + } +} diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy.conf new file mode 100644 index 0000000..a3aacc3 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy.conf @@ -0,0 +1,26 @@ +## Headers +proxy_set_header Host $host; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "Upgrade"; +proxy_set_header X-Original-URL $scheme://$http_host$request_uri; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $http_host; +proxy_set_header X-Forwarded-URI $request_uri; +proxy_set_header X-Forwarded-Ssl on; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Real-IP $remote_addr; + +## Basic Proxy Configuration +client_body_buffer_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. +proxy_redirect http:// $scheme://; +proxy_http_version 1.1; +proxy_cache_bypass $cookie_session; +proxy_no_cache $cookie_session; +proxy_buffers 64 256k; + +## Advanced Proxy Configuration +send_timeout 5m; +proxy_read_timeout 360; +proxy_send_timeout 360; +proxy_connect_timeout 360; \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf new file mode 100644 index 0000000..54c05ab --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/conf/proxy_trusted_headers.conf @@ -0,0 +1,25 @@ +# Header used to validate reverse proxy trust +proxy_set_header X-Proxy-Secret $http_x_proxy_secret; + +# these headers will be copied to the /auth request and are available +# to be mapped in the config to Frigate's remote-user header + +# List of headers sent by common authentication proxies: +# - Authelia +# - Traefik forward auth +# - oauth2_proxy +# - Authentik + +proxy_set_header Remote-User $http_remote_user; +proxy_set_header Remote-Groups $http_remote_groups; +proxy_set_header Remote-Email $http_remote_email; +proxy_set_header Remote-Name $http_remote_name; +proxy_set_header X-Forwarded-User $http_x_forwarded_user; +proxy_set_header X-Forwarded-Groups $http_x_forwarded_groups; +proxy_set_header X-Forwarded-Email $http_x_forwarded_email; +proxy_set_header X-Forwarded-Preferred-Username $http_x_forwarded_preferred_username; +proxy_set_header X-authentik-username $http_x_authentik_username; +proxy_set_header X-authentik-groups $http_x_authentik_groups; +proxy_set_header X-authentik-email $http_x_authentik_email; +proxy_set_header X-authentik-name $http_x_authentik_name; +proxy_set_header X-authentik-uid $http_x_authentik_uid; diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_base_path.py b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_base_path.py new file mode 100644 index 0000000..2e78a7d --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_base_path.py @@ -0,0 +1,11 @@ +"""Prints the base path as json to stdout.""" + +import json +import os +from typing import Any + +base_path = os.environ.get("FRIGATE_BASE_PATH", "") + +result: dict[str, Any] = {"base_path": base_path} + +print(json.dumps(result)) diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_listen_settings.py b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_listen_settings.py new file mode 100644 index 0000000..d879db5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/get_listen_settings.py @@ -0,0 +1,35 @@ +"""Prints the tls config as json to stdout.""" + +import json +import sys +from typing import Any + +from ruamel.yaml import YAML + +sys.path.insert(0, "/opt/frigate") +from frigate.util.config import find_config_file + +sys.path.remove("/opt/frigate") + +yaml = YAML() + +config_file = find_config_file() + +try: + with open(config_file) as f: + raw_config = f.read() + + if config_file.endswith((".yaml", ".yml")): + config: dict[str, Any] = yaml.load(raw_config) + elif config_file.endswith(".json"): + config: dict[str, Any] = json.loads(raw_config) +except FileNotFoundError: + config: dict[str, Any] = {} + +tls_config: dict[str, any] = config.get("tls", {"enabled": True}) +networking_config = config.get("networking", {}) +ipv6_config = networking_config.get("ipv6", {"enabled": False}) + +output = {"tls": tls_config, "ipv6": ipv6_config} + +print(json.dumps(output)) diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl new file mode 100644 index 0000000..ace4443 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/base_path.gotmpl @@ -0,0 +1,19 @@ +{{ if .base_path }} +location = {{ .base_path }} { + return 302 {{ .base_path }}/; +} + +location ^~ {{ .base_path }}/ { + # remove base_url from the path before passing upstream + rewrite ^{{ .base_path }}/(.*) /$1 break; + + proxy_pass $scheme://127.0.0.1:8971; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Ingress-Path {{ .base_path }}; + + access_log off; +} +{{ end }} diff --git a/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl new file mode 100644 index 0000000..066f872 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/main/rootfs/usr/local/nginx/templates/listen.gotmpl @@ -0,0 +1,45 @@ + +# Internal (IPv4 always; IPv6 optional) +listen 5000; +{{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:5000;{{ end }}{{ end }} + + +# intended for external traffic, protected by auth +{{ if .tls }} + {{ if .tls.enabled }} + # external HTTPS (IPv4 always; IPv6 optional) + listen 8971 ssl; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971 ssl;{{ end }}{{ end }} + + ssl_certificate /etc/letsencrypt/live/frigate/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/frigate/privkey.pem; + + # generated 2024-06-01, Mozilla Guideline v5.7, nginx 1.25.3, OpenSSL 1.1.1w, modern configuration, no OCSP + # https://ssl-config.mozilla.org/#server=nginx&version=1.25.3&config=modern&openssl=1.1.1w&ocsp=false&guideline=5.7 + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + + # modern configuration + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HSTS (ngx_http_headers_module is required) (63072000 seconds) + add_header Strict-Transport-Security "max-age=63072000" always; + + # ACME challenge location + location /.well-known/acme-challenge/ { + default_type "text/plain"; + root /etc/letsencrypt/www; + } + {{ else }} + # external HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} + {{ end }} +{{ else }} + # (No tls section) default to HTTP (IPv4 always; IPv6 optional) + listen 8971; + {{ if .ipv6 }}{{ if .ipv6.enabled }}listen [::]:8971;{{ end }}{{ end }} +{{ end }} + diff --git a/sam2-cpu/frigate-dev/docker/memryx/user_installation.sh b/sam2-cpu/frigate-dev/docker/memryx/user_installation.sh new file mode 100644 index 0000000..b92b7e3 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/memryx/user_installation.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e # Exit immediately if any command fails +set -o pipefail + +echo "Starting MemryX driver and runtime installation..." + +# Detect architecture +arch=$(uname -m) + +# Purge existing packages and repo +echo "Removing old MemryX installations..." +# Remove any holds on MemryX packages (if they exist) +sudo apt-mark unhold memx-* mxa-manager || true +sudo apt purge -y memx-* mxa-manager || true +sudo rm -f /etc/apt/sources.list.d/memryx.list /etc/apt/trusted.gpg.d/memryx.asc + +# Install kernel headers +echo "Installing kernel headers for: $(uname -r)" +sudo apt update +sudo apt install -y dkms linux-headers-$(uname -r) + +# Add MemryX key and repo +echo "Adding MemryX GPG key and repository..." +wget -qO- https://developer.memryx.com/deb/memryx.asc | sudo tee /etc/apt/trusted.gpg.d/memryx.asc >/dev/null +echo 'deb https://developer.memryx.com/deb stable main' | sudo tee /etc/apt/sources.list.d/memryx.list >/dev/null + +# Update and install specific SDK 2.1 packages +echo "Installing MemryX SDK 2.1 packages..." +sudo apt update +sudo apt install -y memx-drivers=2.1.* memx-accl=2.1.* mxa-manager=2.1.* + +# Hold packages to prevent automatic upgrades +sudo apt-mark hold memx-drivers memx-accl mxa-manager + +# ARM-specific board setup +if [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then + echo "Running ARM board setup..." + sudo mx_arm_setup +fi + +echo -e "\n\n\033[1;31mYOU MUST RESTART YOUR COMPUTER NOW\033[0m\n\n" + +echo "MemryX SDK 2.1 installation complete!" + diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/coco_subset_20.txt b/sam2-cpu/frigate-dev/docker/rockchip/COCO/coco_subset_20.txt new file mode 100644 index 0000000..aa372fe --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/COCO/coco_subset_20.txt @@ -0,0 +1,20 @@ +./subset/000000005001.jpg +./subset/000000038829.jpg +./subset/000000052891.jpg +./subset/000000075612.jpg +./subset/000000098261.jpg +./subset/000000181542.jpg +./subset/000000215245.jpg +./subset/000000277005.jpg +./subset/000000288685.jpg +./subset/000000301421.jpg +./subset/000000334371.jpg +./subset/000000348481.jpg +./subset/000000373353.jpg +./subset/000000397681.jpg +./subset/000000414673.jpg +./subset/000000419312.jpg +./subset/000000465822.jpg +./subset/000000475732.jpg +./subset/000000559707.jpg +./subset/000000574315.jpg \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000005001.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000005001.jpg new file mode 100644 index 0000000..a7d4437 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000005001.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000038829.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000038829.jpg new file mode 100644 index 0000000..f275500 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000038829.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000052891.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000052891.jpg new file mode 100644 index 0000000..57344ef Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000052891.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000075612.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000075612.jpg new file mode 100644 index 0000000..16555e4 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000075612.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000098261.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000098261.jpg new file mode 100644 index 0000000..57412f7 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000098261.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000181542.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000181542.jpg new file mode 100644 index 0000000..e3676d3 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000181542.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000215245.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000215245.jpg new file mode 100644 index 0000000..624e4f1 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000215245.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000277005.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000277005.jpg new file mode 100644 index 0000000..629cb6e Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000277005.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000288685.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000288685.jpg new file mode 100644 index 0000000..4dc759d Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000288685.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000301421.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000301421.jpg new file mode 100644 index 0000000..2cbfa4e Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000301421.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000334371.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000334371.jpg new file mode 100644 index 0000000..b47ac6d Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000334371.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000348481.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000348481.jpg new file mode 100644 index 0000000..a2cb75c Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000348481.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000373353.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000373353.jpg new file mode 100644 index 0000000..c092511 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000373353.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000397681.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000397681.jpg new file mode 100644 index 0000000..5b94259 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000397681.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000414673.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000414673.jpg new file mode 100644 index 0000000..587c370 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000414673.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000419312.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000419312.jpg new file mode 100644 index 0000000..274ea87 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000419312.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000465822.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000465822.jpg new file mode 100644 index 0000000..3510d11 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000465822.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000475732.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000475732.jpg new file mode 100644 index 0000000..51d9685 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000475732.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000559707.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000559707.jpg new file mode 100644 index 0000000..4811ef1 Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000559707.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000574315.jpg b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000574315.jpg new file mode 100644 index 0000000..ad06b6d Binary files /dev/null and b/sam2-cpu/frigate-dev/docker/rockchip/COCO/subset/000000574315.jpg differ diff --git a/sam2-cpu/frigate-dev/docker/rockchip/Dockerfile b/sam2-cpu/frigate-dev/docker/rockchip/Dockerfile new file mode 100644 index 0000000..70309f0 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels as rk-wheels +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt +COPY docker/rockchip/requirements-wheels-rk.txt /requirements-wheels-rk.txt +RUN sed -i "/https:\/\//d" /requirements-wheels.txt +RUN sed -i "/onnxruntime/d" /requirements-wheels.txt +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.txt +RUN rm -rf /rk-wheels/opencv_python-* +RUN rm -rf /rk-wheels/torch-* + +FROM deps AS rk-frigate +ARG TARGETARCH +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=rk-wheels,source=/rk-wheels,target=/deps/rk-wheels \ + pip3 install --no-deps -U /deps/rk-wheels/*.whl + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / +COPY docker/rockchip/COCO /COCO +COPY docker/rockchip/conv2rknn.py /opt/conv2rknn.py + +ADD https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.3.2/librknnrt.so /usr/lib/ + +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-11/ffmpeg /usr/lib/ffmpeg/6.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-11/ffprobe /usr/lib/ffmpeg/6.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/7.1-1/ffmpeg /usr/lib/ffmpeg/7.0/bin/ +ADD --chmod=111 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/7.1-1/ffprobe /usr/lib/ffmpeg/7.0/bin/ +ENV DEFAULT_FFMPEG_VERSION="6.0" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" diff --git a/sam2-cpu/frigate-dev/docker/rockchip/conv2rknn.py b/sam2-cpu/frigate-dev/docker/rockchip/conv2rknn.py new file mode 100644 index 0000000..4880d98 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/conv2rknn.py @@ -0,0 +1,82 @@ +import os + +import rknn +import yaml +from rknn.api import RKNN + +try: + with open(rknn.__path__[0] + "/VERSION") as file: + tk_version = file.read().strip() +except FileNotFoundError: + pass + +try: + with open("/config/conv2rknn.yaml", "r") as config_file: + configuration = yaml.safe_load(config_file) +except FileNotFoundError: + raise Exception("Please place a config file at /config/conv2rknn.yaml") + +if configuration["config"] != None: + rknn_config = configuration["config"] +else: + rknn_config = {} + +if not os.path.isdir("/config/model_cache/rknn_cache/onnx"): + raise Exception( + "Place the onnx models you want to convert to rknn format in /config/model_cache/rknn_cache/onnx" + ) + +if "soc" not in configuration: + try: + with open("/proc/device-tree/compatible") as file: + soc = file.read().split(",")[-1].strip("\x00") + except FileNotFoundError: + raise Exception("Make sure to run docker in privileged mode.") + + configuration["soc"] = [ + soc, + ] + +if "quantization" not in configuration: + configuration["quantization"] = False + +if "output_name" not in configuration: + configuration["output_name"] = "{{input_basename}}" + +for input_filename in os.listdir("/config/model_cache/rknn_cache/onnx"): + for soc in configuration["soc"]: + quant = "i8" if configuration["quantization"] else "fp16" + + input_path = "/config/model_cache/rknn_cache/onnx/" + input_filename + input_basename = input_filename[: input_filename.rfind(".")] + + output_filename = ( + configuration["output_name"].format( + quant=quant, + input_basename=input_basename, + soc=soc, + tk_version=tk_version, + ) + + ".rknn" + ) + output_path = "/config/model_cache/rknn_cache/" + output_filename + + rknn_config["target_platform"] = soc + + rknn = RKNN(verbose=True) + rknn.config(**rknn_config) + + if rknn.load_onnx(model=input_path) != 0: + raise Exception("Error loading model.") + + if ( + rknn.build( + do_quantization=configuration["quantization"], + dataset="/COCO/coco_subset_20.txt", + ) + != 0 + ): + raise Exception("Error building model.") + + if rknn.export_rknn(output_path) != 0: + raise Exception("Error exporting rknn model.") diff --git a/sam2-cpu/frigate-dev/docker/rockchip/requirements-wheels-rk.txt b/sam2-cpu/frigate-dev/docker/rockchip/requirements-wheels-rk.txt new file mode 100644 index 0000000..f841f26 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/requirements-wheels-rk.txt @@ -0,0 +1,2 @@ +rknn-toolkit2 == 2.3.2 +rknn-toolkit-lite2 == 2.3.2 \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rockchip/rk.hcl b/sam2-cpu/frigate-dev/docker/rockchip/rk.hcl new file mode 100644 index 0000000..9424b46 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/rk.hcl @@ -0,0 +1,27 @@ +target wheels { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "wheels" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target rk { + dockerfile = "docker/rockchip/Dockerfile" + contexts = { + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rockchip/rk.mk b/sam2-cpu/frigate-dev/docker/rockchip/rk.mk new file mode 100644 index 0000000..c8278f6 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rockchip/rk.mk @@ -0,0 +1,15 @@ +BOARDS += rk + +local-rk: version + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=frigate:latest-rk \ + --load + +build-rk: version + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk + +push-rk: build-rk + docker buildx bake --file=docker/rockchip/rk.hcl rk \ + --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk \ + --push \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rocm/Dockerfile b/sam2-cpu/frigate-dev/docker/rocm/Dockerfile new file mode 100644 index 0000000..9edcd60 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rocm/Dockerfile @@ -0,0 +1,87 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive +ARG ROCM=1 +ARG HSA_OVERRIDE_GFX_VERSION +ARG HSA_OVERRIDE + +####################################################################### +FROM wget AS rocm + +ARG ROCM + +RUN apt update -qq && \ + apt install -y wget gpg && \ + wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.1.1/ubuntu/jammy/amdgpu-install_7.1.1.70101-1_all.deb && \ + apt install -y ./rocm.deb && \ + apt update && \ + apt install -qq -y rocm + +RUN mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib +RUN cd /opt/rocm-$ROCM/lib && \ + cp -dpr libMIOpen*.so* libamd*.so* libhip*.so* libhsa*.so* libmigraphx*.so* librocm*.so* librocblas*.so* libroctracer*.so* librocsolver*.so* librocfft*.so* librocprofiler*.so* libroctx*.so* librocroller.so* /opt/rocm-dist/opt/rocm-$ROCM/lib/ && \ + mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib && \ + cp -dpr migraphx/lib/* /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib +RUN cd /opt/rocm-dist/opt/ && ln -s rocm-$ROCM rocm + +RUN mkdir -p /opt/rocm-dist/etc/ld.so.conf.d/ +RUN echo /opt/rocm/lib|tee /opt/rocm-dist/etc/ld.so.conf.d/rocm.conf + +####################################################################### +FROM deps AS deps-prelim + +COPY docker/rocm/debian-backports.sources /etc/apt/sources.list.d/debian-backports.sources +RUN apt-get update && \ + apt-get install -y libnuma1 && \ + apt-get install -qq -y -t bookworm-backports mesa-va-drivers mesa-vulkan-drivers && \ + # Install C++ standard library headers for HIPRTC kernel compilation fallback + apt-get install -qq -y libstdc++-12-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/frigate +COPY --from=rootfs / / + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" --break-system-packages +RUN python3 -m pip config set global.break-system-packages true + +COPY docker/rocm/requirements-wheels-rocm.txt /requirements.txt +RUN pip3 uninstall -y onnxruntime \ + && pip3 install -r /requirements.txt + +####################################################################### +FROM scratch AS rocm-dist + +ARG ROCM + +COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/ +# Copy MIOpen database files for gfx10xx and gfx11xx only (RDNA2/RDNA3) +COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/ +COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx11* /opt/rocm-$ROCM/share/miopen/db/ +# Copy rocBLAS library files for gfx10xx and gfx11xx only +COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx10* /opt/rocm-$ROCM/lib/rocblas/library/ +COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx11* /opt/rocm-$ROCM/lib/rocblas/library/ +COPY --from=rocm /opt/rocm-dist/ / + +####################################################################### +FROM deps-prelim AS rocm-prelim-hsa-override0 +ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1 +ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1 +ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1 +ENV MIGRAPHX_ENABLE_HIPRTC_WORKAROUNDS=1 + +COPY --from=rocm-dist / / + +RUN ldconfig + +####################################################################### +FROM rocm-prelim-hsa-override0 as rocm-prelim-hsa-override1 + +ARG HSA_OVERRIDE_GFX_VERSION +ENV HSA_OVERRIDE_GFX_VERSION=$HSA_OVERRIDE_GFX_VERSION + +####################################################################### +FROM rocm-prelim-hsa-override$HSA_OVERRIDE as rocm-deps + diff --git a/sam2-cpu/frigate-dev/docker/rocm/debian-backports.sources b/sam2-cpu/frigate-dev/docker/rocm/debian-backports.sources new file mode 100644 index 0000000..fc51f4e --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rocm/debian-backports.sources @@ -0,0 +1,6 @@ +Types: deb +URIs: http://deb.debian.org/debian +Suites: bookworm-backports +Components: main +Enabled: yes +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg diff --git a/sam2-cpu/frigate-dev/docker/rocm/requirements-wheels-rocm.txt b/sam2-cpu/frigate-dev/docker/rocm/requirements-wheels-rocm.txt new file mode 100644 index 0000000..b6a202f --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rocm/requirements-wheels-rocm.txt @@ -0,0 +1 @@ +onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.1.0/onnxruntime_migraphx-1.23.1-cp311-cp311-linux_x86_64.whl \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rocm/rocm.hcl b/sam2-cpu/frigate-dev/docker/rocm/rocm.hcl new file mode 100644 index 0000000..6595066 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rocm/rocm.hcl @@ -0,0 +1,42 @@ +variable "ROCM" { + default = "7.1.1" +} +variable "HSA_OVERRIDE_GFX_VERSION" { + default = "" +} +variable "HSA_OVERRIDE" { + default = "1" +} + +target wget { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "wget" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "rootfs" +} + +target rocm { + dockerfile = "docker/rocm/Dockerfile" + contexts = { + deps = "target:deps", + wget = "target:wget", + rootfs = "target:rootfs" + } + platforms = ["linux/amd64"] + args = { + ROCM = ROCM, + HSA_OVERRIDE_GFX_VERSION = HSA_OVERRIDE_GFX_VERSION, + HSA_OVERRIDE = HSA_OVERRIDE + } +} diff --git a/sam2-cpu/frigate-dev/docker/rocm/rocm.mk b/sam2-cpu/frigate-dev/docker/rocm/rocm.mk new file mode 100644 index 0000000..f98d387 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rocm/rocm.mk @@ -0,0 +1,15 @@ +BOARDS += rocm + +local-rocm: version + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=frigate:latest-rocm \ + --load + +build-rocm: version + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm + +push-rocm: build-rocm + docker buildx bake --file=docker/rocm/rocm.hcl rocm \ + --set rocm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rocm \ + --push diff --git a/sam2-cpu/frigate-dev/docker/rpi/Dockerfile b/sam2-cpu/frigate-dev/docker/rpi/Dockerfile new file mode 100644 index 0000000..35a2252 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rpi/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +FROM deps AS rpi-deps +ARG TARGETARCH + +# Install dependencies +RUN --mount=type=bind,source=docker/rpi/install_deps.sh,target=/deps/install_deps.sh \ + /deps/install_deps.sh + +ENV DEFAULT_FFMPEG_VERSION="rpi" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / diff --git a/sam2-cpu/frigate-dev/docker/rpi/install_deps.sh b/sam2-cpu/frigate-dev/docker/rpi/install_deps.sh new file mode 100755 index 0000000..bf537d5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rpi/install_deps.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euxo pipefail + +apt-get -qq update + +apt-get -qq install --no-install-recommends -y \ + apt-transport-https \ + gnupg \ + wget \ + procps vainfo \ + unzip locales tzdata libxml2 xz-utils \ + python3-pip \ + curl \ + jq \ + nethogs + +mkdir -p -m 600 /root/.gnupg + +# enable non-free repo +echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware" | tee -a /etc/apt/sources.list +apt update + +# ffmpeg -> arm64 +if [[ "${TARGETARCH}" == "arm64" ]]; then + # add raspberry pi repo + gpg --no-default-keyring --keyring /usr/share/keyrings/raspbian.gpg --keyserver keyserver.ubuntu.com --recv-keys 82B129927FA3303E + echo "deb [signed-by=/usr/share/keyrings/raspbian.gpg] https://archive.raspberrypi.org/debian/ bookworm main" | tee /etc/apt/sources.list.d/raspi.list + apt-get -qq update + apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg + mkdir -p /usr/lib/ffmpeg/rpi/bin + ln -svf /usr/bin/ffmpeg /usr/lib/ffmpeg/rpi/bin/ffmpeg + ln -svf /usr/bin/ffprobe /usr/lib/ffmpeg/rpi/bin/ffprobe +fi diff --git a/sam2-cpu/frigate-dev/docker/rpi/rpi.hcl b/sam2-cpu/frigate-dev/docker/rpi/rpi.hcl new file mode 100644 index 0000000..66f97c1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rpi/rpi.hcl @@ -0,0 +1,20 @@ +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target rpi { + dockerfile = "docker/rpi/Dockerfile" + contexts = { + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/rpi/rpi.mk b/sam2-cpu/frigate-dev/docker/rpi/rpi.mk new file mode 100644 index 0000000..290b30c --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/rpi/rpi.mk @@ -0,0 +1,15 @@ +BOARDS += rpi + +local-rpi: version + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=frigate:latest-rpi \ + --load + +build-rpi: version + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rpi + +push-rpi: build-rpi + docker buildx bake --file=docker/rpi/rpi.hcl rpi \ + --set rpi.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rpi \ + --push diff --git a/sam2-cpu/frigate-dev/docker/synaptics/Dockerfile b/sam2-cpu/frigate-dev/docker/synaptics/Dockerfile new file mode 100644 index 0000000..6a60fe4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/synaptics/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels AS synap1680-wheels +ARG TARGETARCH + +# Install dependencies +RUN wget -qO- "https://github.com/GaryHuang-ASUS/synaptics_astra_sdk/releases/download/v1.5.0/Synaptics-SL1680-v1.5.0-rt.tar" | tar -C / -xzf - +RUN wget -P /wheels/ "https://github.com/synaptics-synap/synap-python/releases/download/v0.0.4-preview/synap_python-0.0.4-cp311-cp311-manylinux_2_35_aarch64.whl" + +FROM deps AS synap1680-deps +ARG TARGETARCH +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=synap1680-wheels,source=/wheels,target=/deps/synap-wheels \ +pip3 install --no-deps -U /deps/synap-wheels/*.whl + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / + +COPY --from=synap1680-wheels /rootfs/usr/local/lib/*.so /usr/lib + +ADD https://raw.githubusercontent.com/synaptics-astra/synap-release/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80/model.synap /synaptics/mobilenet.synap diff --git a/sam2-cpu/frigate-dev/docker/synaptics/synaptics.hcl b/sam2-cpu/frigate-dev/docker/synaptics/synaptics.hcl new file mode 100644 index 0000000..a22fb44 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/synaptics/synaptics.hcl @@ -0,0 +1,27 @@ +target wheels { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "wheels" +} + +target deps { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "deps" +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/arm64"] + target = "rootfs" +} + +target synaptics { + dockerfile = "docker/synaptics/Dockerfile" + contexts = { + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + platforms = ["linux/arm64"] +} diff --git a/sam2-cpu/frigate-dev/docker/synaptics/synaptics.mk b/sam2-cpu/frigate-dev/docker/synaptics/synaptics.mk new file mode 100644 index 0000000..64cb858 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/synaptics/synaptics.mk @@ -0,0 +1,15 @@ +BOARDS += synaptics + +local-synaptics: version + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=frigate:latest-synaptics \ + --load + +build-synaptics: version + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-synaptics + +push-synaptics: build-synaptics + docker buildx bake --file=docker/synaptics/synaptics.hcl synaptics \ + --set synaptics.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-synaptics \ + --push diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.amd64 b/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.amd64 new file mode 100644 index 0000000..cdf5df9 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.amd64 @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# Globally set pip break-system-packages option to avoid having to specify it every time +ARG PIP_BREAK_SYSTEM_PACKAGES=1 + +FROM wheels AS trt-wheels +ARG PIP_BREAK_SYSTEM_PACKAGES + +# Install TensorRT wheels +COPY docker/tensorrt/requirements-amd64.txt /requirements-tensorrt.txt +COPY docker/main/requirements-wheels.txt /requirements-wheels.txt + +# remove dependencies from the requirements that have type constraints +RUN sed -i '/\[.*\]/d' /requirements-wheels.txt \ + && pip3 wheel --wheel-dir=/trt-wheels -c /requirements-wheels.txt -r /requirements-tensorrt.txt + +FROM deps AS frigate-tensorrt +ARG PIP_BREAK_SYSTEM_PACKAGES + +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + pip3 uninstall -y onnxruntime \ + && pip3 install -U /deps/trt-wheels/*.whl + +COPY --from=rootfs / / +COPY docker/tensorrt/detector/rootfs/etc/ld.so.conf.d /etc/ld.so.conf.d +RUN ldconfig + +WORKDIR /opt/frigate/ + +# Dev Container w/ TRT +FROM devcontainer AS devcontainer-trt + +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + pip3 install -U /deps/trt-wheels/*.whl diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.arm64 b/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.arm64 new file mode 100644 index 0000000..dd3c5de --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/Dockerfile.arm64 @@ -0,0 +1,156 @@ +# syntax=docker/dockerfile:1.6 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive +ARG BASE_IMAGE +ARG TRT_BASE=nvcr.io/nvidia/tensorrt:23.12-py3 + +# Build TensorRT-specific library +FROM ${TRT_BASE} AS trt-deps + +ARG TARGETARCH +ARG COMPUTE_LEVEL + +RUN apt-get update \ + && apt-get install -y git build-essential cuda-nvcc-* cuda-nvtx-* libnvinfer-dev libnvinfer-plugin-dev libnvparsers-dev libnvonnxparsers-dev \ + && rm -rf /var/lib/apt/lists/* +RUN --mount=type=bind,source=docker/tensorrt/detector/tensorrt_libyolo.sh,target=/tensorrt_libyolo.sh \ + /tensorrt_libyolo.sh + +# COPY required individual CUDA deps +RUN mkdir -p /usr/local/cuda-deps +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libcurand.so.* /usr/local/cuda-deps/ && \ + cp /usr/local/cuda-12.3/targets/x86_64-linux/lib/libnvrtc.so.* /usr/local/cuda-deps/ && \ + cd /usr/local/cuda-deps/ && \ + for lib in libnvrtc.so.*; do \ + if [[ "$lib" =~ libnvrtc.so\.([0-9]+\.[0-9]+\.[0-9]+) ]]; then \ + version="${BASH_REMATCH[1]}"; \ + ln -sf "libnvrtc.so.$version" libnvrtc.so; \ + fi; \ + done && \ + for lib in libcurand.so.*; do \ + if [[ "$lib" =~ libcurand.so\.([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]]; then \ + version="${BASH_REMATCH[1]}"; \ + ln -sf "libcurand.so.$version" libcurand.so; \ + fi; \ + done; \ + fi + +# Frigate w/ TensorRT Support as separate image +FROM deps AS tensorrt-base + +#Disable S6 Global timeout +ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 + +# COPY TensorRT Model Generation Deps +COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so +COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos + +# COPY Individual CUDA deps folder +COPY --from=trt-deps /usr/local/cuda-deps /usr/local/cuda + +COPY docker/tensorrt/detector/rootfs/ / +ENV YOLO_MODELS="" + +HEALTHCHECK --start-period=600s --start-interval=5s --interval=15s --timeout=5s --retries=3 \ + CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1 + +FROM ${BASE_IMAGE} AS build-wheels +ARG DEBIAN_FRONTEND + +# Add deadsnakes PPA for python3.11 +RUN apt-get -qq update && \ + apt-get -qq install -y --no-install-recommends \ + software-properties-common \ + && add-apt-repository ppa:deadsnakes/ppa + +# Use a separate container to build wheels to prevent build dependencies in final image +RUN apt-get -qq update \ + && apt-get -qq install -y --no-install-recommends \ + python3.11 python3.11-dev \ + wget build-essential cmake git \ + && rm -rf /var/lib/apt/lists/* + +# Ensure python3 defaults to python3.11 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 + +RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \ + && sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \ + && python3 get-pip.py "pip" + +FROM build-wheels AS trt-wheels +ARG DEBIAN_FRONTEND +ARG TARGETARCH + +# python-tensorrt build deps are 3.4 GB! +RUN apt-get update \ + && apt-get install -y ccache cuda-cudart-dev-* cuda-nvcc-* libnvonnxparsers-dev libnvparsers-dev libnvinfer-plugin-dev \ + && ([ -e /usr/local/cuda ] || ln -s /usr/local/cuda-* /usr/local/cuda) \ + && rm -rf /var/lib/apt/lists/*; + +# Determine version of tensorrt already installed in base image, e.g. "Version: 8.4.1-1+cuda11.4" +RUN NVINFER_VER=$(dpkg -s libnvinfer8 | grep -Po "Version: \K.*") \ + && echo $NVINFER_VER | grep -Po "^\d+\.\d+\.\d+" > /etc/TENSORRT_VER + +RUN --mount=type=bind,source=docker/tensorrt/detector/build_python_tensorrt.sh,target=/deps/build_python_tensorrt.sh \ + --mount=type=cache,target=/root/.ccache \ + export PATH="/usr/lib/ccache:$PATH" CCACHE_DIR=/root/.ccache CCACHE_MAXSIZE=2G \ + && TENSORRT_VER=$(cat /etc/TENSORRT_VER) /deps/build_python_tensorrt.sh + +COPY docker/tensorrt/requirements-arm64.txt /requirements-tensorrt.txt + +RUN pip3 wheel --wheel-dir=/trt-wheels -r /requirements-tensorrt.txt + +# See https://elinux.org/Jetson_Zoo#ONNX_Runtime +ADD https://nvidia.box.com/shared/static/9yvw05k6u343qfnkhdv2x6xhygze0aq1.whl /trt-wheels/onnxruntime_gpu-1.19.0-cp311-cp311-linux_aarch64.whl + +FROM build-wheels AS trt-model-wheels +ARG DEBIAN_FRONTEND + +RUN apt-get update \ + && apt-get install -y protobuf-compiler libprotobuf-dev \ + && rm -rf /var/lib/apt/lists/* +RUN --mount=type=bind,source=docker/tensorrt/requirements-models-arm64.txt,target=/requirements-tensorrt-models.txt \ + pip3 wheel --wheel-dir=/trt-model-wheels --no-deps -r /requirements-tensorrt-models.txt + +FROM wget AS jetson-ffmpeg +ARG DEBIAN_FRONTEND +ENV CCACHE_DIR /root/.ccache +ENV CCACHE_MAXSIZE 2G +RUN --mount=type=bind,source=docker/tensorrt/build_jetson_ffmpeg.sh,target=/deps/build_jetson_ffmpeg.sh \ + --mount=type=cache,target=/root/.ccache \ + /deps/build_jetson_ffmpeg.sh + +# Frigate w/ TensorRT for NVIDIA Jetson platforms +FROM tensorrt-base AS frigate-tensorrt +RUN apt-get update \ + && apt-get install -y python-is-python3 libprotobuf23 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=jetson-ffmpeg /rootfs / +ENV DEFAULT_FFMPEG_VERSION="jetson" +ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:${INCLUDED_FFMPEG_VERSIONS}" + +# ffmpeg runtime dependencies +RUN apt-get -qq update \ + && apt-get -qq install -y --no-install-recommends \ + libx264-163 libx265-199 libegl1 \ + && rm -rf /var/lib/apt/lists/* + +# Fixes "Error loading shared libs" +RUN mkdir -p /etc/ld.so.conf.d && echo /usr/lib/ffmpeg/jetson/lib/ > /etc/ld.so.conf.d/ffmpeg.conf + +COPY --from=trt-wheels /etc/TENSORRT_VER /etc/TENSORRT_VER +RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \ + --mount=type=bind,from=trt-model-wheels,source=/trt-model-wheels,target=/deps/trt-model-wheels \ + pip3 uninstall -y onnxruntime \ + && pip3 install -U /deps/trt-wheels/*.whl \ + && pip3 install -U /deps/trt-model-wheels/*.whl \ + && ldconfig + +WORKDIR /opt/frigate/ +COPY --from=rootfs / / + +# Fixes "Error importing detector runtime: /usr/lib/aarch64-linux-gnu/libstdc++.so.6: cannot allocate memory in static TLS block" +ENV LD_PRELOAD /usr/lib/aarch64-linux-gnu/libstdc++.so.6 \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/build_jetson_ffmpeg.sh b/sam2-cpu/frigate-dev/docker/tensorrt/build_jetson_ffmpeg.sh new file mode 100755 index 0000000..fb29eb2 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/build_jetson_ffmpeg.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# For jetson platforms, build ffmpeg with custom patches. NVIDIA supplies a deb +# with accelerated decoding, but it doesn't have accelerated scaling or encoding + +set -euxo pipefail + +INSTALL_PREFIX=/rootfs/usr/lib/ffmpeg/jetson + +apt-get -qq update +apt-get -qq install -y --no-install-recommends build-essential ccache clang cmake pkg-config +apt-get -qq install -y --no-install-recommends libx264-dev libx265-dev + +pushd /tmp + +# Install libnvmpi to enable nvmpi decoders (h264_nvmpi, hevc_nvmpi) +if [ -e /usr/local/cuda-12 ]; then + # assume Jetpack 6.2 + apt-key adv --fetch-key https://repo.download.nvidia.com/jetson/jetson-ota-public.asc + echo "deb https://repo.download.nvidia.com/jetson/common r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + echo "deb https://repo.download.nvidia.com/jetson/t234 r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + echo "deb https://repo.download.nvidia.com/jetson/ffmpeg r36.4 main" >> /etc/apt/sources.list.d/nvidia-l4t-apt-source.list + + mkdir -p /opt/nvidia/l4t-packages/ + touch /opt/nvidia/l4t-packages/.nv-l4t-disable-boot-fw-update-in-preinstall + + apt-get update + apt-get -qq install -y --no-install-recommends -o Dpkg::Options::="--force-confold" nvidia-l4t-jetson-multimedia-api +elif [ -e /usr/local/cuda-10.2 ]; then + # assume Jetpack 4.X + wget -q https://developer.nvidia.com/embedded/L4T/r32_Release_v5.0/T186/Jetson_Multimedia_API_R32.5.0_aarch64.tbz2 -O jetson_multimedia_api.tbz2 + tar xaf jetson_multimedia_api.tbz2 -C / && rm jetson_multimedia_api.tbz2 +else + # assume Jetpack 5.X + wget -q https://developer.nvidia.com/downloads/embedded/l4t/r35_release_v3.1/release/jetson_multimedia_api_r35.3.1_aarch64.tbz2 -O jetson_multimedia_api.tbz2 + tar xaf jetson_multimedia_api.tbz2 -C / && rm jetson_multimedia_api.tbz2 +fi + +wget -q https://github.com/AndBobsYourUncle/jetson-ffmpeg/archive/9c17b09.zip -O jetson-ffmpeg.zip +unzip jetson-ffmpeg.zip && rm jetson-ffmpeg.zip && mv jetson-ffmpeg-* jetson-ffmpeg && cd jetson-ffmpeg +LD_LIBRARY_PATH=$(pwd)/stubs:$LD_LIBRARY_PATH # tegra multimedia libs aren't available in image, so use stubs for ffmpeg build +mkdir build +cd build +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$INSTALL_PREFIX +make -j$(nproc) +make install +cd ../../ + +# Install nv-codec-headers to enable ffnvcodec filters (scale_cuda) +wget -q https://github.com/FFmpeg/nv-codec-headers/archive/refs/heads/master.zip +unzip master.zip && rm master.zip && cd nv-codec-headers-master +make PREFIX=$INSTALL_PREFIX install +cd ../ && rm -rf nv-codec-headers-master + +# Build ffmpeg with nvmpi patch +wget -q https://ffmpeg.org/releases/ffmpeg-6.0.tar.xz +tar xaf ffmpeg-*.tar.xz && rm ffmpeg-*.tar.xz && cd ffmpeg-* +patch -p1 < ../jetson-ffmpeg/ffmpeg_patches/ffmpeg6.0_nvmpi.patch +export PKG_CONFIG_PATH=$INSTALL_PREFIX/lib/pkgconfig +# enable Jetson codecs but disable dGPU codecs +./configure --cc='ccache gcc' --cxx='ccache g++' \ + --enable-shared --disable-static --prefix=$INSTALL_PREFIX \ + --enable-gpl --enable-libx264 --enable-libx265 \ + --enable-nvmpi --enable-ffnvcodec --enable-cuda-llvm \ + --disable-cuvid --disable-nvenc --disable-nvdec \ + || { cat ffbuild/config.log && false; } +make -j$(nproc) +make install +cd ../ + +rm -rf /var/lib/apt/lists/* +popd diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/build_python_tensorrt.sh b/sam2-cpu/frigate-dev/docker/tensorrt/detector/build_python_tensorrt.sh new file mode 100755 index 0000000..3251034 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/build_python_tensorrt.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -euxo pipefail + +mkdir -p /trt-wheels + +if [[ "${TARGETARCH}" == "arm64" ]]; then + + # NVIDIA supplies python-tensorrt for python3.10, but frigate uses python3.11, + # so we must build python-tensorrt ourselves. + + # Get python-tensorrt source + mkdir -p /workspace + cd /workspace + git clone -b release/8.6 https://github.com/NVIDIA/TensorRT.git --depth=1 + + # Collect dependencies + EXT_PATH=/workspace/external && mkdir -p $EXT_PATH + pip3 install pybind11 && ln -s /usr/local/lib/python3.11/dist-packages/pybind11 $EXT_PATH/pybind11 + ln -s /usr/include/python3.11 $EXT_PATH/python3.11 + ln -s /usr/include/aarch64-linux-gnu/NvOnnxParser.h /workspace/TensorRT/parsers/onnx/ + + # Build wheel + cd /workspace/TensorRT/python + EXT_PATH=$EXT_PATH PYTHON_MAJOR_VERSION=3 PYTHON_MINOR_VERSION=11 TARGET_ARCHITECTURE=aarch64 TENSORRT_MODULE=tensorrt /bin/bash ./build.sh + mv build/bindings_wheel/dist/*.whl /trt-wheels/ + +fi diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf new file mode 100644 index 0000000..b00d4b4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/ld.so.conf.d/cuda_tensorrt.conf @@ -0,0 +1,6 @@ +/usr/local/lib/python3.11/dist-packages/nvidia/cudnn/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cuda_runtime/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cublas/lib +/usr/local/lib/python3.11/dist-packages/nvidia/cufft/lib +/usr/local/lib/python3.11/dist-packages/nvidia/curand/lib/ +/usr/local/lib/python3.11/dist-packages/nvidia/cuda_nvrtc/lib/ \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/trt-model-prepare b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/frigate/dependencies.d/trt-model-prepare new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/dependencies.d/base b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run new file mode 100755 index 0000000..e3440e7 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/run @@ -0,0 +1,115 @@ +#!/command/with-contenv bash +# shellcheck shell=bash +# Generate models for the TensorRT detector + +# One or more comma-separated models may be specified via the YOLO_MODELS env. +# Append "-dla" to the model name to generate a DLA model with GPU fallback; +# otherwise a GPU-only model will be generated. + +set -o errexit -o nounset -o pipefail + +MODEL_CACHE_DIR=${MODEL_CACHE_DIR:-"/config/model_cache/tensorrt"} +TRT_VER=${TRT_VER:-$(cat /etc/TENSORRT_VER)} +OUTPUT_FOLDER="${MODEL_CACHE_DIR}/${TRT_VER}" +YOLO_MODELS=${YOLO_MODELS:-""} + +# Create output folder +mkdir -p ${OUTPUT_FOLDER} + +FIRST_MODEL=true +MODEL_DOWNLOAD="" +MODEL_CONVERT="" + +if [ -z "$YOLO_MODELS" ]; then + echo "tensorrt model preparation disabled" + exit 0 +fi + +for model in ${YOLO_MODELS//,/ } +do + # Remove old link in case path/version changed + rm -f ${MODEL_CACHE_DIR}/${model}.trt + + if [[ ! -f ${OUTPUT_FOLDER}/${model}.trt ]]; then + if [[ ${FIRST_MODEL} = true ]]; then + MODEL_DOWNLOAD="${model%-dla}"; + MODEL_CONVERT="${model}" + FIRST_MODEL=false; + else + MODEL_DOWNLOAD+=",${model%-dla}"; + MODEL_CONVERT+=",${model}"; + fi + else + ln -s ${OUTPUT_FOLDER}/${model}.trt ${MODEL_CACHE_DIR}/${model}.trt + fi +done + +if [[ -z ${MODEL_CONVERT} ]]; then + echo "No models to convert." + exit 0 +fi + +# Setup ENV to select GPU for conversion +if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then + if [ ! -z ${CUDA_VISIBLE_DEVICES+x} ]; then + PREVIOUS_CVD="$CUDA_VISIBLE_DEVICES" + unset CUDA_VISIBLE_DEVICES + fi + export CUDA_VISIBLE_DEVICES="$TRT_MODEL_PREP_DEVICE" +fi + +# On Jetpack 4.6, the nvidia container runtime will mount several host nvidia libraries into the +# container which should not be present in the image - if they are, TRT model generation will +# fail or produce invalid models. Thus we must request the user to install them on the host in +# order to run libyolo here. +# On Jetpack 5.0, these libraries are not mounted by the runtime and are supplied by the image. +if [[ "$(arch)" == "aarch64" ]]; then + if [[ ! -e /usr/lib/aarch64-linux-gnu/tegra && ! -e /usr/lib/aarch64-linux-gnu/tegra-egl ]]; then + echo "ERROR: Container must be launched with nvidia runtime" + exit 1 + elif [[ ! -e /usr/lib/aarch64-linux-gnu/libnvinfer.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvinfer_plugin.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvparsers.so.8 || + ! -e /usr/lib/aarch64-linux-gnu/libnvonnxparser.so.8 ]]; then + echo "ERROR: Please run the following on the HOST:" + echo " sudo apt install libnvinfer8 libnvinfer-plugin8 libnvparsers8 libnvonnxparsers8 nvidia-container" + exit 1 + fi +fi + +echo "Generating the following TRT Models: ${MODEL_CONVERT}" + +# Build trt engine +cd /usr/local/src/tensorrt_demos/yolo + +echo "Downloading yolo weights" +./download_yolo.sh $MODEL_DOWNLOAD 2> /dev/null + +for model in ${MODEL_CONVERT//,/ } +do + python3 yolo_to_onnx.py -m ${model%-dla} > /dev/null + + echo -e "\nGenerating ${model}.trt. This may take a few minutes.\n"; start=$(date +%s) + if [[ $model == *-dla ]]; then + cmd="python3 onnx_to_tensorrt.py -m ${model%-dla} --dla_core 0" + else + cmd="python3 onnx_to_tensorrt.py -m ${model}" + fi + $cmd > /tmp/onnx_to_tensorrt.log || { cat /tmp/onnx_to_tensorrt.log && continue; } + + mv ${model%-dla}.trt ${OUTPUT_FOLDER}/${model}.trt; + ln -s ${OUTPUT_FOLDER}/${model}.trt ${MODEL_CACHE_DIR}/${model}.trt + echo "Generated ${model}.trt in $(($(date +%s)-start)) seconds" +done + +# Restore ENV after conversion +if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then + unset CUDA_VISIBLE_DEVICES + if [ ! -z ${PREVIOUS_CVD+x} ]; then + export CUDA_VISIBLE_DEVICES="$PREVIOUS_CVD" + fi +fi + +# Print which models exist in output folder +echo "Available tensorrt models:" +cd ${OUTPUT_FOLDER} && ls *.trt; diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up new file mode 100644 index 0000000..b9de40a --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/rootfs/etc/s6-overlay/s6-rc.d/trt-model-prepare/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/trt-model-prepare/run diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/detector/tensorrt_libyolo.sh b/sam2-cpu/frigate-dev/docker/tensorrt/detector/tensorrt_libyolo.sh new file mode 100755 index 0000000..46e4077 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/detector/tensorrt_libyolo.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euxo pipefail + +SCRIPT_DIR="/usr/local/src/tensorrt_demos" + +# Clone tensorrt_demos repo +git clone --depth 1 https://github.com/NateMeyer/tensorrt_demos.git -b conditional_download + +# Build libyolo +if [ ! -e /usr/local/cuda ]; then + ln -s /usr/local/cuda-* /usr/local/cuda +fi +cd ./tensorrt_demos/plugins && make all -j$(nproc) computes="${COMPUTE_LEVEL:-}" +cp libyolo_layer.so /usr/local/lib/libyolo_layer.so + +# Store yolo scripts for later conversion +cd ../ +mkdir -p ${SCRIPT_DIR}/plugins +cp plugins/libyolo_layer.so ${SCRIPT_DIR}/plugins/libyolo_layer.so +cp -a yolo ${SCRIPT_DIR}/ diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/requirements-amd64.txt b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-amd64.txt new file mode 100644 index 0000000..63c68b5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-amd64.txt @@ -0,0 +1,18 @@ +# NVidia TensorRT Support (amd64 only) +--extra-index-url 'https://pypi.nvidia.com' +cython==3.0.*; platform_machine == 'x86_64' +nvidia_cuda_cupti_cu12==12.5.82; platform_machine == 'x86_64' +nvidia-cublas-cu12==12.5.3.*; platform_machine == 'x86_64' +nvidia-cudnn-cu12==9.3.0.*; platform_machine == 'x86_64' +nvidia-cufft-cu12==11.2.3.*; platform_machine == 'x86_64' +nvidia-curand-cu12==10.3.6.*; platform_machine == 'x86_64' +nvidia_cuda_nvcc_cu12==12.5.82; platform_machine == 'x86_64' +nvidia-cuda-nvrtc-cu12==12.5.82; platform_machine == 'x86_64' +nvidia_cuda_runtime_cu12==12.5.82; platform_machine == 'x86_64' +nvidia_cusolver_cu12==11.6.3.*; platform_machine == 'x86_64' +nvidia_cusparse_cu12==12.5.1.*; platform_machine == 'x86_64' +nvidia_nccl_cu12==2.23.4; platform_machine == 'x86_64' +nvidia_nvjitlink_cu12==12.5.82; platform_machine == 'x86_64' +onnx==1.16.*; platform_machine == 'x86_64' +onnxruntime-gpu==1.22.*; platform_machine == 'x86_64' +protobuf==3.20.3; platform_machine == 'x86_64' diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/requirements-arm64.txt b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-arm64.txt new file mode 100644 index 0000000..78d6597 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-arm64.txt @@ -0,0 +1,2 @@ +cuda-python == 12.6.*; platform_machine == 'aarch64' +numpy == 1.26.*; platform_machine == 'aarch64' diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/requirements-models-arm64.txt b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-models-arm64.txt new file mode 100644 index 0000000..fe89b47 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/requirements-models-arm64.txt @@ -0,0 +1,2 @@ +onnx == 1.14.0; platform_machine == 'aarch64' +protobuf == 3.20.3; platform_machine == 'aarch64' diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/trt.hcl b/sam2-cpu/frigate-dev/docker/tensorrt/trt.hcl new file mode 100644 index 0000000..501e871 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/trt.hcl @@ -0,0 +1,105 @@ +variable "ARCH" { + default = "amd64" +} +variable "BASE_IMAGE" { + default = null +} +variable "SLIM_BASE" { + default = null +} +variable "TRT_BASE" { + default = null +} +variable "COMPUTE_LEVEL" { + default = "" +} +variable "BASE_HOOK" { + # Ensure an up-to-date python 3.11 is available in jetson images + default = <> /etc/apt/sources.list.d/deadsnakes.list + echo "deb-src https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu $VERSION_CODENAME main" >> /etc/apt/sources.list.d/deadsnakes.list + + # Add deadsnakes signing key + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F23C5A6CF475977595C89F51BA6932366A755776 +fi +EOT +} + +target "_build_args" { + args = { + BASE_IMAGE = BASE_IMAGE, + SLIM_BASE = SLIM_BASE, + TRT_BASE = TRT_BASE, + COMPUTE_LEVEL = COMPUTE_LEVEL, + BASE_HOOK = BASE_HOOK + } + platforms = ["linux/${ARCH}"] +} + +target wget { + dockerfile = "docker/main/Dockerfile" + target = "wget" + inherits = ["_build_args"] +} + +target deps { + dockerfile = "docker/main/Dockerfile" + target = "deps" + inherits = ["_build_args"] +} + +target rootfs { + dockerfile = "docker/main/Dockerfile" + target = "rootfs" + inherits = ["_build_args"] +} + +target wheels { + dockerfile = "docker/main/Dockerfile" + target = "wheels" + inherits = ["_build_args"] +} + +target devcontainer { + dockerfile = "docker/main/Dockerfile" + platforms = ["linux/amd64"] + target = "devcontainer" +} + +target "trt-deps" { + dockerfile = "docker/tensorrt/Dockerfile.base" + context = "." + contexts = { + deps = "target:deps", + } + inherits = ["_build_args"] +} + +target "tensorrt" { + dockerfile = "docker/tensorrt/Dockerfile.${ARCH}" + context = "." + contexts = { + wget = "target:wget", + wheels = "target:wheels", + deps = "target:deps", + rootfs = "target:rootfs" + } + target = "frigate-tensorrt" + inherits = ["_build_args"] +} + +target "devcontainer-trt" { + dockerfile = "docker/tensorrt/Dockerfile.amd64" + context = "." + contexts = { + wheels = "target:wheels", + trt-deps = "target:trt-deps", + devcontainer = "target:devcontainer" + } + platforms = ["linux/amd64"] + target = "devcontainer-trt" +} diff --git a/sam2-cpu/frigate-dev/docker/tensorrt/trt.mk b/sam2-cpu/frigate-dev/docker/tensorrt/trt.mk new file mode 100644 index 0000000..904a2c3 --- /dev/null +++ b/sam2-cpu/frigate-dev/docker/tensorrt/trt.mk @@ -0,0 +1,41 @@ +BOARDS += trt + +JETPACK5_BASE ?= nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime # L4T 35.3.1 JetPack 5.1.1 +JETPACK6_BASE ?= nvcr.io/nvidia/tensorrt:23.12-py3-igpu +X86_DGPU_ARGS := ARCH=amd64 COMPUTE_LEVEL="50 60 70 80 90" +JETPACK5_ARGS := ARCH=arm64 BASE_IMAGE=$(JETPACK5_BASE) SLIM_BASE=$(JETPACK5_BASE) TRT_BASE=$(JETPACK5_BASE) +JETPACK6_ARGS := ARCH=arm64 BASE_IMAGE=$(JETPACK6_BASE) SLIM_BASE=$(JETPACK6_BASE) TRT_BASE=$(JETPACK6_BASE) + +local-trt: version + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt \ + --load + +local-trt-jp5: version + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt-jp5 \ + --load + +local-trt-jp6: version + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=frigate:latest-tensorrt-jp6 \ + --load + +build-trt: + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp5 + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp6 + +push-trt: build-trt + $(X86_DGPU_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt \ + --push + $(JETPACK5_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp5 \ + --push + $(JETPACK6_ARGS) docker buildx bake --file=docker/tensorrt/trt.hcl tensorrt \ + --set tensorrt.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-tensorrt-jp6 \ + --push diff --git a/sam2-cpu/frigate-dev/docs/.gitignore b/sam2-cpu/frigate-dev/docs/.gitignore new file mode 100644 index 0000000..b2d6de3 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/sam2-cpu/frigate-dev/docs/README.md b/sam2-cpu/frigate-dev/docs/README.md new file mode 100644 index 0000000..68b27e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/README.md @@ -0,0 +1,10 @@ +# Website + +This website is built using [Docusaurus 3.5](https://docusaurus.io/docs), a modern static website generator. + +For installation and contributing instructions, please follow the [Contributing Docs](https://docs.frigate.video/development/contributing). + +# Development + +1. Run `npm i` to install dependencies +2. Run `npm run start` to start the website diff --git a/sam2-cpu/frigate-dev/docs/babel.config.js b/sam2-cpu/frigate-dev/docs/babel.config.js new file mode 100644 index 0000000..e00595d --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/advanced.md b/sam2-cpu/frigate-dev/docs/docs/configuration/advanced.md new file mode 100644 index 0000000..17eb205 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/advanced.md @@ -0,0 +1,275 @@ +--- +id: advanced +title: Advanced Options +sidebar_label: Advanced Options +--- + +### Logging + +#### Frigate `logger` + +Change the default log level for troubleshooting purposes. + +```yaml +logger: + # Optional: default log level (default: shown below) + default: info + # Optional: module by module log level configuration + logs: + frigate.mqtt: error +``` + +Available log levels are: `debug`, `info`, `warning`, `error`, `critical` + +Examples of available modules are: + +- `frigate.app` +- `frigate.mqtt` +- `frigate.object_detection.base` +- `detector.` +- `watchdog.` +- `ffmpeg..` NOTE: All FFmpeg logs are sent as `error` level. + +#### Go2RTC Logging + +See [the go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#module-log) for logging configuration + +```yaml +go2rtc: + streams: + # ... + log: + exec: trace +``` + +### `environment_vars` + +This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS. + +Example: + +```yaml +environment_vars: + VARIABLE_NAME: variable_value +``` + +#### TensorFlow Thread Configuration + +If you encounter thread creation errors during classification model training, you can limit TensorFlow's thread usage: + +```yaml +environment_vars: + TF_INTRA_OP_PARALLELISM_THREADS: "2" # Threads within operations (0 = use default) + TF_INTER_OP_PARALLELISM_THREADS: "2" # Threads between operations (0 = use default) + TF_DATASET_THREAD_POOL_SIZE: "2" # Data pipeline threads (0 = use default) +``` + +### `database` + +Tracked object and recording information is managed in a sqlite database at `/config/frigate.db`. If that database is deleted, recordings will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within Home Assistant. + +If you are storing your database on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary. + +This may need to be in a custom location if network storage is used for the media folder. + +```yaml +database: + path: /path/to/frigate.db +``` + +### `model` + +If using a custom model, the width and height will need to be specified. + +Custom models may also require different input tensor formats. The colorspace conversion supports RGB, BGR, or YUV frames to be sent to the object detector. The input tensor shape parameter is an enumeration to match what specified by the model. + +| Tensor Dimension | Description | +| :--------------: | -------------- | +| N | Batch Size | +| H | Model Height | +| W | Model Width | +| C | Color Channels | + +| Available Input Tensor Shapes | +| :---------------------------: | +| "nhwc" | +| "nchw" | + +```yaml +# Optional: model config +model: + path: /path/to/model + width: 320 + height: 320 + input_tensor: "nhwc" + input_pixel_format: "bgr" +``` + +#### `labelmap` + +:::warning + +If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../configuration/review.md#restricting-alerts-to-specific-labels) for more info. + +::: + +The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model. + +```yaml +model: + labelmap: + 2: vehicle + 3: vehicle + 5: vehicle + 7: vehicle + 15: animal + 16: animal + 17: animal +``` + +Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well. + +:::warning + +Some labels have special handling and modifications can disable functionality. + +`person` objects are associated with `face` and `amazon` + +`car` objects are associated with `license_plate`, `ups`, `fedex`, `amazon` + +::: + +## Network Configuration + +Changes to Frigate's internal network configuration can be made by bind mounting nginx.conf into the container. For example: + +```yaml +services: + frigate: + container_name: frigate + ... + volumes: + ... + - /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf +``` + +### Enabling IPv6 + +IPv6 is disabled by default, to enable IPv6 listen.gotmpl needs to be bind mounted with IPv6 enabled. For example: + +``` +{{ if not .enabled }} +# intended for external traffic, protected by auth +listen 8971; +{{ else }} +# intended for external traffic, protected by auth +listen 8971 ssl; + +# intended for internal traffic, not protected by auth +listen 5000; +``` + +becomes + +``` +{{ if not .enabled }} +# intended for external traffic, protected by auth +listen [::]:8971 ipv6only=off; +{{ else }} +# intended for external traffic, protected by auth +listen [::]:8971 ipv6only=off ssl; + +# intended for internal traffic, not protected by auth +listen [::]:5000 ipv6only=off; +``` + +## Base path + +By default, Frigate runs at the root path (`/`). However some setups require to run Frigate under a custom path prefix (e.g. `/frigate`), especially when Frigate is located behind a reverse proxy that requires path-based routing. + +### Set Base Path via HTTP Header + +The preferred way to configure the base path is through the `X-Ingress-Path` HTTP header, which needs to be set to the desired base path in an upstream reverse proxy. + +For example, in Nginx: + +``` +location /frigate { + proxy_set_header X-Ingress-Path /frigate; + proxy_pass http://frigate_backend; +} +``` + +### Set Base Path via Environment Variable + +When it is not feasible to set the base path via a HTTP header, it can also be set via the `FRIGATE_BASE_PATH` environment variable in the Docker Compose file. + +For example: + +``` +services: + frigate: + image: blakeblackshear/frigate:latest + environment: + - FRIGATE_BASE_PATH=/frigate +``` + +This can be used for example to access Frigate via a Tailscale agent (https), by simply forwarding all requests to the base path (http): + +``` +tailscale serve --https=443 --bg --set-path /frigate http://localhost:5000/frigate +``` + +## Custom Dependencies + +### Custom ffmpeg build + +Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, statically built `ffmpeg` and `ffprobe` binaries can be placed in `/config/custom-ffmpeg/bin` for Frigate to use. + +To do this: + +1. Download your ffmpeg build and uncompress it to the `/config/custom-ffmpeg` folder. Verify that both the `ffmpeg` and `ffprobe` binaries are located in `/config/custom-ffmpeg/bin`. +2. Update the `ffmpeg.path` in your Frigate config to `/config/custom-ffmpeg`. +3. Restart Frigate and the custom version will be used if the steps above were done correctly. + +### Custom go2rtc version + +Frigate currently includes go2rtc v1.9.10, there may be certain cases where you want to run a different version of go2rtc. + +To do this: + +1. Download the go2rtc build to the `/config` folder. +2. Rename the build to `go2rtc`. +3. Give `go2rtc` execute permission. +4. Restart Frigate and the custom version will be used, you can verify by checking go2rtc logs. + +## Validating your config.yml file updates + +When frigate starts up, it checks whether your config file is valid, and if it is not, the process exits. To minimize interruptions when updating your config, you have three options -- you can edit the config via the WebUI which has built in validation, use the config API, or you can validate on the command line using the frigate docker container. + +### Via API + +Frigate can accept a new configuration file as JSON at the `/api/config/save` endpoint. When updating the config this way, Frigate will validate the config before saving it, and return a `400` if the config is not valid. + +```bash +curl -X POST http://frigate_host:5000/api/config/save -d @config.json +``` + +if you'd like you can use your yaml config directly by using [`yq`](https://github.com/mikefarah/yq) to convert it to json: + +```bash +yq -o=json '.' config.yaml | curl -X POST 'http://frigate_host:5000/api/config/save?save_option=saveonly' --data-binary @- +``` + +### Via Command Line + +You can also validate your config at the command line by using the docker container itself. In CI/CD, you leverage the return code to determine if your config is valid, Frigate will return `1` if the config is invalid, or `0` if it's valid. + +```bash +docker run \ + -v $(pwd)/config.yml:/config/config.yml \ + --entrypoint python3 \ + ghcr.io/blakeblackshear/frigate:stable \ + -u -m frigate \ + --validate-config +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/audio_detectors.md b/sam2-cpu/frigate-dev/docs/docs/configuration/audio_detectors.md new file mode 100644 index 0000000..f2ff99b --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/audio_detectors.md @@ -0,0 +1,177 @@ +--- +id: audio_detectors +title: Audio Detectors +--- + +Frigate provides a builtin audio detector which runs on the CPU. Compared to object detection in images, audio detection is a relatively lightweight operation so the only option is to run the detection on a CPU. + +## Configuration + +Audio events work by detecting a type of audio and creating an event, the event will end once the type of audio has not been heard for the configured amount of time. Audio events save a snapshot at the beginning of the event as well as recordings throughout the event. The recordings are retained using the configured recording retention. + +### Enabling Audio Events + +Audio events can be enabled for all cameras or only for specific cameras. + +```yaml + +audio: # <- enable audio events for all camera + enabled: True + +cameras: + front_camera: + ffmpeg: + ... + audio: + enabled: True # <- enable audio events for the front_camera +``` + +If you are using multiple streams then you must set the `audio` role on the stream that is going to be used for audio detection, this can be any stream but the stream must have audio included. + +:::note + +The ffmpeg process for capturing audio will be a separate connection to the camera along with the other roles assigned to the camera, for this reason it is recommended that the go2rtc restream is used for this purpose. See [the restream docs](/configuration/restream.md) for more information. + +::: + +```yaml +cameras: + front_camera: + ffmpeg: + inputs: + - path: rtsp://.../main_stream + roles: + - record + - path: rtsp://.../sub_stream # <- this stream must have audio enabled + roles: + - audio + - detect +``` + +### Configuring Minimum Volume + +The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. + +:::tip + +Volume is considered motion for recordings, this means when the `record -> retain -> mode` is set to `motion` any time audio volume is > min_volume that recording segment for that camera will be kept. + +::: + +### Configuring Audio Events + +The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `scream`, `speech`, and `yell` are enabled but these can be customized. + +```yaml +audio: + enabled: True + listen: + - bark + - fire_alarm + - scream + - speech + - yell +``` + +### Audio Transcription + +Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI’s open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background. + +Transcription accuracy also depends heavily on the quality of your camera's microphone and recording conditions. Many cameras use inexpensive microphones, and distance to the speaker, low audio bitrate, or background noise can significantly reduce transcription quality. If you need higher accuracy, more robust long-running queues, or large-scale automatic transcription, consider using the HTTP API in combination with an automation platform and a cloud transcription service. + +#### Configuration + +To enable transcription, enable it in your config. Note that audio detection must also be enabled as described above in order to use audio transcription features. + +```yaml +audio_transcription: + enabled: True + device: ... + model_size: ... +``` + +Disable audio transcription for select cameras at the camera level: + +```yaml +cameras: + back_yard: + ... + audio_transcription: + enabled: False +``` + +:::note + +Audio detection must be enabled and configured as described above in order to use audio transcription features. + +::: + +The optional config parameters that can be set at the global level include: + +- **`enabled`**: Enable or disable the audio transcription feature. + - Default: `False` + - It is recommended to only configure the features at the global level, and enable it at the individual camera level. +- **`device`**: Device to use to run transcription and translation models. + - Default: `CPU` + - This can be `CPU` or `GPU`. The `sherpa-onnx` models are lightweight and run on the CPU only. The `whisper` models can run on GPU but are only supported on CUDA hardware. +- **`model_size`**: The size of the model used for live transcription. + - Default: `small` + - This can be `small` or `large`. The `small` setting uses `sherpa-onnx` models that are fast, lightweight, and always run on the CPU but are not as accurate as the `whisper` model. + - This config option applies to **live transcription only**. Recorded `speech` events will always use a different `whisper` model (and can be accelerated for CUDA hardware if available with `device: GPU`). +- **`language`**: Defines the language used by `whisper` to translate `speech` audio events (and live audio only if using the `large` model). + - Default: `en` + - You must use a valid [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10). + - Transcriptions for `speech` events are translated. + - Live audio is translated only if you are using the `large` model. The `small` `sherpa-onnx` model is English-only. + +The only field that is valid at the camera level is `enabled`. + +#### Live transcription + +The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate//audio/transcription` will also be updated in real-time with transcribed text. + +Results can be error-prone due to a number of factors, including: + +- Poor quality camera microphone +- Distance of the audio source to the camera microphone +- Low audio bitrate setting in the camera +- Background noise +- Using the `small` model - it's fast, but not accurate for poor quality audio + +For speech sources close to the camera with minimal background noise, use the `small` model. + +If you have CUDA hardware, you can experiment with the `large` `whisper` model on GPU. Performance is not quite as fast as the `sherpa-onnx` `small` model, but live transcription is far more accurate. Using the `large` model with CPU will likely be too slow for real-time transcription. + +#### Transcription and translation of `speech` audio events + +Any `speech` events in Explore can be transcribed and/or translated through the Transcribe button in the Tracked Object Details pane. + +In order to use transcription and translation for past events, you must enable audio detection and define `speech` as an audio type to listen for in your config. To have `speech` events translated into the language of your choice, set the `language` config parameter with the correct [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10). + +The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type. + +:::note + +Only one `speech` event may be transcribed at a time. Frigate does not automatically transcribe `speech` events or implement a queue for long-running transcription model inference. + +::: + +Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. + +#### FAQ + +1. Why doesn't Frigate automatically transcribe all `speech` events? + + Frigate does not implement a queue mechanism for speech transcription, and adding one is not trivial. A proper queue would need backpressure, prioritization, memory/disk buffering, retry logic, crash recovery, and safeguards to prevent unbounded growth when events outpace processing. That’s a significant amount of complexity for a feature that, in most real-world environments, would mostly just churn through low-value noise. + + Because transcription is **serialized (one event at a time)** and speech events can be generated far faster than they can be processed, an auto-transcribe toggle would very quickly create an ever-growing backlog and degrade core functionality. For the amount of engineering and risk involved, it adds **very little practical value** for the majority of deployments, which are often on low-powered, edge hardware. + + If you hear speech that’s actually important and worth saving/indexing for the future, **just press the transcribe button in Explore** on that specific `speech` event - that keeps things explicit, reliable, and under your control. + + Other options are being considered for future versions of Frigate to add transcription options that support external `whisper` Docker containers. A single transcription service could then be shared by Frigate and other applications (for example, Home Assistant Voice), and run on more powerful machines when available. + +2. Why don't you save live transcription text and use that for `speech` events? + + There’s no guarantee that a `speech` event is even created from the exact audio that went through the transcription model. Live transcription and `speech` event creation are **separate, asynchronous processes**. Even when both are correctly configured, trying to align the **precise start and end time of a speech event** with whatever audio the model happened to be processing at that moment is unreliable. + + Automatically persisting that data would often result in **misaligned, partial, or irrelevant transcripts**, while still incurring all of the CPU, storage, and privacy costs of transcription. That’s why Frigate treats transcription as an **explicit, user-initiated action** rather than an automatic side-effect of every `speech` event. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/authentication.md b/sam2-cpu/frigate-dev/docs/docs/configuration/authentication.md new file mode 100644 index 0000000..17718c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/authentication.md @@ -0,0 +1,311 @@ +--- +id: authentication +title: Authentication +--- + +# Authentication + +Frigate stores user information in its database. Password hashes are generated using industry standard PBKDF2-SHA256 with 600,000 iterations. Upon successful login, a JWT token is issued with an expiration date and set as a cookie. The cookie is refreshed as needed automatically. This JWT token can also be passed in the Authorization header as a bearer token. + +Users are managed in the UI under Settings > Users. + +The following ports are available to access the Frigate web UI. + +| Port | Description | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `8971` | Authenticated UI and API. Reverse proxies should use this port. | +| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate and do not support authentication. | + +## Onboarding + +On startup, an admin user and password are generated and printed in the logs. It is recommended to set a new password for the admin account after logging in for the first time under Settings > Users. + +## Resetting admin password + +In the event that you are locked out of your instance, you can tell Frigate to reset the admin password and print it in the logs on next startup using the `reset_admin_password` setting in your config file. + +```yaml +auth: + reset_admin_password: true +``` + +## Login failure rate limiting + +In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with SlowApi, and the string notation for valid values is available in [the documentation](https://limits.readthedocs.io/en/stable/quickstart.html#examples). + +For example, `1/second;5/minute;20/hour` will rate limit the login endpoint when failures occur more than: + +- 1 time per second +- 5 times per minute +- 20 times per hour + +Restarting Frigate will reset the rate limits. + +If you are running Frigate behind a proxy, you will want to set `trusted_proxies` or these rate limits will apply to the upstream proxy IP address. This means that a brute force attack will rate limit login attempts from other devices and could temporarily lock you out of your instance. In order to ensure rate limits only apply to the actual IP address where the requests are coming from, you will need to list the upstream networks that you want to trust. These trusted proxies are checked against the `X-Forwarded-For` header when looking for the IP address where the request originated. + +If you are running a reverse proxy in the same Docker Compose file as Frigate, here is an example of how your auth config might look: + +```yaml +auth: + failed_login_rate_limit: "1/second;5/minute;20/hour" + trusted_proxies: + - 172.18.0.0/16 # <---- this is the subnet for the internal Docker Compose network +``` + +## Session Length + +The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required — otherwise, the user will need to log in again. + +While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds. + +The default value of `86400` will expire the authentication session after 24 hours. Some other examples: + +- `0`: Setting the session length to 0 will require a user to log in every time they access the application or after a very short, immediate timeout. +- `604800`: Setting the session length to 604800 will require a user to log in if the token is not refreshed for 7 days. + +```yaml +auth: + session_length: 86400 +``` + +## JWT Token Secret + +The JWT token secret needs to be kept secure. Anyone with this secret can generate valid JWT tokens to authenticate with Frigate. This should be a cryptographically random string of at least 64 characters. + +You can generate a token using the Python secret library with the following command: + +```shell +python3 -c 'import secrets; print(secrets.token_hex(64))' +``` + +Frigate looks for a JWT token secret in the following order: + +1. An environment variable named `FRIGATE_JWT_SECRET` +2. A file named `FRIGATE_JWT_SECRET` in the directory specified by the `CREDENTIALS_DIRECTORY` environment variable (defaults to the Docker Secrets directory: `/run/secrets/`) +3. A `jwt_secret` option from the Home Assistant Add-on options +4. A `.jwt_secret` file in the config directory + +If no secret is found on startup, Frigate generates one and stores it in a `.jwt_secret` file in the config directory. + +Changing the secret will invalidate current tokens. + +## Proxy configuration + +Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. + +If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication as there is no correspondence between users in Frigate's database and users authenticated via the proxy. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret. + +Here is an example of how to disable Frigate's authentication and also ensure the requests come only from your known proxy. + +```yaml +auth: + enabled: False + +proxy: + auth_secret: +``` + +You can use the following code to generate a random secret. + +```shell +python3 -c 'import secrets; print(secrets.token_hex(64))' +``` + +### Header mapping + +If you have disabled Frigate's authentication and your proxy supports passing a header with authenticated usernames and/or roles, you can use the `header_map` config to specify the header name so it is passed to Frigate. For example, the following will map the `X-Forwarded-User` and `X-Forwarded-Groups` values. Header names are not case sensitive. Multiple values can be included in the role header. Frigate expects that the character separating the roles is a comma, but this can be specified using the `separator` config entry. + +```yaml +proxy: + ... + separator: "|" # This value defaults to a comma, but Authentik uses a pipe, for example. + header_map: + user: x-forwarded-user + role: x-forwarded-groups +``` + +Frigate supports `admin`, `viewer`, and custom roles (see below). When using port `8971`, Frigate validates these headers and subsequent requests use the headers `remote-user` and `remote-role` for authorization. + +A default role can be provided. Any value in the mapped `role` header will override the default. + +```yaml +proxy: + ... + default_role: viewer +``` + +## Role mapping + +In some environments, upstream identity providers (OIDC, SAML, LDAP, etc.) do not pass a Frigate-compatible role directly, but instead pass one or more group claims. To handle this, Frigate supports a `role_map` that translates upstream group names into Frigate’s internal roles (`admin`, `viewer`, or custom). + +```yaml +proxy: + ... + header_map: + user: x-forwarded-user + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer + operator: # Custom role mapping + - operators +``` + +In this example: + +- If the proxy passes a role header containing `sysadmins` or `access-level-security`, the user is assigned the `admin` role. +- If the proxy passes a role header containing `camera-viewer`, the user is assigned the `viewer` role. +- If the proxy passes a role header containing `operators`, the user is assigned the `operator` custom role. +- If no mapping matches, Frigate falls back to `default_role` if configured. +- If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name. + +#### Port Considerations + +**Authenticated Port (8971)** + +- Header mapping is **fully supported**. +- The `remote-role` header determines the user’s privileges: + - **admin** → Full access (user management, configuration changes). + - **viewer** → Read-only access. + - **Custom roles** → Read-only access limited to the cameras defined in `auth.roles[role]`. +- Ensure your **proxy sends both user and role headers** for proper role enforcement. + +**Unauthenticated Port (5000)** + +- Headers are **ignored** for role enforcement. +- All requests are treated as **anonymous**. +- The `remote-role` value is **overridden** to **admin-level access**. +- This design ensures **unauthenticated internal use** within a trusted network. + +Note that only the following list of headers are permitted by default: + +``` +Remote-User +Remote-Groups +Remote-Email +Remote-Name +X-Forwarded-User +X-Forwarded-Groups +X-Forwarded-Email +X-Forwarded-Preferred-Username +X-authentik-username +X-authentik-groups +X-authentik-email +X-authentik-name +X-authentik-uid +``` + +If you would like to add more options, you can overwrite the default file with a docker bind mount at `/usr/local/nginx/conf/proxy_trusted_headers.conf`. Reference the source code for the default file formatting. + +### Login page redirection + +Frigate gracefully performs login page redirection that should work with most authentication proxies. If your reverse proxy returns a `Location` header on `401`, `302`, or `307` unauthorized responses, Frigate's frontend will automatically detect it and redirect to that URL. + +### Custom logout url + +If your reverse proxy has a dedicated logout url, you can specify using the `logout_url` config option. This will update the link for the `Logout` link in the UI. + +## User Roles + +Frigate supports user roles to control access to certain features in the UI and API, such as managing users or modifying configuration settings. Roles are assigned to users in the database or through proxy headers and are enforced when accessing the UI or API through the authenticated port (`8971`). + +### Supported Roles + +- **admin**: Full access to all features, including user management and configuration. +- **viewer**: Read-only access to the UI and API, including viewing cameras, review items, and historical footage. Configuration editor and settings in the UI are inaccessible. +- **Custom Roles**: Arbitrary role names (alphanumeric, dots/underscores) with specific camera permissions. These extend the system for granular access (e.g., "operator" for select cameras). + +### Custom Roles and Camera Access + +The viewer role provides read-only access to all cameras in the UI and API. Custom roles allow admins to limit read-only access to specific cameras. Each role specifies an array of allowed camera names. If a user is assigned a custom role, their account is like the **viewer** role - they can only view Live, Review/History, Explore, and Export for the designated cameras. Backend API endpoints enforce this server-side (e.g., returning 403 for unauthorized cameras), and the frontend UI filters content accordingly (e.g., camera dropdowns show only permitted options). + +### Role Configuration Example + +```yaml +cameras: + front_door: + # ... camera config + side_yard: + # ... camera config + garage: + # ... camera config + +auth: + enabled: true + roles: + operator: # Custom role + - front_door + - garage # Operator can access front and garage + neighbor: + - side_yard +``` + +If you want to provide access to all cameras to a specific user, just use the **viewer** role. + +### Managing User Roles + +1. Log in as an **admin** user via port `8971` (preferred), or unauthenticated via port `5000`. +2. Navigate to **Settings**. +3. In the **Users** section, edit a user’s role by selecting from available roles (admin, viewer, or custom). +4. In the **Roles** section, add/edit/delete custom roles (select cameras via switches). Deleting a role auto-reassigns users to "viewer". + +### Role Enforcement + +When using the authenticated port (`8971`), roles are validated via the JWT token or proxy headers (e.g., `remote-role`). + +On the internal **unauthenticated** port (`5000`), roles are **not enforced**. All requests are treated as **anonymous**, granting access equivalent to the **admin** role without restrictions. + +To use role-based access control, you must connect to Frigate via the **authenticated port (`8971`)** directly or through a reverse proxy. + +### Role Visibility in the UI + +- When logged in via port `8971`, your **username and role** are displayed in the **account menu** (bottom corner). +- When using port `5000`, the UI will always display "anonymous" for the username and "admin" for the role. + +### Managing User Roles + +1. Log in as an **admin** user via port `8971`. +2. Navigate to **Settings > Users**. +3. Edit a user’s role by selecting **admin** or **viewer**. + +## API Authentication Guide + +### Getting a Bearer Token + +To use the Frigate API, you need to authenticate first. Follow these steps to obtain a Bearer token: + +#### 1. Login + +Make a POST request to `/login` with your credentials: + +```bash +curl -i -X POST https://frigate_ip:8971/api/login \ + -H "Content-Type: application/json" \ + -d '{"user": "admin", "password": "your_password"}' +``` + +:::note + +You may need to include `-k` in the argument list in these steps (eg: `curl -k -i -X POST ...`) if your Frigate instance is using a self-signed certificate. + +::: + +The response will contain a cookie with the JWT token. + +#### 2. Using the Bearer Token + +Once you have the token, include it in the Authorization header for subsequent requests: + +```bash +curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile +``` + +#### 3. Token Lifecycle + +- Tokens are valid for the configured session length +- Tokens are automatically refreshed when you visit the `/auth` endpoint +- Tokens are invalidated when the user's password is changed +- Use `/logout` to clear your session cookie diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/autotracking.md b/sam2-cpu/frigate-dev/docs/docs/configuration/autotracking.md new file mode 100644 index 0000000..86179a2 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/autotracking.md @@ -0,0 +1,173 @@ +--- +id: autotracking +title: Camera Autotracking +--- + +An ONVIF-capable, PTZ (pan-tilt-zoom) camera that supports relative movement within the field of view (FOV) can be configured to automatically track moving objects and keep them in the center of the frame. + +![Autotracking example with zooming](/img/frigate-autotracking-example.gif) + +## Autotracking behavior + +Once Frigate determines that an object is not a false positive and has entered one of the required zones, the autotracker will move the PTZ camera to keep the object centered in the frame until the object either moves out of the frame, the PTZ is not capable of any more movement, or Frigate loses track of it. + +Upon loss of tracking, Frigate will scan the region of the lost object for `timeout` seconds. If an object of the same type is found in that region, Frigate will autotrack that new object. + +When tracking has ended, Frigate will return to the camera firmware's PTZ preset specified by the `return_preset` configuration entry. + +## Checking ONVIF camera support + +Frigate autotracking functions with PTZ cameras capable of relative movement within the field of view (as specified in the [ONVIF spec](https://www.onvif.org/specs/srv/ptz/ONVIF-PTZ-Service-Spec-v1712.pdf) as `RelativePanTiltTranslationSpace` having a `TranslationSpaceFov` entry). + +Many cheaper or older PTZs may not support this standard. Frigate will report an error message in the log and disable autotracking if your PTZ is unsupported. + +The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/) can provide a starting point to determine a camera's compatibility with Frigate's autotracking. Look to see if a camera lists `PTZRelative`, `PTZRelativePanTilt` and/or `PTZRelativeZoom`. These features are required for autotracking, but some cameras still fail to respond even if they claim support. + +A growing list of cameras and brands that have been reported by users to work with Frigate's autotracking can be found [here](cameras.md). + +## Configuration + +First, set up a PTZ preset in your camera's firmware and give it a name. If you're unsure how to do this, consult the documentation for your camera manufacturer's firmware. Some tutorials for common brands: [Amcrest](https://www.youtube.com/watch?v=lJlE9-krmrM), [Reolink](https://www.youtube.com/watch?v=VAnxHUY5i5w), [Dahua](https://www.youtube.com/watch?v=7sNbc5U-k54). + +Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset. + +An [ONVIF connection](cameras.md) is required for autotracking to function. Also, a [motion mask](masks.md) over your camera's timestamp and any overlay text is recommended to ensure they are completely excluded from scene change calculations when the camera is moving. + +Note that `autotracking` is disabled by default but can be enabled in the configuration or by MQTT. + +```yaml +cameras: + ptzcamera: + ... + onvif: + # Required: host of the camera being connected to. + # NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0". + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional: Skip TLS verification from the ONVIF server (default: shown below) + tls_insecure: False + # Optional: PTZ camera object autotracking. Keeps a moving object in + # the center of the frame by automatically moving the PTZ camera. + autotracking: + # Optional: enable/disable object autotracking. (default: shown below) + enabled: False + # Optional: calibrate the camera on startup (default: shown below) + # A calibration will move the PTZ in increments and measure the time it takes to move. + # The results are used to help estimate the position of tracked objects after a camera move. + # Frigate will update your config file automatically after a calibration with + # a "movement_weights" entry for the camera. You should then set calibrate_on_startup to False. + calibrate_on_startup: False + # Optional: the mode to use for zooming in/out on objects during autotracking. (default: shown below) + # Available options are: disabled, absolute, and relative + # disabled - don't zoom in/out on autotracked objects, use pan/tilt only + # absolute - use absolute zooming (supported by most PTZ capable cameras) + # relative - use relative zooming (not supported on all PTZs, but makes concurrent pan/tilt/zoom movements) + zooming: disabled + # Optional: A value to change the behavior of zooming on autotracked objects. (default: shown below) + # A lower value will keep more of the scene in view around a tracked object. + # A higher value will zoom in more on a tracked object, but Frigate may lose tracking more quickly. + # The value should be between 0.1 and 0.75 + zoom_factor: 0.3 + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Required: Begin automatically tracking an object when it enters any of the listed zones. + required_zones: + - zone_name + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) + return_preset: home + # Optional: Seconds to delay before returning to preset. (default: shown below) + timeout: 10 + # Optional: Values generated automatically by a camera calibration. Do not modify these manually. (default: shown below) + movement_weights: [] +``` + +## Calibration + +PTZ motors operate at different speeds. Performing a calibration will direct Frigate to measure this speed over a variety of movements and use those measurements to better predict the amount of movement necessary to keep autotracked objects in the center of the frame. + +Calibration is optional, but will greatly assist Frigate in autotracking objects that move across the camera's field of view more quickly. + +To begin calibration, set the `calibrate_on_startup` for your camera to `True` and restart Frigate. Frigate will then make a series of small and large movements with your camera. Don't move the PTZ manually while calibration is in progress. Once complete, camera motion will stop and your config file will be automatically updated with a `movement_weights` parameter to be used in movement calculations. You should not modify this parameter manually. + +After calibration has ended, your PTZ will be moved to the preset specified by `return_preset`. + +:::note + +Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal to avoid excessive network traffic or CPU usage during calibration. Calibration for most PTZs will take about two minutes. The Frigate log will show calibration progress and any errors. + +::: + +At this point, Frigate will be running and will continue to refine and update the `movement_weights` parameter in your config automatically as the PTZ moves during autotracking and more measurements are obtained. + +Before restarting Frigate, you should set `calibrate_on_startup` in your config file to `False`, otherwise your refined `movement_weights` will be overwritten and calibration will occur when starting again. + +You can recalibrate at any time by removing the `movement_weights` parameter, setting `calibrate_on_startup` to `True`, and then restarting Frigate. You may need to recalibrate or remove `movement_weights` from your config altogether if autotracking is erratic. If you change your `return_preset` in any way or if you change your camera's detect `fps` value, a recalibration is also recommended. + +If you initially calibrate with zooming disabled and then enable zooming at a later point, you should also recalibrate. + +## Best practices and considerations + +Every PTZ camera is different, so autotracking may not perform ideally in every situation. This experimental feature was initially developed using an EmpireTech/Dahua SD1A404XB-GNR. + +The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 5 fps is sufficient, but if you plan to track faster moving objects, you may want to increase this slightly. Higher frame rates (> 10fps) will only slow down Frigate and the motion estimator and may lead to dropped frames, especially if you are using experimental zooming. + +A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's debug viewer for your camera to see a thicker colored box around the object currently being autotracked. + +![Autotracking Debug View](/img/autotracking-debug.gif) + +A full-frame zone in `required_zones` is not recommended, especially if you've calibrated your camera and there are `movement_weights` defined in the configuration file. Frigate will continue to autotrack an object that has entered one of the `required_zones`, even if it moves outside of that zone. + +Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](index.md). + +## Zooming + +Zooming is a very experimental feature and may use significantly more CPU when tracking objects than panning/tilting only. + +Absolute zooming makes zoom movements separate from pan/tilt movements. Most PTZ cameras will support absolute zooming. Absolute zooming was developed to be very conservative to work best with a variety of cameras and scenes. Absolute zooming usually will not occur until an object has stopped moving or is moving very slowly. + +Relative zooming attempts to make a zoom movement concurrently with any pan/tilt movements. It was tested to work with some Dahua and Amcrest PTZs. But the ONVIF specification indicates that there no assumption about how the generic zoom range is mapped to magnification, field of view or other physical zoom dimension when using relative zooming. So if relative zooming behavior is erratic or just doesn't work, try absolute zooming. + +You can optionally adjust the `zoom_factor` for your camera in your configuration file. Lower values will leave more space from the scene around the tracked object while higher values will cause your camera to zoom in more on the object. However, keep in mind that Frigate needs a fair amount of pixels and scene details outside of the bounding box of the tracked object to estimate the motion of your camera. If the object is taking up too much of the frame, Frigate will not be able to track the motion of the camera and your object will be lost. + +The range of this option is from 0.1 to 0.75. The default value of 0.3 is conservative and should be sufficient for most users. Because every PTZ and scene is different, you should experiment to determine what works best for you. + +## Usage applications + +In security and surveillance, it's common to use "spotter" cameras in combination with your PTZ. When your fixed spotter camera detects an object, you could use an automation platform like Home Assistant to move the PTZ to a specific preset so that Frigate can begin automatically tracking the object. For example: a residence may have fixed cameras on the east and west side of the property, capturing views up and down a street. When the spotter camera on the west side detects a person, a Home Assistant automation could move the PTZ to a camera preset aimed toward the west. When the object enters the specified zone, Frigate's autotracker could then continue to track the person as it moves out of view of any of the fixed cameras. + +## Troubleshooting and FAQ + +### The autotracker loses track of my object. Why? + +There are many reasons this could be the case. If you are using experimental zooming, your `zoom_factor` value might be too high, the object might be traveling too quickly, the scene might be too dark, there are not enough details in the scene (for example, a PTZ looking down on a driveway or other monotone background without a sufficient number of hard edges or corners), or the scene is otherwise less than optimal for Frigate to maintain tracking. + +Your camera's shutter speed may also be set too low so that blurring occurs with motion. Check your camera's firmware to see if you can increase the shutter speed. + +Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it. + +### I'm seeing an error in the logs that my camera "is still in ONVIF 'MOVING' status." What does this mean? + +There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware. + +### I tried calibrating my camera, but the logs show that it is stuck at 0% and Frigate is not starting up. + +This is often caused by the same reason as above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking. + +### I'm seeing this error in the logs: "Autotracker: motion estimator couldn't get transformations". What does this mean? + +To maintain object tracking during PTZ moves, Frigate tracks the motion of your camera based on the details of the frame. If you are seeing this message, it could mean that your `zoom_factor` may be set too high, the scene around your detected object does not have enough details (like hard edges or color variations), or your camera's shutter speed is too slow and motion blur is occurring. Try reducing `zoom_factor`, finding a way to alter the scene around your object, or changing your camera's shutter speed. + +### Calibration seems to have completed, but the camera is not actually moving to track my object. Why? + +Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever. + +### Frigate reports an error saying that calibration has failed. Why? + +Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner. You can try running without calibration (just remove the `movement_weights` line from your config and restart), but if calibration fails, this often means that autotracking will behave unpredictably. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/bird_classification.md b/sam2-cpu/frigate-dev/docs/docs/configuration/bird_classification.md new file mode 100644 index 0000000..3987292 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/bird_classification.md @@ -0,0 +1,31 @@ +--- +id: bird_classification +title: Bird Classification +--- + +Bird classification identifies known birds using a quantized Tensorflow model. When a known bird is recognized, its common name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. + +## Minimum System Requirements + +Bird classification runs a lightweight tflite model on the CPU, there are no significantly different system requirements than running Frigate itself. + +## Model + +The classification model used is the MobileNet INat Bird Classification, [available identifiers can be found here.](https://raw.githubusercontent.com/google-coral/test_data/master/inat_bird_labels.txt) + +## Configuration + +Bird classification is disabled by default, it must be enabled in your config file before it can be used. Bird classification is a global configuration setting. + +```yaml +classification: + bird: + enabled: true +``` + +## Advanced Configuration + +Fine-tune bird classification with these optional parameters: + +- `threshold`: Classification confidence score required to set the sub label on the object. + - Default: `0.9`. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/birdseye.md b/sam2-cpu/frigate-dev/docs/docs/configuration/birdseye.md new file mode 100644 index 0000000..d4bd1a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/birdseye.md @@ -0,0 +1,111 @@ +# Birdseye + +In addition to Frigate's Live camera dashboard, Birdseye allows a portable heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about. + +Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the "+" icon on the Live page, and choose "Birdseye" as one of the cameras. + +Birdseye can also be used in Home Assistant dashboards, cast to media devices, etc. + +## Birdseye Behavior + +### Birdseye Modes + +Birdseye offers different modes to customize which cameras show under which circumstances. + +- **continuous:** All cameras are always included +- **motion:** Cameras that have detected motion within the last 30 seconds are included +- **objects:** Cameras that have tracked an active object within the last 30 seconds are included + +### Custom Birdseye Icon + +A custom icon can be added to the birdseye background by providing a 180x180 image named `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view. + +### Birdseye view override at camera level + +If you want to include a camera in Birdseye view only for specific circumstances, or just don't include it at all, the Birdseye setting can be set at the camera level. + +```yaml +# Include all cameras by default in Birdseye view +birdseye: + enabled: True + mode: continuous + +cameras: + front: + # Only include the "front" camera in Birdseye view when objects are detected + birdseye: + mode: objects + back: + # Exclude the "back" camera from Birdseye view + birdseye: + enabled: False +``` + +### Birdseye Inactivity + +By default birdseye shows all cameras that have had the configured activity in the last 30 seconds, this can be configured: + +```yaml +birdseye: + enabled: True + inactivity_threshold: 15 +``` + +## Birdseye Layout + +### Birdseye Dimensions + +The resolution and aspect ratio of birdseye can be configured. Resolution will increase the quality but does not affect the layout. Changing the aspect ratio of birdseye does affect how cameras are laid out. + +```yaml +birdseye: + enabled: True + width: 1280 + height: 720 +``` + +### Sorting cameras in the Birdseye view + +It is possible to override the order of cameras that are being shown in the Birdseye view. +The order needs to be set at the camera level. + +```yaml +# Include all cameras by default in Birdseye view +birdseye: + enabled: True + mode: continuous + +cameras: + front: + birdseye: + order: 1 + back: + birdseye: + order: 2 +``` + +_Note_: Cameras are sorted by default using their name to ensure a constant view inside Birdseye. + +### Birdseye Cameras + +It is possible to limit the number of cameras shown on birdseye at one time. When this is enabled, birdseye will show the cameras with most recent activity. There is a cooldown to ensure that cameras do not switch too frequently. + +For example, this can be configured to only show the most recently active camera. + +```yaml +birdseye: + enabled: True + layout: + max_cameras: 1 +``` + +### Birdseye Scaling + +By default birdseye tries to fit 2 cameras in each row and then double in size until a suitable layout is found. The scaling can be configured with a value between 1.0 and 5.0 depending on use case. + +```yaml +birdseye: + enabled: True + layout: + scaling_factor: 3.0 +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/camera_specific.md b/sam2-cpu/frigate-dev/docs/docs/configuration/camera_specific.md new file mode 100644 index 0000000..802b265 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/camera_specific.md @@ -0,0 +1,287 @@ +--- +id: camera_specific +title: Camera Specific Configurations +--- + +:::note + +This page makes use of presets of FFmpeg args. For more information on presets, see the [FFmpeg Presets](/configuration/ffmpeg_presets) page. + +::: + +:::note + +Many cameras support encoding options which greatly affect the live view experience, see the [Live view](/configuration/live) page for more info. + +::: + +## H.265 Cameras via Safari + +Some cameras support h265 with different formats, but Safari only supports the annexb format. When using h265 camera streams for recording with devices that use the Safari browser, the `apple_compatibility` option should be used. + +```yaml +cameras: + h265_cam: # <------ Doesn't matter what the camera is called + ffmpeg: + apple_compatibility: true # <- Adds compatibility with MacOS and iPhone +``` + +## MJPEG Cameras + +Note that mjpeg cameras require encoding the video into h264 for recording, and restream roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. It is recommended to use the restream role to create an h264 restream and then use that as the source for ffmpeg. + +```yaml +go2rtc: + streams: + mjpeg_cam: "ffmpeg:http://your_mjpeg_stream_url#video=h264#hardware" # <- use hardware acceleration to create an h264 stream usable for other components. + +cameras: + ... + mjpeg_cam: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/mjpeg_cam + roles: + - detect + - record +``` + +## JPEG Stream Cameras + +Cameras using a live changing jpeg image will need input parameters as below + +```yaml +input_args: preset-http-jpeg-generic +``` + +Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras) + +## RTMP Cameras + +The input parameters need to be adjusted for RTMP cameras + +```yaml +ffmpeg: + input_args: preset-rtmp-generic +``` + +## UDP Only Cameras + +If your cameras do not support TCP connections for RTSP, you can use UDP. + +```yaml +ffmpeg: + input_args: preset-rtsp-udp +``` + +## Model/vendor specific setup + +### Amcrest & Dahua + +Amcrest & Dahua cameras should be connected to via RTSP using the following format: + +``` +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=0 # this is the main stream +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=1 # this is the sub stream, typically supporting low resolutions only +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=2 # higher end cameras support a third stream with a mid resolution (1280x720, 1920x1080) +rtsp://USERNAME:PASSWORD@CAMERA-IP/cam/realmonitor?channel=1&subtype=3 # new higher end cameras support a fourth stream with another mid resolution (1280x720, 1920x1080) + +``` + +### Annke C800 + +This camera is H.265 only. To be able to play clips on some devices (like MacOs or iPhone) the H.265 stream has to be adjusted using the `apple_compatibility` config. + +```yaml +cameras: + annkec800: # <------ Name the camera + ffmpeg: + apple_compatibility: true # <- Adds compatibility with MacOS and iPhone + output_args: + record: preset-record-generic-audio-aac + + inputs: + - path: rtsp://USERNAME:PASSWORD@CAMERA-IP/H264/ch1/main/av_stream # <----- Update for your camera + roles: + - detect + - record + detect: + width: # <- optional, by default Frigate tries to automatically detect resolution + height: # <- optional, by default Frigate tries to automatically detect resolution +``` + +### Blue Iris RTSP Cameras + +You will need to remove `nobuffer` flag for Blue Iris RTSP cameras + +```yaml +ffmpeg: + input_args: preset-rtsp-blue-iris +``` + +### Hikvision Cameras + +Hikvision cameras should be connected to via RTSP using the following format: + +``` +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/101 # this is the main stream +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/102 # this is the sub stream, typically supporting low resolutions only +rtsp://USERNAME:PASSWORD@CAMERA-IP/streaming/channels/103 # higher end cameras support a third stream with a mid resolution (1280x720, 1920x1080) +``` + +:::note + +[Some users have reported](https://www.reddit.com/r/frigate_nvr/comments/1hg4ze7/hikvision_security_settings) that newer Hikvision cameras require adjustments to the security settings: + +``` +RTSP Authentication - digest/basic +RTSP Digest Algorithm - MD5 +WEB Authentication - digest/basic +WEB Digest Algorithm - MD5 +``` + +::: + +### Reolink Cameras + +Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations. + +| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | +| ----------------- | ------------------------- | --------------------------------- | ----------------------------------------------------------------------- | +| 5MP or lower | All | http-flv | Stream is h264 | +| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 | +| 6MP or higher | Older (ex: RLC-8##) | rtsp | | + +Frigate works much better with newer reolink cameras that are setup with the below options: + +If available, recommended settings are: + +- `On, fluency first` this sets the camera to CBR (constant bit rate) +- `Interframe Space 1x` this sets the iframe interval to the same as the frame rate + +According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink. + +Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels. +The setup of main stream can be also done via RTSP, but isn't always reliable on all hardware versions. The example configuration is working with the oldest HW version RLN16-410 device with multiple types of cameras. + +
+ Example Config + +:::tip + +Reolink's latest cameras support two way audio via go2rtc and other applications. It is important that the http-flv stream is still used for stability, a secondary rtsp stream can be added that will be using for the two way audio only. + +NOTE: The RTSP stream can not be prefixed with `ffmpeg:`, as go2rtc needs to handle the stream to support two way audio. + +Ensure HTTP is enabled in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). + +::: + +```yaml +go2rtc: + streams: + # example for connecting to a standard Reolink camera + your_reolink_camera: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" + your_reolink_camera_sub: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + # example for connectin to a Reolink camera that supports two way talk + your_reolink_camera_twt: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" + - "rtsp://username:password@reolink_ip/Preview_01_sub + your_reolink_camera_twt_sub: + - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" + - "rtsp://username:password@reolink_ip/Preview_01_sub + # example for connecting to a Reolink NVR + your_reolink_camera_via_nvr: + - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_main.bcs&user=username&password=password" # channel numbers are 0-15 + - "ffmpeg:your_reolink_camera_via_nvr#audio=aac" + your_reolink_camera_via_nvr_sub: + - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_ext.bcs&user=username&password=password" + +cameras: + your_reolink_camera: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/your_reolink_camera + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/your_reolink_camera_sub + input_args: preset-rtsp-restream + roles: + - detect + reolink_via_nvr: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/your_reolink_camera_via_nvr?video=copy&audio=aac + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/your_reolink_camera_via_nvr_sub?video=copy + input_args: preset-rtsp-restream + roles: + - detect +``` +
+ +### Unifi Protect Cameras + +Unifi protect cameras require the rtspx stream to be used with go2rtc. +To utilize a Unifi protect camera, modify the rtsps link to begin with rtspx. +Additionally, remove the "?enableSrtp" from the end of the Unifi link. + +```yaml +go2rtc: + streams: + front: + - rtspx://192.168.1.1:7441/abcdefghijk +``` + +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-rtsp) + +In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect. + +```yaml +ffmpeg: + output_args: + record: preset-record-ubiquiti +``` + +### TP-Link VIGI Cameras + +TP-Link VIGI cameras need some adjustments to the main stream settings on the camera itself to avoid issues. The stream needs to be configured as `H264` with `Smart Coding` set to `off`. Without these settings you may have problems when trying to watch recorded footage. For example Firefox will stop playback after a few seconds and show the following error message: `The media playback was aborted due to a corruption problem or because the media used features your browser did not support.`. + +## USB Cameras (aka Webcams) + +To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's [FFmpeg Device](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg-device) support: + +- Preparation outside of Frigate: + + - Get USB camera path. Run `v4l2-ctl --list-devices` to get a listing of locally-connected cameras available. (You may need to install `v4l-utils` in a way appropriate for your Linux distribution). In the sample configuration below, we use `video=0` to correlate with a detected device path of `/dev/video0` + - Get USB camera formats & resolutions. Run `ffmpeg -f v4l2 -list_formats all -i /dev/video0` to get an idea of what formats and resolutions the USB Camera supports. In the sample configuration below, we use a width of 1024 and height of 576 in the stream and detection settings based on what was reported back. + - If using Frigate in a container (e.g. Docker on TrueNAS), ensure you have USB Passthrough support enabled, along with a specific Host Device (`/dev/video0`) + Container Device (`/dev/video0`) listed. + +- In your Frigate Configuration File, add the go2rtc stream and roles as appropriate: + +``` +go2rtc: + streams: + usb_camera: + - "ffmpeg:device?video=0&video_size=1024x576#video=h264" + +cameras: + usb_camera: + enabled: true + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/usb_camera + input_args: preset-rtsp-restream + roles: + - detect + - record + detect: + enabled: false # <---- disable detection until you have a working camera feed + width: 1024 + height: 576 +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/cameras.md b/sam2-cpu/frigate-dev/docs/docs/configuration/cameras.md new file mode 100644 index 0000000..8048e98 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/cameras.md @@ -0,0 +1,144 @@ +--- +id: cameras +title: Camera Configuration +--- + +## Setting Up Camera Inputs + +Several inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa. + +A camera is enabled by default but can be disabled by using `enabled: False`. Cameras that are disabled through the configuration file will not appear in the Frigate UI and will not consume system resources. + +Each role can only be assigned to one input per camera. The options for roles are as follows: + +| Role | Description | +| -------- | ----------------------------------------------------------------------------------- | +| `detect` | Main feed for object detection. [docs](object_detectors.md) | +| `record` | Saves segments of the video feed based on configuration settings. [docs](record.md) | +| `audio` | Feed for audio based detection. [docs](audio_detectors.md) | + +```yaml +mqtt: + host: mqtt.server.com +cameras: + back: + enabled: True + ffmpeg: + inputs: + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + roles: + - detect + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live + roles: + - record + detect: + width: 1280 # <- optional, by default Frigate tries to automatically detect resolution + height: 720 # <- optional, by default Frigate tries to automatically detect resolution +``` + +Additional cameras are simply added to the config under the `cameras` entry. + +```yaml +mqtt: ... +cameras: + back: ... + front: ... + side: ... +``` + +:::note + +If you only define one stream in your `inputs` and do not assign a `detect` role to it, Frigate will automatically assign it the `detect` role. Frigate will always decode a stream to support motion detection, Birdseye, the API image endpoints, and other features, even if you have disabled object detection with `enabled: False` in your config's `detect` section. + +If you plan to use Frigate for recording only, it is still recommended to define a `detect` role for a low resolution stream to minimize resource usage from the required stream decoding. + +::: + +For camera model specific settings check the [camera specific](camera_specific.md) infos. + +## Setting up camera PTZ controls + +:::warning + +Not every PTZ supports ONVIF, which is the standard protocol Frigate uses to communicate with your camera. Check the [official list of ONVIF conformant products](https://www.onvif.org/conformant-products/), your camera documentation, or camera manufacturer's website to ensure your PTZ supports ONVIF. Also, ensure your camera is running the latest firmware. + +::: + +Add the onvif section to your camera in your configuration file: + +```yaml +cameras: + back: + ffmpeg: ... + onvif: + host: 10.0.10.10 + port: 8000 + user: admin + password: password +``` + +If the ONVIF connection is successful, PTZ controls will be available in the camera's WebUI. + +:::tip + +If your ONVIF camera does not require authentication credentials, you may still need to specify an empty string for `user` and `password`, eg: `user: ""` and `password: ""`. + +::: + +An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. For autotracking setup, see the [autotracking](autotracking.md) docs. + +## ONVIF PTZ camera recommendations + +This list of working and non-working PTZ cameras is based on user feedback. If you'd like to report specific quirks or issues with a manufacturer or camera that would be helpful for other users, open a pull request to add to this list. + +The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/) can provide a starting point to determine a camera's compatibility with Frigate's autotracking. Look to see if a camera lists `PTZRelative`, `PTZRelativePanTilt` and/or `PTZRelativeZoom`. These features are required for autotracking, but some cameras still fail to respond even if they claim support. If they are missing, autotracking will not work (though basic PTZ in the WebUI might). Avoid cameras with no database entry unless they are confirmed as working below. + +| Brand or specific camera | PTZ Controls | Autotracking | Notes | +| ---------------------------- | :----------: | :----------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | +| Amcrest | ✅ | ✅ | ⛔️ Generally, Amcrest should work, but some older models (like the common IP2M-841) don't support autotracking | +| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 | +| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. | +| Amcrest IP5M-1190EW | ✅ | ❌ | ONVIF Port: 80. FOV relative movement not supported. | +| Annke CZ504 | ✅ | ✅ | Annke support provide specific firmware ([V5.7.1 build 250227](https://github.com/pierrepinon/annke_cz504/raw/refs/heads/main/digicap_V5-7-1_build_250227.dav)) to fix issue with ONVIF "TranslationSpaceFov" | +| Ctronics PTZ | ✅ | ❌ | | +| Dahua | ✅ | ✅ | Some low-end Dahuas (lite series, picoo series (commonly), among others) have been reported to not support autotracking. These models usually don't have a four digit model number with chassis prefix and options postfix (e.g. DH-P5AE-PV vs DH-SD49825GB-HNR). | +| Dahua DH-SD2A500HB | ✅ | ❌ | | +| Dahua DH-SD49825GB-HNR | ✅ | ✅ | | +| Dahua DH-P5AE-PV | ❌ | ❌ | | +| Foscam | ✅ | ❌ | In general support PTZ, but not relative move. There are no official ONVIF certifications and tests available on the ONVIF Conformant Products Database | | +| Foscam R5 | ✅ | ❌ | | +| Foscam SD4 | ✅ | ❌ | | +| Hanwha XNP-6550RH | ✅ | ❌ | | +| Hikvision | ✅ | ❌ | Incomplete ONVIF support (MoveStatus won't update even on latest firmware) - reported with HWP-N4215IH-DE and DS-2DE3304W-DE, but likely others | +| Hikvision DS-2DE3A404IWG-E/W | ✅ | ✅ | | +| Reolink | ✅ | ❌ | | +| Speco O8P32X | ✅ | ❌ | | +| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatable. | +| Tapo | ✅ | ❌ | Many models supported, ONVIF Service Port: 2020 | +| Uniview IPC672LR-AX4DUPK | ✅ | ❌ | Firmware says FOV relative movement is supported, but camera doesn't actually move when sending ONVIF commands | +| Uniview IPC6612SR-X33-VG | ✅ | ✅ | Leave `calibrate_on_startup` as `False`. A user has reported that zooming with `absolute` is working. | +| Vikylin PTZ-2804X-I2 | ❌ | ❌ | Incomplete ONVIF support | + +## Setting up camera groups + +:::tip + +It is recommended to set up camera groups using the UI. + +::: + +Cameras can be grouped together and assigned a name and icon, this allows them to be reviewed and filtered together. There will always be the default group for all cameras. + +```yaml +camera_groups: + front: + cameras: + - driveway_cam + - garage_cam + icon: LuCar + order: 0 +``` + +## Two-Way Audio + +See the guide [here](/configuration/live/#two-way-talk) diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/object_classification.md b/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/object_classification.md new file mode 100644 index 0000000..f94e5e0 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/object_classification.md @@ -0,0 +1,116 @@ +--- +id: object_classification +title: Object Classification +--- + +Object classification allows you to train a custom MobileNetV2 classification model to run on tracked objects (persons, cars, animals, etc.) to identify a finer category or attribute for that object. + +## Minimum System Requirements + +Object classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate. + +Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer. + +## Classes + +Classes are the categories your model will learn to distinguish between. Each class represents a distinct visual category that the model will predict. + +For object classification: + +- Define classes that represent different types or attributes of the detected object +- Examples: For `person` objects, classes might be `delivery_person`, `resident`, `stranger` +- Include a `none` class for objects that don't fit any specific category +- Keep classes visually distinct to improve accuracy + +### Classification Type + +- **Sub label**: + + - Applied to the object’s `sub_label` field. + - Ideal for a single, more specific identity or type. + - Example: `cat` → `Leo`, `Charlie`, `None`. + +- **Attribute**: + - Added as metadata to the object (visible in /events): `: `. + - Ideal when multiple attributes can coexist independently. + - Example: Detecting if a `person` in a construction yard is wearing a helmet or not. + +## Assignment Requirements + +Sub labels and attributes are only assigned when both conditions are met: + +1. **Threshold**: Each classification attempt must have a confidence score that meets or exceeds the configured `threshold` (default: `0.8`). +2. **Class Consensus**: After at least 3 classification attempts, 60% of attempts must agree on the same class label. If the consensus class is `none`, no assignment is made. + +This two-step verification prevents false positives by requiring consistent predictions across multiple frames before assigning a sub label or attribute. + +## Example use cases + +### Sub label + +- **Known pet vs unknown**: For `dog` objects, set sub label to your pet’s name (e.g., `buddy`) or `none` for others. +- **Mail truck vs normal car**: For `car`, classify as `mail_truck` vs `car` to filter important arrivals. +- **Delivery vs non-delivery person**: For `person`, classify `delivery` vs `visitor` based on uniform/props. + +### Attributes + +- **Backpack**: For `person`, add attribute `backpack: yes/no`. +- **Helmet**: For `person` (worksite), add `helmet: yes/no`. +- **Leash**: For `dog`, add `leash: yes/no` (useful for park or yard rules). +- **Ladder rack**: For `truck`, add `ladder_rack: yes/no` to flag service vehicles. + +## Configuration + +Object classification is configured as a custom classification model. Each model has its own name and settings. You must list which object labels should be classified. + +```yaml +classification: + custom: + dog: + threshold: 0.8 + object_config: + objects: [dog] # object labels to classify + classification_type: sub_label # or: attribute +``` + +## Training the model + +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of two steps: + +### Step 1: Name and Define + +Enter a name for your model, select the object label to classify (e.g., `person`, `dog`, `car`), choose the classification type (sub label or attribute), and define your classes. Include a `none` class for objects that don't fit any specific category. + +### Step 2: Assign Training Examples + +The system will automatically generate example images from detected objects matching your selected label. You'll be guided through each class one at a time to select which images represent that class. Any images not assigned to a specific class will automatically be assigned to `none` when you complete the last class. Once all images are processed, training will begin automatically. + +When choosing which objects to classify, start with a small number of visually distinct classes and ensure your training samples match camera viewpoints and distances typical for those objects. + +### Improving the Model + +- **Problem framing**: Keep classes visually distinct and relevant to the chosen object types. +- **Data collection**: Use the model’s Recent Classification tab to gather balanced examples across times of day, weather, and distances. +- **Preprocessing**: Ensure examples reflect object crops similar to Frigate’s boxes; keep the subject centered. +- **Labels**: Keep label names short and consistent; include a `none` class if you plan to ignore uncertain predictions for sub labels. +- **Threshold**: Tune `threshold` per model to reduce false assignments. Start at `0.8` and adjust based on validation. + +## Debugging Classification Models + +To troubleshoot issues with object classification models, enable debug logging to see detailed information about classification attempts, scores, and consensus calculations. + +Enable debug logs for classification models by adding `frigate.data_processing.real_time.custom_classification: debug` to your `logger` configuration. These logs are verbose, so only keep this enabled when necessary. Restart Frigate after this change. + +```yaml +logger: + default: info + logs: + frigate.data_processing.real_time.custom_classification: debug +``` + +The debug logs will show: + +- Classification probabilities for each attempt +- Whether scores meet the threshold requirement +- Consensus calculations and when assignments are made +- Object classification history and weighted scores diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/state_classification.md b/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/state_classification.md new file mode 100644 index 0000000..2b7d16d --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/custom_classification/state_classification.md @@ -0,0 +1,103 @@ +--- +id: state_classification +title: State Classification +--- + +State classification allows you to train a custom MobileNetV2 classification model on a fixed region of your camera frame(s) to determine a current state. The model can be configured to run on a schedule and/or when motion is detected in that region. + +## Minimum System Requirements + +State classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate. + +Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer. + +## Classes + +Classes are the different states an area on your camera can be in. Each class represents a distinct visual state that the model will learn to recognize. + +For state classification: + +- Define classes that represent mutually exclusive states +- Examples: `open` and `closed` for a garage door, `on` and `off` for lights +- Use at least 2 classes (typically binary states work best) +- Keep class names clear and descriptive + +## Example use cases + +- **Door state**: Detect if a garage or front door is open vs closed. +- **Gate state**: Track if a driveway gate is open or closed. +- **Trash day**: Bins at curb vs no bins present. +- **Pool cover**: Cover on vs off. + +## Configuration + +State classification is configured as a custom classification model. Each model has its own name and settings. You must provide at least one camera crop under `state_config.cameras`. + +```yaml +classification: + custom: + front_door: + threshold: 0.8 + state_config: + motion: true # run when motion overlaps the crop + interval: 10 # also run every N seconds (optional) + cameras: + front: + crop: [0, 180, 220, 400] +``` + +## Training the model + +Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps: + +### Step 1: Name and Define + +Enter a name for your model and define at least 2 classes (states) that represent mutually exclusive states. For example, `open` and `closed` for a door, or `on` and `off` for lights. + +### Step 2: Select the Crop Area + +Choose one or more cameras and draw a rectangle over the area of interest for each camera. The crop should be tight around the region you want to classify to avoid extra signals unrelated to what is being classified. You can drag and resize the rectangle to adjust the crop area. + +### Step 3: Assign Training Examples + +The system will automatically generate example images from your camera feeds. You'll be guided through each class one at a time to select which images represent that state. It's not strictly required to select all images you see. If a state is missing from the samples, you can train it from the Recent tab later. + +Once some images are assigned, training will begin automatically. + +### Improving the Model + +- **Problem framing**: Keep classes visually distinct and state-focused (e.g., `open`, `closed`, `unknown`). Avoid combining object identity with state in a single model unless necessary. +- **Data collection**: Use the model's Recent Classifications tab to gather balanced examples across times of day and weather. +- **When to train**: Focus on cases where the model is entirely incorrect or flips between states when it should not. There's no need to train additional images when the model is already working consistently. +- **Selecting training images**: Images scoring below 100% due to new conditions (e.g., first snow of the year, seasonal changes) or variations (e.g., objects temporarily in view, insects at night) are good candidates for training, as they represent scenarios different from the default state. Training these lower-scoring images that differ from existing training data helps prevent overfitting. Avoid training large quantities of images that look very similar, especially if they already score 100% as this can lead to overfitting. + +## Debugging Classification Models + +To troubleshoot issues with state classification models, enable debug logging to see detailed information about classification attempts, scores, and state verification. + +Enable debug logs for classification models by adding `frigate.data_processing.real_time.custom_classification: debug` to your `logger` configuration. These logs are verbose, so only keep this enabled when necessary. Restart Frigate after this change. + +```yaml +logger: + default: info + logs: + frigate.data_processing.real_time.custom_classification: debug +``` + +The debug logs will show: + +- Classification probabilities for each attempt +- Whether scores meet the threshold requirement +- State verification progress (consecutive detections needed) +- When state changes are published + +### Recent Classifications + +For state classification, images are only added to recent classifications under specific circumstances: + +- **First detection**: The first classification attempt for a camera is always saved +- **State changes**: Images are saved when the detected state differs from the current verified state +- **Pending verification**: Images are saved when there's a pending state change being verified (requires 3 consecutive identical states) +- **Low confidence**: Images with scores below 100% are saved even if the state matches the current state (useful for training) + +Images are **not** saved when the state is stable (detected state matches current state) **and** the score is 100%. This prevents unnecessary storage of redundant high-confidence classifications. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/face_recognition.md b/sam2-cpu/frigate-dev/docs/docs/configuration/face_recognition.md new file mode 100644 index 0000000..713671a --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/face_recognition.md @@ -0,0 +1,213 @@ +--- +id: face_recognition +title: Face Recognition +--- + +Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known `person` is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. + +## Model Requirements + +### Face Detection + +When running a Frigate+ model (or any custom model that natively detects faces) should ensure that `face` is added to the [list of objects to track](../plus/#available-label-types) either globally or for a specific camera. This will allow face detection to run at the same time as object detection and be more efficient. + +When running a default COCO model or another model that does not include `face` as a detectable label, face detection will run via CV2 using a lightweight DNN model that runs on the CPU. In this case, you should _not_ define `face` in your list of objects to track. + +:::note + +Frigate needs to first detect a `person` before it can detect and recognize a face. + +::: + +### Face Recognition + +Frigate has support for two face recognition model types: + +- **small**: Frigate will run a FaceNet embedding model to recognize faces, which runs locally on the CPU. This model is optimized for efficiency and is not as accurate. +- **large**: Frigate will run a large ArcFace embedding model that is optimized for accuracy. It is only recommended to be run when an integrated or dedicated GPU / NPU is available. + +In both cases, a lightweight face landmark detection model is also used to align faces before running recognition. + +All of these features run locally on your system. + +## Minimum System Requirements + +The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently. + +The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + +## Configuration + +Face recognition is disabled by default, face recognition must be enabled in the UI or in your config file before it can be used. Face recognition is a global configuration setting. + +```yaml +face_recognition: + enabled: true +``` + +Like the other real-time processors in Frigate, face recognition runs on the camera stream defined by the `detect` role in your config. To ensure optimal performance, select a suitable resolution for this stream in your camera's firmware that fits your specific scene and requirements. + +## Advanced Configuration + +Fine-tune face recognition with these optional parameters at the global level of your config. The only optional parameters that can be set at the camera level are `enabled` and `min_area`. + +### Detection + +- `detection_threshold`: Face detection confidence score required before recognition runs: + - Default: `0.7` + - Note: This is field only applies to the standalone face detection model, `min_score` should be used to filter for models that have face detection built in. +- `min_area`: Defines the minimum size (in pixels) a face must be before recognition runs. + - Default: `500` pixels. + - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces. + +### Recognition + +- `model_size`: Which model size to use, options are `small` or `large` +- `unknown_score`: Min score to mark a person as a potential match, matches at or below this will be marked as unknown. + - Default: `0.8`. +- `recognition_threshold`: Recognition confidence score required to add the face to the object as a sub label. + - Default: `0.9`. +- `min_faces`: Min face recognitions for the sub label to be applied to the person object. + - Default: `1` +- `save_attempts`: Number of images of recognized faces to save for training. + - Default: `200`. +- `blur_confidence_filter`: Enables a filter that calculates how blurry the face is and adjusts the confidence based on this. + - Default: `True`. +- `device`: Target a specific device to run the face recognition model on (multi-GPU installation). + - Default: `None`. + - Note: This setting is only applicable when using the `large` model. See [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/) + +## Usage + +Follow these steps to begin: + +1. **Enable face recognition** in your configuration file and restart Frigate. +2. **Upload one face** using the **Add Face** button's wizard in the Face Library section of the Frigate UI. Read below for the best practices on expanding your training set. +3. When Frigate detects and attempts to recognize a face, it will appear in the **Train** tab of the Face Library, along with its associated recognition confidence. +4. From the **Train** tab, you can **assign the face** to a new or existing person to improve recognition accuracy for the future. + +## Creating a Robust Training Set + +The number of images needed for a sufficient training set for face recognition varies depending on several factors: + +- Diversity of the dataset: A dataset with diverse images, including variations in lighting, pose, and facial expressions, will require fewer images per person than a less diverse dataset. +- Desired accuracy: The higher the desired accuracy, the more images are typically needed. + +However, here are some general guidelines: + +- Minimum: For basic face recognition tasks, a minimum of 5-10 images per person is often recommended. +- Recommended: For more robust and accurate systems, 20-30 images per person is a good starting point. +- Ideal: For optimal performance, especially in challenging conditions, 50-100 images per person can be beneficial. + +The accuracy of face recognition is heavily dependent on the quality of data given to it for training. It is recommended to build the face training library in phases. + +:::tip + +When choosing images to include in the face training set it is recommended to always follow these recommendations: + +- If it is difficult to make out details in a persons face it will not be helpful in training. +- Avoid images with extreme under/over-exposure. +- Avoid blurry / pixelated images. +- Avoid training on infrared (gray-scale). The models are trained on color images and will be able to extract features from gray-scale images. +- Using images of people wearing hats / sunglasses may confuse the model. +- Do not upload too many similar images at the same time, it is recommended to train no more than 4-6 similar images for each person to avoid over-fitting. + +::: + +### Understanding the Recent Recognitions Tab + +The Recent Recognitions tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching. + +Each face image is labeled with a name (or `Unknown`) along with the confidence score of the recognition attempt. While each image can be used to train the system for a specific person, not all images are suitable for training. + +Refer to the guidelines below for best practices on selecting images for training. + +### Step 1 - Building a Strong Foundation + +When first enabling face recognition it is important to build a foundation of strong images. It is recommended to start by uploading 1-5 photos containing just this person's face. It is important that the person's face in the photo is front-facing and not turned, this will ensure a good starting point. + +Then it is recommended to use the `Face Library` tab in Frigate to select and train images for each person as they are detected. When building a strong foundation it is strongly recommended to only train on images that are front-facing. Ignore images from cameras that recognize faces from an angle. Aim to strike a balance between the quality of images while also having a range of conditions (day / night, different weather conditions, different times of day, etc.) in order to have diversity in the images used for each person and not have over-fitting. + +You do not want to train images that are 90%+ as these are already being confidently recognized. In this step the goal is to train on clear, lower scoring front-facing images until the majority of front-facing images for a given person are consistently recognized correctly. Then it is time to move on to step 2. + +### Step 2 - Expanding The Dataset + +Once front-facing images are performing well, start choosing slightly off-angle images to include for training. It is important to still choose images where enough face detail is visible to recognize someone, and you still only want to train on images that score lower. + +## FAQ + +### How do I debug Face Recognition issues? + +Start with the [Usage](#usage) section and re-read the [Model Requirements](#model-requirements) above. + +1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library. + + If you are using a Frigate+ or `face` detecting model: + + - Watch the debug view (Settings --> Debug) to ensure that `face` is being detected along with `person`. + - You may need to adjust the `min_score` for the `face` object if faces are not being detected. + + If you are **not** using a Frigate+ or `face` detecting model: + + - Check your `detect` stream resolution and ensure it is sufficiently high enough to capture face details on `person` objects. + - You may need to lower your `detection_threshold` if faces are not being detected. + +2. Any detected faces will then be _recognized_. + + - Make sure you have trained at least one face per the recommendations above. + - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). + +### Detection does not work well with blurry images? + +Accuracy is definitely a going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work. + +Some users have also noted that setting the stream in camera firmware to a constant bit rate (CBR) leads to better image clarity than with a variable bit rate (VBR). + +### Why can't I bulk upload photos? + +It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to over-fitting in that particular scenario and hurt recognition performance. + +### Why can't I bulk reprocess faces? + +Face embedding models work by breaking apart faces into different features. This means that when reprocessing an image, only images from a similar angle will have its score affected. + +### Why do unknown people score similarly to known people? + +This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to over-fitting: + +- If you train with only a few images per person, especially if those images are very similar, the recognition model becomes overly specialized to those specific images. +- When you provide images with different poses, lighting, and expressions, the algorithm extracts features that are consistent across those variations. +- By training on a diverse set of images, the algorithm becomes less sensitive to minor variations and noise in the input image. + +Review your face collections and remove most of the unclear or low-quality images. Then, use the **Reprocess** button on each face in the **Train** tab to evaluate how the changes affect recognition scores. + +Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower - ideally with different lighting, angles, and conditions—to help the model generalize more effectively. + +### Frigate misidentified a face. Can I tell it that a face is "not" a specific person? + +No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person. +For more guidance, refer to the section above on improving recognition accuracy. + +### I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned? + +The Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results. + +### Can I use other face recognition software like DoubleTake at the same time as the built in face recognition? + +No, using another face recognition service will interfere with Frigate's built in face recognition. When using double-take the sub_label feature must be disabled if the built in face recognition is also desired. + +### Does face recognition run on the recording stream? + +Face recognition does not run on the recording stream, this would be suboptimal for many reasons: + +1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after. +2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway. +3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution. + +### I get an unknown error when taking a photo directly with my iPhone + +By default iOS devices will use HEIC (High Efficiency Image Container) for images, but this format is not supported for uploads. Choosing `large` as the format instead of `original` will use JPG which will work correctly. + +### How can I delete the face database and start over? + +Frigate does not store anything in its database related to face recognition. You can simply delete all of your faces through the Frigate UI or remove the contents of the `/media/frigate/clips/faces` directory. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/ffmpeg_presets.md b/sam2-cpu/frigate-dev/docs/docs/configuration/ffmpeg_presets.md new file mode 100644 index 0000000..8bba62e --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/ffmpeg_presets.md @@ -0,0 +1,80 @@ +--- +id: ffmpeg_presets +title: FFmpeg presets +--- + +Some presets of FFmpeg args are provided by default to make the configuration easier. All presets can be seen in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py). + +### Hwaccel Presets + +It is highly recommended to use hwaccel presets in the config. These presets not only replace the longer args, but they also give Frigate hints of what hardware is available and allows Frigate to make other optimizations using the GPU such as when encoding the birdseye restream or when scaling a stream that has a size different than the native stream size. + +See [the hwaccel docs](/configuration/hardware_acceleration_video.md) for more info on how to setup hwaccel for your GPU / iGPU. + +| Preset | Usage | Other Notes | +| --------------------- | ------------------------------ | ----------------------------------------------------- | +| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | | +| preset-rpi-64-h265 | 64 bit Rpi with h265 stream | | +| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen | +| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead | +| preset-intel-qsv-h265 | Intel QSV with h265 stream | If issues occur recommend using vaapi preset instead | +| preset-nvidia | Nvidia GPU | | +| preset-jetson-h264 | Nvidia Jetson with h264 stream | | +| preset-jetson-h265 | Nvidia Jetson with h265 stream | | +| preset-rkmpp | Rockchip MPP | Use image with \*-rk suffix and privileged mode | + +### Input Args Presets + +Input args presets help make the config more readable and handle use cases for different types of streams to ensure maximum compatibility. + +See [the camera specific docs](/configuration/camera_specific.md) for more info on non-standard cameras and recommendations for using them in Frigate. + +| Preset | Usage | Other Notes | +| -------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ | +| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead | +| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead | +| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp | +| preset-rtmp-generic | RTMP Stream | | +| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified | +| preset-rtsp-restream | RTSP Stream from restream | Use for rtsp restream as source for frigate | +| preset-rtsp-restream-low-latency | RTSP Stream from restream | Use for rtsp restream as source for frigate to lower latency, may cause issues with some cameras | +| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only | +| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris | + +:::warning + +It is important to be mindful of input args when using restream because you can have a mix of protocols. `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, when using a reolink cam with the rtsp restream as a source for record the preset-http-reolink will cause a crash. In this case presets will need to be set at the stream level. See the example below. + +::: + +```yaml +go2rtc: + streams: + reolink_cam: http://192.168.0.139/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password + +cameras: + reolink_cam: + ffmpeg: + inputs: + - path: http://192.168.0.139/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password + input_args: preset-http-reolink + roles: + - detect + - path: rtsp://127.0.0.1:8554/reolink_cam + input_args: preset-rtsp-generic + roles: + - record +``` + +### Output Args Presets + +Output args presets help make the config more readable and handle use cases for different types of streams to ensure consistent recordings. + +| Preset | Usage | Other Notes | +| -------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| preset-record-generic | Record WITHOUT audio | If your camera doesn’t have audio, or if you don’t want to record audio, use this option | +| preset-record-generic-audio-copy | Record WITH original audio | Use this to enable audio in recordings | +| preset-record-generic-audio-aac | Record WITH transcoded aac audio | This is the default when no option is specified. Use it to transcode audio to AAC. If the source is already in AAC format, use preset-record-generic-audio-copy instead to avoid unnecessary re-encoding | +| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead | +| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead | +| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio | diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/genai.md b/sam2-cpu/frigate-dev/docs/docs/configuration/genai.md new file mode 100644 index 0000000..018dc20 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/genai.md @@ -0,0 +1,231 @@ +--- +id: genai +title: Generative AI +--- + +Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail. + +Requests for a description are sent off automatically to your AI provider at the end of the tracked object's lifecycle, or can optionally be sent earlier after a number of significantly changed frames, for example in use in more real-time notifications. Descriptions can also be regenerated manually via the Frigate UI. Note that if you are manually entering a description for tracked objects prior to its end, this will be overwritten by the generated response. + +## Configuration + +Generative AI can be enabled for all cameras or only for specific cameras. If GenAI is disabled for a camera, you can still manually generate descriptions for events using the HTTP API. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below. + +To use Generative AI, you must define a single provider at the global level of your Frigate configuration. If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`. + +```yaml +genai: + provider: gemini + api_key: "{FRIGATE_GEMINI_API_KEY}" + model: gemini-2.0-flash + +cameras: + front_camera: + genai: + enabled: True # <- enable GenAI for your front camera + use_snapshot: True + objects: + - person + required_zones: + - steps + indoor_camera: + objects: + genai: + enabled: False # <- disable GenAI for your indoor camera +``` + +By default, descriptions will be generated for all tracked objects and all zones. But you can also optionally specify `objects` and `required_zones` to only generate descriptions for certain tracked objects or zones. + +Optionally, you can generate the description using a snapshot (if enabled) by setting `use_snapshot` to `True`. By default, this is set to `False`, which sends the uncompressed images from the `detect` stream collected over the object's lifetime to the model. Once the object lifecycle ends, only a single compressed and cropped thumbnail is saved with the tracked object. Using a snapshot might be useful when you want to _regenerate_ a tracked object's description as it will provide the AI with a higher-quality image (typically downscaled by the AI itself) than the cropped/compressed thumbnail. Using a snapshot otherwise has a trade-off in that only a single image is sent to your provider, which will limit the model's ability to determine object movement or direction. + +Generative AI can also be toggled dynamically for a camera via MQTT with the topic `frigate//object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_nameobjectdescriptionsset). + +## Ollama + +:::warning + +Using Ollama on CPU is not recommended, high inference times make using Generative AI impractical. + +::: + +[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It provides a nice API over [llama.cpp](https://github.com/ggerganov/llama.cpp). It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance. + +Most of the 7b parameter 4-bit vision models will fit inside 8GB of VRAM. There is also a [Docker container](https://hub.docker.com/r/ollama/ollama) available. + +Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests). + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/library). At the time of writing, this includes `llava`, `llava-llama3`, `llava-phi3`, and `moondream`. Note that Frigate will not automatically download the model you specify in your config, you must download the model to your local instance of Ollama first i.e. by running `ollama pull llava:7b` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. + +:::note + +You should have at least 8 GB of RAM available (or VRAM if running on GPU) to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models. + +::: + +### Configuration + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: qwen3-vl:4b +``` + +## Google Gemini + +Google Gemini has a free tier allowing [15 queries per minute](https://ai.google.dev/pricing) to the API, which is more than sufficient for standard Frigate usage. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini). + +### Get API Key + +To start using Gemini, you must first get an API key from [Google AI Studio](https://aistudio.google.com). + +1. Accept the Terms of Service +2. Click "Get API Key" from the right hand navigation +3. Click "Create API key in new project" +4. Copy the API key for use in your config + +### Configuration + +```yaml +genai: + provider: gemini + api_key: "{FRIGATE_GEMINI_API_KEY}" + model: gemini-2.0-flash +``` + +:::note + +To use a different Gemini-compatible API endpoint, set the `GEMINI_BASE_URL` environment variable to your provider's API URL. + +::: + +## OpenAI + +OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models). + +### Get API Key + +To start using OpenAI, you must first [create an API key](https://platform.openai.com/api-keys) and [configure billing](https://platform.openai.com/settings/organization/billing/overview). + +### Configuration + +```yaml +genai: + provider: openai + api_key: "{FRIGATE_OPENAI_API_KEY}" + model: gpt-4o +``` + +:::note + +To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL. + +::: + +## Azure OpenAI + +Microsoft offers several vision models through Azure OpenAI. A subscription is required. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). + +### Create Resource and Get API Key + +To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key, model name, and resource URL, which must include the `api-version` parameter (see the example below). + +### Configuration + +```yaml +genai: + provider: azure_openai + base_url: https://instance.cognitiveservices.azure.com/openai/responses?api-version=2025-04-01-preview + model: gpt-5-mini + api_key: "{FRIGATE_OPENAI_API_KEY}" +``` + +## Usage and Best Practices + +Frigate's thumbnail search excels at identifying specific details about tracked objects – for example, using an "image caption" approach to find a "person wearing a yellow vest," "a white dog running across the lawn," or "a red car on a residential street." To enhance this further, Frigate’s default prompts are designed to ask your AI provider about the intent behind the object's actions, rather than just describing its appearance. + +While generating simple descriptions of detected objects is useful, understanding intent provides a deeper layer of insight. Instead of just recognizing "what" is in a scene, Frigate’s default prompts aim to infer "why" it might be there or "what" it could do next. Descriptions tell you what’s happening, but intent gives context. For instance, a person walking toward a door might seem like a visitor, but if they’re moving quickly after hours, you can infer a potential break-in attempt. Detecting a person loitering near a door at night can trigger an alert sooner than simply noting "a person standing by the door," helping you respond based on the situation’s context. + +### Using GenAI for notifications + +Frigate provides an [MQTT topic](/integrations/mqtt), `frigate/tracked_object_update`, that is updated with a JSON payload containing `event_id` and `description` when your AI provider returns a description for a tracked object. This description could be used directly in notifications, such as sending alerts to your phone or making audio announcements. If additional details from the tracked object are needed, you can query the [HTTP API](/integrations/api/event-events-event-id-get) using the `event_id`, eg: `http://frigate_ip:5000/api/events/`. + +If looking to get notifications earlier than when an object ceases to be tracked, an additional send trigger can be configured of `after_significant_updates`. + +```yaml +genai: + send_triggers: + tracked_object_end: true # default + after_significant_updates: 3 # how many updates to a tracked object before we should send an image +``` + +## Custom Prompts + +Frigate sends multiple frames from the tracked object along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows: + +``` +Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next. +``` + +:::tip + +Prompts can use variable replacements `{label}`, `{sub_label}`, and `{camera}` to substitute information from the tracked object as part of the prompt. + +::: + +You are also able to define custom prompts in your configuration. + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: llava + +objects: + prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance." + object_prompts: + person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details." + car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company." +``` + +Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire. + +```yaml +cameras: + front_door: + objects: + genai: + enabled: True + use_snapshot: True + prompt: "Analyze the {label} in these images from the {camera} security camera at the front door. Focus on the actions and potential intent of the {label}." + object_prompts: + person: "Examine the person in these images. What are they doing, and how might their actions suggest their purpose (e.g., delivering something, approaching, leaving)? If they are carrying or interacting with a package, include details about its source or destination." + cat: "Observe the cat in these images. Focus on its movement and intent (e.g., wandering, hunting, interacting with objects). If the cat is near the flower pots or engaging in any specific actions, mention it." + objects: + - person + - cat + required_zones: + - steps +``` + +### Experiment with prompts + +Many providers also have a public facing chat interface for their models. Download a couple of different thumbnails or snapshots from Frigate and try new things in the playground to get descriptions to your liking before updating the prompt in Frigate. + +- OpenAI - [ChatGPT](https://chatgpt.com) +- Gemini - [Google AI Studio](https://aistudio.google.com) +- Ollama - [Open WebUI](https://docs.openwebui.com/) diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/genai/config.md b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/config.md new file mode 100644 index 0000000..7e5618b --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/config.md @@ -0,0 +1,142 @@ +--- +id: genai_config +title: Configuring Generative AI +--- + +## Configuration + +A Generative AI provider can be configured in the global config, which will make the Generative AI features available for use. There are currently 3 native providers available to integrate with Frigate. Other providers that support the OpenAI standard API can also be used. See the OpenAI section below. + +To use Generative AI, you must define a single provider at the global level of your Frigate configuration. If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`. + +## Ollama + +:::warning + +Using Ollama on CPU is not recommended, high inference times make using Generative AI impractical. + +::: + +[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It provides a nice API over [llama.cpp](https://github.com/ggerganov/llama.cpp). It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance. + +Most of the 7b parameter 4-bit vision models will fit inside 8GB of VRAM. There is also a [Docker container](https://hub.docker.com/r/ollama/ollama) available. + +Parallel requests also come with some caveats. You will need to set `OLLAMA_NUM_PARALLEL=1` and choose a `OLLAMA_MAX_QUEUE` and `OLLAMA_MAX_LOADED_MODELS` values that are appropriate for your hardware and preferences. See the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-does-ollama-handle-concurrent-requests). + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their model library](https://ollama.com/library). Note that Frigate will not automatically download the model you specify in your config, Ollama will try to download the model but it may take longer than the timeout, it is recommended to pull the model beforehand by running `ollama pull your_model` on your Ollama server/Docker container. Note that the model specified in Frigate's config must match the downloaded model tag. + +:::info + +Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger sizes are more capable of complex tasks and understanding of situations, but requires more memory and computational resources. It is recommended to try multiple models and experiment to see which performs best. + +::: + +:::tip + +If you are trying to use a single model for Frigate and HomeAssistant, it will need to support vision and tools calling. qwen3-VL supports vision and tools simultaneously in Ollama. + +::: + +The following models are recommended: + +| Model | Notes | +| ----------------- | -------------------------------------------------------------------- | +| `qwen3-vl` | Strong visual and situational understanding, higher vram requirement | +| `Intern3.5VL` | Relatively fast with good vision comprehension | +| `gemma3` | Strong frame-to-frame understanding, slower inference times | +| `qwen2.5-vl` | Fast but capable model with good vision comprehension | + +:::note + +You should have at least 8 GB of RAM available (or VRAM if running on GPU) to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models. + +::: + +### Configuration + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: minicpm-v:8b + provider_options: # other Ollama client options can be defined + keep_alive: -1 + options: + num_ctx: 8192 # make sure the context matches other services that are using ollama +``` + +## Google Gemini + +Google Gemini has a free tier allowing [15 queries per minute](https://ai.google.dev/pricing) to the API, which is more than sufficient for standard Frigate usage. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://ai.google.dev/gemini-api/docs/models/gemini). At the time of writing, this includes `gemini-1.5-pro` and `gemini-1.5-flash`. + +### Get API Key + +To start using Gemini, you must first get an API key from [Google AI Studio](https://aistudio.google.com). + +1. Accept the Terms of Service +2. Click "Get API Key" from the right hand navigation +3. Click "Create API key in new project" +4. Copy the API key for use in your config + +### Configuration + +```yaml +genai: + provider: gemini + api_key: "{FRIGATE_GEMINI_API_KEY}" + model: gemini-1.5-flash +``` + +## OpenAI + +OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://platform.openai.com/docs/models). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`. + +### Get API Key + +To start using OpenAI, you must first [create an API key](https://platform.openai.com/api-keys) and [configure billing](https://platform.openai.com/settings/organization/billing/overview). + +### Configuration + +```yaml +genai: + provider: openai + api_key: "{FRIGATE_OPENAI_API_KEY}" + model: gpt-4o +``` + +:::note + +To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL. + +::: + +## Azure OpenAI + +Microsoft offers several vision models through Azure OpenAI. A subscription is required. + +### Supported Models + +You must use a vision capable model with Frigate. Current model variants can be found [in their documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). At the time of writing, this includes `gpt-4o` and `gpt-4-turbo`. + +### Create Resource and Get API Key + +To start using Azure OpenAI, you must first [create a resource](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource). You'll need your API key and resource URL, which must include the `api-version` parameter (see the example below). The model field is not required in your configuration as the model is part of the deployment name you chose when deploying the resource. + +### Configuration + +```yaml +genai: + provider: azure_openai + base_url: https://example-endpoint.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2023-03-15-preview + api_key: "{FRIGATE_OPENAI_API_KEY}" +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/genai/objects.md b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/objects.md new file mode 100644 index 0000000..e5aa92c --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/objects.md @@ -0,0 +1,77 @@ +--- +id: genai_objects +title: Object Descriptions +--- + +Generative AI can be used to automatically generate descriptive text based on the thumbnails of your tracked objects. This helps with [Semantic Search](/configuration/semantic_search) in Frigate to provide more context about your tracked objects. Descriptions are accessed via the _Explore_ view in the Frigate UI by clicking on a tracked object's thumbnail. + +Requests for a description are sent off automatically to your AI provider at the end of the tracked object's lifecycle, or can optionally be sent earlier after a number of significantly changed frames, for example in use in more real-time notifications. Descriptions can also be regenerated manually via the Frigate UI. Note that if you are manually entering a description for tracked objects prior to its end, this will be overwritten by the generated response. + +By default, descriptions will be generated for all tracked objects and all zones. But you can also optionally specify `objects` and `required_zones` to only generate descriptions for certain tracked objects or zones. + +Optionally, you can generate the description using a snapshot (if enabled) by setting `use_snapshot` to `True`. By default, this is set to `False`, which sends the uncompressed images from the `detect` stream collected over the object's lifetime to the model. Once the object lifecycle ends, only a single compressed and cropped thumbnail is saved with the tracked object. Using a snapshot might be useful when you want to _regenerate_ a tracked object's description as it will provide the AI with a higher-quality image (typically downscaled by the AI itself) than the cropped/compressed thumbnail. Using a snapshot otherwise has a trade-off in that only a single image is sent to your provider, which will limit the model's ability to determine object movement or direction. + +Generative AI object descriptions can also be toggled dynamically for a camera via MQTT with the topic `frigate//object_descriptions/set`. See the [MQTT documentation](/integrations/mqtt/#frigatecamera_nameobjectdescriptionsset). + +## Usage and Best Practices + +Frigate's thumbnail search excels at identifying specific details about tracked objects – for example, using an "image caption" approach to find a "person wearing a yellow vest," "a white dog running across the lawn," or "a red car on a residential street." To enhance this further, Frigate’s default prompts are designed to ask your AI provider about the intent behind the object's actions, rather than just describing its appearance. + +While generating simple descriptions of detected objects is useful, understanding intent provides a deeper layer of insight. Instead of just recognizing "what" is in a scene, Frigate’s default prompts aim to infer "why" it might be there or "what" it could do next. Descriptions tell you what’s happening, but intent gives context. For instance, a person walking toward a door might seem like a visitor, but if they’re moving quickly after hours, you can infer a potential break-in attempt. Detecting a person loitering near a door at night can trigger an alert sooner than simply noting "a person standing by the door," helping you respond based on the situation’s context. + +## Custom Prompts + +Frigate sends multiple frames from the tracked object along with a prompt to your Generative AI provider asking it to generate a description. The default prompt is as follows: + +``` +Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next. +``` + +:::tip + +Prompts can use variable replacements `{label}`, `{sub_label}`, and `{camera}` to substitute information from the tracked object as part of the prompt. + +::: + +You are also able to define custom prompts in your configuration. + +```yaml +genai: + provider: ollama + base_url: http://localhost:11434 + model: llava + +objects: + prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance." + object_prompts: + person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details." + car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company." +``` + +Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire. + +```yaml +cameras: + front_door: + objects: + genai: + enabled: True + use_snapshot: True + prompt: "Analyze the {label} in these images from the {camera} security camera at the front door. Focus on the actions and potential intent of the {label}." + object_prompts: + person: "Examine the person in these images. What are they doing, and how might their actions suggest their purpose (e.g., delivering something, approaching, leaving)? If they are carrying or interacting with a package, include details about its source or destination." + cat: "Observe the cat in these images. Focus on its movement and intent (e.g., wandering, hunting, interacting with objects). If the cat is near the flower pots or engaging in any specific actions, mention it." + objects: + - person + - cat + required_zones: + - steps +``` + +### Experiment with prompts + +Many providers also have a public facing chat interface for their models. Download a couple of different thumbnails or snapshots from Frigate and try new things in the playground to get descriptions to your liking before updating the prompt in Frigate. + +- OpenAI - [ChatGPT](https://chatgpt.com) +- Gemini - [Google AI Studio](https://aistudio.google.com) +- Ollama - [Open WebUI](https://docs.openwebui.com/) diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/genai/review_summaries.md b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/review_summaries.md new file mode 100644 index 0000000..8a492f4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/genai/review_summaries.md @@ -0,0 +1,119 @@ +--- +id: genai_review +title: Review Summaries +--- + +Generative AI can be used to automatically generate structured summaries of review items. These summaries will show up in Frigate's native notifications as well as in the UI. Generative AI can also be used to take a collection of summaries over a period of time and provide a report, which may be useful to get a quick report of everything that happened while out for some amount of time. + +Requests for a summary are requested automatically to your AI provider for alert review items when the activity has ended, they can also be optionally enabled for detections as well. + +Generative AI review summaries can also be toggled dynamically for a [camera via MQTT](/integrations/mqtt/#frigatecamera_namereviewdescriptionsset). + +## Review Summary Usage and Best Practices + +Review summaries provide structured JSON responses that are saved for each review item: + +``` +- `title` (string): A concise, direct title that describes the purpose or overall action (e.g., "Person taking out trash", "Joe walking dog"). +- `scene` (string): A narrative description of what happens across the sequence from start to finish, including setting, detected objects, and their observable actions. +- `confidence` (float): 0-1 confidence in the analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. +- `other_concerns` (list): List of user-defined concerns that may need additional investigation. +- `potential_threat_level` (integer): 0, 1, or 2 as defined below. +``` + +This will show in multiple places in the UI to give additional context about each activity, and allow viewing more details when extra attention is required. Frigate's built in notifications will also automatically show the title and description when the data is available. + +### Defining Typical Activity + +Each installation and even camera can have different parameters for what is considered suspicious activity. Frigate allows the `activity_context_prompt` to be defined globally and at the camera level, which allows you to define more specifically what should be considered normal activity. It is important that this is not overly specific as it can sway the output of the response. + +
+ Default Activity Context Prompt + +``` +### Normal Activity Indicators (Level 0) +- Known/verified people in any zone at any time +- People with pets in residential areas +- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving +- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime +- Activity confined to public areas only (sidewalks, streets) without entering property at any time + +### Suspicious Activity Indicators (Level 1) +- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration +- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration +- Taking items that don't belong to them (packages, objects from porches/driveways) +- Climbing or jumping fences/barriers to access property +- Attempting to conceal actions or items from view +- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence + +### Critical Threat Indicators (Level 2) +- Holding break-in tools (crowbars, pry bars, bolt cutters) +- Weapons visible (guns, knives, bats used aggressively) +- Forced entry in progress +- Physical aggression or violence +- Active property damage or theft in progress + +### Assessment Guidance +Evaluate in this order: + +1. **If person is verified/known** → Level 0 regardless of time or activity +2. **If person is unidentified:** + - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 + - Check actions: If testing doors/handles, taking items, climbing → Level 1 + - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0 +3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) + +The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is. +``` + +
+ +### Image Source + +By default, review summaries use preview images (cached preview frames) which have a lower resolution but use fewer tokens per image. For better image quality and more detailed analysis, you can configure Frigate to extract frames directly from recordings at a higher resolution: + +```yaml +review: + genai: + enabled: true + image_source: recordings # Options: "preview" (default) or "recordings" +``` + +When using `recordings`, frames are extracted at 480px height while maintaining the camera's original aspect ratio, providing better detail for the LLM while being mindful of context window size. This is particularly useful for scenarios where fine details matter, such as identifying license plates, reading text, or analyzing distant objects. + +The number of frames sent to the LLM is dynamically calculated based on: + +- Your LLM provider's context window size +- The camera's resolution and aspect ratio (ultrawide cameras like 32:9 use more tokens per image) +- The image source (recordings use more tokens than preview images) + +Frame counts are automatically optimized to use ~98% of the available context window while capping at 20 frames maximum to ensure reasonable inference times. Note that using recordings will: + +- Provide higher quality images to the LLM (480p vs 180p preview images) +- Use more tokens per image due to higher resolution +- Result in fewer frames being sent for ultrawide cameras due to larger image size +- Require that recordings are enabled for the camera + +If recordings are not available for a given time period, the system will automatically fall back to using preview frames. + +### Additional Concerns + +Along with the concern of suspicious activity or immediate threat, you may have concerns such as animals in your garden or a gate being left open. These concerns can be configured so that the review summaries will make note of them if the activity requires additional review. For example: + +```yaml +review: + genai: + enabled: true + additional_concerns: + - animals in the garden +``` + +## Review Reports + +Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review. + +### Requesting Reports Programmatically + +Review reports can be requested via the [API](/integrations/api#review-summarization) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps. + +For Home Assistant users, there is a built-in service (`frigate.review_summarize`) that makes it easy to request review reports as part of automations or scripts. This allows you to automatically generate daily summaries, vacation reports, or custom time period reports based on your specific needs. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_enrichments.md b/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_enrichments.md new file mode 100644 index 0000000..fac2ffa --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_enrichments.md @@ -0,0 +1,37 @@ +--- +id: hardware_acceleration_enrichments +title: Enrichments +--- + +# Enrichments + +Some of Frigate's enrichments can use a discrete GPU or integrated GPU for accelerated processing. + +## Requirements + +Object detection and enrichments (like Semantic Search, Face Recognition, and License Plate Recognition) are independent features. To use a GPU / NPU for object detection, see the [Object Detectors](/configuration/object_detectors.md) documentation. If you want to use your GPU for any supported enrichments, you must choose the appropriate Frigate Docker image for your GPU / NPU and configure the enrichment according to its specific documentation. + +- **AMD** + + - ROCm support in the `-rocm` Frigate image is automatically detected for enrichments, but only some enrichment models are available due to ROCm's focus on LLMs and limited stability with certain neural network models. Frigate disables models that perform poorly or are unstable to ensure reliable operation, so only compatible enrichments may be active. + +- **Intel** + + - OpenVINO will automatically be detected and used for enrichments in the default Frigate image. + - **Note:** Intel NPUs have limited model support for enrichments. GPU is recommended for enrichments when available. + +- **Nvidia** + + - Nvidia GPUs will automatically be detected and used for enrichments in the `-tensorrt` Frigate image. + - Jetson devices will automatically be detected and used for enrichments in the `-tensorrt-jp6` Frigate image. + +- **RockChip** + - RockChip NPU will automatically be detected and used for semantic search v1 and face recognition in the `-rk` Frigate image. + +Utilizing a GPU for enrichments does not require you to use the same GPU for object detection. For example, you can run the `tensorrt` Docker image for enrichments and still use other dedicated hardware like a Coral or Hailo for object detection. However, one combination that is not supported is TensorRT for object detection and OpenVINO for enrichments. + +:::note + +A Google Coral is a TPU (Tensor Processing Unit), not a dedicated GPU (Graphics Processing Unit) and therefore does not provide any kind of acceleration for Frigate's enrichments. + +::: diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_video.md b/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_video.md new file mode 100644 index 0000000..b7cb794 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/hardware_acceleration_video.md @@ -0,0 +1,455 @@ +--- +id: hardware_acceleration_video +title: Video Decoding +--- + +# Video Decoding + +It is highly recommended to use a GPU for hardware acceleration video decoding in Frigate. Some types of hardware acceleration are detected and used automatically, but you may need to update your configuration to enable hardware accelerated decoding in ffmpeg. + +Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro + + +## Raspberry Pi 3/4 + +Ensure you increase the allocated RAM for your GPU to at least 128 (`raspi-config` > Performance Options > GPU Memory). +If you are using the HA Add-on, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration. + +```yaml +# if you want to decode a h264 stream +ffmpeg: + hwaccel_args: preset-rpi-64-h264 + +# if you want to decode a h265 (hevc) stream +ffmpeg: + hwaccel_args: preset-rpi-64-h265 +``` + +:::note + +If running Frigate through Docker, you either need to run in privileged mode or +map the `/dev/video*` devices to Frigate. With Docker Compose add: + +```yaml +services: + frigate: + ... + devices: + - /dev/video11:/dev/video11 +``` + +Or with `docker run`: + +```bash +docker run -d \ + --name frigate \ + ... + --device /dev/video11 \ + ghcr.io/blakeblackshear/frigate:stable +``` + +`/dev/video11` is the correct device (on Raspberry Pi 4B). You can check +by running the following and looking for `H264`: + +```bash +for d in /dev/video*; do + echo -e "---\n$d" + v4l2-ctl --list-formats-ext -d $d +done +``` + +Or map in all the `/dev/video*` devices. + +::: + +## Intel-based CPUs + +:::info + +**Recommended hwaccel Preset** + +| CPU Generation | Intel Driver | Recommended Preset | Notes | +| -------------- | ------------ | ------------------- | ------------------------------------ | +| gen1 - gen5 | i965 | preset-vaapi | qsv is not supported | +| gen6 - gen7 | iHD | preset-vaapi | qsv is not supported | +| gen8 - gen12 | iHD | preset-vaapi | preset-intel-qsv-\* can also be used | +| gen13+ | iHD / Xe | preset-intel-qsv-\* | | +| Intel Arc GPU | iHD / Xe | preset-intel-qsv-\* | | + +::: + +:::note + +The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA Add-on users](advanced.md#environment_vars). + +See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is. + +::: + +### Via VAAPI + +VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. + +```yaml +ffmpeg: + hwaccel_args: preset-vaapi +``` + +### Via Quicksync + +#### H.264 streams + +```yaml +ffmpeg: + hwaccel_args: preset-intel-qsv-h264 +``` + +#### H.265 streams + +```yaml +ffmpeg: + hwaccel_args: preset-intel-qsv-h265 +``` + +### Configuring Intel GPU Stats in Docker + +Additional configuration is needed for the Docker container to be able to access the `intel_gpu_top` command for GPU stats. There are two options: + +1. Run the container as privileged. +2. Add the `CAP_PERFMON` capability (note: you might need to set the `perf_event_paranoid` low enough to allow access to the performance event system.) + +#### Run as privileged + +This method works, but it gives more permissions to the container than are actually needed. + +##### Docker Compose - Privileged + +```yaml +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable + privileged: true +``` + +##### Docker Run CLI - Privileged + +```bash +docker run -d \ + --name frigate \ + ... + --privileged \ + ghcr.io/blakeblackshear/frigate:stable +``` + +#### CAP_PERFMON + +Only recent versions of Docker support the `CAP_PERFMON` capability. You can test to see if yours supports it by running: `docker run --cap-add=CAP_PERFMON hello-world` + +##### Docker Compose - CAP_PERFMON + +```yaml +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable + cap_add: + - CAP_PERFMON +``` + +##### Docker Run CLI - CAP_PERFMON + +```bash +docker run -d \ + --name frigate \ + ... + --cap-add=CAP_PERFMON \ + ghcr.io/blakeblackshear/frigate:stable +``` + +#### perf_event_paranoid + +_Note: This setting must be changed for the entire system._ + +For more information on the various values across different distributions, see https://askubuntu.com/questions/1400874/what-does-perf-paranoia-level-four-do. + +Depending on your OS and kernel configuration, you may need to change the `/proc/sys/kernel/perf_event_paranoid` kernel tunable. You can test the change by running `sudo sh -c 'echo 2 >/proc/sys/kernel/perf_event_paranoid'` which will persist until a reboot. Make it permanent by running `sudo sh -c 'echo kernel.perf_event_paranoid=2 >> /etc/sysctl.d/local.conf'` + +#### Stats for SR-IOV or other devices + +When using virtualized GPUs via SR-IOV, you need to specify the device path to use to gather stats from `intel_gpu_top`. This example may work for some systems using SR-IOV: + +```yaml +telemetry: + stats: + intel_gpu_device: "sriov" +``` + +For other virtualized GPUs, try specifying the direct path to the device instead: + +```yaml +telemetry: + stats: + intel_gpu_device: "drm:/dev/dri/card0" +``` + +If you are passing in a device path, make sure you've passed the device through to the container. + +## AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver + +VAAPI supports automatic profile selection so it will work automatically with both H.264 and H.265 streams. + +:::note + +You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA Add-on users](advanced.md#environment_vars). + +::: + +```yaml +ffmpeg: + hwaccel_args: preset-vaapi +``` + +## NVIDIA GPUs + +While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all. + +A more complete list of cards and their compatible drivers is available in the [driver release readme](https://download.nvidia.com/XFree86/Linux-x86_64/525.85.05/README/supportedchips.html). + +If your distribution does not offer NVIDIA driver packages, you can [download them here](https://www.nvidia.com/en-us/drivers/unix/). + +### Configuring Nvidia GPUs in Docker + +Additional configuration is needed for the Docker container to be able to access the NVIDIA GPU. The supported method for this is to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker) and specify the GPU to Docker. How you do this depends on how Docker is being run: + +#### Docker Compose - Nvidia GPU + +```yaml +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable-tensorrt + deploy: # <------------- Add this section + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0'] # this is only needed when using multiple GPUs + count: 1 # number of GPUs + capabilities: [gpu] +``` + +#### Docker Run CLI - Nvidia GPU + +```bash +docker run -d \ + --name frigate \ + ... + --gpus=all \ + ghcr.io/blakeblackshear/frigate:stable-tensorrt +``` + +### Setup Decoder + +Using `preset-nvidia` ffmpeg will automatically select the necessary profile for the incoming video, and will log an error if the profile is not supported by your GPU. + +```yaml +ffmpeg: + hwaccel_args: preset-nvidia +``` + +If everything is working correctly, you should see a significant improvement in performance. +Verify that hardware decoding is working by running `nvidia-smi`, which should show `ffmpeg` +processes: + +:::note + +`nvidia-smi` may not show `ffmpeg` processes when run inside the container [due to docker limitations](https://github.com/NVIDIA/nvidia-docker/issues/179#issuecomment-645579458). + +::: + +``` ++-----------------------------------------------------------------------------+ +| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 | +|-------------------------------+----------------------+----------------------+ +| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | +| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | +| | | MIG M. | +|===============================+======================+======================| +| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A | +| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default | +| | | N/A | ++-------------------------------+----------------------+----------------------+ + ++-----------------------------------------------------------------------------+ +| Processes: | +| GPU GI CI PID Type Process name GPU Memory | +| ID ID Usage | +|=============================================================================| +| 0 N/A N/A 12737 C ffmpeg 249MiB | +| 0 N/A N/A 12751 C ffmpeg 249MiB | +| 0 N/A N/A 12772 C ffmpeg 249MiB | +| 0 N/A N/A 12775 C ffmpeg 249MiB | +| 0 N/A N/A 12800 C ffmpeg 249MiB | +| 0 N/A N/A 12811 C ffmpeg 417MiB | +| 0 N/A N/A 12827 C ffmpeg 417MiB | ++-----------------------------------------------------------------------------+ +``` + +If you do not see these processes, check the `docker logs` for the container and look for decoding errors. + +These instructions were originally based on the [Jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux). + +# Community Supported + +## NVIDIA Jetson (Orin AGX, Orin NX, Orin Nano\*, Xavier AGX, Xavier NX, TX2, TX1, Nano) + +A separate set of docker images is available that is based on Jetpack/L4T. They come with an `ffmpeg` build +with codecs that use the Jetson's dedicated media engine. If your Jetson host is running Jetpack 6.0+ use the `stable-tensorrt-jp6` tagged image. Note that the Orin Nano has no video encoder, so frigate will use software encoding on this platform, but the image will still allow hardware decoding and tensorrt object detection. + +You will need to use the image with the nvidia container runtime: + +### Docker Run CLI - Jetson + +```bash +docker run -d \ + ... + --runtime nvidia + ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6 +``` + +### Docker Compose - Jetson + +```yaml +services: + frigate: + ... + image: ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6 + runtime: nvidia # Add this +``` + +:::note + +The `runtime:` tag is not supported on older versions of docker-compose. If you run into this, you can instead use the nvidia runtime system-wide by adding `"default-runtime": "nvidia"` to `/etc/docker/daemon.json`: + +``` +{ + "runtimes": { + "nvidia": { + "path": "nvidia-container-runtime", + "runtimeArgs": [] + } + }, + "default-runtime": "nvidia" +} +``` + +::: + +### Setup Decoder + +The decoder you need to pass in the `hwaccel_args` will depend on the input video. + +A list of supported codecs (you can use `ffmpeg -decoders | grep nvmpi` in the container to get the ones your card supports) + +``` + V..... h264_nvmpi h264 (nvmpi) (codec h264) + V..... hevc_nvmpi hevc (nvmpi) (codec hevc) + V..... mpeg2_nvmpi mpeg2 (nvmpi) (codec mpeg2video) + V..... mpeg4_nvmpi mpeg4 (nvmpi) (codec mpeg4) + V..... vp8_nvmpi vp8 (nvmpi) (codec vp8) + V..... vp9_nvmpi vp9 (nvmpi) (codec vp9) +``` + +For example, for H264 video, you'll select `preset-jetson-h264`. + +```yaml +ffmpeg: + hwaccel_args: preset-jetson-h264 +``` + +If everything is working correctly, you should see a significant reduction in ffmpeg CPU load and power consumption. +Verify that hardware decoding is working by running `jtop` (`sudo pip3 install -U jetson-stats`), which should show +that NVDEC/NVDEC1 are in use. + +## Rockchip platform + +Hardware accelerated video de-/encoding is supported on all Rockchip SoCs using [Nyanmisaka's FFmpeg 6.1 Fork](https://github.com/nyanmisaka/ffmpeg-rockchip) based on [Rockchip's mpp library](https://github.com/rockchip-linux/mpp). + +### Prerequisites + +Make sure to follow the [Rockchip specific installation instructions](/frigate/installation#rockchip-platform). + +### Configuration + +Add one of the following FFmpeg presets to your `config.yml` to enable hardware video processing: + +```yaml +ffmpeg: + hwaccel_args: preset-rkmpp +``` + +:::note + +Make sure that your SoC supports hardware acceleration for your input stream. For example, if your camera streams with h265 encoding and a 4k resolution, your SoC must be able to de- and encode h265 with a 4k resolution or higher. If you are unsure whether your SoC meets the requirements, take a look at the datasheet. + +::: + +:::warning + +If one or more of your cameras are not properly processed and this error is shown in the logs: + +``` +[segment @ 0xaaaaff694790] Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly +[Parsed_scale_rkrga_0 @ 0xaaaaff819070] No hw context provided on input +[Parsed_scale_rkrga_0 @ 0xaaaaff819070] Failed to configure output pad on Parsed_scale_rkrga_0 +Error initializing filters! +Error marking filters as finished +[out#1/rawvideo @ 0xaaaaff3d8730] Nothing was written into output file, because at least one of its streams received no packets. +Restarting ffmpeg... +``` + +you should try to uprade to FFmpeg 7. This can be done using this config option: + +``` +ffmpeg: + path: "7.0" +``` + +You can set this option globally to use FFmpeg 7 for all cameras or on camera level to use it only for specific cameras. Do not confuse this option with: + +``` +cameras: + name: + ffmpeg: + inputs: + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + +::: + +## Synaptics + +Hardware accelerated video de-/encoding is supported on Synpatics SL-series SoC. + +### Prerequisites + +Make sure to follow the [Synaptics specific installation instructions](/frigate/installation#synaptics). + +### Configuration + +Add one of the following FFmpeg presets to your `config.yml` to enable hardware video processing: + +```yaml +ffmpeg: + hwaccel_args: -c:v h264_v4l2m2m + input_args: preset-rtsp-restream +output_args: + record: preset-record-generic-audio-aac +``` + +:::warning + +Make sure that your SoC supports hardware acceleration for your input stream and your input stream is h264 encoding. For example, if your camera streams with h264 encoding, your SoC must be able to de- and encode with it. If you are unsure whether your SoC meets the requirements, take a look at the datasheet. + +::: diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/index.md b/sam2-cpu/frigate-dev/docs/docs/configuration/index.md new file mode 100644 index 0000000..b1fa876 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/index.md @@ -0,0 +1,263 @@ +--- +id: index +title: Frigate Configuration +--- + +For Home Assistant Add-on installations, the config file should be at `/addon_configs//config.yml`, where `` is specific to the variant of the Frigate Add-on you are running. See the list of directories [here](#accessing-add-on-config-dir). + +For all other installation types, the config file should be mapped to `/config/config.yml` inside the container. + +It can be named `config.yml` or `config.yaml`, but if both files exist `config.yml` will be preferred and `config.yaml` will be ignored. + +It is recommended to start with a minimal configuration and add to it as described in [this guide](../guides/getting_started.md) and use the built in configuration editor in Frigate's UI which supports validation. + +```yaml +mqtt: + enabled: False + +cameras: + dummy_camera: # <--- this will be changed to your actual camera later + enabled: False + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:554/rtsp + roles: + - detect +``` + +## Accessing the Home Assistant Add-on configuration directory {#accessing-add-on-config-dir} + +When running Frigate through the HA Add-on, the Frigate `/config` directory is mapped to `/addon_configs/` in the host, where `` is specific to the variant of the Frigate Add-on you are running. + +| Add-on Variant | Configuration directory | +| -------------------------- | -------------------------------------------- | +| Frigate | `/addon_configs/ccab4aaf_frigate` | +| Frigate (Full Access) | `/addon_configs/ccab4aaf_frigate-fa` | +| Frigate Beta | `/addon_configs/ccab4aaf_frigate-beta` | +| Frigate Beta (Full Access) | `/addon_configs/ccab4aaf_frigate-fa-beta` | + +**Whenever you see `/config` in the documentation, it refers to this directory.** + +If for example you are running the standard Add-on variant and use the [VS Code Add-on](https://github.com/hassio-addons/addon-vscode) to browse your files, you can click _File_ > _Open folder..._ and navigate to `/addon_configs/ccab4aaf_frigate` to access the Frigate `/config` directory and edit the `config.yaml` file. You can also use the built-in file editor in the Frigate UI to edit the configuration file. + +## VS Code Configuration Schema + +VS Code supports JSON schemas for automatically validating configuration files. You can enable this feature by adding `# yaml-language-server: $schema=http://frigate_host:5000/api/config/schema.json` to the beginning of the configuration file. Replace `frigate_host` with the IP address or hostname of your Frigate server. If you're using both VS Code and Frigate as an Add-on, you should use `ccab4aaf-frigate` instead. Make sure to expose the internal unauthenticated port `5000` when accessing the config from VS Code on another machine. + +## Environment Variable Substitution + +Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./reference.md). For example, the following values can be replaced at runtime by using environment variables: + +```yaml +mqtt: + user: "{FRIGATE_MQTT_USER}" + password: "{FRIGATE_MQTT_PASSWORD}" +``` + +```yaml +- path: rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:8554/unicast +``` + +```yaml +onvif: + host: 10.0.10.10 + port: 8000 + user: "{FRIGATE_RTSP_USER}" + password: "{FRIGATE_RTSP_PASSWORD}" +``` + +```yaml +go2rtc: + rtsp: + username: "{FRIGATE_GO2RTC_RTSP_USERNAME}" + password: "{FRIGATE_GO2RTC_RTSP_PASSWORD}" +``` + +```yaml +genai: + api_key: "{FRIGATE_GENAI_API_KEY}" +``` + +## Common configuration examples + +Here are some common starter configuration examples. Refer to the [reference config](./reference.md) for detailed information about all the config values. + +### Raspberry Pi Home Assistant Add-on with USB Coral + +- Single camera with 720p, 5fps stream for detect +- MQTT connected to the Home Assistant Mosquitto Add-on +- Hardware acceleration for decoding video +- USB Coral detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + +```yaml +mqtt: + host: core-mosquitto + user: mqtt-user + password: xxxxxxxxxx + +ffmpeg: + hwaccel_args: preset-rpi-64-h264 + +detectors: + coral: + type: edgetpu + device: usb + +record: + enabled: True + retain: + days: 7 + mode: motion + alerts: + retain: + days: 30 + detections: + retain: + days: 30 + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + - 0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400 +``` + +### Standalone Intel Mini PC with USB Coral + +- Single camera with 720p, 5fps stream for detect +- MQTT disabled (not integrated with home assistant) +- VAAPI hardware acceleration for decoding video +- USB Coral detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + +```yaml +mqtt: + enabled: False + +ffmpeg: + hwaccel_args: preset-vaapi + +detectors: + coral: + type: edgetpu + device: usb + +record: + enabled: True + retain: + days: 7 + mode: motion + alerts: + retain: + days: 30 + detections: + retain: + days: 30 + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + - 0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400 +``` + +### Home Assistant integrated Intel Mini PC with OpenVino + +- Single camera with 720p, 5fps stream for detect +- MQTT connected to same mqtt server as home assistant +- VAAPI hardware acceleration for decoding video +- OpenVino detector +- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not +- Continue to keep all video if it qualified as an alert or detection for 30 days +- Save snapshots for 30 days +- Motion mask for the camera timestamp + +```yaml +mqtt: + host: 192.168.X.X # <---- same mqtt broker that home assistant uses + user: mqtt-user + password: xxxxxxxxxx + +ffmpeg: + hwaccel_args: preset-vaapi + +detectors: + ov: + type: openvino + device: AUTO + +model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt + +record: + enabled: True + retain: + days: 7 + mode: motion + alerts: + retain: + days: 30 + detections: + retain: + days: 30 + +snapshots: + enabled: True + retain: + default: 30 + +cameras: + name_of_your_camera: + detect: + width: 1280 + height: 720 + fps: 5 + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + - 0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400 +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/license_plate_recognition.md b/sam2-cpu/frigate-dev/docs/docs/configuration/license_plate_recognition.md new file mode 100644 index 0000000..a18c822 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/license_plate_recognition.md @@ -0,0 +1,437 @@ +--- +id: license_plate_recognition +title: License Plate Recognition (LPR) +--- + +Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a [known](#matching) name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. + +LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. When a vehicle becomes stationary, LPR continues to run for a short time after to attempt recognition. + +When a plate is recognized, the details are: + +- Added as a `sub_label` (if [known](#matching)) or the `recognized_license_plate` field (if unknown) to a tracked object. +- Viewable in the Details pane in Review/History. +- Viewable in the Tracked Object Details pane in Explore (sub labels and recognized license plates). +- Filterable through the More Filters menu in Explore. +- Published via the `frigate/events` MQTT topic as a `sub_label` ([known](#matching)) or `recognized_license_plate` (unknown) for the `car` or `motorcycle` tracked object. +- Published via the `frigate/tracked_object_update` MQTT topic with `name` (if [known](#matching)) and `plate`. + +## Model Requirements + +Users running a Frigate+ model (or any custom model that natively detects license plates) should ensure that `license_plate` is added to the [list of objects to track](https://docs.frigate.video/plus/#available-label-types) either globally or for a specific camera. This will improve the accuracy and performance of the LPR model. + +Users without a model that detects license plates can still run LPR. Frigate uses a lightweight YOLOv9 license plate detection model that can be configured to run on your CPU or GPU. In this case, you should _not_ define `license_plate` in your list of objects to track. + +:::note + +In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle` before it can recognize a license plate. If you're using a dedicated LPR camera and have a zoomed-in view where a `car` or `motorcycle` will not be detected, you can still run LPR, but the configuration parameters will differ from the default mode. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section below. + +::: + +## Minimum System Requirements + +License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM is required. + +## Configuration + +License plate recognition is disabled by default. Enable it in your config file: + +```yaml +lpr: + enabled: True +``` + +Like other enrichments in Frigate, LPR **must be enabled globally** to use the feature. You should disable it for specific cameras at the camera level if you don't want to run LPR on cars on those cameras: + +```yaml +cameras: + garage: + ... + lpr: + enabled: False +``` + +For non-dedicated LPR cameras, ensure that your camera is configured to detect objects of type `car` or `motorcycle`, and that a car or motorcycle is actually being detected by Frigate. Otherwise, LPR will not run. + +Like the other real-time processors in Frigate, license plate recognition runs on the camera stream defined by the `detect` role in your config. To ensure optimal performance, select a suitable resolution for this stream in your camera's firmware that fits your specific scene and requirements. + +## Advanced Configuration + +Fine-tune the LPR feature using these optional parameters at the global level of your config. The only optional parameters that can be set at the camera level are `enabled`, `min_area`, and `enhancement`. + +### Detection + +- **`detection_threshold`**: License plate object detection confidence score required before recognition runs. + - Default: `0.7` + - Note: This is field only applies to the standalone license plate detection model, `threshold` and `min_score` object filters should be used for models like Frigate+ that have license plate detection built in. +- **`min_area`**: Defines the minimum area (in pixels) a license plate must be before recognition runs. + - Default: `1000` pixels. Note: this is intentionally set very low as it is an _area_ measurement (length x width). For reference, 1000 pixels represents a ~32x32 pixel square in your camera image. + - Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant plates. +- **`device`**: Device to use to run license plate detection _and_ recognition models. + - Default: `CPU` + - This can be `CPU`, `GPU`, or the GPU's device number. For users without a model that detects license plates natively, using a GPU may increase performance of the YOLOv9 license plate detector model. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. However, for users who run a model that detects `license_plate` natively, there is little to no performance gain reported with running LPR on GPU compared to the CPU. +- **`model_size`**: The size of the model used to identify regions of text on plates. + - Default: `small` + - This can be `small` or `large`. + - The `small` model is fast and identifies groups of Latin and Chinese characters. + - The `large` model identifies Latin characters only, and uses an enhanced text detector to find characters on multi-line plates. It is significantly slower than the `small` model. + - If your country or region does not use multi-line plates, you should use the `small` model as performance is much better for single-line plates. + +### Recognition + +- **`recognition_threshold`**: Recognition confidence score required to add the plate to the object as a `recognized_license_plate` and/or `sub_label`. + - Default: `0.9`. +- **`min_plate_length`**: Specifies the minimum number of characters a detected license plate must have to be added as a `recognized_license_plate` and/or `sub_label` to an object. + - Use this to filter out short, incomplete, or incorrect detections. +- **`format`**: A regular expression defining the expected format of detected plates. Plates that do not match this format will be discarded. + - `"^[A-Z]{1,3} [A-Z]{1,2} [0-9]{1,4}$"` matches plates like "B AB 1234" or "M X 7" + - `"^[A-Z]{2}[0-9]{2} [A-Z]{3}$"` matches plates like "AB12 XYZ" or "XY68 ABC" + - Websites like https://regex101.com/ can help test regular expressions for your plates. + +### Matching + +- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` and `motorcycle` objects when a recognized plate matches a known value. + - These labels appear in the UI, filters, and notifications. + - Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`. +- **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. + - For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. + - This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`. + +### Image Enhancement + +- **`enhancement`**: A value between 0 and 10 that adjusts the level of image enhancement applied to captured license plates before they are processed for recognition. This preprocessing step can sometimes improve accuracy but may also have the opposite effect. + - Default: `0` (no enhancement) + - Higher values increase contrast, sharpen details, and reduce noise, but excessive enhancement can blur or distort characters, actually making them much harder for Frigate to recognize. + - This setting is best adjusted at the camera level if running LPR on multiple cameras. + - If Frigate is already recognizing plates correctly, leave this setting at the default of `0`. However, if you're experiencing frequent character issues or incomplete plates and you can already easily read the plates yourself, try increasing the value gradually, starting at 5 and adjusting as needed. You should see how different enhancement levels affect your plates. Use the `debug_save_plates` configuration option (see below). + +### Normalization Rules + +- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially and are applied _before_ the `format` regex, if specified. Each rule must have a `pattern` (which can be a string or a regex) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0'). + +These rules must be defined at the global level of your `lpr` config. + +```yaml +lpr: + replace_rules: + - pattern: "[%#*?]" # Remove noise symbols + replacement: "" + - pattern: "[= ]" # Normalize = or space to dash + replacement: "-" + - pattern: "O" # Swap 'O' to '0' (common OCR error) + replacement: "0" + - pattern: "I" # Swap 'I' to '1' + replacement: "1" + - pattern: '(\w{3})(\w{3})' # Split 6 chars into groups (e.g., ABC123 → ABC-123) - use single quotes to preserve backslashes + replacement: '\1-\2' +``` + +- Rules fire in order: In the example above: clean noise first, then separators, then swaps, then splits. +- Backrefs (`\1`, `\2`) allow dynamic replacements (e.g., capture groups). +- Any changes made by the rules are printed to the LPR debug log. +- Tip: You can test patterns with tools like regex101.com. + +### Debugging + +- **`debug_save_plates`**: Set to `True` to save captured text on plates for debugging. These images are stored in `/media/frigate/clips/lpr`, organized into subdirectories by `/`, and named based on the capture timestamp. + - These saved images are not full plates but rather the specific areas of text detected on the plates. It is normal for the text detection model to sometimes find multiple areas of text on the plate. Use them to analyze what text Frigate recognized and how image enhancement affects detection. + - **Note:** Frigate does **not** automatically delete these debug images. Once LPR is functioning correctly, you should disable this option and manually remove the saved files to free up storage. + +## Configuration Examples + +These configuration parameters are available at the global level of your config. The only optional parameters that should be set at the camera level are `enabled`, `min_area`, and `enhancement`. + +```yaml +lpr: + enabled: True + min_area: 1500 # Ignore plates with an area (length x width) smaller than 1500 pixels + min_plate_length: 4 # Only recognize plates with 4 or more characters + known_plates: + Wife's Car: + - "ABC-1234" + - "ABC-I234" # Accounts for potential confusion between the number one (1) and capital letter I + Johnny: + - "J*N-*234" # Matches JHN-1234 and JMN-I234, but also note that "*" matches any number of characters + Sally: + - "[S5]LL 1234" # Matches both SLL 1234 and 5LL 1234 + Work Trucks: + - "EMP-[0-9]{3}[A-Z]" # Matches plates like EMP-123A, EMP-456Z +``` + +```yaml +lpr: + enabled: True + min_area: 4000 # Run recognition on larger plates only (4000 pixels represents a 63x63 pixel square in your image) + recognition_threshold: 0.85 + format: "^[A-Z]{2} [A-Z][0-9]{4}$" # Only recognize plates that are two letters, followed by a space, followed by a single letter and 4 numbers + match_distance: 1 # Allow one character variation in plate matching + replace_rules: + - pattern: "O" + replacement: "0" # Replace the letter O with the number 0 in every plate + known_plates: + Delivery Van: + - "RJ K5678" + - "UP A1234" + Supervisor: + - "MN D3163" +``` + +:::note + +If a camera is configured to detect `car` or `motorcycle` but you don't want Frigate to run LPR for that camera, disable LPR at the camera level: + +```yaml +cameras: + side_yard: + lpr: + enabled: False + ... +``` + +::: + +## Dedicated LPR Cameras + +Dedicated LPR cameras are single-purpose cameras with powerful optical zoom to capture license plates on distant vehicles, often with fine-tuned settings to capture plates at night. + +To mark a camera as a dedicated LPR camera, add `type: "lpr"` the camera configuration. + +:::note + +Frigate's dedicated LPR mode is optimized for cameras with a narrow field of view, specifically positioned and zoomed to capture license plates exclusively. If your camera provides a general overview of a scene rather than a tightly focused view, this mode is not recommended. + +::: + +Users can configure Frigate's dedicated LPR mode in two different ways depending on whether a Frigate+ (or native `license_plate` detecting) model is used: + +### Using a Frigate+ (or Native `license_plate` Detecting) Model + +Users running a Frigate+ model (or any model that natively detects `license_plate`) can take advantage of `license_plate` detection. This allows license plates to be treated as standard objects in dedicated LPR mode, meaning that alerts, detections, snapshots, and other Frigate features work as usual, and plates are detected efficiently through your configured object detector. + +An example configuration for a dedicated LPR camera using a `license_plate`-detecting model: + +```yaml +# LPR global configuration +lpr: + enabled: True + device: CPU # can also be GPU if available + +# Dedicated LPR camera configuration +cameras: + dedicated_lpr_camera: + type: "lpr" # required to use dedicated LPR camera mode + ffmpeg: ... # add your streams + detect: + enabled: True + fps: 5 # increase to 10 if vehicles move quickly across your frame. Higher than 10 is unnecessary and is not recommended. + min_initialized: 2 + width: 1920 + height: 1080 + objects: + track: + - license_plate + filters: + license_plate: + threshold: 0.7 + motion: + threshold: 30 + contour_area: 60 # use an increased value to tune out small motion changes + improve_contrast: false + mask: 0.704,0.007,0.709,0.052,0.989,0.055,0.993,0.001 # ensure your camera's timestamp is masked + record: + enabled: True # disable recording if you only want snapshots + snapshots: + enabled: True + review: + detections: + labels: + - license_plate +``` + +With this setup: + +- License plates are treated as normal objects in Frigate. +- Scores, alerts, detections, and snapshots work as expected. +- Snapshots will have license plate bounding boxes on them. +- The `frigate/events` MQTT topic will publish tracked object updates. +- Debug view will display `license_plate` bounding boxes. +- If you are using a Frigate+ model and want to submit images from your dedicated LPR camera for model training and fine-tuning, annotate both the `car` / `motorcycle` and the `license_plate` in the snapshots on the Frigate+ website, even if the car is barely visible. + +### Using the Secondary LPR Pipeline (Without Frigate+) + +If you are not running a Frigate+ model, you can use Frigate’s built-in secondary dedicated LPR pipeline. In this mode, Frigate bypasses the standard object detection pipeline and runs a local license plate detector model on the full frame whenever motion activity occurs. + +An example configuration for a dedicated LPR camera using the secondary pipeline: + +```yaml +# LPR global configuration +lpr: + enabled: True + device: CPU # can also be GPU if available and correct Docker image is used + detection_threshold: 0.7 # change if necessary + +# Dedicated LPR camera configuration +cameras: + dedicated_lpr_camera: + type: "lpr" # required to use dedicated LPR camera mode + lpr: + enabled: True + enhancement: 3 # optional, enhance the image before trying to recognize characters + ffmpeg: ... # add your streams + detect: + enabled: False # disable Frigate's standard object detection pipeline + fps: 5 # increase if necessary, though high values may slow down Frigate's enrichments pipeline and use considerable CPU + width: 1920 + height: 1080 + objects: + track: [] # required when not using a Frigate+ model for dedicated LPR mode + motion: + threshold: 30 + contour_area: 60 # use an increased value here to tune out small motion changes + improve_contrast: false + mask: 0.704,0.007,0.709,0.052,0.989,0.055,0.993,0.001 # ensure your camera's timestamp is masked + record: + enabled: True # disable recording if you only want snapshots + review: + detections: + enabled: True + retain: + default: 7 +``` + +With this setup: + +- The standard object detection pipeline is bypassed. Any detected license plates on dedicated LPR cameras are treated similarly to manual events in Frigate. You must **not** specify `license_plate` as an object to track. +- The license plate detector runs on the full frame whenever motion is detected and processes frames according to your detect `fps` setting. +- Review items will always be classified as a `detection`. +- Snapshots will always be saved. +- Zones and object masks are **not** used. +- The `frigate/events` MQTT topic will **not** publish tracked object updates with the license plate bounding box and score, though `frigate/reviews` will publish if recordings are enabled. If a plate is recognized as a [known](#matching) plate, publishing will occur with an updated `sub_label` field. If characters are recognized, publishing will occur with an updated `recognized_license_plate` field. +- License plate snapshots are saved at the highest-scoring moment and appear in Explore. +- Debug view will not show `license_plate` bounding boxes. + +### Summary + +| Feature | Native `license_plate` detecting Model (like Frigate+) | Secondary Pipeline (without native model or Frigate+) | +| ----------------------- | ------------------------------------------------------ | --------------------------------------------------------------- | +| License Plate Detection | Uses `license_plate` as a tracked object | Runs a dedicated LPR pipeline | +| FPS Setting | 5 (increase for fast-moving cars) | 5 (increase for fast-moving cars, but it may use much more CPU) | +| Object Detection | Standard Frigate+ detection applies | Bypasses standard object detection | +| Debug View | May show `license_plate` bounding boxes | May **not** show `license_plate` bounding boxes | +| MQTT `frigate/events` | Publishes tracked object updates | Publishes limited updates | +| Explore | Recognized plates available in More Filters | Recognized plates available in More Filters | + +By selecting the appropriate configuration, users can optimize their dedicated LPR cameras based on whether they are using a Frigate+ model or the secondary LPR pipeline. + +### Best practices for using Dedicated LPR camera mode + +- Tune your motion detection and increase the `contour_area` until you see only larger motion boxes being created as cars pass through the frame (likely somewhere between 50-90 for a 1920x1080 detect stream). Increasing the `contour_area` filters out small areas of motion and will prevent excessive resource use from looking for license plates in frames that don't even have a car passing through it. +- Disable the `improve_contrast` motion setting, especially if you are running LPR at night and the frame is mostly dark. This will prevent small pixel changes and smaller areas of motion from triggering license plate detection. +- Ensure your camera's timestamp is covered with a motion mask so that it's not incorrectly detected as a license plate. +- For non-Frigate+ users, you may need to change your camera settings for a clearer image or decrease your global `recognition_threshold` config if your plates are not being accurately recognized at night. +- The secondary pipeline mode runs a local AI model on your CPU or GPU (depending on how `device` is configured) to detect plates. Increasing detect `fps` will increase resource usage proportionally. + +## FAQ + +### Why isn't my license plate being detected and recognized? + +Ensure that: + +- Your camera has a clear, human-readable, well-lit view of the plate. If you can't read the plate's characters, Frigate certainly won't be able to, even if the model is recognizing a `license_plate`. This may require changing video size, quality, or frame rate settings on your camera, depending on your scene and how fast the vehicles are traveling. +- The plate is large enough in the image (try adjusting `min_area`) or increasing the resolution of your camera's stream. +- Your `enhancement` level (if you've changed it from the default of `0`) is not too high. Too much enhancement will run too much denoising and cause the plate characters to become blurry and unreadable. + +If you are using a Frigate+ model or a custom model that detects license plates, ensure that `license_plate` is added to your list of objects to track. +If you are using the free model that ships with Frigate, you should _not_ add `license_plate` to the list of objects to track. + +Recognized plates will show as object labels in the debug view and will appear in the "Recognized License Plates" select box in the More Filters popout in Explore. + +If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below. + +### Can I run LPR without detecting `car` or `motorcycle` objects? + +In normal LPR mode, Frigate requires a `car` or `motorcycle` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above. + +### How can I improve detection accuracy? + +- Use high-quality cameras with good resolution. +- Adjust `detection_threshold` and `recognition_threshold` values. +- Define a `format` regex to filter out invalid detections. + +### Does LPR work at night? + +Yes, but performance depends on camera quality, lighting, and infrared capabilities. Make sure your camera can capture clear images of plates at night. + +### Can I limit LPR to specific zones? + +LPR, like other Frigate enrichments, runs at the camera level rather than the zone level. While you can't restrict LPR to specific zones directly, you can control when recognition runs by setting a `min_area` value to filter out smaller detections. + +### How can I match known plates with minor variations? + +Use `match_distance` to allow small character mismatches. Alternatively, define multiple variations in `known_plates`. + +### How do I debug LPR issues? + +Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps. + +1. Start with a simplified LPR config. + + - Remove or comment out everything in your LPR config, including `min_area`, `min_plate_length`, `format`, `known_plates`, or `enhancement` values so that the only values left are `enabled` and `debug_save_plates`. This will run LPR with Frigate's default values. + + ```yaml + lpr: + enabled: true + debug_save_plates: true + ``` + +2. Enable debug logs to see exactly what Frigate is doing. + + - Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary. Restart Frigate after this change. + + ```yaml + logger: + default: info + logs: + frigate.data_processing.common.license_plate: debug + ``` + +3. Ensure your plates are being _detected_. + + If you are using a Frigate+ or `license_plate` detecting model: + + - Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected. + - View MQTT messages for `frigate/events` to verify detected plates. + - You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected. + + If you are **not** using a Frigate+ or `license_plate` detecting model: + + - Watch the debug logs for messages from the YOLOv9 plate detector. + - You may need to adjust your `detection_threshold` if your plates are not being detected. + +4. Ensure the characters on detected plates are being _recognized_. + + - Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear. + - Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working. + - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). + +### Will LPR slow down my system? + +LPR's performance impact depends on your hardware. Ensure you have at least 4GB RAM and a capable CPU or GPU for optimal results. If you are running the Dedicated LPR Camera mode, resource usage will be higher compared to users who run a model that natively detects license plates. Tune your motion detection settings for your dedicated LPR camera so that the license plate detection model runs only when necessary. + +### I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects `license_plate`. Why is the YOLOv9 model running? + +The YOLOv9 license plate detector model will run (and the metric will appear) if you've enabled LPR but haven't defined `license_plate` as an object to track, either at the global or camera level. + +If you are detecting `car` or `motorcycle` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track. + +### It looks like Frigate picked up my camera's timestamp or overlay text as the license plate. How can I prevent this? + +This could happen if cars or motorcycles travel close to your camera's timestamp or overlay text. You could either move the text through your camera's firmware, or apply a mask to it in Frigate. + +If you are using a model that natively detects `license_plate`, add an _object mask_ of type `license_plate` and a _motion mask_ over your text. + +If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required. + +### I see "Error running ... model" in my logs. How can I fix this? + +This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/live.md b/sam2-cpu/frigate-dev/docs/docs/configuration/live.md new file mode 100644 index 0000000..50d0b72 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/live.md @@ -0,0 +1,352 @@ +--- +id: live +title: Live View +--- + +Frigate intelligently displays your camera streams on the Live view dashboard. By default, Frigate employs "smart streaming" where camera images update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any motion or active objects are detected, cameras seamlessly switch to a live stream. + +### Live View technologies + +Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be configured as shown in the [step by step guide](/guides/configuring_go2rtc). + +The jsmpeg live view will use more browser and client GPU resources. Using go2rtc is highly recommended and will provide a superior experience. + +| Source | Frame Rate | Resolution | Audio | Requires go2rtc | Notes | +| ------ | ------------------------------------- | ---------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| jsmpeg | same as `detect -> fps`, capped at 10 | 720p | no | no | Resolution is configurable, but go2rtc is recommended if you want higher resolutions and better frame rates. jsmpeg is Frigate's default without go2rtc configured. | +| mse | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only. This is Frigate's default when go2rtc is configured. | +| webrtc | native | native | yes (depends on audio codec) | yes | Requires extra configuration, doesn't support h.265. Frigate attempts to use WebRTC when MSE fails or when using a camera's two-way talk feature. | + +### Camera Settings Recommendations + +If you are using go2rtc, you should adjust the following settings in your camera's firmware for the best experience with Live view: + +- Video codec: **H.264** - provides the most compatible video codec with all Live view technologies and browsers. Avoid any kind of "smart codec" or "+" codec like _H.264+_ or _H.265+_. as these non-standard codecs remove keyframes (see below). +- Audio codec: **AAC** - provides the most compatible audio codec with all Live view technologies and browsers that support audio. +- I-frame interval (sometimes called the keyframe interval, the interframe space, or the GOP length): match your camera's frame rate, or choose "1x" (for interframe space on Reolink cameras). For example, if your stream outputs 20fps, your i-frame interval should be 20 (or 1x on Reolink). Values higher than the frame rate will cause the stream to take longer to begin playback. See [this page](https://gardinal.net/understanding-the-keyframe-interval/) for more on keyframes. For many users this may not be an issue, but it should be noted that a 1x i-frame interval will cause more storage utilization if you are using the stream for the `record` role as well. + +The default video and audio codec on your camera may not always be compatible with your browser, which is why setting them to H.264 and AAC is recommended. See the [go2rtc docs](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness) for codec support information. + +### Audio Support + +MSE Requires PCMA/PCMU or AAC audio, WebRTC requires PCMA/PCMU or opus audio. If you want to support both MSE and WebRTC then your restream config needs to make sure both are enabled. + +```yaml +go2rtc: + streams: + rtsp_cam: # <- for RTSP streams + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio + - "ffmpeg:rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + http_cam: # <- for http streams + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio + - "ffmpeg:http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) +``` + +If your camera does not support AAC audio or are having problems with Live view, try transcoding to AAC audio directly: + +```yaml +go2rtc: + streams: + rtsp_cam: # <- for RTSP streams + - "ffmpeg:rtsp://192.168.1.5:554/live0#video=copy#audio=aac" # <- copies video stream and transcodes to aac audio + - "ffmpeg:rtsp_cam#audio=opus" # <- provides support for WebRTC +``` + +If your camera does not have audio and you are having problems with Live view, you should have go2rtc send video only: + +```yaml +go2rtc: + streams: + no_audio_camera: + - ffmpeg:rtsp://192.168.1.5:554/live0#video=copy +``` + +### Setting Streams For Live UI + +You can configure Frigate to allow manual selection of the stream you want to view in the Live UI. For example, you may want to view your camera's substream on mobile devices, but the full resolution stream on desktop devices. Setting the `live -> streams` list will populate a dropdown in the UI's Live view that allows you to choose between the streams. This stream setting is _per device_ and is saved in your browser's local storage. + +Additionally, when creating and editing camera groups in the UI, you can choose the stream you want to use for your camera group's Live dashboard. + +:::note + +Frigate's default dashboard ("All Cameras") will always use the first entry you've defined in `streams:` when playing live streams from your cameras. + +::: + +Configure the `streams` option with a "friendly name" for your stream followed by the go2rtc stream name. + +Using Frigate's internal version of go2rtc is required to use this feature. You cannot specify paths in the `streams` configuration, only go2rtc stream names. + +```yaml +go2rtc: + streams: + test_cam: + - rtsp://192.168.1.5:554/live_main # <- stream which supports video & aac audio. + - "ffmpeg:test_cam#audio=opus" # <- copy of the stream which transcodes audio to opus for webrtc + test_cam_sub: + - rtsp://192.168.1.5:554/live_sub # <- stream which supports video & aac audio. + test_cam_another_sub: + - rtsp://192.168.1.5:554/live_alt # <- stream which supports video & aac audio. + +cameras: + test_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/test_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/test_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - detect + live: + streams: # <--- Multiple streams for Frigate 0.16 and later + Main Stream: test_cam # <--- Specify a "friendly name" followed by the go2rtc stream name + Sub Stream: test_cam_sub + Special Stream: test_cam_another_sub +``` + +### WebRTC extra configuration: + +WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration: + +- For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP. +- For internal/local access, unless you are running through the HA Add-on, you will also need to set the WebRTC candidates list in the go2rtc config. For example, if `192.168.1.10` is the local IP of the device running Frigate: + + ```yaml title="config.yml" + go2rtc: + streams: + test_cam: ... + webrtc: + candidates: + - 192.168.1.10:8555 + - stun:8555 + ``` + +- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block. +- Note that WebRTC does not support H.265. + +:::tip + +This extra configuration may not be required if Frigate has been installed as a Home Assistant Add-on, as Frigate uses the Supervisor's API to generate a WebRTC candidate. + +However, it is recommended if issues occur to define the candidates manually. You should do this if the Frigate Add-on fails to generate a valid candidate. If an error occurs you will see some warnings like the below in the Add-on logs page during the initialization: + +```log +[WARN] Failed to get IP address from supervisor +[WARN] Failed to get WebRTC port from supervisor +``` + +::: + +:::note + +If you are having difficulties getting WebRTC to work and you are running Frigate with docker, you may want to try changing the container network mode: + +- `network: host`, in this mode you don't need to forward any ports. The services inside of the Frigate container will have full access to the network interfaces of your host machine as if they were running natively and not in a container. Any port conflicts will need to be resolved. This network mode is recommended by go2rtc, but we recommend you only use it if necessary. +- `network: bridge` is the default network driver, a bridge network is a Link Layer device which forwards traffic between network segments. You need to forward any ports that you want to be accessible from the host IP. + +If not running in host mode, port 8555 will need to be mapped for the container: + +docker-compose.yml + +```yaml +services: + frigate: + ... + ports: + - "8555:8555/tcp" # WebRTC over tcp + - "8555:8555/udp" # WebRTC over udp +``` + +::: + +See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.3#module-webrtc) for more information about this. + +### Two way talk + +For devices that support two way talk, Frigate can be configured to use the feature from the camera's Live view in the Web UI. You should: + +- Set up go2rtc with [WebRTC](#webrtc-extra-configuration). +- Ensure you access Frigate via https (may require [opening port 8971](/frigate/installation/#ports)). +- For the Home Assistant Frigate card, [follow the docs](http://card.camera/#/usage/2-way-audio) for the correct source. + +To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-cameras) + +As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability. + +To prevent go2rtc from blocking other applications from accessing your camera's two-way audio, you must configure your stream with `#backchannel=0`. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation. + +### Streaming options on camera group dashboards + +Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage. + +- Stream selection using the `live -> streams` configuration option (see _Setting Streams For Live UI_ above) +- Streaming type: + - _No streaming_: Camera images will only update once per minute and no live streaming will occur. + - _Smart Streaming_ (default, recommended setting): Smart streaming will update your camera image once per minute when no detectable activity is occurring to conserve bandwidth and resources, since a static picture is the same as a streaming image with no motion or objects. When motion or objects are detected, the image seamlessly switches to a live stream. + - _Continuous Streaming_: Camera image will always be a live stream when visible on the dashboard, even if no activity is being detected. Continuous streaming may cause high bandwidth usage and performance issues. **Use with caution.** +- _Compatibility mode_: Enable this option only if your camera's live stream is displaying color artifacts and has a diagonal line on the right side of the image. Before enabling this, try setting your camera's `detect` width and height to a standard aspect ratio (for example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your config fails to resolve the color artifacts and diagonal line. + +:::note + +The default dashboard ("All Cameras") will always use: + +- Smart Streaming, unless you've disabled the global Automatic Live View in Settings. +- The first entry set in your `streams` configuration, if defined. + +Use a camera group if you want to change any of these settings from the defaults. + +::: + +### Disabling cameras + +Cameras can be temporarily disabled through the Frigate UI and through [MQTT](/integrations/mqtt#frigatecamera_nameenabledset) to conserve system resources. When disabled, Frigate's ffmpeg processes are terminated — recording stops, object detection is paused, and the Live dashboard displays a blank image with a disabled message. Review items, tracked objects, and historical footage for disabled cameras can still be accessed via the UI. + +:::note + +Disabling a camera via the Frigate UI or MQTT is temporary and does not persist through restarts of Frigate. + +::: + +For restreamed cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source). + +Note that disabling a camera through the config file (`enabled: False`) removes all related UI elements, including historical footage access. To retain access while disabling the camera, keep it enabled in the config and use the UI or MQTT to disable it temporarily. + +### Live player error messages + +When your browser runs into problems playing back your camera streams, it will log short error messages to the browser console. They indicate playback, codec, or network issues on the client/browser side, not something server side with Frigate itself. Below are the common messages you may see and simple actions you can take to try to resolve them. + +- **startup** + + - What it means: The player failed to initialize or connect to the live stream (network or startup error). + - What to try: Reload the Live view or click _Reset_. Verify `go2rtc` is running and the camera stream is reachable. Try switching to a different stream from the Live UI dropdown (if available) or use a different browser. + + - Possible console messages from the player code: + + - `Error opening MediaSource.` + - `Browser reported a network error.` + - `Max error count ${errorCount} exceeded.` (the numeric value will vary) + +- **mse-decode** + + - What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames. + - What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer. + + - Possible console messages from the player code: + + - `Safari cannot open MediaSource.` + - `Safari reported InvalidStateError.` + - `Safari reported decoding errors.` + +- **stalled** + + - What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving). + - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval — shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings. + + - Possible console messages from the player code: + + - `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.` + - `Media playback has stalled after seconds due to insufficient buffering or a network interruption.` (the seconds value will vary) + +## Live view FAQ + +1. **Why don't I have audio in my Live view?** + + You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. + + Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. + +2. **Frigate shows that my live stream is in "low bandwidth mode". What does this mean?** + + Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. + + When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. + + If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. + + Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: + + - Network issues (e.g., MSE or WebRTC network connection problems). + - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). + - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. + - Browser compatibility problems (e.g., iOS Safari limitations with MSE). + + To view browser console logs: + + 1. Open the Frigate Live View in your browser. + 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). + 3. Reproduce the error (e.g., load a problematic stream or simulate network issues). + 4. Look for messages prefixed with the camera name. + + These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: + + - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera_settings_recommendations)). + - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). + - Test with a different stream via the UI dropdown (if `live -> streams` is configured). + - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see (WebRTC Extra Configuration)(#webrtc-extra-configuration)). + - If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + +3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?** + + On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. + +4. **I see a strange diagonal line on my live view, but my recordings look fine. How can I fix it?** + + This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + +5. **How does "smart streaming" work?** + + Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. + + This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. + + Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. + + This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. + +6. **I have unmuted some cameras on my dashboard, but I do not hear sound. Why?** + + If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. + +7. **My camera streams have lots of visual artifacts / distortion.** + + Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. + +8. **Why does my camera stream switch aspect ratios on the Live dashboard?** + + Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + + To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. + + Example: Resolutions from two streams + + - Mismatched (may cause aspect ratio switching on the dashboard): + + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x352 (~1.82:1, not 16:9) + + - Matched (prevents switching): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x360 (16:9) + + You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: + + ```yaml + cameras: + front_door: + detect: + width: 640 + height: 360 # set this to 360 instead of 352 + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 + roles: + - record + - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 + roles: + - detect + ``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/masks.md b/sam2-cpu/frigate-dev/docs/docs/configuration/masks.md new file mode 100644 index 0000000..4a47225 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/masks.md @@ -0,0 +1,84 @@ +--- +id: masks +title: Masks +--- + +## Motion masks + +Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the Debug feed (Settings --> Debug) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._ + +See [further clarification](#further-clarification) below on why you may not want to use a motion mask. + +## Object filter masks + +Object filter masks are used to filter out false positives for a given object type based on location. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell Frigate that anything in your yard is a false positive. + +Object filter masks can be used to filter out stubborn false positives in fixed locations. For example, the base of this tree may be frequently detected as a person. The following image shows an example of an object filter mask (shaded red area) over the location where the bottom center is typically located to filter out person detections in a precise location. + +![object mask](/img/bottom-center-mask.jpg) + +## Using the mask creator + +To create a poly mask: + +1. Visit the Web UI +2. Click/tap the gear icon and open "Settings" +3. Select "Mask / zone editor" +4. At the top right, select the camera you wish to create a mask or zone for +5. Click the plus icon under the type of mask or zone you would like to create +6. Click on the camera's latest image to create the points for a masked area. Click the first point again to close the polygon. +7. When you've finished creating your mask, press Save. + +Your config file will be updated with the relative coordinates of the mask/zone: + +```yaml +motion: + mask: "0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456,0.700,0.424,0.701,0.311,0.507,0.294,0.453,0.347,0.451,0.400" +``` + +Multiple masks can be listed in your config. + +```yaml +motion: + mask: + - 0.239,1.246,0.175,0.901,0.165,0.805,0.195,0.802 + - 0.000,0.427,0.002,0.000,0.999,0.000,0.999,0.781,0.885,0.456 +``` + +### Further Clarification + +This is a response to a [question posed on reddit](https://www.reddit.com/r/homeautomation/comments/ppxdve/replacing_my_doorbell_with_a_security_camera_a_6/hd876w4?utm_source=share&utm_medium=web2x&context=3): + +It is helpful to understand a bit about how Frigate uses motion detection and object detection together. + +First, Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection. + +Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer. + +After the area with motion is identified, Frigate creates a "region" (the green boxes in the debug viewer) to run object detection on. The models are trained on square images, so these regions are always squares. It adds a margin around the motion area in hopes of capturing a cropped view of the object moving that fills most of the image passed to object detection, but doesn't cut anything off. It also takes into consideration the location of the bounding box from the previous frame if it is tracking an object. + +After object detection runs, if there are detected objects that seem to be cut off, Frigate reframes the region and runs object detection again on the same frame to get a better look. + +All of this happens for each area of motion and tracked object. + +> Are you simply saying that INITIAL triggering of any kind of detection will only happen in un-masked areas, but that once this triggering happens, the masks become irrelevant and object detection takes precedence? + +Essentially, yes. I wouldn't describe it as object detection taking precedence though. The motion masks just prevent those areas from being counted as motion. Those masks do not modify the regions passed to object detection in any way, so you can absolutely detect objects in areas masked for motion. + +> If so, this is completely expected and intuitive behavior for me. Because obviously if a "foot" starts motion detection the camera should be able to check if it's an entire person before it fully crosses into the zone. The docs imply this is the behavior, so I also don't understand why this would be detrimental to object detection on the whole. + +When just a foot is triggering motion, Frigate will zoom in and look only at the foot. If that even qualifies as a person, it will determine the object is being cut off and look again and again until it zooms back out enough to find the whole person. + +It is also detrimental to how Frigate tracks a moving object. Motion nearby the bounding box from the previous frame is used to intelligently determine where the region should be in the next frame. With too much masking, tracking is hampered and if an object walks from an unmasked area into a fully masked area, they essentially disappear and will be picked up as a "new" object if they leave the masked area. This is important because Frigate uses the history of scores while tracking an object to determine if it is a false positive or not. It takes a minimum of 3 frames for Frigate to determine is the object type it thinks it is, and the median score must be greater than the threshold. If a person meets this threshold while on the sidewalk before they walk into your stoop, you will get an alert the instant they step a single foot into a zone. + +> I thought the main point of this feature was to cut down on CPU use when motion is happening in unnecessary areas. + +It is, but the definition of "unnecessary" varies. I want to ignore areas of motion that I know are definitely not being triggered by objects of interest. Timestamps, trees, sky, rooftops. I don't want to ignore motion from objects that I want to track and know where they go. + +> For me, giving my masks ANY padding results in a lot of people detection I'm not interested in. I live in the city and catch a lot of the sidewalk on my camera. People walk by my front door all the time and the margin between the sidewalk and actually walking onto my stoop is very thin, so I basically have everything but the exact contours of my stoop masked out. This results in very tidy detections but this info keeps throwing me off. Am I just overthinking it? + +This is what `required_zones` are for. You should define a zone (remember this is evaluated based on the bottom center of the bounding box) and make it required to save snapshots and clips (previously events in 0.9.0 to 0.13.0 and review items in 0.14.0 and later). You can also use this in your conditions for a notification. + +> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image. + +That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/metrics.md b/sam2-cpu/frigate-dev/docs/docs/configuration/metrics.md new file mode 100644 index 0000000..6624042 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/metrics.md @@ -0,0 +1,99 @@ +--- +id: metrics +title: Metrics +--- + +# Metrics + +Frigate exposes Prometheus metrics at the `/api/metrics` endpoint that can be used to monitor the performance and health of your Frigate instance. + +## Available Metrics + +### System Metrics +- `frigate_cpu_usage_percent{pid="", name="", process="", type="", cmdline=""}` - Process CPU usage percentage +- `frigate_mem_usage_percent{pid="", name="", process="", type="", cmdline=""}` - Process memory usage percentage +- `frigate_gpu_usage_percent{gpu_name=""}` - GPU utilization percentage +- `frigate_gpu_mem_usage_percent{gpu_name=""}` - GPU memory usage percentage + +### Camera Metrics +- `frigate_camera_fps{camera_name=""}` - Frames per second being consumed from your camera +- `frigate_detection_fps{camera_name=""}` - Number of times detection is run per second +- `frigate_process_fps{camera_name=""}` - Frames per second being processed +- `frigate_skipped_fps{camera_name=""}` - Frames per second skipped for processing +- `frigate_detection_enabled{camera_name=""}` - Detection enabled status for camera +- `frigate_audio_dBFS{camera_name=""}` - Audio dBFS for camera +- `frigate_audio_rms{camera_name=""}` - Audio RMS for camera + +### Detector Metrics +- `frigate_detector_inference_speed_seconds{name=""}` - Time spent running object detection in seconds +- `frigate_detection_start{name=""}` - Detector start time (unix timestamp) + +### Storage Metrics +- `frigate_storage_free_bytes{storage=""}` - Storage free bytes +- `frigate_storage_total_bytes{storage=""}` - Storage total bytes +- `frigate_storage_used_bytes{storage=""}` - Storage used bytes +- `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info + +### Service Metrics +- `frigate_service_uptime_seconds` - Uptime in seconds +- `frigate_service_last_updated_timestamp` - Stats recorded time (unix timestamp) +- `frigate_device_temperature{device=""}` - Device Temperature + +### Event Metrics +- `frigate_camera_events{camera="", label=""}` - Count of camera events since exporter started + +## Configuring Prometheus + +To scrape metrics from Frigate, add the following to your Prometheus configuration: + +```yaml +scrape_configs: + - job_name: 'frigate' + metrics_path: '/api/metrics' + static_configs: + - targets: ['frigate:5000'] + scrape_interval: 15s +``` + +## Example Queries + +Here are some example PromQL queries that might be useful: + +```promql +# Average CPU usage across all processes +avg(frigate_cpu_usage_percent) + +# Total GPU memory usage +sum(frigate_gpu_mem_usage_percent) + +# Detection FPS by camera +rate(frigate_detection_fps{camera_name="front_door"}[5m]) + +# Storage usage percentage +(frigate_storage_used_bytes / frigate_storage_total_bytes) * 100 + +# Event count by camera in last hour +increase(frigate_camera_events[1h]) +``` + +## Grafana Dashboard + +You can use these metrics to create Grafana dashboards to monitor your Frigate instance. Here's an example of metrics you might want to track: + +- CPU, Memory and GPU usage over time +- Camera FPS and detection rates +- Storage usage and trends +- Event counts by camera +- System temperatures + +A sample Grafana dashboard JSON will be provided in a future update. + +## Metric Types + +The metrics exposed by Frigate use the following Prometheus metric types: + +- **Counter**: Cumulative values that only increase (e.g., `frigate_camera_events`) +- **Gauge**: Values that can go up and down (e.g., `frigate_cpu_usage_percent`) +- **Info**: Key-value pairs for metadata (e.g., `frigate_storage_mount_type`) + +For more information about Prometheus metric types, see the [Prometheus documentation](https://prometheus.io/docs/concepts/metric_types/). diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/motion_detection.md b/sam2-cpu/frigate-dev/docs/docs/configuration/motion_detection.md new file mode 100644 index 0000000..c22491f --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/motion_detection.md @@ -0,0 +1,107 @@ +--- +id: motion_detection +title: Motion Detection +--- + +# Tuning Motion Detection + +Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection. + +Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer. + +## The Goal + +The default motion settings should work well for the majority of cameras, however there are cases where tuning motion detection can lead to better and more optimal results. Each camera has its own environment with different variables that affect motion, this means that the same motion settings will not fit all of your cameras. + +Before tuning motion it is important to understand the goal. In an optimal configuration, motion from people and cars would be detected, but not grass moving, lighting changes, timestamps, etc. If your motion detection is too sensitive, you will experience higher CPU loads and greater false positives from the increased rate of object detection. If it is not sensitive enough, you will miss objects that you want to track. + +## Create Motion Masks + +First, mask areas with regular motion not caused by the objects you want to detect. The best way to find candidates for motion masks is by watching the debug stream with motion boxes enabled. Good use cases for motion masks are timestamps or tree limbs and large bushes that regularly move due to wind. When possible, avoid creating motion masks that would block motion detection for objects you want to track **even if they are in locations where you don't want alerts or detections**. Motion masks should not be used to avoid detecting objects in specific areas. More details can be found [in the masks docs.](/configuration/masks.md). + +## Prepare For Testing + +The easiest way to tune motion detection is to use the Frigate UI under Settings > Motion Tuner. This screen allows the changing of motion detection values live to easily see the immediate effect on what is detected as motion. + +## Tuning Motion Detection During The Day + +Now that things are set up, find a time to tune that represents normal circumstances. For example, if you tune your motion on a day that is sunny and windy you may find later that the motion settings are not sensitive enough on a cloudy and still day. + +:::note + +Remember that motion detection is just used to determine when object detection should be used. You should aim to have motion detection sensitive enough that you won't miss objects you want to detect with object detection. The goal is to prevent object detection from running constantly for every small pixel change in the image. Windy days are still going to result in lots of motion being detected. + +::: + +### Threshold + +The threshold value dictates how much of a change in a pixels luminance is required to be considered motion. + +```yaml +# default threshold value +motion: + # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) + # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. + # The value should be between 1 and 255. + threshold: 30 +``` + +Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dogs blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion. + +Watching the motion boxes in the debug view, increase the threshold until you only see motion that is visible to the eye. Once this is done, it is important to test and ensure that desired motion is still detected. + +### Contour Area + +```yaml +# default contour_area value +motion: + # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) + # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will + # make motion detection more sensitive to smaller moving objects. + # As a rule of thumb: + # - 10 - high sensitivity + # - 30 - medium sensitivity + # - 50 - low sensitivity + contour_area: 10 +``` + +Once the threshold calculation is run, the pixels that have changed are grouped together. The contour area value is used to decide which groups of changed pixels qualify as motion. Smaller values are more sensitive meaning people that are far away, small animals, etc. are more likely to be detected as motion, but it also means that small changes in shadows, leaves, etc. are detected as motion. Higher values are less sensitive meaning these things won't be detected as motion but with the risk that desired motion won't be detected until closer to the camera. + +Watching the motion boxes in the debug view, adjust the contour area until there are no motion boxes smaller than the smallest you'd expect frigate to detect something moving. + +### Improve Contrast + +At this point if motion is working as desired there is no reason to continue with tuning for the day. If you were unable to find a balance between desired and undesired motion being detected, you can try disabling improve contrast and going back to the threshold and contour area steps. + +## Tuning Motion Detection During The Night + +Once daytime motion detection is tuned, there is a chance that the settings will work well for motion detection during the night as well. If this is the case then the preferred settings can be written to the config file and left alone. + +However, if the preferred day settings do not work well at night it is recommended to use Home Assistant or some other solution to automate changing the settings. That way completely separate sets of motion settings can be used for optimal day and night motion detection. + +## Tuning For Large Changes In Motion + +```yaml +# default lightning_threshold: +motion: + # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection + # needs to recalibrate. (default: shown below) + # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. + # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching + # a doorbell camera. + lightning_threshold: 0.8 +``` + +:::warning + +Some cameras like doorbell cameras may have missed detections when someone walks directly in front of the camera and the lightning_threshold causes motion detection to be re-calibrated. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. + +::: + +:::note + +Lightning threshold does not stop motion based recordings from being saved. + +::: + +Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. This is done via the `lightning_threshold` configuration. It is defined as the percentage of the image used to detect lightning or other substantial changes where motion detection needs to recalibrate. Increasing this value will make motion detection more likely to consider lightning or IR mode changes as valid motion. Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/notifications.md b/sam2-cpu/frigate-dev/docs/docs/configuration/notifications.md new file mode 100644 index 0000000..b5e1600 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/notifications.md @@ -0,0 +1,66 @@ +--- +id: notifications +title: Notifications +--- + +# Notifications + +Frigate offers native notifications using the [WebPush Protocol](https://web.dev/articles/push-notifications-web-push-protocol) which uses the [VAPID spec](https://tools.ietf.org/html/draft-thomson-webpush-vapid) to deliver notifications to web apps using encryption. + +## Setting up Notifications + +In order to use notifications the following requirements must be met: + +- Frigate must be accessed via a secure `https` connection ([see the authorization docs](/configuration/authentication)). +- A supported browser must be used. Currently Chrome, Firefox, and Safari are known to be supported. +- In order for notifications to be usable externally, Frigate must be accessible externally. +- For iOS devices, some users have also indicated that the Notifications switch needs to be enabled in iOS Settings --> Apps --> Safari --> Advanced --> Features. + +### Configuration + +To configure notifications, go to the Frigate WebUI -> Settings -> Notifications and enable, then fill out the fields and save. + +Optionally, you can change the default cooldown period for notifications through the `cooldown` parameter in your config file. This parameter can also be overridden at the camera level. + +Notifications will be prevented if either: + +- The global cooldown period hasn't elapsed since any camera's last notification +- The camera-specific cooldown period hasn't elapsed for the specific camera + +```yaml +notifications: + enabled: True + email: "johndoe@gmail.com" + cooldown: 10 # wait 10 seconds before sending another notification from any camera +``` + +```yaml +cameras: + doorbell: + ... + notifications: + enabled: True + cooldown: 30 # wait 30 seconds before sending another notification from the doorbell camera +``` + +### Registration + +Once notifications are enabled, press the `Register for Notifications` button on all devices that you would like to receive notifications on. This will register the background worker. After this Frigate must be restarted and then notifications will begin to be sent. + +## Supported Notifications + +Currently notifications are only supported for review alerts. More notifications will be supported in the future. + +:::note + +Currently, only Chrome supports images in notifications. Safari and Firefox will only show a title and message in the notification. + +::: + +## Reduce Notification Latency + +Different platforms handle notifications differently, some settings changes may be required to get optimal notification delivery. + +### Android + +Most Android phones have battery optimization settings. To get reliable Notification delivery the browser (Chrome, Firefox) should have battery optimizations disabled. If Frigate is running as a PWA then the Frigate app should have battery optimizations disabled as well. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/object_detectors.md b/sam2-cpu/frigate-dev/docs/docs/configuration/object_detectors.md new file mode 100644 index 0000000..282310b --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/object_detectors.md @@ -0,0 +1,1582 @@ +--- +id: object_detectors +title: Object Detectors +--- + +import CommunityBadge from '@site/src/components/CommunityBadge'; + +# Supported Hardware + +:::info + +Frigate supports multiple different detectors that work on different types of hardware: + +**Most Hardware** + +- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB, Mini PCIe, and m.2 formats allowing for a wide range of compatibility with devices. +- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices. +- [MemryX](#memryx-mx3): The MX3 Acceleration module is available in m.2 format, offering broad compatibility across various platforms. +- [DeGirum](#degirum): Service for using hardware devices in the cloud or locally. Hardware and models provided on the cloud on [their website](https://hub.degirum.com). + +**AMD** + +- [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection. +- [ONNX](#onnx): ROCm will automatically be detected and used as a detector in the `-rocm` Frigate image when a supported ONNX model is configured. + +**Apple Silicon** + +- [Apple Silicon](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices. + +**Intel** + +- [OpenVino](#openvino-detector): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel CPUs to provide efficient object detection. +- [ONNX](#onnx): OpenVINO will automatically be detected and used as a detector in the default Frigate image when a supported ONNX model is configured. + +**Nvidia GPU** + +- [ONNX](#onnx): TensorRT will automatically be detected and used as a detector in the `-tensorrt` Frigate image when a supported ONNX model is configured. + +**Nvidia Jetson** + +- [TensortRT](#nvidia-tensorrt-detector): TensorRT can run on Jetson devices, using one of many default models. +- [ONNX](#onnx): TensorRT will automatically be detected and used as a detector in the `-tensorrt-jp6` Frigate image when a supported ONNX model is configured. + +**Rockchip** + +- [RKNN](#rockchip-platform): RKNN models can run on Rockchip devices with included NPUs. + +**Synaptics** + +- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs. + +**For Testing** + +- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results. + +::: + +:::note + +Multiple detectors can not be mixed for object detection (ex: OpenVINO and Coral EdgeTPU can not be used for object detection at the same time). + +This does not affect using hardware for accelerating other tasks such as [semantic search](./semantic_search.md) + +::: + +# Officially Supported Detectors + +Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `hailo8l`, `memryx`, `onnx`, `openvino`, `rknn`, and `tensorrt`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. + +## Edge TPU Detector + +The Edge TPU detector type runs TensorFlow Lite models utilizing the Google Coral delegate for hardware acceleration. To configure an Edge TPU detector, set the `"type"` attribute to `"edgetpu"`. + +The Edge TPU device can be specified using the `"device"` attribute according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api). If not set, the delegate will use the first device it finds. + +:::tip + +See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edge TPU is not detected. + +::: + +### Single USB Coral + +```yaml +detectors: + coral: + type: edgetpu + device: usb +``` + +### Multiple USB Corals + +```yaml +detectors: + coral1: + type: edgetpu + device: usb:0 + coral2: + type: edgetpu + device: usb:1 +``` + +### Native Coral (Dev Board) + +_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_ + +```yaml +detectors: + coral: + type: edgetpu + device: "" +``` + +### Single PCIE/M.2 Coral + +```yaml +detectors: + coral: + type: edgetpu + device: pci +``` + +### Multiple PCIE/M.2 Corals + +```yaml +detectors: + coral1: + type: edgetpu + device: pci:0 + coral2: + type: edgetpu + device: pci:1 +``` + +### Mixing Corals + +```yaml +detectors: + coral_usb: + type: edgetpu + device: usb + coral_pci: + type: edgetpu + device: pci +``` + +### EdgeTPU Supported Models + +| Model | Notes | +| ------------------------------------- | ------------------------------------------- | +| [MobileNet v2](#ssdlite-mobilenet-v2) | Default model | +| [YOLOv9](#yolo-v9) | More accurate but slower than default model | + +#### SSDLite MobileNet v2 + +A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + +#### YOLO v9 + +[YOLOv9](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite) models that are compiled for Tensorflow Lite and properly quantized are supported, but not included by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. Note that the model may require a custom label file (eg. [use this 17 label file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) for the model linked above.) + +
+ YOLOv9 Setup & Config + +After placing the downloaded files for the tflite model and labels in your config folder, you can use the following configuration: + +```yaml +detectors: + coral: + type: edgetpu + device: usb + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize of the model, typically 320 + height: 320 # <--- should match the imgsize of the model, typically 320 + path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite + labelmap_path: /config/labels-coco17.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 17 objects. + +
+ +--- + +## Hailo-8 + +This detector is available for use with both Hailo-8 and Hailo-8L AI Acceleration Modules. The integration automatically detects your hardware architecture via the Hailo CLI and selects the appropriate default model if no custom model is specified. + +See the [installation docs](../frigate/installation.md#hailo-8l) for information on configuring the Hailo hardware. + +### Configuration + +When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**. +If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`. + +#### YOLO + +Use this configuration for YOLO-based models. When no custom model path or URL is provided, the detector automatically downloads the default model based on the detected hardware: + +- **Hailo-8 hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`) +- **Hailo-8L hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`) + +```yaml +detectors: + hailo: + type: hailo8l + device: PCIe + +model: + width: 320 + height: 320 + input_tensor: nhwc + input_pixel_format: rgb + input_dtype: int + model_type: yolo-generic + labelmap_path: /labelmap/coco-80.txt + + # The detector automatically selects the default model based on your hardware: + # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef) + # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef) + # + # Optionally, you can specify a local model path to override the default. + # If a local path is provided and the file exists, it will be used instead of downloading. + # Example: + # path: /config/model_cache/hailo/yolov6n.hef + # + # You can also override using a custom URL: + # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef + # just make sure to give it the write configuration based on the model +``` + +#### SSD + +For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary. + +```yaml +detectors: + hailo: + type: hailo8l + device: PCIe + +model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: rgb + model_type: ssd + # Specify the local model path (if available) or URL for SSD MobileNet v1. + # Example with a local path: + # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef + # + # Or override using a custom URL: + # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef +``` + +#### Custom Models + +The Hailo detector supports all YOLO models compiled for Hailo hardware that include post-processing. You can specify a custom URL or a local path to download or use your model directly. If both are provided, the detector checks the local path first. + +```yaml +detectors: + hailo: + type: hailo8l + device: PCIe + +model: + width: 640 + height: 640 + input_tensor: nhwc + input_pixel_format: rgb + input_dtype: int + model_type: yolo-generic + labelmap_path: /labelmap/coco-80.txt + # Optional: Specify a local model path. + # path: /config/model_cache/hailo/custom_model.hef + # + # Alternatively, or as a fallback, provide a custom URL: + # path: https://custom-model-url.com/path/to/model.hef +``` + +For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo + +Hailo8 supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation. + +> **Note:** +> The config.path parameter can accept either a local file path or a URL ending with .hef. When provided, the detector will first check if the path is a local file path. If the file exists locally, it will use it directly. If the file is not found locally or if a URL was provided, it will attempt to download the model from the specified URL. + +--- + +## OpenVINO Detector + +The OpenVINO detector type runs an OpenVINO IR model on AMD and Intel CPUs, Intel GPUs and Intel NPUs. To configure an OpenVINO detector, set the `"type"` attribute to `"openvino"`. + +The OpenVINO device to be used is specified using the `"device"` attribute according to the naming conventions in the [Device Documentation](https://docs.openvino.ai/2025/openvino-workflow/running-inference/inference-devices-and-modes.html). The most common devices are `CPU`, `GPU`, or `NPU`. + +OpenVINO is supported on 6th Gen Intel platforms (Skylake) and newer. It will also run on AMD CPUs despite having no official support for it. A supported Intel platform is required to use the `GPU` or `NPU` device with OpenVINO. For detailed system requirements, see [OpenVINO System Requirements](https://docs.openvino.ai/2025/about-openvino/release-notes-openvino/system-requirements.html) + +:::tip + +**NPU + GPU Systems:** If you have both NPU and GPU available (Intel Core Ultra processors), use NPU for object detection and GPU for enrichments (semantic search, face recognition, etc.) for best performance and compatibility. + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: + +```yaml +detectors: + ov_0: + type: openvino + device: GPU # or NPU + ov_1: + type: openvino + device: GPU # or NPU +``` + +::: + +### OpenVINO Supported Models + +| Model | GPU | NPU | Notes | +| ------------------------------------- | --- | --- | ------------------------------------------------------------ | +| [YOLOv9](#yolo-v3-v4-v7-v9) | ✅ | ✅ | Recommended for GPU & NPU | +| [RF-DETR](#rf-detr) | ✅ | ✅ | Requires XE iGPU or Arc | +| [YOLO-NAS](#yolo-nas) | ✅ | ✅ | | +| [MobileNet v2](#ssdlite-mobilenet-v2) | ✅ | ✅ | Fast and lightweight model, less accurate than larger models | +| [YOLOX](#yolox) | ✅ | ? | | +| [D-FINE](#d-fine) | ❌ | ❌ | | + +#### SSDLite MobileNet v2 + +An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model. + +
+ MobileNet v2 Config + +Use the model configuration shown below when using the OpenVINO detector with the default OpenVINO model: + +```yaml +detectors: + ov: + type: openvino + device: GPU # Or NPU + +model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt +``` + +
+ +#### YOLOX + +This detector also supports YOLOX. Frigate does not come with any YOLOX models preloaded, so you will need to supply your own models. + +#### YOLO-NAS + +[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. + +
+ YOLO-NAS Setup & Config + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + ov: + type: openvino + device: GPU + +model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_tensor: nchw + input_pixel_format: bgr + path: /config/yolo_nas_s.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +
+ +#### YOLO (v3, v4, v7, v9) + +YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. + +:::tip + +The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. + +::: + +
+ YOLOv Setup & Config + +:::warning + +If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. + +::: + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + ov: + type: openvino + device: GPU # or NPU + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +
+ +#### RF-DETR + +[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more informatoin on downloading the RF-DETR model for use in Frigate. + +:::warning + +Due to the size and complexity of the RF-DETR model, it is only recommended to be run with discrete Arc Graphics Cards. + +::: + +
+ RF-DETR Setup & Config + +After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: + +```yaml +detectors: + ov: + type: openvino + device: GPU + +model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx +``` + +
+ +#### D-FINE + +[D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate. + +:::warning + +Currently D-FINE models only run on OpenVINO in CPU mode, GPUs currently fail to compile the model + +::: + +
+ D-FINE Setup & Config + +After placing the downloaded onnx model in your config/model_cache folder, you can use the following configuration: + +```yaml +detectors: + ov: + type: openvino + device: GPU + +model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine-s.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +
+ +## Apple Silicon detector + +The NPU in Apple Silicon can't be accessed from within a container, so the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) must first be setup. It is recommended to use the Frigate docker image with `-standard-arm64` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-standard-arm64`. + +### Setup + +1. Setup the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) and run the client +2. Configure the detector in Frigate and startup Frigate + +### Configuration + +Using the detector config below will connect to the client: + +```yaml +detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 +``` + +### Apple Silicon Supported Models + +There is no default model provided, the following formats are supported: + +#### YOLO (v3, v4, v7, v9) + +YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. + +:::tip + +The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. + +::: + +When Frigate is started with the following config it will connect to the detector client and transfer the model automatically: + +```yaml +detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +## AMD/ROCm GPU detector + +### Setup + +Support for AMD GPUs is provided using the [ONNX detector](#ONNX). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`. + +### Docker settings for GPU access + +ROCm needs access to the `/dev/kfd` and `/dev/dri` devices. When docker or frigate is not run under root then also `video` (and possibly `render` and `ssl/_ssl`) groups should be added. + +When running docker directly the following flags should be added for device access: + +```bash +$ docker run --device=/dev/kfd --device=/dev/dri \ + ... +``` + +When using Docker Compose: + +```yaml +services: + frigate: +--- +devices: + - /dev/dri + - /dev/kfd +``` + +For reference on recommended settings see [running ROCm/pytorch in Docker](https://rocm.docs.amd.com/projects/install-on-linux/en/develop/how-to/3rd-party/pytorch-install.html#using-docker-with-pytorch-pre-installed). + +### Docker settings for overriding the GPU chipset + +Your GPU might work just fine without any special configuration but in many cases they need manual settings. AMD/ROCm software stack comes with a limited set of GPU drivers and for newer or missing models you will have to override the chipset version to an older/generic version to get things working. + +Also AMD/ROCm does not "officially" support integrated GPUs. It still does work with most of them just fine but requires special settings. One has to configure the `HSA_OVERRIDE_GFX_VERSION` environment variable. See the [ROCm bug report](https://github.com/ROCm/ROCm/issues/1743) for context and examples. + +For the rocm frigate build there is some automatic detection: + +- gfx1031 -> 10.3.0 +- gfx1103 -> 11.0.0 + +If you have something else you might need to override the `HSA_OVERRIDE_GFX_VERSION` at Docker launch. Suppose the version you want is `10.0.0`, then you should configure it from command line as: + +```bash +$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.0.0 \ + ... +``` + +When using Docker Compose: + +```yaml +services: + frigate: + +environment: + HSA_OVERRIDE_GFX_VERSION: "10.0.0" +``` + +Figuring out what version you need can be complicated as you can't tell the chipset name and driver from the AMD brand name. + +- first make sure that rocm environment is running properly by running `/opt/rocm/bin/rocminfo` in the frigate container -- it should list both the CPU and the GPU with their properties +- find the chipset version you have (gfxNNN) from the output of the `rocminfo` (see below) +- use a search engine to query what `HSA_OVERRIDE_GFX_VERSION` you need for the given gfx name ("gfxNNN ROCm HSA_OVERRIDE_GFX_VERSION") +- override the `HSA_OVERRIDE_GFX_VERSION` with relevant value +- if things are not working check the frigate docker logs + +#### Figuring out if AMD/ROCm is working and found your GPU + +```bash +$ docker exec -it frigate /opt/rocm/bin/rocminfo +``` + +#### Figuring out your AMD GPU chipset version: + +We unset the `HSA_OVERRIDE_GFX_VERSION` to prevent an existing override from messing up the result: + +```bash +$ docker exec -it frigate /bin/bash -c '(unset HSA_OVERRIDE_GFX_VERSION && /opt/rocm/bin/rocminfo |grep gfx)' +``` + +### ROCm Supported Models + +:::tip + +The AMD GPU kernel is known problematic especially when converting models to mxr format. The recommended approach is: + +1. Disable object detection in the config. +2. Startup Frigate with the onnx detector configured, the main object detection model will be converted to mxr format and cached in the config directory. +3. Once this is finished as indicated by the logs, enable object detection in the UI and confirm that it is working correctly. +4. Re-enable object detection in the config. + +::: + +See [ONNX supported models](#supported-models) for supported models, there are some caveats: + +- D-FINE models are not supported +- YOLO-NAS models are known to not run well on integrated GPUs + +## ONNX + +ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available. + +:::info + +If the correct build is used for your GPU then the GPU will be detected and used automatically. + +- **AMD** + + - ROCm will automatically be detected and used with the ONNX detector in the `-rocm` Frigate image. + +- **Intel** + + - OpenVINO will automatically be detected and used with the ONNX detector in the default Frigate image. + +- **Nvidia** + - Nvidia GPUs will automatically be detected and used with the ONNX detector in the `-tensorrt` Frigate image. + - Jetson devices will automatically be detected and used with the ONNX detector in the `-tensorrt-jp6` Frigate image. + +::: + +:::tip + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: + +```yaml +detectors: + onnx_0: + type: onnx + onnx_1: + type: onnx +``` + +::: + +### ONNX Supported Models + +| Model | Nvidia GPU | AMD GPU | Notes | +| ----------------------------- | ---------- | ------- | --------------------------------------------------- | +| [YOLOv9](#yolo-v3-v4-v7-v9-2) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | +| [RF-DETR](#rf-detr) | ✅ | ❌ | Supports CUDA Graphs for optimal Nvidia performance | +| [YOLO-NAS](#yolo-nas-1) | ⚠️ | ⚠️ | Not supported by CUDA Graphs | +| [YOLOX](#yolox-1) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | +| [D-FINE](#d-fine) | ⚠️ | ❌ | Not supported by CUDA Graphs | + +There is no default model provided, the following formats are supported: + +#### YOLO-NAS + +[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. + +
+ YOLO-NAS Setup & Config + +:::warning + +If you are using a Frigate+ YOLO-NAS model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. + +::: + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_pixel_format: bgr + input_tensor: nchw + path: /config/yolo_nas_s.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +
+ +#### YOLO (v3, v4, v7, v9) + +YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. + +:::tip + +The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. + +::: + +
+ YOLOv Setup & Config + +:::warning + +If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. + +::: + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +
+ +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +#### YOLOx + +[YOLOx](https://github.com/Megvii-BaseDetection/YOLOX) models are supported, but not included by default. See [the models section](#downloading-yolo-models) for more information on downloading the YOLOx model for use in Frigate. + +
+ YOLOx Setup & Config + +After placing the downloaded onnx model in your config folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: yolox + width: 416 # <--- should match the imgsize set during model export + height: 416 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float_denorm + path: /config/model_cache/yolox_tiny.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +
+ +#### RF-DETR + +[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more information on downloading the RF-DETR model for use in Frigate. + +
+ RF-DETR Setup & Config + +After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx +``` + +
+ +#### D-FINE + +[D-FINE](https://github.com/Peterande/D-FINE) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-d-fine-model) for more information on downloading the D-FINE model for use in Frigate. + +
+ D-FINE Setup & Config + +After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: + +```yaml +detectors: + onnx: + type: onnx + +model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine_m_obj2coco.onnx + labelmap_path: /labelmap/coco-80.txt +``` + +
+ +Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. + +## CPU Detector (not recommended) + +The CPU detector type runs a TensorFlow Lite model utilizing the CPU without hardware acceleration. It is recommended to use a hardware accelerated detector type instead for better performance. To configure a CPU based detector, set the `"type"` attribute to `"cpu"`. + +:::danger + +The CPU detector is not recommended for general use. If you do not have GPU or Edge TPU hardware, using the [OpenVINO Detector](#openvino-detector) in CPU mode is often more efficient than using the CPU detector. + +::: + +The number of threads used by the interpreter can be specified using the `"num_threads"` attribute, and defaults to `3.` + +A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`. + +```yaml +detectors: + cpu1: + type: cpu + num_threads: 3 + cpu2: + type: cpu + num_threads: 3 + +model: + path: "/custom_model.tflite" +``` + +When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance. + +## Deepstack / CodeProject.AI Server Detector + +The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking. + +### Setup + +To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation. + +To integrate CodeProject.AI into Frigate, you'll need to make the following changes to your Frigate configuration file: + +```yaml +detectors: + deepstack: + api_url: http://:/v1/vision/detection + type: deepstack + api_timeout: 0.1 # seconds +``` + +Replace `` and `` with the IP address and port of your CodeProject.AI server. + +To verify that the integration is working correctly, start Frigate and observe the logs for any error messages related to CodeProject.AI. Additionally, you can check the Frigate web interface to see if the objects detected by CodeProject.AI are being displayed and tracked properly. + +# Community Supported Detectors + +## MemryX MX3 + +This detector is available for use with the MemryX MX3 accelerator M.2 module. Frigate supports the MX3 on compatible hardware platforms, providing efficient and high-performance object detection. + +See the [installation docs](../frigate/installation.md#memryx-mx3) for information on configuring the MemryX hardware. + +To configure a MemryX detector, simply set the `type` attribute to `memryx` and follow the configuration guide below. + +### Configuration + +To configure the MemryX detector, use the following example configuration: + +#### Single PCIe MemryX MX3 + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 +``` + +#### Multiple PCIe MemryX MX3 Modules + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + + memx1: + type: memryx + device: PCIe:1 + + memx2: + type: memryx + device: PCIe:2 +``` + +### Supported Models + +MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. + +#### YOLO-NAS + +The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded from the [Models Section](#downloading-yolo-nas-model) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +**Note:** The default model for the MemryX detector is YOLO-NAS 320x320. + +The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. + +- The default size of **320x320** is optimized for lower CPU usage and faster inference times. + +##### Configuration + +Below is the recommended configuration for using the **YOLO-NAS** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolonas + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolonas.zip + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### YOLOv9 + +The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) like in the [Models Section](#yolov9-1) and compiled to DFP with [mx_nc](https://developer.memryx.com/tools/neural_compiler.html#usage). + +##### Configuration + +Below is the recommended configuration for using the **YOLOv9** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolo-generic + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolov9.zip + # The .zip file must contain: + # ├── yolov9.dfp (a file ending with .dfp) +``` + +#### YOLOX + +The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP. + +##### Configuration + +Below is the recommended configuration for using the **YOLOX** (small) model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: yolox + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float_denorm + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolox.zip + # The .zip file must contain: + # ├── yolox.dfp (a file ending with .dfp) +``` + +#### SSDLite MobileNet v2 + +The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP. + +##### Configuration + +Below is the recommended configuration for using the **SSDLite MobileNet v2** model with the MemryX detector: + +```yaml +detectors: + memx0: + type: memryx + device: PCIe:0 + +model: + model_type: ssd + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/ssdlite_mobilenet.zip + # The .zip file must contain: + # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) + # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +#### Using a Custom Model + +To use your own model: + +1. Package your compiled model into a `.zip` file. + +2. The `.zip` must contain the compiled `.dfp` file. + +3. Depending on the model, the compiler may also generate a cropped post-processing network. If present, it will be named with the suffix `_post.onnx`. + +4. Bind-mount the `.zip` file into the container and specify its path using `model.path` in your config. + +5. Update the `labelmap_path` to match your custom model's labels. + +For detailed instructions on compiling models, refer to the [MemryX Compiler](https://developer.memryx.com/tools/neural_compiler.html#usage) docs and [Tutorials](https://developer.memryx.com/tutorials/tutorials.html). + +```yaml +# The detector automatically selects the default model if nothing is provided in the config. +# +# Optionally, you can specify a local model path as a .zip file to override the default. +# If a local path is provided and the file exists, it will be used instead of downloading. +# +# Example: +# path: /config/yolonas.zip +# +# The .zip file must contain: +# ├── yolonas.dfp (a file ending with .dfp) +# └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) +``` + +--- + +## NVidia TensorRT Detector + +Nvidia Jetson devices may be used for object detection using the TensorRT libraries. Due to the size of the additional libraries, this detector is only provided in images with the `-tensorrt-jp6` tag suffix, e.g. `ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6`. This detector is designed to work with Yolo models for object detection. + +### Generate Models + +The model used for TensorRT must be preprocessed on the same hardware platform that they will run on. This means that each user must run additional setup to generate a model file for the TensorRT library. A script is included that will build several common models. + +The Frigate image will generate model files during startup if the specified model is not found. Processed models are stored in the `/config/model_cache` folder. Typically the `/config` path is mapped to a directory on the host already and the `model_cache` does not need to be mapped separately unless the user wants to store it in a different location on the host. + +By default, no models will be generated, but this can be overridden by specifying the `YOLO_MODELS` environment variable in Docker. One or more models may be listed in a comma-separated format, and each one will be generated. Models will only be generated if the corresponding `{model}.trt` file is not present in the `model_cache` folder, so you can force a model to be regenerated by deleting it from your Frigate data folder. + +If you have a Jetson device with DLAs (Xavier or Orin), you can generate a model that will run on the DLA by appending `-dla` to your model name, e.g. specify `YOLO_MODELS=yolov7-320-dla`. The model will run on DLA0 (Frigate does not currently support DLA1). DLA-incompatible layers will fall back to running on the GPU. + +If your GPU does not support FP16 operations, you can pass the environment variable `USE_FP16=False` to disable it. + +Specific models can be selected by passing an environment variable to the `docker run` command or in your `docker-compose.yml` file. Use the form `-e YOLO_MODELS=yolov4-416,yolov4-tiny-416` to select one or more model names. The models available are shown below. + +
+Available Models +``` +yolov3-288 +yolov3-416 +yolov3-608 +yolov3-spp-288 +yolov3-spp-416 +yolov3-spp-608 +yolov3-tiny-288 +yolov3-tiny-416 +yolov4-288 +yolov4-416 +yolov4-608 +yolov4-csp-256 +yolov4-csp-512 +yolov4-p5-448 +yolov4-p5-896 +yolov4-tiny-288 +yolov4-tiny-416 +yolov4x-mish-320 +yolov4x-mish-640 +yolov7-tiny-288 +yolov7-tiny-416 +yolov7-640 +yolov7-416 +yolov7-320 +yolov7x-640 +yolov7x-320 +``` +
+ +An example `docker-compose.yml` fragment that converts the `yolov4-608` and `yolov7x-640` models would look something like this: + +```yml +frigate: + environment: + - YOLO_MODELS=yolov7-320,yolov7x-640 + - USE_FP16=false +``` + +### Configuration Parameters + +The TensorRT detector can be selected by specifying `tensorrt` as the model type. The GPU will need to be passed through to the docker container using the same methods described in the [Hardware Acceleration](hardware_acceleration_video.md#nvidia-gpus) section. If you pass through multiple GPUs, you can select which GPU is used for a detector with the `device` configuration parameter. The `device` parameter is an integer value of the GPU index, as shown by `nvidia-smi` within the container. + +The TensorRT detector uses `.trt` model files that are located in `/config/model_cache/tensorrt` by default. These model path and dimensions used will depend on which model you have generated. + +Use the config below to work with generated TRT models: + +```yaml +detectors: + tensorrt: + type: tensorrt + device: 0 #This is the default, select the first GPU + +model: + path: /config/model_cache/tensorrt/yolov7-320.trt + labelmap_path: /labelmap/coco-80.txt + input_tensor: nchw + input_pixel_format: rgb + width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416 + height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416 +``` + +## Synaptics + +Hardware accelerated object detection is supported on the following SoCs: + +- SL1680 + +This implementation uses the [Synaptics model conversion](https://synaptics-synap.github.io/doc/v/latest/docs/manual/introduction.html#offline-model-conversion), version v3.1.0. + +This implementation is based on sdk `v1.5.0`. + +See the [installation docs](../frigate/installation.md#synaptics) for information on configuring the SL-series NPU hardware. + +### Configuration + +When configuring the Synap detector, you have to specify the model: a local **path**. + +#### SSD Mobilenet + +A synap model is provided in the container at /mobilenet.synap and is used by this detector type by default. The model comes from [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80). + +Use the model configuration shown below when using the synaptics detector with the default synap model: + +```yaml +detectors: # required + synap_npu: # required + type: synaptics # required + +model: # required + path: /synaptics/mobilenet.synap # required + width: 224 # required + height: 224 # required + tensor_format: nhwc # default value (optional. If you change the model, it is required) + labelmap_path: /labelmap/coco-80.txt # required +``` + +## Rockchip platform + +Hardware accelerated object detection is supported on the following SoCs: + +- RK3562 +- RK3566 +- RK3568 +- RK3576 +- RK3588 + +This implementation uses the [Rockchip's RKNN-Toolkit2](https://github.com/airockchip/rknn-toolkit2/), version v2.3.2. + +:::tip + +When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming NPU resources are available. An example configuration would be: + +```yaml +detectors: + rknn_0: + type: rknn + num_cores: 0 + rknn_1: + type: rknn + num_cores: 0 +``` + +::: + +### Prerequisites + +Make sure to follow the [Rockchip specific installation instructions](/frigate/installation#rockchip-platform). + +:::tip + +You can get the load of your NPU with the following command: + +```bash +$ cat /sys/kernel/debug/rknpu/load +>> NPU load: Core0: 0%, Core1: 0%, Core2: 0%, +``` + +::: + +### RockChip Supported Models + +This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for two). Lines that are required at least to use the detector are labeled as required, all other lines are optional. + +```yaml +detectors: # required + rknn: # required + type: rknn # required + # number of NPU cores to use + # 0 means choose automatically + # increase for better performance if you have a multicore NPU e.g. set to 3 on rk3588 + num_cores: 0 +``` + +The inference time was determined on a rk3588 with 3 NPU cores. + +| Model | Size in mb | Inference time in ms | +| --------------------- | ---------- | -------------------- | +| deci-fp16-yolonas_s | 24 | 25 | +| deci-fp16-yolonas_m | 62 | 35 | +| deci-fp16-yolonas_l | 81 | 45 | +| frigate-fp16-yolov9-t | 6 | 35 | +| rock-i8-yolox_nano | 3 | 14 | +| rock-i8_yolox_tiny | 6 | 18 | + +- All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space. +- You can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models. + +#### YOLO-NAS + +```yaml +model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - deci-fp16-yolonas_s + # - deci-fp16-yolonas_m + # - deci-fp16-yolonas_l + # your yolonas_model.rknn + path: deci-fp16-yolonas_s + model_type: yolonas + width: 320 + height: 320 + input_pixel_format: bgr + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt +``` + +:::warning + +The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + +::: + +#### YOLO (v9) + +```yaml +model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - frigate-fp16-yolov9-t + # - frigate-fp16-yolov9-s + # - frigate-fp16-yolov9-m + # - frigate-fp16-yolov9-c + # - frigate-fp16-yolov9-e + # your yolo_model.rknn + path: frigate-fp16-yolov9-t + model_type: yolo-generic + width: 320 + height: 320 + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt +``` + +#### YOLOx + +```yaml +model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - rock-i8-yolox_nano + # - rock-i8-yolox_tiny + # - rock-fp16-yolox_nano + # - rock-fp16-yolox_tiny + # your yolox_model.rknn + path: rock-i8-yolox_nano + model_type: yolox + width: 416 + height: 416 + input_tensor: nhwc + labelmap_path: /labelmap/coco-80.txt +``` + +### Converting your own onnx model to rknn format + +To convert a onnx model to the rknn format using the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2/) you have to: + +- Place one ore more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges). +- Save the configuration file under `config/conv2rknn.yaml` (see below for details). +- Run `docker exec python3 /opt/conv2rknn.py`. If the conversion was successful, the rknn models will be placed in `config/model_cache/rknn_cache`. + +This is an example configuration file that you need to adjust to your specific onnx model: + +```yaml +soc: ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] +quantization: false + +output_name: "{input_basename}" + +config: + mean_values: [[0, 0, 0]] + std_values: [[255, 255, 255]] + quant_img_RGB2BGR: true +``` + +Explanation of the paramters: + +- `soc`: A list of all SoCs you want to build the rknn model for. If you don't specify this parameter, the script tries to find out your SoC and builds the rknn model for this one. +- `quantization`: true: 8 bit integer (i8) quantization, false: 16 bit float (fp16). Default: false. +- `output_name`: The output name of the model. The following variables are available: + - `quant`: "i8" or "fp16" depending on the config + - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is calles "my_model.onnx") + - `soc`: the SoC this model was build for (e.g. "rk3588") + - `tk_version`: Version of `rknn-toolkit2` (e.g. "2.3.0") + - **example**: Specifying `output_name = "frigate-{quant}-{input_basename}-{soc}-v{tk_version}"` could result in a model called `frigate-i8-my_model-rk3588-v2.3.0.rknn`. +- `config`: Configuration passed to `rknn-toolkit2` for model conversion. For an explanation of all available parameters have a look at section "2.2. Model configuration" of [this manual](https://github.com/MarcA711/rknn-toolkit2/releases/download/v2.3.2/03_Rockchip_RKNPU_API_Reference_RKNN_Toolkit2_V2.3.2_EN.pdf). + +## DeGirum + +DeGirum is a detector that can use any type of hardware listed on [their website](https://hub.degirum.com). DeGirum can be used with local hardware through a DeGirum AI Server, or through the use of `@local`. You can also connect directly to DeGirum's AI Hub to run inferences. **Please Note:** This detector _cannot_ be used for commercial purposes. + +### Configuration + +#### AI Server Inference + +Before starting with the config file for this section, you must first launch an AI server. DeGirum has an AI server ready to use as a docker container. Add this to your `docker-compose.yml` to get started: + +```yaml +degirum_detector: + container_name: degirum + image: degirum/aiserver:latest + privileged: true + ports: + - "8778:8778" +``` + +All supported hardware will automatically be found on your AI server host as long as relevant runtimes and drivers are properly installed on your machine. Refer to [DeGirum's docs site](https://docs.degirum.com/pysdk/runtimes-and-drivers) if you have any trouble. + +Once completed, changing the `config.yml` file is simple. + +```yaml +degirum_detector: + type: degirum + location: degirum # Set to service name (degirum_detector), container_name (degirum), or a host:port (192.168.29.4:8778) + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. If you aren't pulling a model from the AI Hub, leave this and 'token' blank. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server +``` + +Setting up a model in the `config.yml` is similar to setting up an AI server. +You can set it to: + +- A model listed on the [AI Hub](https://hub.degirum.com), given that the correct zoo name is listed in your detector + - If this is what you choose to do, the correct model will be downloaded onto your machine before running. +- A local directory acting as a zoo. See DeGirum's docs site [for more information](https://docs.degirum.com/pysdk/user-guide-pysdk/organizing-models#model-zoo-directory-structure). +- A path to some model.json. + +```yaml +model: + path: ./mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 # directory to model .json and file + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here +``` + +#### Local Inference + +It is also possible to eliminate the need for an AI server and run the hardware directly. The benefit of this approach is that you eliminate any bottlenecks that occur when transferring prediction results from the AI server docker container to the frigate one. However, the method of implementing local inference is different for every device and hardware combination, so it's usually more trouble than it's worth. A general guideline to achieve this would be: + +1. Ensuring that the frigate docker container has the runtime you want to use. So for instance, running `@local` for Hailo means making sure the container you're using has the Hailo runtime installed. +2. To double check the runtime is detected by the DeGirum detector, make sure the `degirum sys-info` command properly shows whatever runtimes you mean to install. +3. Create a DeGirum detector in your `config.yml` file. + +```yaml +degirum_detector: + type: degirum + location: "@local" # For accessing AI Hub devices and models + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server +``` + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```yaml +model: + path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here +``` + +#### AI Hub Cloud Inference + +If you do not possess whatever hardware you want to run, there's also the option to run cloud inferences. Do note that your detection fps might need to be lowered as network latency does significantly slow down this method of detection. For use with Frigate, we highly recommend using a local AI server as described above. To set up cloud inferences, + +1. Sign up at [DeGirum's AI Hub](https://hub.degirum.com). +2. Get an access token. +3. Create a DeGirum detector in your `config.yml` file. + +```yaml +degirum_detector: + type: degirum + location: "@cloud" # For accessing AI Hub devices and models + zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. + token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the (AI Hub)[https://hub.degirum.com). +``` + +Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. + +```yaml +model: + path: mobilenet_v2_ssd_coco--300x300_quant_n2x_orca1_1 + width: 300 # width is in the model name as the first number in the "int"x"int" section + height: 300 # height is in the model name as the second number in the "int"x"int" section + input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here +``` + +# Models + +Some model types are not included in Frigate by default. + +## Downloading Models + +Here are some tips for getting different model types + +### Downloading D-FINE Model + +D-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size. + +```sh +docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' +FROM python:3.11 AS build +RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* +COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ +WORKDIR /dfine +RUN git clone https://github.com/Peterande/D-FINE.git . +RUN uv pip install --system -r requirements.txt +RUN uv pip install --system onnx onnxruntime onnxsim onnxscript +# Create output directory and download checkpoint +RUN mkdir -p output +ARG MODEL_SIZE +RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth +# Modify line 58 of export_onnx.py to change batch size to 1 +RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py +RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth +FROM scratch +ARG MODEL_SIZE +COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx +EOF +``` + +### Download RF-DETR Model + +RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size. + +```sh +docker build . --build-arg MODEL_SIZE=Nano --output . -f- <<'EOF' +FROM python:3.11 AS build +RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* +COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ +WORKDIR /rfdetr +RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnxscript +ARG MODEL_SIZE +RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" +FROM scratch +ARG MODEL_SIZE +COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx +EOF +``` + +### Downloading YOLO-NAS Model + +You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb). + +:::warning + +The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + +::: + +The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired. + +### Downloading YOLO Models + +#### YOLOx + +YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime). + +#### YOLOv3, YOLOv4, and YOLOv7 + +To export as ONNX: + +```sh +git clone https://github.com/NateMeyer/tensorrt_demos +cd tensorrt_demos/yolo +./download_yolo.sh +python3 yolo_to_onnx.py -m yolov7-320 +``` + +#### YOLOv9 + +YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`). + +```sh +docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' +FROM python:3.11 AS build +RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* +COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ +WORKDIR /yolov9 +ADD https://github.com/WongKinYiu/yolov9.git . +RUN uv pip install --system -r requirements.txt +RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 onnxscript +ARG MODEL_SIZE +ARG IMG_SIZE +ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt +RUN sed -i "s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g" models/experimental.py +RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx +FROM scratch +ARG MODEL_SIZE +ARG IMG_SIZE +COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx +EOF +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/object_filters.md b/sam2-cpu/frigate-dev/docs/docs/configuration/object_filters.md new file mode 100644 index 0000000..3f36086 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/object_filters.md @@ -0,0 +1,57 @@ +--- +id: object_filters +title: Filters +--- + +There are several types of object filters that can be used to reduce false positive rates. + +## Object Scores + +For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85: + +| Frame | Current Score | Score History | Computed Score | Detected Object | +| ----- | ------------- | --------------------------------- | -------------- | --------------- | +| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No | +| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No | +| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No | +| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes | +| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes | +| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes | + +In frame 2, the score is below the `min_score` value, so Frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example. + +### Minimum Score + +Any detection below `min_score` will be immediately thrown out and never tracked because it is considered a false positive. If `min_score` is too low then false positives may be detected and tracked which can confuse the object tracker and may lead to wasted resources. If `min_score` is too high then lower scoring true positives like objects that are further away or partially occluded may be thrown out which can also confuse the tracker and cause valid tracked objects to be lost or disjointed. + +### Threshold + +`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create an tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough. + +## Object Shape + +False positives can also be reduced by filtering a detection based on its shape. + +### Object Area + +`min_area` and `max_area` filter on the area of an objects bounding box and can be used to reduce false positives that are outside the range of expected sizes. For example when a leaf is detected as a dog or when a large tree is detected as a person, these can be reduced by adding a `min_area` / `max_area` filter. These values can either be in pixels or as a percentage of the frame (for example, 0.12 represents 12% of the frame). + +### Object Proportions + +`min_ratio` and `max_ratio` values are compared against a given detected object's width/height ratio (in pixels). If the ratio is outside this range, the object will be ignored as a false positive. This allows objects that are proportionally too short-and-wide (higher ratio) or too tall-and-narrow (smaller ratio) to be ignored. + +:::info + +Conceptually, a ratio of 1 is a square, 0.5 is a "tall skinny" box, and 2 is a "wide flat" box. If `min_ratio` is 1.0, any object that is taller than it is wide will be ignored. Similarly, if `max_ratio` is 1.0, then any object that is wider than it is tall will be ignored. + +::: + +## Other Tools + +### Zones + +[Required zones](/configuration/zones.md) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone. + +### Object Masks + +[Object Filter Masks](/configuration/masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/objects.md b/sam2-cpu/frigate-dev/docs/docs/configuration/objects.md new file mode 100644 index 0000000..796d312 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/objects.md @@ -0,0 +1,29 @@ +--- +id: objects +title: Available Objects +--- + +import labels from "../../../labelmap.txt"; + +Frigate includes the object labels listed below from the Google Coral test data. + +Please note: + +- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused. +- `person` is the only tracked object by default. See the [full configuration reference](reference.md) for an example of expanding the list of tracked objects. + +
    + {labels.split("\n").map((label) => ( +
  • {label.replace(/^\d+\s+/, "")}
  • + ))} +
+ +## Custom Models + +Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts: + +- CPU Model: `/cpu_model.tflite` +- EdgeTPU Model: `/edgetpu_model.tflite` +- Labels: `/labelmap.txt` + +You also need to update the [model config](advanced.md#model) if they differ from the defaults. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/pwa.md b/sam2-cpu/frigate-dev/docs/docs/configuration/pwa.md new file mode 100644 index 0000000..fd1aae5 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/pwa.md @@ -0,0 +1,24 @@ +--- +id: pwa +title: Installing Frigate App +--- + +Frigate supports being installed as a [Progressive Web App](https://web.dev/explore/progressive-web-apps) on Desktop, Android, and iOS. + +This adds features including the ability to deep link directly into the app. + +## Requirements + +In order to install Frigate as a PWA, the following requirements must be met: + +- Frigate must be accessed via a secure context (localhost, secure https, etc.) +- On Android, Firefox, Chrome, Edge, Opera, and Samsung Internet Browser all support installing PWAs. +- On iOS 16.4 and later, PWAs can be installed from the Share menu in Safari, Chrome, Edge, Firefox, and Orion. + +## Installation + +Installation varies slightly based on the device that is being used: + +- Desktop: Use the install button typically found in right edge of the address bar +- Android: Use the `Install as App` button in the more options menu for Chrome, and the `Add app to Home screen` button for Firefox +- iOS: Use the `Add to Homescreen` button in the share menu diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/record.md b/sam2-cpu/frigate-dev/docs/docs/configuration/record.md new file mode 100644 index 0000000..4dfd8b7 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/record.md @@ -0,0 +1,166 @@ +--- +id: record +title: Recording +--- + +Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM-DD/HH//MM.SS.mp4` in **UTC time**. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the tracked object retention when determining if a recording should be removed. + +New recording segments are written from the camera stream to cache, they are only moved to disk if they match the setup recording retention policy. + +H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other browsers require recordings to be encoded with H264. + +## Common recording configurations + +### Most conservative: Ensure all video is saved + +For users deploying Frigate in environments where it is important to have contiguous video stored even if there was no detectable motion, the following config will store all video for 3 days. After 3 days, only video containing motion will be saved for 7 days. After 7 days, only video containing motion and overlapping with alerts or detections will be retained until 30 days have passed. + +```yaml +record: + enabled: True + continuous: + days: 3 + motion: + days: 7 + alerts: + retain: + days: 30 + mode: all + detections: + retain: + days: 30 + mode: all +``` + +### Reduced storage: Only saving video when motion is detected + +In order to reduce storage requirements, you can adjust your config to only retain video where motion / activity was detected. + +```yaml +record: + enabled: True + motion: + days: 3 + alerts: + retain: + days: 30 + mode: motion + detections: + retain: + days: 30 + mode: motion +``` + +### Minimum: Alerts only + +If you only want to retain video that occurs during activity caused by tracked object(s), this config will discard video unless an alert is ongoing. + +```yaml +record: + enabled: True + continuous: + days: 0 + alerts: + retain: + days: 30 + mode: motion +``` + +## Will Frigate delete old recordings if my storage runs out? + +As of Frigate 0.12 if there is less than an hour left of storage, the oldest 2 hours of recordings will be deleted. + +## Configuring Recording Retention + +Frigate supports both continuous and tracked object based recordings with separate retention modes and retention periods. + +:::tip + +Retention configs support decimals meaning they can be configured to retain `0.5` days, for example. + +::: + +### Continuous and Motion Recording + +The number of days to retain continuous and motion recordings can be set via the following config where X is a number, by default continuous recording is disabled. + +```yaml +record: + enabled: True + continuous: + days: 1 # <- number of days to keep continuous recordings + motion: + days: 2 # <- number of days to keep motion recordings +``` + +Continuous recording supports different retention modes [which are described below](#what-do-the-different-retain-modes-mean) + +### Object Recording + +The number of days to record review items can be specified for review items classified as alerts as well as tracked objects. + +```yaml +record: + enabled: True + alerts: + retain: + days: 10 # <- number of days to keep alert recordings + detections: + retain: + days: 10 # <- number of days to keep detections recordings +``` + +This configuration will retain recording segments that overlap with alerts and detections for 10 days. Because multiple tracked objects can reference the same recording segments, this avoids storing duplicate footage for overlapping tracked objects and reduces overall storage needs. + +**WARNING**: Recordings still must be enabled in the config. If a camera has recordings disabled in the config, enabling via the methods listed above will have no effect. + +## Can I have "continuous" recordings, but only at certain times? + +Using Frigate UI, Home Assistant, or MQTT, cameras can be automated to only record in certain situations or at certain times. + +## How do I export recordings? + +Footage can be exported from Frigate by right-clicking (desktop) or long pressing (mobile) on a review item in the Review pane or by clicking the Export button in the History view. Exported footage is then organized and searchable through the Export view, accessible from the main navigation bar. + +### Time-lapse export + +Time lapse exporting is available only via the [HTTP API](../integrations/api/export-recording-export-camera-name-start-start-time-end-end-time-post.api.mdx). + +When exporting a time-lapse the default speed-up is 25x with 30 FPS. This means that every 25 seconds of (real-time) recording is condensed into 1 second of time-lapse video (always without audio) with a smoothness of 30 FPS. + +To configure the speed-up factor, the frame rate and further custom settings, the configuration parameter `timelapse_args` can be used. The below configuration example would change the time-lapse speed to 60x (for fitting 1 hour of recording into 1 minute of time-lapse) with 25 FPS: + +```yaml +record: + enabled: True + export: + timelapse_args: "-vf setpts=PTS/60 -r 25" +``` + +:::tip + +When using `hwaccel_args` globally hardware encoding is used for time lapse generation. The encoder determines its own behavior so the resulting file size may be undesirably large. +To reduce the output file size the ffmpeg parameter `-qp n` can be utilized (where `n` stands for the value of the quantisation parameter). The value can be adjusted to get an acceptable tradeoff between quality and file size for the given scenario. + +::: + +## Apple Compatibility with H.265 Streams + +Apple devices running the Safari browser may fail to playback h.265 recordings. The [apple compatibility option](../configuration/camera_specific.md#h265-cameras-via-safari) should be used to ensure seamless playback on Apple devices. + +## Syncing Recordings With Disk + +In some cases the recordings files may be deleted but Frigate will not know this has happened. Recordings sync can be enabled which will tell Frigate to check the file system and delete any db entries for files which don't exist. + +```yaml +record: + sync_recordings: True +``` + +This feature is meant to fix variations in files, not completely delete entries in the database. If you delete all of your media, don't use `sync_recordings`, just stop Frigate, delete the `frigate.db` database, and restart. + +:::warning + +The sync operation uses considerable CPU resources and in most cases is not needed, only enable when necessary. + +::: diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/reference.md b/sam2-cpu/frigate-dev/docs/docs/configuration/reference.md new file mode 100644 index 0000000..cccaf3e --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/reference.md @@ -0,0 +1,1048 @@ +--- +id: reference +title: Full Reference Config +--- + +### Full configuration reference: + +:::warning + +It is not recommended to copy this full configuration file. Only specify values that are different from the defaults. Configuration options and default values may change in future versions. + +::: + +```yaml +mqtt: + # Optional: Enable mqtt server (default: shown below) + enabled: True + # Required: host name + host: mqtt.server.com + # Optional: port (default: shown below) + port: 1883 + # Optional: topic prefix (default: shown below) + # NOTE: must be unique if you are running multiple instances + topic_prefix: frigate + # Optional: client id (default: shown below) + # NOTE: must be unique if you are running multiple instances + client_id: frigate + # Optional: user + # NOTE: MQTT user can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. user: '{FRIGATE_MQTT_USER}' + user: mqtt_user + # Optional: password + # NOTE: MQTT password can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. password: '{FRIGATE_MQTT_PASSWORD}' + password: password + # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None) + tls_ca_certs: /path/to/ca.crt + # Optional: tls_client_cert and tls_client key in order to use self-signed client + # certificates (default: None) + # NOTE: certificate must not be password-protected + # do not set user and password when using a client certificate + tls_client_cert: /path/to/client.crt + tls_client_key: /path/to/client.key + # Optional: tls_insecure (true/false) for enabling TLS verification of + # the server hostname in the server certificate (default: None) + tls_insecure: false + # Optional: interval in seconds for publishing stats (default: shown below) + stats_interval: 60 + # Optional: QoS level for subscriptions and publishing (default: shown below) + # 0 = at most once + # 1 = at least once + # 2 = exactly once + qos: 0 + +# Optional: Detectors configuration. Defaults to a single CPU detector +detectors: + # Required: name of the detector + detector_name: + # Required: type of the detector + # Frigate provides many types, see https://docs.frigate.video/configuration/object_detectors for more details (default: shown below) + # Additional detector types can also be plugged in. + # Detectors may require additional configuration. + # Refer to the Detectors configuration page for more information. + type: cpu + +# Optional: Database configuration +database: + # The path to store the SQLite DB (default: shown below) + path: /config/frigate.db + +# Optional: TLS configuration +tls: + # Optional: Enable TLS for port 8971 (default: shown below) + enabled: True + +# Optional: IPv6 configuration +networking: + # Optional: Enable IPv6 on 5000, and 8971 if tls is configured (default: shown below) + ipv6: + enabled: False + +# Optional: Proxy configuration +proxy: + # Optional: Mapping for headers from upstream proxies. Only used if Frigate's auth + # is disabled. + # NOTE: Many authentication proxies pass a header downstream with the authenticated + # user name and role. Not all values are supported. It must be a whitelisted header. + # See the docs for more info. + header_map: + user: x-forwarded-user + role: x-forwarded-groups + role_map: + admin: + - sysadmins + - access-level-security + viewer: + - camera-viewer + # Optional: Url for logging out a user. This sets the location of the logout url in + # the UI. + logout_url: /api/logout + # Optional: Auth secret that is checked against the X-Proxy-Secret header sent from + # the proxy. If not set, all requests are trusted regardless of origin. + auth_secret: None + # Optional: The default role to use for proxy auth. Must be "admin" or "viewer" + default_role: viewer + # Optional: The character used to separate multiple values in the proxy headers. (default: shown below) + separator: "," + +# Optional: Authentication configuration +auth: + # Optional: Enable authentication + enabled: True + # Optional: Reset the admin user password on startup (default: shown below) + # New password is printed in the logs + reset_admin_password: False + # Optional: Cookie to store the JWT token for native auth (default: shown below) + cookie_name: frigate_token + # Optional: Set secure flag on cookie. (default: shown below) + # NOTE: This should be set to True if you are using TLS + cookie_secure: False + # Optional: Session length in seconds (default: shown below) + session_length: 86400 # 24 hours + # Optional: Refresh time in seconds (default: shown below) + # When the session is going to expire in less time than this setting, + # it will be refreshed back to the session_length. + refresh_time: 1800 # 30 minutes + # Optional: Rate limiting for login failures to help prevent brute force + # login attacks (default: shown below) + # See the docs for more information on valid values + failed_login_rate_limit: None + # Optional: Trusted proxies for determining IP address to rate limit + # NOTE: This is only used for rate limiting login attempts and does not bypass + # authentication. See the authentication docs for more details. + trusted_proxies: [] + # Optional: Number of hashing iterations for user passwords + # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 + # NOTE: changing this value will not automatically update password hashes, you + # will need to change each user password for it to apply + hash_iterations: 600000 + +# Optional: model modifications +# NOTE: The default values are for the EdgeTPU detector. +# Other detectors will require the model config to be set. +model: + # Required: path to the model. Frigate+ models use plus:// (default: automatic based on detector) + path: /edgetpu_model.tflite + # Required: path to the labelmap (default: shown below) + labelmap_path: /labelmap.txt + # Required: Object detection model input width (default: shown below) + width: 320 + # Required: Object detection model input height (default: shown below) + height: 320 + # Required: Object detection model input colorspace + # Valid values are rgb, bgr, or yuv. (default: shown below) + input_pixel_format: rgb + # Required: Object detection model input tensor format + # Valid values are nhwc or nchw (default: shown below) + input_tensor: nhwc + # Required: Object detection model type, currently only used with the OpenVINO detector + # Valid values are ssd, yolox, yolonas (default: shown below) + model_type: ssd + # Required: Label name modifications. These are merged into the standard labelmap. + labelmap: + 2: vehicle + # Optional: Map of object labels to their attribute labels (default: depends on model) + attributes_map: + person: + - amazon + - face + car: + - amazon + - fedex + - license_plate + - ups + +# Optional: Audio Events Configuration +# NOTE: Can be overridden at the camera level +audio: + # Optional: Enable audio events (default: shown below) + enabled: False + # Optional: Configure the amount of seconds without detected audio to end the event (default: shown below) + max_not_heard: 30 + # Optional: Configure the min rms volume required to run audio detection (default: shown below) + # As a rule of thumb: + # - 200 - high sensitivity + # - 500 - medium sensitivity + # - 1000 - low sensitivity + min_volume: 500 + # Optional: Types of audio to listen for (default: shown below) + listen: + - bark + - fire_alarm + - scream + - speech + - yell + # Optional: Filters to configure detection. + filters: + # Label that matches label in listen config. + speech: + # Minimum score that triggers an audio event (default: shown below) + threshold: 0.8 + +# Optional: logger verbosity settings +logger: + # Optional: Default log verbosity (default: shown below) + default: info + # Optional: Component specific logger overrides + logs: + frigate.event: debug + +# Optional: set environment variables +environment_vars: + EXAMPLE_VAR: value + +# Optional: birdseye configuration +# NOTE: Can (enabled, mode) be overridden at the camera level +birdseye: + # Optional: Enable birdseye view (default: shown below) + enabled: True + # Optional: Restream birdseye via RTSP (default: shown below) + # NOTE: Enabling this will set birdseye to run 24/7 which may increase CPU usage somewhat. + restream: False + # Optional: Width of the output resolution (default: shown below) + width: 1280 + # Optional: Height of the output resolution (default: shown below) + height: 720 + # Optional: Encoding quality of the mpeg1 feed (default: shown below) + # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. + quality: 8 + # Optional: Mode of the view. Available options are: objects, motion, and continuous + # objects - cameras are included if they have had a tracked object within the last 30 seconds + # motion - cameras are included if motion was detected in the last 30 seconds + # continuous - all cameras are included always + mode: objects + # Optional: Threshold for camera activity to stop showing camera (default: shown below) + inactivity_threshold: 30 + # Optional: Configure the birdseye layout + layout: + # Optional: Scaling factor for the layout calculator, range 1.0-5.0 (default: shown below) + scaling_factor: 2.0 + # Optional: Maximum number of cameras to show at one time, showing the most recent (default: show all cameras) + max_cameras: 1 + # Optional: Frames-per-second to re-send the last composed Birdseye frame when idle (no motion or active updates). (default: shown below) + idle_heartbeat_fps: 0.0 + +# Optional: ffmpeg configuration +# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets +ffmpeg: + # Optional: ffmpeg binary path (default: shown below) + # can also be set to `7.0` or `5.0` to specify one of the included versions + # or can be set to any path that holds `bin/ffmpeg` & `bin/ffprobe` + path: "default" + # Optional: global ffmpeg args (default: shown below) + global_args: -hide_banner -loglevel warning -threads 2 + # Optional: global hwaccel args (default: auto detect) + # NOTE: See hardware acceleration docs for your specific device + hwaccel_args: "auto" + # Optional: global input args (default: shown below) + input_args: preset-rtsp-generic + # Optional: global output args + output_args: + # Optional: output args for detect streams (default: shown below) + detect: -threads 2 -f rawvideo -pix_fmt yuv420p + # Optional: output args for record streams (default: shown below) + record: preset-record-generic + # Optional: Time in seconds to wait before ffmpeg retries connecting to the camera. (default: shown below) + # If set too low, frigate will retry a connection to the camera's stream too frequently, using up the limited streams some cameras can allow at once + # If set too high, then if a ffmpeg crash or camera stream timeout occurs, you could potentially lose up to a maximum of retry_interval second(s) of footage + # NOTE: this can be a useful setting for Wireless / Battery cameras to reduce how much footage is potentially lost during a connection timeout. + retry_interval: 10 + # Optional: Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players. (default: shown below) + apple_compatibility: false + # Optional: Set the index of the GPU to use for hardware acceleration. (default: shown below) + gpu: 0 + +# Optional: Detect configuration +# NOTE: Can be overridden at the camera level +detect: + # Optional: enables detection for the camera (default: shown below) + enabled: False + # Optional: width of the frame for the input with the detect role (default: use native stream resolution) + width: 1280 + # Optional: height of the frame for the input with the detect role (default: use native stream resolution) + height: 720 + # Optional: desired fps for your camera for the input with the detect role (default: shown below) + # NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera. + fps: 5 + # Optional: Number of consecutive detection hits required for an object to be initialized in the tracker. (default: 1/2 the frame rate) + min_initialized: 2 + # Optional: Number of frames without a detection before Frigate considers an object to be gone. (default: 5x the frame rate) + max_disappeared: 25 + # Optional: Configuration for stationary object tracking + stationary: + # Optional: Stationary classifier that uses visual characteristics to determine if an object + # is stationary even if the box changes enough to be considered motion (default: shown below). + classifier: True + # Optional: Frequency for confirming stationary objects (default: same as threshold) + # When set to 1, object detection will run to confirm the object still exists on every frame. + # If set to 10, object detection will run to confirm the object still exists on every 10th frame. + interval: 50 + # Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s) + threshold: 50 + # Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever) + # This can help with false positives for objects that should only be stationary for a limited amount of time. + # It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave + # car at the default. + # WARNING: Setting these values overrides default behavior and disables stationary object tracking. + # There are very few situations where you would want it disabled. It is NOT recommended to + # copy these values from the example config into your config unless you know they are needed. + max_frames: + # Optional: Default for all object types (default: not set, track forever) + default: 3000 + # Optional: Object specific values + objects: + person: 1000 + # Optional: Milliseconds to offset detect annotations by (default: shown below). + # There can often be latency between a recording and the detect process, + # especially when using separate streams for detect and record. + # Use this setting to make the timeline bounding boxes more closely align + # with the recording. The value can be positive or negative. + # TIP: Imagine there is an tracked object clip with a person walking from left to right. + # If the tracked object lifecycle bounding box is consistently to the left of the person + # then the value should be decreased. Similarly, if a person is walking from + # left to right and the bounding box is consistently ahead of the person + # then the value should be increased. + # TIP: This offset is dynamic so you can change the value and it will update existing + # tracked objects, this makes it easy to tune. + # WARNING: Fast moving objects will likely not have the bounding box align. + annotation_offset: 0 + +# Optional: Object configuration +# NOTE: Can be overridden at the camera level +objects: + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Optional: mask to prevent all object types from being detected in certain areas (default: no mask) + # Checks based on the bottom center of the bounding box of the object. + # NOTE: This mask is COMBINED with the object type specific mask below + mask: 0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278 + # Optional: filters to reduce false positives for specific object types + filters: + person: + # Optional: minimum size of the bounding box for the detected object (default: 0). + # Can be specified as an integer for width*height in pixels or as a decimal representing the percentage of the frame (0.000001 to 0.99). + min_area: 5000 + # Optional: maximum size of the bounding box for the detected object (default: 24000000). + # Can be specified as an integer for width*height in pixels or as a decimal representing the percentage of the frame (0.000001 to 0.99). + max_area: 100000 + # Optional: minimum width/height of the bounding box for the detected object (default: 0) + min_ratio: 0.5 + # Optional: maximum width/height of the bounding box for the detected object (default: 24000000) + max_ratio: 2.0 + # Optional: minimum score for the object to initiate tracking (default: shown below) + min_score: 0.5 + # Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below) + threshold: 0.7 + # Optional: mask to prevent this object type from being detected in certain areas (default: no mask) + # Checks based on the bottom center of the bounding box of the object + mask: 0.000,0.000,0.781,0.000,0.781,0.278,0.000,0.278 + # Optional: Configuration for AI generated tracked object descriptions + genai: + # Optional: Enable AI object description generation (default: shown below) + enabled: False + # Optional: Use the object snapshot instead of thumbnails for description generation (default: shown below) + use_snapshot: False + # Optional: The default prompt for generating descriptions. Can use replacement + # variables like "label", "sub_label", "camera" to make more dynamic. (default: shown below) + prompt: "Describe the {label} in the sequence of images with as much detail as possible. Do not describe the background." + # Optional: Object specific prompts to customize description results + # Format: {label}: {prompt} + object_prompts: + person: "My special person prompt." + # Optional: objects to generate descriptions for (default: all objects that are tracked) + objects: + - person + - cat + # Optional: Restrict generation to objects that entered any of the listed zones (default: none, all zones qualify) + required_zones: [] + # Optional: What triggers to use to send frames for a tracked object to generative AI (default: shown below) + send_triggers: + # Once the object is no longer tracked + tracked_object_end: True + # Optional: After X many significant updates are received (default: shown below) + after_significant_updates: None + # Optional: Save thumbnails sent to generative AI for review/debugging purposes (default: shown below) + debug_save_thumbnails: False + +# Optional: Review configuration +# NOTE: Can be overridden at the camera level +review: + # Optional: alerts configuration + alerts: + # Optional: enables alerts for the camera (default: shown below) + enabled: True + # Optional: labels that qualify as an alert (default: shown below) + labels: + - car + - person + # Time to cutoff alerts after no alert-causing activity has occurred (default: shown below) + cutoff_time: 40 + # Optional: required zones for an object to be marked as an alert (default: none) + # NOTE: when settings required zones globally, this zone must exist on all cameras + # or the config will be considered invalid. In that case the required_zones + # should be configured at the camera level. + required_zones: + - driveway + # Optional: detections configuration + detections: + # Optional: enables detections for the camera (default: shown below) + enabled: True + # Optional: labels that qualify as a detection (default: all labels that are tracked / listened to) + labels: + - car + - person + # Time to cutoff detections after no detection-causing activity has occurred (default: shown below) + cutoff_time: 30 + # Optional: required zones for an object to be marked as a detection (default: none) + # NOTE: when settings required zones globally, this zone must exist on all cameras + # or the config will be considered invalid. In that case the required_zones + # should be configured at the camera level. + required_zones: + - driveway + # Optional: GenAI Review Summary Configuration + genai: + # Optional: Enable the GenAI review summary feature (default: shown below) + enabled: False + # Optional: Enable GenAI review summaries for alerts (default: shown below) + alerts: True + # Optional: Enable GenAI review summaries for detections (default: shown below) + detections: False + # Optional: Activity Context Prompt to give context to the GenAI what activity is and is not suspicious. + # It is important to be direct and detailed. See documentation for the default prompt structure. + activity_context_prompt: """Define what is and is not suspicious +""" + # Optional: Image source for GenAI (default: preview) + # Options: "preview" (uses cached preview frames at ~180p) or "recordings" (extracts frames from recordings at 480p) + # Using "recordings" provides better image quality but uses more tokens per image. + # Frame count is automatically calculated based on context window size, aspect ratio, and image source (capped at 20 frames). + image_source: preview + # Optional: Additional concerns that the GenAI should make note of (default: None) + additional_concerns: + - Animals in the garden + # Optional: Preferred response language (default: English) + preferred_language: English + +# Optional: Motion configuration +# NOTE: Can be overridden at the camera level +motion: + # Optional: enables detection for the camera (default: True) + # NOTE: Motion detection is required for object detection, + # setting this to False and leaving detect enabled + # will result in an error on startup. + enabled: False + # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) + # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. + # The value should be between 1 and 255. + threshold: 30 + # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection + # needs to recalibrate. (default: shown below) + # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. + # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching + # a doorbell camera. + lightning_threshold: 0.8 + # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) + # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will + # make motion detection more sensitive to smaller moving objects. + # As a rule of thumb: + # - 10 - high sensitivity + # - 30 - medium sensitivity + # - 50 - low sensitivity + contour_area: 10 + # Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below) + # Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster. + # Low values will cause things like moving shadows to be detected as motion for longer. + # https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/ + frame_alpha: 0.01 + # Optional: Height of the resized motion frame (default: 100) + # Higher values will result in more granular motion detection at the expense of higher CPU usage. + # Lower values result in less CPU, but small changes may not register as motion. + frame_height: 100 + # Optional: motion mask + # NOTE: see docs for more detailed info on creating masks + mask: 0.000,0.469,1.000,0.469,1.000,1.000,0.000,1.000 + # Optional: improve contrast (default: shown below) + # Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive + # for daytime. + improve_contrast: True + # Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below). + mqtt_off_delay: 30 + +# Optional: Notification Configuration +# NOTE: Can be overridden at the camera level (except email) +notifications: + # Optional: Enable notification service (default: shown below) + enabled: False + # Optional: Email for push service to reach out to + # NOTE: This is required to use notifications + email: "admin@example.com" + # Optional: Cooldown time for notifications in seconds (default: shown below) + cooldown: 0 + +# Optional: Record configuration +# NOTE: Can be overridden at the camera level +record: + # Optional: Enable recording (default: shown below) + # WARNING: If recording is disabled in the config, turning it on via + # the UI or MQTT later will have no effect. + enabled: False + # Optional: Number of minutes to wait between cleanup runs (default: shown below) + # This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o + expire_interval: 60 + # Optional: Two-way sync recordings database with disk on startup and once a day (default: shown below). + sync_recordings: False + # Optional: Continuous retention settings + continuous: + # Optional: Number of days to retain recordings regardless of tracked objects or motion (default: shown below) + # NOTE: This should be set to 0 and retention should be defined in alerts and detections section below + # if you only want to retain recordings of alerts and detections. + days: 0 + # Optional: Motion retention settings + motion: + # Optional: Number of days to retain recordings regardless of tracked objects (default: shown below) + # NOTE: This should be set to 0 and retention should be defined in alerts and detections section below + # if you only want to retain recordings of alerts and detections. + days: 0 + # Optional: Recording Export Settings + export: + # Optional: Timelapse Output Args (default: shown below). + # NOTE: The default args are set to fit 24 hours of recording into 1 hour playback. + # See https://stackoverflow.com/a/58268695 for more info on how these args work. + # As an example: if you wanted to go from 24 hours to 30 minutes that would be going + # from 86400 seconds to 1800 seconds which would be 1800 / 86400 = 0.02. + # The -r (framerate) dictates how smooth the output video is. + # So the args would be -vf setpts=0.02*PTS -r 30 in that case. + timelapse_args: "-vf setpts=0.04*PTS -r 30" + # Optional: Recording Preview Settings + preview: + # Optional: Quality of recording preview (default: shown below). + # Options are: very_low, low, medium, high, very_high + quality: medium + # Optional: alert recording settings + alerts: + # Optional: Number of seconds before the alert to include (default: shown below) + pre_capture: 5 + # Optional: Number of seconds after the alert to include (default: shown below) + post_capture: 5 + # Optional: Retention settings for recordings of alerts + retain: + # Required: Retention days (default: shown below) + days: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all recording segments for alerts regardless of activity + # motion - save all recordings segments for alerts with any detected motion + # active_objects - save all recording segments for alerts with active/moving objects + # + # NOTE: If the retain mode for the camera is more restrictive than the mode configured + # here, the segments will already be gone by the time this mode is applied. + # For example, if the camera retain mode is "motion", the segments without motion are + # never stored, so setting the mode to "all" here won't bring them back. + mode: motion + # Optional: detection recording settings + detections: + # Optional: Number of seconds before the detection to include (default: shown below) + pre_capture: 5 + # Optional: Number of seconds after the detection to include (default: shown below) + post_capture: 5 + # Optional: Retention settings for recordings of detections + retain: + # Required: Retention days (default: shown below) + days: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all recording segments for detections regardless of activity + # motion - save all recordings segments for detections with any detected motion + # active_objects - save all recording segments for detections with active/moving objects + # + # NOTE: If the retain mode for the camera is more restrictive than the mode configured + # here, the segments will already be gone by the time this mode is applied. + # For example, if the camera retain mode is "motion", the segments without motion are + # never stored, so setting the mode to "all" here won't bring them back. + mode: motion + +# Optional: Configuration for the jpg snapshots written to the clips directory for each tracked object +# NOTE: Can be overridden at the camera level +snapshots: + # Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below) + enabled: False + # Optional: save a clean copy of the snapshot image (default: shown below) + clean_copy: True + # Optional: print a timestamp on the snapshots (default: shown below) + timestamp: False + # Optional: draw bounding box on the snapshots (default: shown below) + bounding_box: True + # Optional: crop the snapshot (default: shown below) + crop: False + # Optional: height to resize the snapshot to (default: original size) + height: 175 + # Optional: Restrict snapshots to objects that entered any of the listed zones (default: no required zones) + required_zones: [] + # Optional: Camera override for retention settings (default: global values) + retain: + # Required: Default retention days (default: shown below) + default: 10 + # Optional: Per object retention days + objects: + person: 15 + # Optional: quality of the encoded jpeg, 0-100 (default: shown below) + quality: 70 + +# Optional: Configuration for semantic search capability +semantic_search: + # Optional: Enable semantic search (default: shown below) + enabled: False + # Optional: Re-index embeddings database from historical tracked objects (default: shown below) + reindex: False + # Optional: Set the model used for embeddings. (default: shown below) + model: "jinav1" + # Optional: Set the model size used for embeddings. (default: shown below) + # NOTE: small model runs on CPU and large model runs on GPU + model_size: "small" + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None + +# Optional: Configuration for face recognition capability +# NOTE: enabled, min_area can be overridden at the camera level +face_recognition: + # Optional: Enable face recognition (default: shown below) + enabled: False + # Optional: Minimum face distance score required to mark as a potential match (default: shown below) + unknown_score: 0.8 + # Optional: Minimum face detection score required to detect a face (default: shown below) + # NOTE: This only applies when not running a Frigate+ model + detection_threshold: 0.7 + # Optional: Minimum face distance score required to be considered a match (default: shown below) + recognition_threshold: 0.9 + # Optional: Min area of detected face box to consider running face recognition (default: shown below) + min_area: 500 + # Optional: Min face recognitions for the sub label to be applied to the person object (default: shown below) + min_faces: 1 + # Optional: Number of images of recognized faces to save for training (default: shown below) + save_attempts: 200 + # Optional: Apply a blur quality filter to adjust confidence based on the blur level of the image (default: shown below) + blur_confidence_filter: True + # Optional: Set the model size used face recognition. (default: shown below) + model_size: small + # Optional: Target a specific device to run the model (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: None + +# Optional: Configuration for license plate recognition capability +# NOTE: enabled, min_area, and enhancement can be overridden at the camera level +lpr: + # Optional: Enable license plate recognition (default: shown below) + enabled: False + # Optional: The device to run the models on (default: shown below) + # NOTE: See https://onnxruntime.ai/docs/execution-providers/ for more information + device: CPU + # Optional: Set the model size used for text detection. (default: shown below) + model_size: small + # Optional: License plate object confidence score required to begin running recognition (default: shown below) + detection_threshold: 0.7 + # Optional: Minimum area of license plate to begin running recognition (default: shown below) + min_area: 1000 + # Optional: Recognition confidence score required to add the plate to the object as a sub label (default: shown below) + recognition_threshold: 0.9 + # Optional: Minimum number of characters a license plate must have to be added to the object as a sub label (default: shown below) + min_plate_length: 4 + # Optional: Regular expression for the expected format of a license plate (default: shown below) + format: None + # Optional: Allow this number of missing/incorrect characters to still cause a detected plate to match a known plate + match_distance: 1 + # Optional: Known plates to track (strings or regular expressions) (default: shown below) + known_plates: {} + # Optional: Enhance the detected plate image with contrast adjustment and denoising (default: shown below) + # A value between 0 and 10. Higher values are not always better and may perform worse than lower values. + enhancement: 0 + # Optional: Save plate images to /media/frigate/clips/lpr for debugging purposes (default: shown below) + debug_save_plates: False + # Optional: List of regex replacement rules to normalize detected plates (default: shown below) + replace_rules: {} + +# Optional: Configuration for AI / LLM provider +# WARNING: Depending on the provider, this will send thumbnails over the internet +# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at +# the camera level to enhance privacy for indoor cameras. +genai: + # Required: Provider must be one of ollama, gemini, or openai + provider: ollama + # Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider. + base_url: http://localhost::11434 + # Required if gemini or openai + api_key: "{FRIGATE_GENAI_API_KEY}" + # Required: The model to use with the provider. + model: gemini-1.5-flash + # Optional additional args to pass to the GenAI Provider (default: None) + provider_options: + keep_alive: -1 + +# Optional: Configuration for audio transcription +# NOTE: only the enabled option can be overridden at the camera level +audio_transcription: + # Optional: Enable live and speech event audio transcription (default: shown below) + enabled: False + # Optional: The device to run the models on for live transcription. (default: shown below) + device: CPU + # Optional: Set the model size used for live transcription. (default: shown below) + model_size: small + # Optional: Set the language used for transcription translation. (default: shown below) + # List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10 + language: en + +# Optional: Configuration for classification models +classification: + # Optional: Configuration for bird classification + bird: + # Optional: Enable bird classification (default: shown below) + enabled: False + # Optional: Minimum classification score required to be considered a match (default: shown below) + threshold: 0.9 + custom: + # Required: name of the classification model + model_name: + # Optional: Enable running the model (default: shown below) + enabled: True + # Optional: Name of classification model (default: shown below) + name: None + # Optional: Classification score threshold to change the state (default: shown below) + threshold: 0.8 + # Optional: Number of classification attempts to save in the recent classifications tab (default: shown below) + # NOTE: Defaults to 200 for object classification and 100 for state classification if not specified + save_attempts: None + # Optional: Object classification configuration + object_config: + # Required: Object types to classify + objects: [dog] + # Optional: Type of classification that is applied (default: shown below) + classification_type: sub_label + # Optional: State classification configuration + state_config: + # Required: Cameras to run classification on + cameras: + camera_name: + # Required: Crop of image frame on this camera to run classification on + crop: [0, 180, 220, 400] + # Optional: If classification should be run when motion is detected in the crop (default: shown below) + motion: False + # Optional: Interval to run classification on in seconds (default: shown below) + interval: None + +# Optional: Restream configuration +# Uses https://github.com/AlexxIT/go2rtc (v1.9.10) +# NOTE: The default go2rtc API port (1984) must be used, +# changing this port for the integrated go2rtc instance is not supported. +go2rtc: + +# Optional: Live stream configuration for WebUI. +# NOTE: Can be overridden at the camera level +live: + # Optional: Set the streams configured in go2rtc + # that should be used for live view in frigate WebUI. (default: name of camera) + # NOTE: In most cases this should be set at the camera level only. + streams: + main_stream: main_stream_name + sub_stream: sub_stream_name + # Optional: Set the height of the jsmpeg stream. (default: 720) + # This must be less than or equal to the height of the detect stream. Lower resolutions + # reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio. + height: 720 + # Optional: Set the encode quality of the jsmpeg stream (default: shown below) + # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources. + quality: 8 + +# Optional: in-feed timestamp style configuration +# NOTE: Can be overridden at the camera level +timestamp_style: + # Optional: Position of the timestamp (default: shown below) + # "tl" (top left), "tr" (top right), "bl" (bottom left), "br" (bottom right) + position: "tl" + # Optional: Format specifier conform to the Python package "datetime" (default: shown below) + # Additional Examples: + # german: "%d.%m.%Y %H:%M:%S" + format: "%m/%d/%Y %H:%M:%S" + # Optional: Color of font + color: + # All Required when color is specified (default: shown below) + red: 255 + green: 255 + blue: 255 + # Optional: Line thickness of font (default: shown below) + thickness: 2 + # Optional: Effect of lettering (default: shown below) + # None (No effect), + # "solid" (solid background in inverse color of font) + # "shadow" (shadow for font) + effect: None + +# Required +cameras: + # Required: name of the camera + back: + # Optional: Enable/Disable the camera (default: shown below). + # If disabled: config is used but no live stream and no capture etc. + # Events/Recordings are still viewable. + enabled: True + # Optional: camera type used for some Frigate features (default: shown below) + # Options are "generic" and "lpr" + type: "generic" + # Required: ffmpeg settings for the camera + ffmpeg: + # Required: A list of input streams for the camera. See documentation for more information. + inputs: + # Required: the path to the stream + # NOTE: path may include environment variables or docker secrets, which must begin with 'FRIGATE_' and be referenced in {} + - path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + # Required: list of roles for this stream. valid values are: audio,detect,record + # NOTICE: In addition to assigning the audio, detect, and record roles + # they must also be enabled in the camera config. + roles: + - audio + - detect + - record + # Optional: stream specific global args (default: inherit) + # global_args: + # Optional: stream specific hwaccel args (default: inherit) + # hwaccel_args: + # Optional: stream specific input args (default: inherit) + # input_args: + # Optional: camera specific global args (default: inherit) + # global_args: + # Optional: camera specific hwaccel args (default: inherit) + # hwaccel_args: + # Optional: camera specific input args (default: inherit) + # input_args: + # Optional: camera specific output args (default: inherit) + # output_args: + + # Optional: timeout for highest scoring image before allowing it + # to be replaced by a newer image. (default: shown below) + best_image_timeout: 60 + + # Optional: URL to visit the camera web UI directly from the system page. Might not be available on every camera. + webui_url: "" + + # Optional: zones for this camera + zones: + # Required: name of the zone + # NOTE: This must be different than any camera names, but can match with another zone on another + # camera. + front_steps: + # Optional: A friendly name or descriptive text for the zones + friendly_name: "" + # Required: List of x,y coordinates to define the polygon of the zone. + # NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box. + coordinates: 0.033,0.306,0.324,0.138,0.439,0.185,0.042,0.428 + # Optional: The real-world distances of a 4-sided zone used for zones with speed estimation enabled (default: none) + # List distances in order of the zone points coordinates and use the unit system defined in the ui config + distances: 10,15,12,11 + # Optional: Number of consecutive frames required for object to be considered present in the zone (default: shown below). + inertia: 3 + # Optional: Number of seconds that an object must loiter to be considered in the zone (default: shown below) + loitering_time: 0 + # Optional: List of objects that can trigger this zone (default: all tracked objects) + objects: + - person + # Optional: Zone level object filters. + # NOTE: The global and camera filters are applied upstream. + filters: + person: + min_area: 5000 + max_area: 100000 + threshold: 0.7 + + # Optional: Configuration for the jpg snapshots published via MQTT + mqtt: + # Optional: Enable publishing snapshot via mqtt for camera (default: shown below) + # NOTE: Only applies to publishing image data to MQTT via 'frigate///snapshot'. + # All other messages will still be published. + enabled: True + # Optional: print a timestamp on the snapshots (default: shown below) + timestamp: True + # Optional: draw bounding box on the snapshots (default: shown below) + bounding_box: True + # Optional: crop the snapshot (default: shown below) + crop: True + # Optional: height to resize the snapshot to (default: shown below) + height: 270 + # Optional: jpeg encode quality (default: shown below) + quality: 70 + # Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones) + required_zones: [] + + # Optional: Configuration for how camera is handled in the GUI. + ui: + # Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below) + # By default the cameras are sorted alphabetically. + order: 0 + # Optional: Whether or not to show the camera in the Frigate UI (default: shown below) + dashboard: True + + # Optional: connect to ONVIF camera + # to enable PTZ controls. + onvif: + # Required: host of the camera being connected to. + # NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0". + host: 0.0.0.0 + # Optional: ONVIF port for device (default: shown below). + port: 8000 + # Optional: username for login. + # NOTE: Some devices require admin to access ONVIF. + user: admin + # Optional: password for login. + password: admin + # Optional: Skip TLS verification and disable digest authentication for the ONVIF server (default: shown below) + tls_insecure: False + # Optional: Ignores time synchronization mismatches between the camera and the server during authentication. + # Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents. + ignore_time_mismatch: False + # Optional: PTZ camera object autotracking. Keeps a moving object in + # the center of the frame by automatically moving the PTZ camera. + autotracking: + # Optional: enable/disable object autotracking. (default: shown below) + enabled: False + # Optional: calibrate the camera on startup (default: shown below) + # A calibration will move the PTZ in increments and measure the time it takes to move. + # The results are used to help estimate the position of tracked objects after a camera move. + # Frigate will update your config file automatically after a calibration with + # a "movement_weights" entry for the camera. You should then set calibrate_on_startup to False. + calibrate_on_startup: False + # Optional: the mode to use for zooming in/out on objects during autotracking. (default: shown below) + # Available options are: disabled, absolute, and relative + # disabled - don't zoom in/out on autotracked objects, use pan/tilt only + # absolute - use absolute zooming (supported by most PTZ capable cameras) + # relative - use relative zooming (not supported on all PTZs, but makes concurrent pan/tilt/zoom movements) + zooming: disabled + # Optional: A value to change the behavior of zooming on autotracked objects. (default: shown below) + # A lower value will keep more of the scene in view around a tracked object. + # A higher value will zoom in more on a tracked object, but Frigate may lose tracking more quickly. + # The value should be between 0.1 and 0.75 + zoom_factor: 0.3 + # Optional: list of objects to track from labelmap.txt (default: shown below) + track: + - person + # Required: Begin automatically tracking an object when it enters any of the listed zones. + required_zones: + - zone_name + # Required: Name of ONVIF preset in camera's firmware to return to when tracking is over. (default: shown below) + return_preset: home + # Optional: Seconds to delay before returning to preset. (default: shown below) + timeout: 10 + # Optional: Values generated automatically by a camera calibration. Do not modify these manually. (default: shown below) + movement_weights: [] + + # Optional: Configuration for how to sort the cameras in the Birdseye view. + birdseye: + # Optional: Adjust sort order of cameras in the Birdseye view. Larger numbers come later (default: shown below) + # By default the cameras are sorted alphabetically. + order: 0 + + # Optional: Configuration for triggers to automate actions based on semantic search results. + triggers: + # Required: Unique identifier for the trigger (generated automatically from friendly_name if not specified). + trigger_name: + # Required: Enable or disable the trigger. (default: shown below) + enabled: true + # Optional: A friendly name or descriptive text for the trigger + friendly_name: Unique name or descriptive text + # Type of trigger, either `thumbnail` for image-based matching or `description` for text-based matching. (default: none) + type: thumbnail + # Reference data for matching, either an event ID for `thumbnail` or a text string for `description`. (default: none) + data: 1751565549.853251-b69j73 + # Similarity threshold for triggering. (default: shown below) + threshold: 0.8 + # List of actions to perform when the trigger fires. (default: none) + # Available options: + # - `notification` (send a webpush notification) + # - `sub_label` (add trigger friendly name as a sub label to the triggering tracked object) + # - `attribute` (add trigger's name and similarity score as a data attribute to the triggering tracked object) + actions: + - notification + +# Optional +ui: + # Optional: Set a timezone to use in the UI (default: use browser local time) + # timezone: America/Denver + # Optional: Set the time format used. + # Options are browser, 12hour, or 24hour (default: shown below) + time_format: browser + # Optional: Set the date style for a specified length. + # Options are: full, long, medium, short + # Examples: + # short: 2/11/23 + # medium: Feb 11, 2023 + # full: Saturday, February 11, 2023 + # (default: shown below). + date_style: short + # Optional: Set the time style for a specified length. + # Options are: full, long, medium, short + # Examples: + # short: 8:14 PM + # medium: 8:15:22 PM + # full: 8:15:22 PM Mountain Standard Time + # (default: shown below). + time_style: medium + # Optional: Set the unit system to either "imperial" or "metric" (default: metric) + # Used in the UI and in MQTT topics + unit_system: metric + +# Optional: Telemetry configuration +telemetry: + # Optional: Enabled network interfaces for bandwidth stats monitoring (default: empty list, let nethogs search all) + network_interfaces: + - eth + - enp + - eno + - ens + - wl + - lo + # Optional: Configure system stats + stats: + # Optional: Enable AMD GPU stats (default: shown below) + amd_gpu_stats: True + # Optional: Enable Intel GPU stats (default: shown below) + intel_gpu_stats: True + # Optional: Treat GPU as SR-IOV to fix GPU stats (default: shown below) + intel_gpu_device: None + # Optional: Enable network bandwidth stats monitoring for camera ffmpeg processes, go2rtc, and object detectors. (default: shown below) + # NOTE: The container must either be privileged or have cap_net_admin, cap_net_raw capabilities enabled. + network_bandwidth: False + # Optional: Enable the latest version outbound check (default: shown below) + # NOTE: If you use the Home Assistant integration, disabling this will prevent it from reporting new versions + version_check: True + +# Optional: Camera groups (default: no groups are setup) +# NOTE: It is recommended to use the UI to setup camera groups +camera_groups: + # Required: Name of camera group + front: + # Required: list of cameras in the group + cameras: + - front_cam + - side_cam + - front_doorbell_cam + # Required: icon used for group + icon: LuCar + # Required: index of this group + order: 0 +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/restream.md b/sam2-cpu/frigate-dev/docs/docs/configuration/restream.md new file mode 100644 index 0000000..9b93a60 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/restream.md @@ -0,0 +1,198 @@ +--- +id: restream +title: Restream +--- + +## RTSP + +Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. + +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.10) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#configuration) for more advanced configurations and features. + +:::note + +You can access the go2rtc stream info at `/api/go2rtc/streams` which can be helpful to debug as well as provide useful information about your camera streams. + +::: + +### Birdseye Restream + +Birdseye RTSP restream can be accessed at `rtsp://:8554/birdseye`. Enabling the birdseye restream will cause birdseye to run 24/7 which may increase CPU usage somewhat. + +```yaml +birdseye: + restream: True +``` + +:::tip + +To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `1–2`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency. + +::: + +### Securing Restream With Authentication + +The go2rtc restream can be secured with RTSP based username / password authentication. Ex: + +```yaml +go2rtc: + rtsp: + username: "admin" + password: "pass" + streams: ... +``` + +**NOTE:** This does not apply to localhost requests, there is no need to provide credentials when using the restream as a source for frigate cameras. + +## Reduce Connections To Camera + +Some cameras only support one active connection or you may just want to have a single connection open to the camera. The RTSP restream allows this to be possible. + +### With Single Stream + +One connection is made to the camera. One for the restream, `detect` and `record` connect to the restream. + +```yaml +go2rtc: + streams: + name_your_rtsp_cam: # <- for RTSP streams + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio + - "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + name_your_http_cam: # <- for other streams + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio + - "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus) + +cameras: + name_your_rtsp_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - detect + - audio # <- only necessary if audio detection is enabled + name_your_http_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - detect + - audio # <- only necessary if audio detection is enabled +``` + +### With Sub Stream + +Two connections are made to the camera. One for the sub stream, one for the restream, `record` connects to the restream. + +```yaml +go2rtc: + streams: + name_your_rtsp_cam: + - rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_rtsp_cam_sub: + - rtsp://192.168.1.5:554/substream # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_rtsp_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_http_cam: + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to opus + name_your_http_cam_sub: + - http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg + - "ffmpeg:name_your_http_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus + +cameras: + name_your_rtsp_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/name_your_rtsp_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - audio # <- only necessary if audio detection is enabled + - detect + name_your_http_cam: + ffmpeg: + output_args: + record: preset-record-generic-audio-copy + inputs: + - path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream + input_args: preset-rtsp-restream + roles: + - record + - path: rtsp://127.0.0.1:8554/name_your_http_cam_sub # <--- the name here must match the name of the camera_sub in restream + input_args: preset-rtsp-restream + roles: + - audio # <- only necessary if audio detection is enabled + - detect +``` + +## Handling Complex Passwords + +go2rtc expects URL-encoded passwords in the config, [urlencoder.org](https://urlencoder.org) can be used for this purpose. + +For example: + +```yaml +go2rtc: + streams: + my_camera: rtsp://username:$@foo%@192.168.1.100 +``` + +becomes + +```yaml +go2rtc: + streams: + my_camera: rtsp://username:$%40foo%25@192.168.1.100 +``` + +See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information. + +## Preventing go2rtc from blocking two-way audio {#two-way-talk-restream} + +For cameras that support two-way talk, go2rtc will automatically establish an audio output backchannel when connecting to an RTSP stream. This backchannel blocks access to the camera's audio output for two-way talk functionality, preventing both Frigate and other applications from using it. + +To prevent this, you must configure two separate stream instances: + +1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel) +2. A second stream instance without `#backchannel=0` for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications) + +Configuration example: + +```yaml +go2rtc: + streams: + front_door: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#backchannel=0 + front_door_twoway: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + +In this configuration: + +- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access. +- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel. + +## Advanced Restream Configurations + +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: + +NOTE: The output will need to be passed with two curly braces `{{output}}` + +```yaml +go2rtc: + streams: + stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}} +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/review.md b/sam2-cpu/frigate-dev/docs/docs/configuration/review.md new file mode 100644 index 0000000..752c496 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/review.md @@ -0,0 +1,90 @@ +--- +id: review +title: Review +--- + +The Review page of the Frigate UI is for quickly reviewing historical footage of interest from your cameras. _Review items_ are indicated on a vertical timeline and displayed as a grid of previews - bandwidth-optimized, low frame rate, low resolution videos. Hovering over or swiping a preview plays the video and marks it as reviewed. If more in-depth analysis is required, the preview can be clicked/tapped and the full frame rate, full resolution recording is displayed. + +Review items are filterable by date, object type, and camera. + +### Review items vs. tracked objects (formerly "events") + +In Frigate 0.13 and earlier versions, the UI presented "events". An event was synonymous with a tracked or detected object. In Frigate 0.14 and later, a review item is a time period where any number of tracked objects were active. + +For example, consider a situation where two people walked past your house. One was walking a dog. At the same time, a car drove by on the street behind them. + +In this scenario, Frigate 0.13 and earlier would show 4 "events" in the UI - one for each person, another for the dog, and yet another for the car. You would have had 4 separate videos to watch even though they would have all overlapped. + +In 0.14 and later, all of that is bundled into a single review item which starts and ends to capture all of that activity. Reviews for a single camera cannot overlap. Once you have watched that time period on that camera, it is marked as reviewed. + +## Alerts and Detections + +Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate 0.14 categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring required zones for them. + +:::note + +Alerts and detections categorize the tracked objects in review items, but Frigate must first detect those objects with your configured object detector (Coral, OpenVINO, etc). By default, the object tracker only detects `person`. Setting `labels` for `alerts` and `detections` does not automatically enable detection of new objects. To detect more than `person`, you should add the following to your config: + +```yaml +objects: + track: + - person + - car + - ... +``` + +See the [objects documentation](objects.md) for the list of objects that Frigate's default model tracks. +::: + +## Restricting alerts to specific labels + +By default a review item will only be marked as an alert if a person or car is detected. This can be configured to include any object or audio label using the following config: + +```yaml +# can be overridden at the camera level +review: + alerts: + labels: + - car + - cat + - dog + - person + - speech +``` + +## Restricting detections to specific labels + +By default all detections that do not qualify as an alert qualify as a detection. However, detections can further be filtered to only include certain labels or certain zones. + +```yaml +# can be overridden at the camera level +review: + detections: + labels: + - bark + - dog +``` + +## Excluding a camera from alerts or detections + +To exclude a specific camera from alerts or detections, simply provide an empty list to the alerts or detections field _at the camera level_. + +For example, to exclude objects on the camera _gatecamera_ from any detections, include this in your config: + +```yaml +cameras: + gatecamera: + review: + detections: + labels: [] +``` + +## Restricting review items to specific zones + +By default a review item will be created if any `review -> alerts -> labels` and `review -> detections -> labels` are detected anywhere in the camera frame. You will likely want to configure review items to only be created when the object enters an area of interest, [see the zone docs for more information](./zones.md#restricting-alerts-and-detections-to-specific-zones) + +:::info + +Because zones don't apply to audio, audio labels will always be marked as a detection by default. + +::: diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/semantic_search.md b/sam2-cpu/frigate-dev/docs/docs/configuration/semantic_search.md new file mode 100644 index 0000000..91f435f --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/semantic_search.md @@ -0,0 +1,166 @@ +--- +id: semantic_search +title: Semantic Search +--- + +Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_ — numerical vector representations — for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results. + +Frigate uses models from [Jina AI](https://huggingface.co/jinaai) to create and save embeddings to Frigate's database. All of this runs locally. + +Semantic Search is accessed via the _Explore_ view in the Frigate UI. + +## Minimum System Requirements + +Semantic Search works by running a large AI model locally on your system. Small or underpowered systems like a Raspberry Pi will not run Semantic Search reliably or at all. + +A minimum of 8GB of RAM is required to use Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems. + +For best performance, 16GB or more of RAM and a dedicated GPU are recommended. + +## Configuration + +Semantic Search is disabled by default, and must be enabled in your config file or in the UI's Enrichments Settings page before it can be used. Semantic Search is a global configuration setting. + +```yaml +semantic_search: + enabled: True + reindex: False +``` + +:::tip + +The embeddings database can be re-indexed from the existing tracked objects in your database by pressing the "Reindex" button in the Enrichments Settings in the UI or by adding `reindex: True` to your `semantic_search` configuration and restarting Frigate. Depending on the number of tracked objects you have, it can take a long while to complete and may max out your CPU while indexing. + +If you are enabling Semantic Search for the first time, be advised that Frigate does not automatically index older tracked objects. You will need to reindex as described above. + +::: + +### Jina AI CLIP (version 1) + +The [V1 model from Jina](https://huggingface.co/jinaai/jina-clip-v1) has a vision model which is able to embed both images and text into the same vector space, which allows `image -> image` and `text -> image` similarity searches. Frigate uses this model on tracked objects to encode the thumbnail image and store it in the database. When searching for tracked objects via text in the search box, Frigate will perform a `text -> image` similarity search against this embedding. When clicking "Find Similar" in the tracked object detail pane, Frigate will perform an `image -> image` similarity search to retrieve the closest matching thumbnails. + +The V1 text model is used to embed tracked object descriptions and perform searches against them. Descriptions can be created, viewed, and modified on the Explore page when clicking on thumbnail of a tracked object. See [the object description docs](/configuration/genai/objects.md) for more information on how to automatically generate tracked object descriptions. + +Differently weighted versions of the Jina models are available and can be selected by setting the `model_size` config option as `small` or `large`: + +```yaml +semantic_search: + enabled: True + model: "jinav1" + model_size: small +``` + +- Configuring the `large` model employs the full Jina model and will automatically run on the GPU if applicable. +- Configuring the `small` model employs a quantized version of the Jina model that uses less RAM and runs on CPU with a very negligible difference in embedding quality. + +### Jina AI CLIP (version 2) + +Frigate also supports the [V2 model from Jina](https://huggingface.co/jinaai/jina-clip-v2), which introduces multilingual support (89 languages). In contrast, the V1 model only supports English. + +V2 offers only a 3% performance improvement over V1 in both text-image and text-text retrieval tasks, an upgrade that is unlikely to yield noticeable real-world benefits. Additionally, V2 has _significantly_ higher RAM and GPU requirements, leading to increased inference time and memory usage. If you plan to use V2, ensure your system has ample RAM and a discrete GPU. CPU inference (with the `small` model) using V2 is not recommended. + +To use the V2 model, update the `model` parameter in your config: + +```yaml +semantic_search: + enabled: True + model: "jinav2" + model_size: large +``` + +For most users, especially native English speakers, the V1 model remains the recommended choice. + +:::note + +Switching between V1 and V2 requires reindexing your embeddings. The embeddings from V1 and V2 are incompatible, and failing to reindex will result in incorrect search results. + +::: + +### GPU Acceleration + +The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU hardware, when available. This depends on the Docker build that is used. You can also target a specific device in a multi-GPU installation. + +```yaml +semantic_search: + enabled: True + model_size: large + # Optional, if using the 'large' model in a multi-GPU installation + device: 0 +``` + +:::info + +If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU will be detected and used automatically. +Specify the `device` option to target a specific GPU in a multi-GPU system (see [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/)). +If you do not specify a device, the first available GPU will be used. + +See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation. + +::: + +## Usage and Best Practices + +1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results. +2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object. +3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the the ones without them are more relevant. +4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day". +5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well. +6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you. + +## Triggers + +Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes a specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab. + +:::note + +Semantic Search must be enabled to use Triggers. + +::: + +### Configuration + +Triggers are defined within the `semantic_search` configuration for each camera in your Frigate configuration file or through the UI. Each trigger consists of a `friendly_name`, a `type` (either `thumbnail` or `description`), a `data` field (the reference image event ID or text), a `threshold` for similarity matching, and a list of `actions` to perform when the trigger fires - `notification`, `sub_label`, and `attribute`. + +Triggers are best configured through the Frigate UI. + +#### Managing Triggers in the UI + +1. Navigate to the **Settings** page and select the **Triggers** tab. +2. Choose a camera from the dropdown menu to view or manage its triggers. +3. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one. +4. In the **Create Trigger** wizard: + - Enter a **Name** for the trigger (e.g., "Red Car Alert"). + - Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera"). + - Select the **Type** (`Thumbnail` or `Description`). + - For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold. + - For `Description`, enter text to trigger this action when a similar tracked object description is detected. + - Set the **Threshold** for similarity matching. + - Select **Actions** to perform when the trigger fires. + If native webpush notifications are enabled, check the `Send Notification` box to send a notification. + Check the `Add Sub Label` box to add the trigger's friendly name as a sub label to any triggering tracked objects. + Check the `Add Attribute` box to add the trigger's internal ID (e.g., "red_car_alert") to a data attribute on the tracked object that can be processed via the API or MQTT. +5. Save the trigger to update the configuration and store the embedding in the database. + +When a trigger fires, the UI highlights the trigger with a blue dot for 3 seconds for easy identification. Additionally, the UI will show the last date/time and tracked object ID that activated your trigger. The last triggered timestamp is not saved to the database or persisted through restarts of Frigate. + +### Usage and Best Practices + +1. **Thumbnail Triggers**: Select a representative image (event ID) from the Explore page that closely matches the object you want to detect. For best results, choose images where the object is prominent and fills most of the frame. +2. **Description Triggers**: Write concise, specific text descriptions (e.g., "Person in a red jacket") that align with the tracked object’s description. Avoid vague terms to improve matching accuracy. +3. **Threshold Tuning**: Adjust the threshold to balance sensitivity and specificity. A higher threshold (e.g., 0.8) requires closer matches, reducing false positives but potentially missing similar objects. A lower threshold (e.g., 0.6) is more inclusive but may trigger more often. +4. **Using Explore**: Use the context menu or right-click / long-press on a tracked object in the Grid View in Explore to quickly add a trigger based on the tracked object's thumbnail. +5. **Editing triggers**: For the best experience, triggers should be edited via the UI. However, Frigate will ensure triggers edited in the config will be synced with triggers created and edited in the UI. + +### Notes + +- Triggers rely on the same Jina AI CLIP models (V1 or V2) used for semantic search. Ensure `semantic_search` is enabled and properly configured. +- Reindexing embeddings (via the UI or `reindex: True`) does not affect trigger configurations but may update the embeddings used for matching. +- For optimal performance, use a system with sufficient RAM (8GB minimum, 16GB recommended) and a GPU for `large` model configurations, as described in the Semantic Search requirements. + +### FAQ + +#### Why can't I create a trigger on thumbnails for some text, like "person with a blue shirt" and have it trigger when a person with a blue shirt is detected? + +TL;DR: Text-to-image triggers aren’t supported because CLIP can confuse similar images and give inconsistent scores, making automation unreliable. The same word–image pair can give different scores and the score ranges can be too close together to set a clear cutoff. + +Text-to-image triggers are not supported due to fundamental limitations of CLIP-based similarity search. While CLIP works well for exploratory, manual queries, it is unreliable for automated triggers based on a threshold. Issues include embedding drift (the same text–image pair can yield different cosine distances over time), lack of true semantic grounding (visually similar but incorrect matches), and unstable thresholding (distance distributions are dataset-dependent and often too tightly clustered to separate relevant from irrelevant results). Instead, it is recommended to set up a workflow with thumbnail triggers: first use text search to manually select 3–5 representative reference tracked objects, then configure thumbnail triggers based on that visual similarity. This provides robust automation without the semantic ambiguity of text to image matching. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/snapshots.md b/sam2-cpu/frigate-dev/docs/docs/configuration/snapshots.md new file mode 100644 index 0000000..815e301 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/snapshots.md @@ -0,0 +1,12 @@ +--- +id: snapshots +title: Snapshots +--- + +Frigate can save a snapshot image to `/media/frigate/clips` for each object that is detected named as `-.jpg`. They are also accessible [via the api](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx) + +Snapshots are accessible in the UI in the Explore pane. This allows for quick submission to the Frigate+ service. + +To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones) + +Snapshots sent via MQTT are configured in the [config file](https://docs.frigate.video/configuration/) under `cameras -> your_camera -> mqtt` diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/stationary_objects.md b/sam2-cpu/frigate-dev/docs/docs/configuration/stationary_objects.md new file mode 100644 index 0000000..341d1ea --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/stationary_objects.md @@ -0,0 +1,52 @@ +# Stationary Objects + +An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs within the object at which point object detection will start running again. If the object changes location, it will be considered active. + +## Why does it matter if an object is stationary? + +Once an object becomes stationary, object detection will not be continually run on that object. This serves to reduce resource usage and redundant detections when there has been no motion near the tracked object. This also means that Frigate is contextually aware, and can for example [filter out recording segments](record.md#what-do-the-different-retain-modes-mean) to only when the object is considered active. Motion alone does not determine if an object is "active" for active_objects segment retention. Lighting changes for a parked car won't make an object active. + +## Tuning stationary behavior + +The default config is: + +```yaml +detect: + stationary: + interval: 50 + threshold: 50 +``` + +`interval` is defined as the frequency for running detection on stationary objects. This means that by default once an object is considered stationary, detection will not be run on it until motion is detected or until the interval (every 50th frame by default). With `interval >= 1`, every nth frames detection will be run to make sure the object is still there. + +NOTE: There is no way to disable stationary object tracking with this value. + +`threshold` is the number of frames an object needs to remain relatively still before it is considered stationary. + +## Why does Frigate track stationary objects? + +Frigate didn't always track stationary objects. In fact, it didn't even track objects at all initially. + +Let's look at an example use case: I want to record any cars that enter my driveway. + +One might simply think "Why not just run object detection any time there is motion around the driveway area and notify if the bounding box is in that zone?" + +With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time. + +Couldn't you just look at when motion stopped and started? Motion for a video feed is nothing more than looking for pixels that are different than they were in previous frames. If the car entered the driveway while someone was mowing the grass, how would you know which motion was for the car and which was for the person when they mow along the driveway or street? What if another car was driving the other direction on the street? Or what if its a windy day and the bush by your mailbox is blowing around? + +In order to do it more accurately, you need to identify objects and track them with a unique id. In each subsequent frame, everything has moved a little and you need to determine which bounding boxes go with each object from the previous frame. + +Tracking objects across frames is a challenging problem. Especially if you want to do it in real time. There are entire competitions for research algorithms to see which of them can do it the most accurately. Zero of them are accurate 100% of the time. Even the ones that can't do it in realtime. There is always an error rate in the algorithm. + +Now consider that the car is driving down a street that has other cars parked along it. It will drive behind some of these cars and in front of others. There may even be a car driving the opposite direction. + +Let's assume for now that we are NOT already tracking two parked cars on the street or the car parked in the driveway, ie, there is no stationary object tracking. + +As the car you are tracking approaches an area with 2 cars parked, the headlights reflect off the parked cars and the car parked in your driveway. The pixel values are different in that area, so there is motion detected. Object detection runs and identifies the remaining 3 cars. In the previous frame, you had a single bounding box from the car you are tracking. Now you have 4. The original object, the 2 cars on the street and the one in your driveway. + +Now you have to determine which of the bounding boxes in this frame should be matched to the tracking id from the previous frame where you only had one. Remember, you have never seen these additional 3 cars before, so you know nothing about them. On top of that the bounding box for the car you are tracking has now moved to a new location, so which of the 4 belongs to the car you were originally tracking? The algorithms here are fairly good. They use a Kalman filter to predict the next location of an object using the historical bounding boxes and the bounding box closest to the predicted location is linked. It's right sometimes, but the error rate is going to be high when there are 4 possible bounding boxes. + +Now let's assume that those other 3 cars were already being tracked as stationary objects, so the car driving down the street is a new 4th car. The object tracker knows we have had 3 cars and we now have 4. As the new car approaches the parked cars, the bounding boxes for all 4 cars is predicted based on the previous frames. The predicted boxes for the parked cars is pretty much a 100% overlap with the bounding boxes in the new frame. The parked cars are slam dunk matches to the tracking ids they had before and the only one left is the remaining bounding box which gets assigned to the new car. This results in a much lower error rate. Not perfect, but better. + +The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and its no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/tls.md b/sam2-cpu/frigate-dev/docs/docs/configuration/tls.md new file mode 100644 index 0000000..5c3867e --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/tls.md @@ -0,0 +1,59 @@ +--- +id: tls +title: TLS +--- + +# TLS + +Frigate's integrated NGINX server supports TLS certificates. By default Frigate will generate a self signed certificate that will be used for port 8971. Frigate is designed to make it easy to use whatever tool you prefer to manage certificates. + +Frigate is often running behind a reverse proxy that manages TLS certificates for multiple services. You will likely need to set your reverse proxy to allow self signed certificates or you can disable TLS in Frigate's config. However, if you are running on a dedicated device that's separate from your proxy or if you expose Frigate directly to the internet, you may want to configure TLS with valid certificates. + +In many deployments, TLS will be unnecessary. It can be disabled in the config with the following yaml: + +```yaml +tls: + enabled: False +``` + +## Certificates + +TLS certificates can be mounted at `/etc/letsencrypt/live/frigate` using a bind mount or docker volume. + +```yaml +frigate: + ... + volumes: + - /path/to/your/certificate_folder:/etc/letsencrypt/live/frigate:ro + ... +``` + +Within the folder, the private key is expected to be named `privkey.pem` and the certificate is expected to be named `fullchain.pem`. + +Note that certbot uses symlinks, and those can't be followed by the container unless it has access to the targets as well, so if using certbot you'll also have to mount the `archive` folder for your domain, e.g.: + +```yaml +frigate: + ... + volumes: + - /etc/letsencrypt/live/your.fqdn.net:/etc/letsencrypt/live/frigate:ro + - /etc/letsencrypt/archive/your.fqdn.net:/etc/letsencrypt/archive/your.fqdn.net:ro + ... + +``` + +Frigate automatically compares the fingerprint of the certificate at `/etc/letsencrypt/live/frigate/fullchain.pem` against the fingerprint of the TLS cert in NGINX every minute. If these differ, the NGINX config is reloaded to pick up the updated certificate. + +If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8971 to 443. + +```yaml +frigate: + ... + ports: + - "443:8971" + ... +``` + +## ACME Challenge + +Frigate also supports hosting the acme challenge files for the HTTP challenge method if needed. The challenge files should be mounted at `/etc/letsencrypt/www`. diff --git a/sam2-cpu/frigate-dev/docs/docs/configuration/zones.md b/sam2-cpu/frigate-dev/docs/docs/configuration/zones.md new file mode 100644 index 0000000..c0a11d4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/configuration/zones.md @@ -0,0 +1,196 @@ +--- +id: zones +title: Zones +--- + +Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Presence in a zone is evaluated based on the bottom center of the bounding box for the object. It does not matter how much of the bounding box overlaps with the zone. + +For example, the cat in this image is currently in Zone 1, but **not** Zone 2. +![bottom center](/img/bottom-center.jpg) + +Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera. + +During testing, enable the Zones option for the Debug view of your camera (Settings --> Debug) so you can adjust as needed. The zone line will increase in thickness when any object enters the zone. + +To create a zone, follow [the steps for a "Motion mask"](masks.md), but use the section of the web UI for creating a zone instead. + +### Restricting alerts and detections to specific zones + +Often you will only want alerts to be created when an object enters areas of interest. This is done using zones along with setting required_zones. Let's say you only want to have an alert created when an object enters your entire_yard zone, the config would be: + +```yaml +cameras: + name_of_your_camera: + review: + alerts: + required_zones: + - entire_yard + zones: + entire_yard: + friendly_name: Entire yard # You can use characters from any language text + coordinates: ... +``` + +You may also want to filter detections to only be created when an object enters a secondary area of interest. This is done using zones along with setting required_zones. Let's say you want alerts when an object enters the inner area of the yard but detections when an object enters the edge of the yard, the config would be + +```yaml +cameras: + name_of_your_camera: + review: + alerts: + required_zones: + - inner_yard + detections: + required_zones: + - edge_yard + zones: + edge_yard: + friendly_name: Edge yard # You can use characters from any language text + coordinates: ... + inner_yard: + friendly_name: Inner yard # You can use characters from any language text + coordinates: ... +``` + +### Restricting snapshots to specific zones + +```yaml +cameras: + name_of_your_camera: + snapshots: + required_zones: + - entire_yard + zones: + entire_yard: + friendly_name: Entire yard + coordinates: ... +``` + +### Restricting zones to specific objects + +Sometimes you want to limit a zone to specific object types to have more granular control of when alerts, detections, and snapshots are saved. The following example will limit one zone to person objects and the other to cars. + +```yaml +cameras: + name_of_your_camera: + zones: + entire_yard: + coordinates: ... (everywhere you want a person) + objects: + - person + front_yard_street: + coordinates: ... (just the street) + objects: + - car +``` + +Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. Objects will be tracked for any `person` that enter anywhere in the yard, and for cars only if they enter the street. + + +### Zone Loitering + +Sometimes objects are expected to be passing through a zone, but an object loitering in an area is unexpected. Zones can be configured to have a minimum loitering time after which the object will be considered in the zone. + +:::note + +When using loitering zones, a review item will behave in the following way: +- When a person is in a loitering zone, the review item will remain active until the person leaves the loitering zone, regardless of if they are stationary. +- When any other object is in a loitering zone, the review item will remain active until the loitering time is met. Then if the object is stationary the review item will end. + +::: + +```yaml +cameras: + name_of_your_camera: + zones: + sidewalk: + loitering_time: 4 # unit is in seconds + objects: + - person +``` + +### Zone Inertia + +Sometimes an objects bounding box may be slightly incorrect and the bottom center of the bounding box is inside the zone while the object is not actually in the zone. Zone inertia helps guard against this by requiring an object's bounding box to be within the zone for multiple consecutive frames. This value can be configured: + +```yaml +cameras: + name_of_your_camera: + zones: + front_yard: + inertia: 3 + objects: + - person +``` + +There may also be cases where you expect an object to quickly enter and exit a zone, like when a car is pulling into the driveway, and you may want to have the object be considered present in the zone immediately: + +```yaml +cameras: + name_of_your_camera: + zones: + driveway_entrance: + inertia: 1 + objects: + - car +``` + +### Speed Estimation + +Frigate can be configured to estimate the speed of objects moving through a zone. This works by combining data from Frigate's object tracker and "real world" distance measurements of the edges of the zone. The recommended use case for this feature is to track the speed of vehicles on a road as they move through the zone. + +Your zone must be defined with exactly 4 points and should be aligned to the ground where objects are moving. + +![Ground plane 4-point zone](/img/ground-plane.jpg) + +Speed estimation requires a minimum number of frames for your object to be tracked before a valid estimate can be calculated, so create your zone away from places where objects enter and exit for the best results. The object's bounding box must be stable and remain a constant size as it enters and exits the zone. _Your zone should not take up the full frame, and the zone does **not** need to be the same size or larger than the objects passing through it._ An object's speed is tracked while it passes through the zone and then saved to Frigate's database. + +Accurate real-world distance measurements are required to estimate speeds. These distances can be specified in your zone config through the `distances` field. + +```yaml +cameras: + name_of_your_camera: + zones: + street: + coordinates: 0.033,0.306,0.324,0.138,0.439,0.185,0.042,0.428 + distances: 10,12,11,13.5 # in meters or feet +``` + +Each number in the `distance` field represents the real-world distance between the points in the `coordinates` list. So in the example above, the distance between the first two points ([0.033,0.306] and [0.324,0.138]) is 10. The distance between the second and third set of points ([0.324,0.138] and [0.439,0.185]) is 12, and so on. The fastest and most accurate way to configure this is through the Zone Editor in the Frigate UI. + +The `distance` values are measured in meters (metric) or feet (imperial), depending on how `unit_system` is configured in your `ui` config: + +```yaml +ui: + # can be "metric" or "imperial", default is metric + unit_system: metric +``` + +The average speed of your object as it moved through your zone is saved in Frigate's database and can be seen in the UI in the Tracked Object Details pane in Explore. Current estimated speed can also be seen on the debug view as the third value in the object label (see the caveats below). Current estimated speed, average estimated speed, and velocity angle (the angle of the direction the object is moving relative to the frame) of tracked objects is also sent through the `events` MQTT topic. See the [MQTT docs](../integrations/mqtt.md#frigateevents). + +These speed values are output as a number in miles per hour (mph) or kilometers per hour (kph). For miles per hour, set `unit_system` to `imperial`. For kilometers per hour, set `unit_system` to `metric`. + +#### Best practices and caveats + +- Speed estimation works best with a straight road or path when your object travels in a straight line across that path. Avoid creating your zone near intersections or anywhere that objects would make a turn. +- Create a zone where the bottom center of your object's bounding box travels directly through it and does not become obscured at any time. +- A large zone can be used (as in the photo example above), but it may cause inaccurate estimation if the object's bounding box changes shape (such as when it turns or becomes partially hidden). Generally it's best to make your zone large enough to capture a few frames, but small enough so that the bounding box doesn't change size as it enters, travels through, and exits the zone. +- Depending on the size and location of your zone, you may want to decrease the zone's `inertia` value from the default of 3. +- The more accurate your real-world dimensions can be measured, the more accurate speed estimation will be. However, due to the way Frigate's tracking algorithm works, you may need to tweak the real-world distance values so that estimated speeds better match real-world speeds. +- Once an object leaves the zone, speed accuracy will likely decrease due to perspective distortion and misalignment with the calibrated area. Therefore, speed values will show as a zero through MQTT and will not be visible on the debug view when an object is outside of a speed tracking zone. +- The speeds are only an _estimation_ and are highly dependent on camera position, zone points, and real-world measurements. This feature should not be used for law enforcement. + +### Speed Threshold + +Zones can be configured with a minimum speed requirement, meaning an object must be moving at or above this speed to be considered inside the zone. Zone `distances` must be defined as described above. + +```yaml +cameras: + name_of_your_camera: + zones: + sidewalk: + coordinates: ... + distances: ... + inertia: 1 + speed_threshold: 20 # unit is in kph or mph, depending on how unit_system is set (see above) +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/development/contributing-boards.md b/sam2-cpu/frigate-dev/docs/docs/development/contributing-boards.md new file mode 100644 index 0000000..930c99d --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/development/contributing-boards.md @@ -0,0 +1,94 @@ +--- +id: contributing-boards +title: Community Supported Boards +--- + +## About Community Supported Boards + +There are many SBCs (small board computers) that have a passionate community behind them, Jetson Nano for example. These SBCs often have dedicated hardware that can greatly accelerate Frigate's AI and video workloads, but this hardware requires very specific frameworks for interfacing with it. + +This means it would be very difficult for Frigate's maintainers to support these different boards especially given the relatively low userbase. + +The community support boards framework allows a user in the community to be the codeowner to add support for an SBC or other detector by providing the code, maintenance, and user support. + +## Getting Started + +1. Follow the steps from [the main contributing docs](/development/contributing.md). +2. Create a new build type under `docker/` +3. Get build working as expected, all board-specific changes should be done inside of the board specific docker file. + +## Required Structure + +Each board will have different build requirements, run on different architectures, etc. however there are set of files that all boards will need. + +### Bake File .hcl + +The `board.hcl` file is what allows the community boards build to be built using the main build as a cache. This enables a clean base and quicker build times. For more information on the format and options available in the Bake file, [see the official Buildx Bake docs](https://docs.docker.com/build/bake/reference/) + +### Board Make File + +The `board.mk` file is what allows automated and configurable Make targets to be included in the main Make file. Below is the general format for this file: + +```Makefile +BOARDS += board # Replace `board` with the board suffix ex: rpi + +local-rpi: version + docker buildx bake --load --file=docker/board/board.hcl --set board.tags=frigate:latest-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board + +build-rpi: version + docker buildx bake --file=docker/board/board.hcl --set board.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board + +push-rpi: build-rpi + docker buildx bake --push --file=docker/board/board.hcl --set board.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-board bake-target # Replace `board` with the board suffix ex: rpi. Bake target is the target in the board.hcl file ex: board +``` + +### Dockerfile + +The `Dockerfile` is what orchestrates the build, this will vary greatly depending on the board but some parts are required for things to work. Below are the required parts of the Dockerfile: + +```Dockerfile +# syntax=docker/dockerfile:1.4 + +# https://askubuntu.com/questions/972516/debian-frontend-environment-variable +ARG DEBIAN_FRONTEND=noninteractive + +# All board-specific work should be done with `deps` as the base +FROM deps AS board-deps + +# do stuff specific +# to the board + +# set workdir +WORKDIR /opt/frigate/ + +# copies base files from the main frigate build +COPY --from=rootfs / / +``` + +## Other Required Changes + +### CI/CD + +The images for each board will be built for each Frigate release, this is done in the `.github/workflows/ci.yml` file. The board build workflow will need to be added here. + +```yml +- name: Build and push board build + uses: docker/bake-action@v3 + with: + push: true + targets: board # this is the target in the board.hcl file + files: docker/board/board.hcl # this should be updated with the actual board type + # the tags should be updated with the actual board types as well + # the community board builds should never push to cache, but it can pull from cache + set: | + board.tags=ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ github.ref_name }}-${{ env.SHORT_SHA }}-board + *.cache-from=type=gha +``` + +### Code Owner File + +The `CODEOWNERS` file should be updated to include the `docker/board` along with `@user` for each user that is a code owner of this board + +# Docs + +At a minimum the `installation`, `object_detectors`, `hardware_acceleration_video`, and `ffmpeg-presets` docs should be updated (if applicable) to reflect the configuration of this community board. diff --git a/sam2-cpu/frigate-dev/docs/docs/development/contributing.md b/sam2-cpu/frigate-dev/docs/docs/development/contributing.md new file mode 100644 index 0000000..a123f70 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/development/contributing.md @@ -0,0 +1,246 @@ +--- +id: contributing +title: Contributing To The Main Code Base +--- + +## Getting the source + +### Core, Web, Docker, and Documentation + +This repository holds the main Frigate application and all of its dependencies. + +Fork [blakeblackshear/frigate](https://github.com/blakeblackshear/frigate.git) to your own GitHub profile, then clone the forked repo to your local machine. + +From here, follow the guides for: + +- [Core](#core) +- [Web Interface](#web-interface) +- [Documentation](#documentation) + +### Frigate Home Assistant Add-on + +This repository holds the Home Assistant Add-on, for use with Home Assistant OS and compatible installations. It is the piece that allows you to run Frigate from your Home Assistant Supervisor tab. + +Fork [blakeblackshear/frigate-hass-addons](https://github.com/blakeblackshear/frigate-hass-addons) to your own Github profile, then clone the forked repo to your local machine. + +### Frigate Home Assistant Integration + +This repository holds the custom integration that allows your Home Assistant installation to automatically create entities for your Frigate instance, whether you are running Frigate as a standalone Docker container or as a [Home Assistant Add-on](#frigate-home-assistant-add-on). + +Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshear/frigate-hass-integration) to your own GitHub profile, then clone the forked repo to your local machine. + +## Core + +### Prerequisites + +- GNU make +- Docker (including buildx plugin) +- An extra detector (Coral, OpenVINO, etc.) is optional but recommended to simulate real world performance. + +:::note + +A Coral device can only be used by a single process at a time, so an extra Coral device is recommended if using a coral for development purposes. + +::: + +### Setup + +#### 1. Open the repo with Visual Studio Code + +Upon opening, you should be prompted to open the project in a remote container. This will build a container on top of the base Frigate container with all the development dependencies installed. This ensures everyone uses a consistent development environment without the need to install any dependencies on your host machine. + +#### 2. Modify your local config file for testing + +Place the file at `config/config.yml` in the root of the repo. + +Here is an example, but modify for your needs: + +```yaml +mqtt: + host: mqtt + +cameras: + test: + ffmpeg: + inputs: + - path: /media/frigate/car-stopping.mp4 + input_args: -re -stream_loop -1 -fflags +genpts + roles: + - detect +``` + +These input args tell ffmpeg to read the mp4 file in an infinite loop. You can use any valid ffmpeg input here. + +#### 3. Gather some mp4 files for testing + +Create and place these files in a `debug` folder in the root of the repo. This is also where recordings will be created if you enable them in your test config. Update your config from step 2 above to point at the right file. You can check the `docker-compose.yml` file in the repo to see how the volumes are mapped. + +#### 4. Run Frigate from the command line + +VS Code will start the Docker Compose file for you and open a terminal window connected to `frigate-dev`. + +- Depending on what hardware you're developing on, you may need to amend `docker-compose.yml` in the project root to pass through a USB Coral or GPU for hardware acceleration. +- Run `python3 -m frigate` to start the backend. +- In a separate terminal window inside VS Code, change into the `web` directory and run `npm install && npm run dev` to start the frontend. + +#### 5. Teardown + +After closing VS Code, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers. + +### Testing + +#### FFMPEG Hardware Acceleration + +The following commands are used inside the container to ensure hardware acceleration is working properly. + +**Raspberry Pi (64bit)** + +This should show less than 50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`. + +```shell +ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**NVIDIA GPU** + +```shell +ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**NVIDIA Jetson** + +```shell +ffmpeg -c:v h264_nvmpi -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**VAAPI** + +```shell +ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +**QSV** + +```shell +ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null +``` + +## Web Interface + +### Prerequisites + +- All [core](#core) prerequisites _or_ another running Frigate instance locally available +- Node.js 20 + +### Making changes + +#### 1. Set up a Frigate instance + +The Web UI requires an instance of Frigate to interact with for all of its data. You can either run an instance locally (recommended) or attach to a separate instance accessible on your network. + +To run the local instance, follow the [core](#core) development instructions. + +If you won't be making any changes to the Frigate HTTP API, you can attach the web development server to any Frigate instance on your network. Skip this step and go to [3a](#3a-run-the-development-server-against-a-non-local-instance). + +#### 2. Install dependencies + +```console +cd web && npm install +``` + +#### 3. Run the development server + +```console +cd web && npm run dev +``` + +##### 3a. Run the development server against a non-local instance + +To run the development server against a non-local instance, you will need to +replace the `localhost` values in `vite.config.ts` with the IP address of the +non-local backend server. + +#### 4. Making changes + +The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com). + +Light guidelines and advice: + +- Avoid adding more dependencies. The web UI intends to be lightweight and fast to load. +- Do not make large sweeping changes. [Open a discussion on GitHub](https://github.com/blakeblackshear/frigate/discussions/new) for any large or architectural ideas. +- Ensure `lint` passes. This command will ensure basic conformance to styles, applying as many automatic fixes as possible, including Prettier formatting. + +```console +npm run lint +``` + +- Add to unit tests and ensure they pass. As much as possible, you should strive to _increase_ test coverage whenever making changes. This will help ensure features do not accidentally become broken in the future. +- If you run into error messages like "TypeError: Cannot read properties of undefined (reading 'context')" when running tests, this may be due to these issues (https://github.com/vitest-dev/vitest/issues/1910, https://github.com/vitest-dev/vitest/issues/1652) in vitest, but I haven't been able to resolve them. + +```console +npm run test +``` + +- Test in different browsers. Firefox, Chrome, and Safari all have different quirks that make them unique targets to interact with. + +## Documentation + +### Prerequisites + +- Node.js 20 + +### Making changes + +#### 1. Installation + +```console +cd docs && npm install +``` + +#### 2. Local Development + +```console +npm run start +``` + +This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. + +The docs are built using [Docusaurus v3](https://docusaurus.io). Please refer to the Docusaurus docs for more information on how to modify Frigate's documentation. + +#### 3. Build (optional) + +```console +npm run build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Official builds + +Setup buildx for multiarch + +``` +docker buildx stop builder && docker buildx rm builder # <---- if existing +docker run --privileged --rm tonistiigi/binfmt --install all +docker buildx create --name builder --driver docker-container --driver-opt network=host --use +docker buildx inspect builder --bootstrap +make push +``` + +## Other + +### Nginx + +When testing nginx config changes from within the dev container, the following command can be used to copy and reload the config for testing without rebuilding the container: + +```console +sudo cp docker/main/rootfs/usr/local/nginx/conf/* /usr/local/nginx/conf/ && sudo /usr/local/nginx/sbin/nginx -s reload +``` + +## Contributing translations of the Web UI + +Frigate uses [Weblate](https://weblate.org) to manage translations of the Web UI. To contribute translation, sign up for an account at Weblate and navigate to the Frigate NVR project: + +https://hosted.weblate.org/projects/frigate-nvr/ + +When translating, maintain the existing key structure while translating only the values. Ensure your translations maintain proper formatting, including any placeholder variables (like `{{example}}`). diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/camera_setup.md b/sam2-cpu/frigate-dev/docs/docs/frigate/camera_setup.md new file mode 100644 index 0000000..06d7d3b --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/camera_setup.md @@ -0,0 +1,39 @@ +--- +id: camera_setup +title: Camera setup +--- + +Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but less compatibility. Firefox 134+/136+/137+ (Windows/Mac/Linux & Android), Chrome 108+, Safari and Edge are the only browsers able to play H.265 and only support a limited number of H.265 profiles. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around. + +- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher (10fps is the recommended maximum for most users) for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server. + +- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed in your camera's firmware to 15 fps. + +- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections. + +### Choosing a detect resolution + +The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions. + +Larger resolutions **do** improve performance if the objects are very small in the frame. + +![Resolutions](/img/resolutions-min.jpg) + +### Example Camera Configuration + +For the Dahua/Loryta 5442 camera, I use the following settings: + +**Main Stream (Recording & RTSP)** + +- Encode Mode: H.264 +- Resolution: 2688\*1520 +- Frame Rate(FPS): 15 +- I Frame Interval: 30 (15 can also be used to prioritize streaming performance - see the [camera settings recommendations](/configuration/live#camera_settings_recommendations) for more info) + +**Sub Stream (Detection)** + +- Enable: Sub Stream 2 +- Encode Mode: H.264 +- Resolution: 1280\*720 +- Frame Rate: 5 +- I Frame Interval: 5 diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/glossary.md b/sam2-cpu/frigate-dev/docs/docs/frigate/glossary.md new file mode 100644 index 0000000..5bfbfaf --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/glossary.md @@ -0,0 +1,69 @@ +--- +id: glossary +title: Glossary +--- + +The glossary explains terms commonly used in Frigate's documentation. + +## Bounding Box + +A box returned from the object detection model that outlines an object in the frame. These have multiple colors depending on object type in the debug live view. + +### Bounding Box Colors + +- At startup different colors will be assigned to each object label +- A dark blue thin line indicates that object is not detected at this current point in time +- A gray thin line indicates that object is detected as being stationary +- A thick line indicates that object is the subject of autotracking (when enabled). + +## False Positive + +An incorrect detection of an object type. For example a dog being detected as a person, a chair being detected as a dog, etc. A person being detected in an area you want to ignore is not a false positive. + +## Mask + +There are two types of masks in Frigate. [See the mask docs for more info](/configuration/masks) + +### Motion Mask + +Motion masks prevent detection of [motion](#motion) in masked areas from triggering Frigate to run object detection, but do not prevent objects from being detected if object detection runs due to motion in nearby areas. For example: camera timestamps, skies, the tops of trees, etc. + +### Object Mask + +Object filter masks drop any bounding boxes where the bottom center (overlap doesn't matter) is in the masked area. It forces them to be considered a [false positive](#false-positive) so that they are ignored. + +## Min Score + +The lowest score that an object can be detected with during tracking, any detection with a lower score will be assumed to be a false positive + +## Motion + +When pixels in the current camera frame are different than previous frames. When many nearby pixels are different in the current frame they grouped together and indicated with a red motion box in the live debug view. [See the motion detection docs for more info](/configuration/motion_detection) + +## Region + +A portion of the camera frame that is sent to object detection, regions can be sent due to motion, active objects, or occasionally for stationary objects. These are represented by green boxes in the debug live view. + +## Review Item + +A review item is a time period where any number of events/tracked objects were active. [See the review docs for more info](/configuration/review) + +## Snapshot Score + +The score shown in a snapshot is the score of that object at that specific moment in time. + +## Threshold + +The threshold is the median score that an object must reach in order to be considered a true positive. + +## Top Score + +The top score for an object is the highest median score for an object. + +## Tracked Object ("event" in previous versions) + +The time period starting when a tracked object entered the frame and ending when it left the frame, including any time that the object remained still. Tracked objects are saved when it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording to be saved. + +## Zone + +Zones are areas of interest, zones can be used for notifications and for limiting the areas where Frigate will create a [review item](#review-item). [See the zone docs for more info](/configuration/zones) diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/hardware.md b/sam2-cpu/frigate-dev/docs/docs/frigate/hardware.md new file mode 100644 index 0000000..2a08128 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/hardware.md @@ -0,0 +1,310 @@ +--- +id: hardware +title: Recommended hardware +--- + +import CommunityBadge from '@site/src/components/CommunityBadge'; + +## Cameras + +Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, and recordings without re-encoding. + +I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options. + +WiFi cameras are not recommended as [their streams are less reliable and cause connection loss and/or lost video data](https://ipcamtalk.com/threads/camera-conflicts.68142/#post-738821), especially when more than a few WiFi cameras will be used at the same time. + +Many users have reported various issues with 4K-plus Reolink cameras, it is best to stick with 5MP and lower for Reolink cameras. If you are using Reolink, I suggest the [Reolink specific configuration](../configuration/camera_specific.md#reolink-cameras). + +Here are some of the cameras I recommend: + +- Loryta(Dahua) IPC-T549M-ALED-S3 (affiliate link) +- Loryta(Dahua) IPC-T54IR-AS (affiliate link) +- Amcrest IP5M-T1179EW-AI-V3 (affiliate link) +- HIKVISION DS-2CD2387G2P-LSU/SL ColorVu 8MP Panoramic Turret IP Camera (affiliate link) + +I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. + +## Server + +My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators. + +Note that many of these mini PCs come with Windows pre-installed, and you will need to install Linux according to the [getting started guide](../guides/getting_started.md). + +I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. + +:::warning + +If the EQ13 is out of stock, the link below may take you to a suggested alternative on Amazon. The Beelink EQ14 has some known compatibility issues, so you should avoid that model for now. + +::: + +| Name | Coral Inference Speed | Coral Compatibility | Notes | +| ------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------- | ----------------------------------------------------------------------------------------- | +| Beelink EQ13 (Amazon) | 5-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. | + +## Detectors + +A detector is a device which is optimized for running inferences efficiently to detect objects. Using a recommended detector means there will be less latency between detections and more detections can be run per second. Frigate is designed around the expectation that a detector is used to achieve very low inference speeds. Offloading TensorFlow to a detector is an order of magnitude faster and will reduce your CPU load dramatically. + +:::info + +Frigate supports multiple different detectors that work on different types of hardware: + +**Most Hardware** + +- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices. + + - [Supports many model architectures](../../configuration/object_detectors#configuration) + - Runs best with tiny or small size models + +- [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices. + + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#edge-tpu-detector) + +- [MemryX](#memryx-mx3): The MX3 M.2 accelerator module is available in m.2 format allowing for a wide range of compatibility with devices. + - [Supports many model architectures](../../configuration/object_detectors#memryx-mx3) + - Runs best with tiny, small, or medium-size models + +**AMD** + +- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection + - [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models) + - Runs best on discrete AMD GPUs + +**Apple Silicon** + +- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models) + - Runs well with any size models including large + - Runs via ZMQ proxy which adds some latency, only recommended for local connection + +**Intel** + +- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection. + - [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models) + - Runs best with tiny, small, or medium models + +**Nvidia** + +- [TensortRT](#tensorrt---nvidia-gpu): TensorRT can run on Nvidia GPUs to provide efficient object detection. + + - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models) + - Runs well with any size models including large + +- [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. + +**Rockchip** + +- [RKNN](#rockchip-platform): RKNN models can run on Rockchip devices with included NPUs to provide efficient object detection. + - [Supports limited model architectures](../../configuration/object_detectors#choosing-a-model) + - Runs best with tiny or small size models + - Runs efficiently on low power hardware + +**Synaptics** + +- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection. + +::: + +### Hailo-8 + +Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided. + +**Default Model Configuration:** + +- **Hailo-8L:** Default model is **YOLOv6n**. +- **Hailo-8:** Default model is **YOLOv6n**. + +In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms—with dual PCIe lanes—yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup. + +| Name | Hailo‑8 Inference Time | Hailo‑8L Inference Time | +| ---------------- | ---------------------- | ----------------------- | +| ssd mobilenet v1 | ~ 6 ms | ~ 10 ms | +| yolov9-tiny | | 320: 18ms | +| yolov6n | ~ 7 ms | ~ 11 ms | + +### Google Coral TPU + +Frigate supports both the USB and M.2 versions of the Google Coral. + +- The USB version is compatible with the widest variety of hardware and does not require a driver on the host machine. However, it does lack the automatic throttling features of the other versions. +- The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai + +A single Coral can handle many cameras using the default model and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed. + +### OpenVINO - Intel + +The OpenVINO detector type is able to run on: + +- 6th Gen Intel Platforms and newer that have an iGPU +- x86 hosts with an Intel Arc GPU +- Intel NPUs +- Most modern AMD CPUs (though this is officially not supported by Intel) +- x86 & Arm64 hosts via CPU (generally not recommended) + +:::note + +Intel NPUs have seen [limited success in community deployments](https://github.com/blakeblackshear/frigate/discussions/13248#discussioncomment-12347357), although they remain officially unsupported. + +In testing, the NPU delivered performance that was only comparable to — or in some cases worse than — the integrated GPU. + +::: + +More information is available [in the detector docs](/configuration/object_detectors#openvino-detector) + +Inference speeds vary greatly depending on the CPU or GPU used, some known examples of GPU inference times are below: + +| Name | MobileNetV2 Inference Time | YOLOv9 | YOLO-NAS Inference Time | RF-DETR Inference Time | Notes | +| -------------- | -------------------------- | ------------------------------------------------- | ------------------------- | ---------------------- | ---------------------------------- | +| Intel HD 530 | 15 - 35 ms | | | | Can only run one detector instance | +| Intel HD 620 | 15 - 25 ms | | 320: ~ 35 ms | | | +| Intel HD 630 | ~ 15 ms | | 320: ~ 30 ms | | | +| Intel UHD 730 | ~ 10 ms | t-320: 14ms s-320: 24ms t-640: 34ms s-640: 65ms | 320: ~ 19 ms 640: ~ 54 ms | | | +| Intel UHD 770 | ~ 15 ms | t-320: ~ 16 ms s-320: ~ 20 ms s-640: ~ 40 ms | 320: ~ 20 ms 640: ~ 46 ms | | | +| Intel N100 | ~ 15 ms | s-320: 30 ms | 320: ~ 25 ms | | Can only run one detector instance | +| Intel N150 | ~ 15 ms | t-320: 16 ms s-320: 24 ms | | | | +| Intel Iris XE | ~ 10 ms | t-320: 6 ms t-640: 14 ms s-320: 8 ms s-640: 16 ms | 320: ~ 10 ms 640: ~ 20 ms | 320-n: 33 ms | | +| Intel NPU | ~ 6 ms | s-320: 11 ms | 320: ~ 14 ms 640: ~ 34 ms | 320-n: 40 ms | | +| Intel Arc A310 | ~ 5 ms | t-320: 7 ms t-640: 11 ms s-320: 8 ms s-640: 15 ms | 320: ~ 8 ms 640: ~ 14 ms | | | +| Intel Arc A380 | ~ 6 ms | | 320: ~ 10 ms 640: ~ 22 ms | 336: 20 ms 448: 27 ms | | +| Intel Arc A750 | ~ 4 ms | | 320: ~ 8 ms | | | + +### TensorRT - Nvidia GPU + +Frigate is able to utilize an Nvidia GPU which supports the 12.x series of CUDA libraries. + +#### Minimum Hardware Support + +12.x series of CUDA libraries are used which have minor version compatibility. The minimum driver version on the host system must be `>=545`. Also the GPU must support a Compute Capability of `5.0` or greater. This generally correlates to a Maxwell-era GPU or newer, check the NVIDIA GPU Compute Capability table linked below. + +Make sure your host system has the [nvidia-container-runtime](https://docs.docker.com/config/containers/resource_constraints/#access-an-nvidia-gpu) installed to pass through the GPU to the container and the host system has a compatible driver installed for your GPU. + +There are improved capabilities in newer GPU architectures that TensorRT can benefit from, such as INT8 operations and Tensor cores. The features compatible with your hardware will be optimized when the model is converted to a trt file. Currently the script provided for generating the model provides a switch to enable/disable FP16 operations. If you wish to use newer features such as INT8 optimization, more work is required. + +#### Compatibility References: + +[NVIDIA TensorRT Support Matrix](https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-841/support-matrix/index.html) + +[NVIDIA CUDA Compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/index.html) + +[NVIDIA GPU Compute Capability](https://developer.nvidia.com/cuda-gpus) + +Inference speeds will vary greatly depending on the GPU and the model used. +`tiny (t)` variants are faster than the equivalent non-tiny model, some known examples are below: + +✅ - Accelerated with CUDA Graphs +❌ - Not accelerated with CUDA Graphs + +| Name | ✅ YOLOv9 Inference Time | ✅ RF-DETR Inference Time | ❌ YOLO-NAS Inference Time | +| --------- | ------------------------------------- | ------------------------- | -------------------------- | +| GTX 1070 | s-320: 16 ms | | 320: 14 ms | +| RTX 3050 | t-320: 8 ms s-320: 10 ms s-640: 28 ms | Nano-320: ~ 12 ms | 320: ~ 10 ms 640: ~ 16 ms | +| RTX 3070 | t-320: 6 ms s-320: 8 ms s-640: 25 ms | Nano-320: ~ 9 ms | 320: ~ 8 ms 640: ~ 14 ms | +| RTX A4000 | | | 320: ~ 15 ms | +| Tesla P40 | | | 320: ~ 105 ms | + +### Apple Silicon + +With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon. + +:::warning + +Apple Silicon can not run within a container, so a ZMQ proxy is utilized to communicate with [the Apple Silicon Frigate detector](https://github.com/frigate-nvr/apple-silicon-detector) which runs on the host. This should add minimal latency when run on the same device. + +::: + +| Name | YOLOv9 Inference Time | +| ------ | ------------------------------------ | +| M4 | s-320: 10 ms | +| M3 Pro | t-320: 6 ms s-320: 8 ms s-640: 20 ms | +| M1 | s-320: 9ms | + +### ROCm - AMD GPU + +With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs. + +| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | +| --------- | --------------------------- | ------------------------- | +| AMD 780M | t-320: ~ 14 ms s-320: 20 ms | 320: ~ 25 ms 640: ~ 50 ms | +| AMD 8700G | | 320: ~ 20 ms 640: ~ 40 ms | + +## Community Supported Detectors + +### MemryX MX3 + +Frigate supports the MemryX MX3 M.2 AI Acceleration Module on compatible hardware platforms, including both x86 (Intel/AMD) and ARM-based SBCs such as Raspberry Pi 5. + +A single MemryX MX3 module is capable of handling multiple camera streams using the default models, making it sufficient for most users. For larger deployments with more cameras or bigger models, multiple MX3 modules can be used. Frigate supports multi-detector configurations, allowing you to connect multiple MX3 modules to scale inference capacity. + +Detailed information is available [in the detector docs](/configuration/object_detectors#memryx-mx3). + +**Default Model Configuration:** + +- Default model is **YOLO-NAS-Small**. + +The MX3 is a pipelined architecture, where the maximum frames per second supported (and thus supported number of cameras) cannot be calculated as `1/latency` (1/"Inference Time") and is measured separately. When estimating how many camera streams you may support with your configuration, use the **MX3 Total FPS** column to approximate of the detector's limit, not the Inference Time. + +| Model | Input Size | MX3 Inference Time | MX3 Total FPS | +| -------------------- | ---------- | ------------------ | ------------- | +| YOLO-NAS-Small | 320 | ~ 9 ms | ~ 378 | +| YOLO-NAS-Small | 640 | ~ 21 ms | ~ 138 | +| YOLOv9s | 320 | ~ 16 ms | ~ 382 | +| YOLOv9s | 640 | ~ 41 ms | ~ 110 | +| YOLOX-Small | 640 | ~ 16 ms | ~ 263 | +| SSDlite MobileNet v2 | 320 | ~ 5 ms | ~ 1056 | + +Inference speeds may vary depending on the host platform. The above data was measured on an **Intel 13700 CPU**. Platforms like Raspberry Pi, Orange Pi, and other ARM-based SBCs have different levels of processing capability, which may limit total FPS. + +### Nvidia Jetson + +Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. It will [make use of the Jetson's hardware media engine](/configuration/hardware_acceleration_video#nvidia-jetson-orin-agx-orin-nx-orin-nano-xavier-agx-xavier-nx-tx2-tx1-nano) when configured with the [appropriate presets](/configuration/ffmpeg_presets#hwaccel-presets), and will make use of the Jetson's GPU and DLA for object detection when configured with the [TensorRT detector](/configuration/object_detectors#nvidia-tensorrt-detector). + +Inference speed will vary depending on the YOLO model, jetson platform and jetson nvpmodel (GPU/DLA/EMC clock speed). It is typically 20-40 ms for most models. The DLA is more efficient than the GPU, but not faster, so using the DLA will reduce power consumption but will slightly increase inference time. + +### Rockchip platform + +Frigate supports hardware video processing on all Rockchip boards. However, hardware object detection is only supported on these boards: + +- RK3562 +- RK3566 +- RK3568 +- RK3576 +- RK3588 + +| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | YOLOx Inference Time | +| -------------- | --------------------- | --------------------------- | ----------------------- | +| rk3588 3 cores | tiny: ~ 35 ms | small: ~ 20 ms med: ~ 30 ms | nano: 14 ms tiny: 18 ms | +| rk3566 1 core | | small: ~ 96 ms | | + +The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms for yolo-nas s. + +### Synaptics + +- **Synaptics** Default model is **mobilenet** + +| Name | Synaptics SL1680 Inference Time | +| ------------- | ------------------------------- | +| ssd mobilenet | ~ 25 ms | +| yolov5m | ~ 118 ms | + +## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version) + +This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity. + +CPU Usage: I am a CPU, Mendel is a Google Coral + +My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it. + +Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score! + +_What happens when I increase the resolution of my camera?_ + +However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby. + +Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it. + +## Do hwaccel args help if I am using a Coral? + +YES! The Coral does not help with decoding video streams. + +Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://support.video.ibm.com/hc/en-us/articles/18106203580316-Keyframes-InterFrame-Video-Compression). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work. diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/index.md b/sam2-cpu/frigate-dev/docs/docs/frigate/index.md new file mode 100644 index 0000000..8316202 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/index.md @@ -0,0 +1,29 @@ +--- +id: index +title: Introduction +slug: / +--- + +A complete and local NVR designed for Home Assistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras. + +Use of a [Recommended Detector](/frigate/hardware#detectors) is optional, but strongly recommended. CPU detection should only be used for testing purposes. + +- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration) +- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary +- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame +- Uses a very low overhead motion detection to determine where to run object detection +- Object detection with TensorFlow runs in separate processes for maximum FPS +- Communicates over MQTT for easy integration into other systems +- Recording with retention based on detected objects +- Re-streaming via RTSP to reduce the number of connections to your camera +- A dynamic combined camera view of all tracked cameras. + +## Screenshots + +![Live View](/img/live-view.png) + +![Review Items](/img/review-items.png) + +![Media Browser](/img/media_browser-min.png) + +![Notification](/img/notification-min.png) diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/installation.md b/sam2-cpu/frigate-dev/docs/docs/frigate/installation.md new file mode 100644 index 0000000..a8271d0 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/installation.md @@ -0,0 +1,538 @@ +--- +id: installation +title: Installation +--- + +Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant Add-on](https://www.home-assistant.io/addons/). Note that the Home Assistant Add-on is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant Add-on. + +:::tip + +If you already have Frigate installed as a Home Assistant Add-on, check out the [getting started guide](../guides/getting_started#configuring-frigate) to configure Frigate. + +::: + +## Dependencies + +**MQTT broker (optional)** - An MQTT broker is optional with Frigate, but is required for the Home Assistant integration. If using Home Assistant, Frigate and Home Assistant must be connected to the same MQTT broker. + +## Preparing your hardware + +### Operating System + +Frigate runs best with Docker installed on bare metal Debian-based distributions. For ideal performance, Frigate needs low overhead access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended though [some users have had success with Proxmox](#proxmox). + +Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help. + +### Storage + +Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine. + +- `/config`: Used to store the Frigate config file and sqlite database. You will also see a few files alongside the database file while Frigate is running. +- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually. +- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually. +- `/media/frigate/exports`: Storage for clips and timelapses that have been exported via the WebUI or API. +- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder. Segments generated via the `clip.mp4` endpoints are also concatenated and processed here. It is recommended to use a [`tmpfs`](https://docs.docker.com/storage/tmpfs/) mount for this. +- `/dev/shm`: Internal cache for raw decoded frames in shared memory. It is not recommended to modify this directory or map it with docker. The minimum size is impacted by the `shm-size` calculations below. + +### Ports + +The following ports are used by Frigate and can be mapped via docker as required. + +| Port | Description | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `8971` | Authenticated UI and API access without TLS. Reverse proxies should use this port. | +| `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate. | +| `8554` | RTSP restreaming. By default, these streams are unauthenticated. Authentication can be configured in go2rtc section of config. | +| `8555` | WebRTC connections for cameras with two-way talk support. | + +#### Common Docker Compose storage configurations + +Writing to a local disk or external USB drive: + +```yaml +services: + frigate: + ... + volumes: + - /path/to/your/config:/config + - /path/to/your/storage:/media/frigate + - type: tmpfs # Recommended: 1GB of memory + target: /tmp/cache + tmpfs: + size: 1000000000 + ... +``` + +:::warning + +Users of the Snapcraft build of Docker cannot use storage locations outside your $HOME folder. + +::: + +### Calculating required shm-size + +Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. + +The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). + +The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well. + +You can calculate the **minimum** shm size for each camera with the following formula using the resolution specified for detect: + +```console +# Template for one camera without logs, replace and +$ python -c 'print("{:.2f}MB".format(( * * 1.5 * 20 + 270480) / 1048576))' + +# Example for 1280x720, including logs +$ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 20 + 270480) / 1048576 + 40))' +66.63MB + +# Example for eight cameras detecting at 1280x720, including logs +$ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 20 + 270480) / 1048576) * 8 + 40))' +253MB +``` + +The shm size cannot be set per container for Home Assistant add-ons. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration. + +### Raspberry Pi 3/4 + +By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options). + +Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with this (affiliate link). + +### Hailo-8 + +The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments. + +#### Installation + +For Raspberry Pi 5 users with the AI Kit, installation is straightforward. Simply follow this [guide](https://www.raspberrypi.com/documentation/accessories/ai-kit.html#ai-kit-installation) to install the driver and software. + +For other installations, follow these steps for installation: + +1. Install the driver from the [Hailo GitHub repository](https://github.com/hailo-ai/hailort-drivers). A convenient script for Linux is available to clone the repository, build the driver, and install it. +2. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/dev/docker/hailo8l/user_installation.sh). +3. Ensure it has execution permissions with `sudo chmod +x user_installation.sh` +4. Run the script with `./user_installation.sh` + +#### Setup + +To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable` + +Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file: + +```yaml +devices: + - /dev/hailo0 +``` + +If you are using `docker run`, add this option to your command `--device /dev/hailo0` + +#### Configuration + +Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8l) to complete the setup. + +### MemryX MX3 + +The MemryX MX3 Accelerator is available in the M.2 2280 form factor (like an NVMe SSD), and supports a variety of configurations: + +- x86 (Intel/AMD) PCs +- Raspberry Pi 5 +- Orange Pi 5 Plus/Max +- Multi-M.2 PCIe carrier cards + +#### Configuration + +#### Installation + +To get started with MX3 hardware setup for your system, refer to the [Hardware Setup Guide](https://developer.memryx.com/get_started/hardware_setup.html). + +Then follow these steps for installing the correct driver/runtime configuration: + +1. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/dev/docker/memryx/user_installation.sh). +2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh` +3. Run the script with `./user_installation.sh` +4. **Restart your computer** to complete driver installation. + +#### Setup + +To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable` + +Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file: + +```yaml +devices: + - /dev/memx0 +``` + +During configuration, you must run Docker in privileged mode and ensure the container can access the max-manager. + +In your `docker-compose.yml`, also add: + +```yaml +privileged: true + +volumes: + - /run/mxa_manager:/run/mxa_manager +``` + +If you can't use Docker Compose, you can run the container with something similar to this: + +```bash + docker run -d \ + --name frigate-memx \ + --restart=unless-stopped \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --shm-size=256m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -v /run/mxa_manager:/run/mxa_manager \ + -e FRIGATE_RTSP_PASSWORD='password' \ + --privileged=true \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 5000:5000 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + --device /dev/memx0 \ + ghcr.io/blakeblackshear/frigate:stable +``` + +#### Configuration + +Finally, configure [hardware object detection](/configuration/object_detectors#memryx-mx3) to complete the setup. + +### Rockchip platform + +Make sure that you use a linux distribution that comes with the rockchip BSP kernel 5.10 or 6.1 and necessary drivers (especially rkvdec2 and rknpu). To check, enter the following commands: + +``` +$ uname -r +5.10.xxx-rockchip # or 6.1.xxx; the -rockchip suffix is important +$ ls /dev/dri +by-path card0 card1 renderD128 renderD129 # should list renderD128 (VPU) and renderD129 (NPU) +$ sudo cat /sys/kernel/debug/rknpu/version +RKNPU driver: v0.9.2 # or later version +``` + +I recommend [Armbian](https://www.armbian.com/download/?arch=aarch64), if your board is supported. + +#### Setup + +Follow Frigate's default installation instructions, but use a docker image with `-rk` suffix for example `ghcr.io/blakeblackshear/frigate:stable-rk`. + +Next, you need to grant docker permissions to access your hardware: + +- During the configuration process, you should run docker in privileged mode to avoid any errors due to insufficient permissions. To do so, add `privileged: true` to your `docker-compose.yml` file or the `--privileged` flag to your docker run command. +- After everything works, you should only grant necessary permissions to increase security. Disable the privileged mode and add the lines below to your `docker-compose.yml` file: + +```yaml +security_opt: + - apparmor=unconfined + - systempaths=unconfined +devices: + - /dev/dri + - /dev/dma_heap + - /dev/rga + - /dev/mpp_service +volumes: + - /sys/:/sys/:ro +``` + +or add these options to your `docker run` command: + +``` +--security-opt systempaths=unconfined \ +--security-opt apparmor=unconfined \ +--device /dev/dri \ +--device /dev/dma_heap \ +--device /dev/rga \ +--device /dev/mpp_service \ +--volume /sys/:/sys/:ro +``` + +#### Configuration + +Next, you should configure [hardware object detection](/configuration/object_detectors#rockchip-platform) and [hardware video processing](/configuration/hardware_acceleration_video#rockchip-platform). + +### Synaptics + +- SL1680 + +#### Setup + +Follow Frigate's default installation instructions, but use a docker image with `-synaptics` suffix for example `ghcr.io/blakeblackshear/frigate:stable-synaptics`. + +Next, you need to grant docker permissions to access your hardware: + +- During the configuration process, you should run docker in privileged mode to avoid any errors due to insufficient permissions. To do so, add `privileged: true` to your `docker-compose.yml` file or the `--privileged` flag to your docker run command. + +```yaml +devices: + - /dev/synap + - /dev/video0 + - /dev/video1 +``` + +or add these options to your `docker run` command: + +``` +--device /dev/synap \ +--device /dev/video0 \ +--device /dev/video1 +``` + +#### Configuration + +Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics). + +## Docker + +Running through Docker with Docker Compose is the recommended install method. + +```yaml +services: + frigate: + container_name: frigate + privileged: true # this may not be necessary for all setups + restart: unless-stopped + stop_grace_period: 30s # allow enough time to shut down the various services + image: ghcr.io/blakeblackshear/frigate:stable + shm_size: "512mb" # update for your cameras based on calculation above + devices: + - /dev/bus/usb:/dev/bus/usb # Passes the USB Coral, needs to be modified for other versions + - /dev/apex_0:/dev/apex_0 # Passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux + - /dev/video11:/dev/video11 # For Raspberry Pi 4B + - /dev/dri/renderD128:/dev/dri/renderD128 # AMD / Intel GPU, needs to be updated for your hardware + - /dev/accel:/dev/accel # Intel NPU + volumes: + - /etc/localtime:/etc/localtime:ro + - /path/to/your/config:/config + - /path/to/your/storage:/media/frigate + - type: tmpfs # Recommended: 1GB of memory + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "8971:8971" + # - "5000:5000" # Internal unauthenticated access. Expose carefully. + - "8554:8554" # RTSP feeds + - "8555:8555/tcp" # WebRTC over tcp + - "8555:8555/udp" # WebRTC over udp + environment: + FRIGATE_RTSP_PASSWORD: "password" +``` + +If you can't use Docker Compose, you can run the container with something similar to this: + +```bash +docker run -d \ + --name frigate \ + --restart=unless-stopped \ + --stop-timeout 30 \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --device /dev/bus/usb:/dev/bus/usb \ + --device /dev/dri/renderD128 \ + --shm-size=64m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -e FRIGATE_RTSP_PASSWORD='password' \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + ghcr.io/blakeblackshear/frigate:stable +``` + +The official docker image tags for the current stable version are: + +- `stable` - Standard Frigate build for amd64 & RPi Optimized Frigate build for arm64. This build includes support for Hailo devices as well. +- `stable-standard-arm64` - Standard Frigate build for arm64 +- `stable-tensorrt` - Frigate build specific for amd64 devices running an nvidia GPU +- `stable-rocm` - Frigate build for [AMD GPUs](../configuration/object_detectors.md#amdrocm-gpu-detector) + +The community supported docker image tags for the current stable version are: + +- `stable-tensorrt-jp6` - Frigate build optimized for nvidia Jetson devices running Jetpack 6 +- `stable-rk` - Frigate build for SBCs with Rockchip SoC + +## Home Assistant Add-on + +:::warning + +As of Home Assistant Operating System 10.2 and Home Assistant 2023.6 defining separate network storage for media is supported. + +There are important limitations in HA OS to be aware of: + +- Separate local storage for media is not yet supported by Home Assistant +- AMD GPUs are not supported because HA OS does not include the mesa driver. +- Nvidia GPUs are not supported because addons do not support the nvidia runtime. + +::: + +:::tip + +See [the network storage guide](/guides/ha_network_storage.md) for instructions to setup network storage for frigate. + +::: + +Home Assistant OS users can install via the Add-on repository. + +1. In Home Assistant, navigate to _Settings_ > _Add-ons_ > _Add-on Store_ > _Repositories_ +2. Add `https://github.com/blakeblackshear/frigate-hass-addons` +3. Install the desired variant of the Frigate Add-on (see below) +4. Setup your network configuration in the `Configuration` tab +5. Start the Add-on +6. Use the _Open Web UI_ button to access the Frigate UI, then click in the _cog icon_ > _Configuration editor_ and configure Frigate to your liking + +There are several variants of the Add-on available: + +| Add-on Variant | Description | +| -------------------------- | ---------------------------------------------------------- | +| Frigate | Current release with protection mode on | +| Frigate (Full Access) | Current release with the option to disable protection mode | +| Frigate Beta | Beta release with protection mode on | +| Frigate Beta (Full Access) | Beta release with the option to disable protection mode | + +If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the Add-on. This is because the Frigate Add-on runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system. + +You can also edit the Frigate configuration file through the [VS Code Add-on](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs//config.yml`, where `` is specific to the variant of the Frigate Add-on you are running. See the list of directories [here](../configuration/index.md#accessing-add-on-config-dir). + +## Kubernetes + +Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate). + +## Unraid + +Many people have powerful enough NAS devices or home servers to also run docker. There is a Unraid Community App. +To install make sure you have the [community app plugin here](https://forums.unraid.net/topic/38582-plug-in-community-applications/). Then search for "Frigate" in the apps section within Unraid - you can see the online store [here](https://unraid.net/community/apps?q=frigate#r) + +## Proxmox + +[According to Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_pct) it is recommended that you run application containers like Frigate inside a Proxmox QEMU VM. This will give you all the advantages of application containerization, while also providing the benefits that VMs offer, such as strong isolation from the host and the ability to live-migrate, which otherwise isn’t possible with containers. Ensure that ballooning is **disabled**, especially if you are passing through a GPU to the VM. + +:::warning + +If you choose to run Frigate via LXC in Proxmox the setup can be complex so be prepared to read the Proxmox and LXC documentation, Frigate does not officially support running inside of an LXC. + +::: + +Suggestions include: + +- For Intel-based hardware acceleration, to allow access to the `/dev/dri/renderD128` device with major number 226 and minor number 128, add the following lines to the `/etc/pve/lxc/.conf` LXC configuration: + - `lxc.cgroup2.devices.allow: c 226:128 rwm` + - `lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file` +- The LXC configuration will likely also need `features: fuse=1,nesting=1`. This allows running a Docker container in an LXC container (`nesting`) and prevents duplicated files and wasted storage (`fuse`). +- Successfully passing hardware devices through multiple levels of containerization (LXC then Docker) can be difficult. Many people make devices like `/dev/dri/renderD128` world-readable in the host or run Frigate in a privileged LXC container. +- The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices, but [not in all circumstances](https://github.com/blakeblackshear/frigate/discussions/1837). + +See the [Proxmox LXC discussion](https://github.com/blakeblackshear/frigate/discussions/5773) for more general information. + +## ESXi + +For details on running Frigate using ESXi, please see the instructions [here](https://williamlam.com/2023/05/frigate-nvr-with-coral-tpu-igpu-passthrough-using-esxi-on-intel-nuc.html). + +If you're running Frigate on a rack mounted server and want to passthrough the Google Coral, [read this.](https://github.com/blakeblackshear/frigate/issues/305) + +## Synology NAS on DSM 7 + +These settings were tested on DSM 7.1.1-42962 Update 4 + +**General:** + +The `Execute container using high privilege` option needs to be enabled in order to give the frigate container the elevated privileges it may need. + +The `Enable auto-restart` option can be enabled if you want the container to automatically restart whenever it improperly shuts down due to an error. + +![image](https://user-images.githubusercontent.com/4516296/232586790-0b659a82-561d-4bc5-899b-0f5b39c6b11d.png) + +**Advanced Settings:** + +If you want to use the password template feature, you should add the "FRIGATE_RTSP_PASSWORD" environment variable and set it to your preferred password under advanced settings. The rest of the environment variables should be left as default for now. + +![image](https://user-images.githubusercontent.com/4516296/232587163-0eb662d4-5e28-4914-852f-9db1ec4b9c3d.png) + +**Port Settings:** + +The network mode should be set to `bridge`. You need to map the default frigate container ports to your local Synology NAS ports that you want to use to access Frigate. + +There may be other services running on your NAS that are using the same ports that frigate uses. In that instance you can set the ports to auto or a specific port. + +![image](https://user-images.githubusercontent.com/4516296/232582642-773c0e37-7ef5-4373-8ce3-41401b1626e6.png) + +**Volume Settings:** + +You need to configure 2 paths: + +- The location of your config directory which will be different depending on your NAS folder structure e.g. `/docker/frigate/config` will mount to `/config` within the container. +- The location on your NAS where the recordings will be saved this needs to be a folder e.g. `/docker/volumes/frigate-0-media` + +![image](https://user-images.githubusercontent.com/4516296/232585872-44431d15-55e0-4004-b78b-1e512702b911.png) + +## QNAP NAS + +These instructions were tested on a QNAP with an Intel J3455 CPU and 16G RAM, running QTS 4.5.4.2117. + +QNAP has a graphic tool named Container Station to install and manage docker containers. However, there are two limitations with Container Station that make it unsuitable to install Frigate: + +1. Container Station does not incorporate GitHub Container Registry (ghcr), which hosts Frigate docker image version 0.12.0 and above. +2. Container Station uses default 64 Mb shared memory size (shm-size), and does not have a mechanism to adjust it. Frigate requires a larger shm-size to be able to work properly with more than two high resolution cameras. + +Because of above limitations, the installation has to be done from command line. Here are the steps: + +**Preparation** + +1. Install Container Station from QNAP App Center if it is not installed. +2. Enable ssh on your QNAP (please do an Internet search on how to do this). +3. Prepare Frigate config file, name it `config.yml`. +4. Calculate shared memory size according to [documentation](https://docs.frigate.video/frigate/installation). +5. Find your time zone value from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +6. ssh to QNAP. + +**Installation** + +Run the following commands to install Frigate (using `stable` version as example): + +```shell +# Download Frigate image +docker pull ghcr.io/blakeblackshear/frigate:stable +# Create directory to host Frigate config file on QNAP file system. +# E.g., you can choose to create it under /share/Container. +mkdir -p /share/Container/frigate/config +# Copy the config file prepared in step 2 into the newly created config directory. +cp path/to/your/config/file /share/Container/frigate/config +# Create directory to host Frigate media files on QNAP file system. +# (if you have a surveillance disk, create media directory on the surveillance disk. +# Example command assumes share_vol2 is the surveillance drive +mkdir -p /share/share_vol2/frigate/media +# Create Frigate docker container. Replace shm-size value with the value from preparation step 3. +# Also replace the time zone value for 'TZ' in the sample command. +# Example command will create a docker container that uses at most 2 CPUs and 4G RAM. +# You may need to add "--env=LIBVA_DRIVER_NAME=i965 \" to the following docker run command if you +# have certain CPU (e.g., J4125). See https://docs.frigate.video/configuration/hardware_acceleration_video. +docker run \ + --name=frigate \ + --shm-size=256m \ + --restart=unless-stopped \ + --env=TZ=America/New_York \ + --volume=/share/Container/frigate/config:/config:rw \ + --volume=/share/share_vol2/frigate/media:/media/frigate:rw \ + --network=bridge \ + --privileged \ + --workdir=/opt/frigate \ + -p 8971:8971 \ + -p 8554:8554 \ + -p 8555:8555 \ + -p 8555:8555/udp \ + --label='com.qnap.qcs.network.mode=nat' \ + --label='com.qnap.qcs.gpu=False' \ + --memory="4g" \ + --cpus="2" \ + --detach=true \ + -t \ + ghcr.io/blakeblackshear/frigate:stable +``` + +Log into QNAP, open Container Station. Frigate docker container should be listed under 'Overview' and running. Visit Frigate Web UI by clicking Frigate docker, and then clicking the URL shown at the top of the detail page. diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/planning_setup.md b/sam2-cpu/frigate-dev/docs/docs/frigate/planning_setup.md new file mode 100644 index 0000000..cddd502 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/planning_setup.md @@ -0,0 +1,74 @@ +--- +id: planning_setup +title: Planning a New Installation +--- + +Choosing the right hardware for your Frigate NVR setup is important for optimal performance and a smooth experience. This guide will walk you through the key considerations, focusing on the number of cameras and the hardware required for efficient object detection. + +## Key Considerations + +### Number of Cameras and Simultaneous Activity + +The most fundamental factor in your hardware decision is the number of cameras you plan to use. However, it's not just about the raw count; it's also about how many of those cameras are likely to see activity and require object detection simultaneously. + +When motion is detected in a camera's feed, regions of that frame are sent to your chosen [object detection hardware](/configuration/object_detectors). + +- **Low Simultaneous Activity (1-6 cameras with occasional motion)**: If you have a few cameras in areas with infrequent activity (e.g., a seldom-used backyard, a quiet interior), the demand on your object detection hardware will be lower. A single, entry-level AI accelerator will suffice. +- **Moderate Simultaneous Activity (6-12 cameras with some overlapping motion)**: For setups with more cameras, especially in areas like a busy street or a property with multiple access points, it's more likely that several cameras will capture activity at the same time. This increases the load on your object detection hardware, requiring more processing power. +- **High Simultaneous Activity (12+ cameras or highly active zones)**: Large installations or scenarios where many cameras frequently capture activity (e.g., busy street with overview, identification, dedicated LPR cameras, etc.) will necessitate robust object detection capabilities. You'll likely need multiple entry-level AI accelerators or a more powerful single unit such as a discrete GPU. +- **Commercial Installations (40+ cameras)**: Commercial installations or scenarios where a substantial number of cameras capture activity (e.g., a commercial property, an active public space) will necessitate robust object detection capabilities. You'll likely need a modern discrete GPU. + +### Video Decoding + +Modern CPUs with integrated GPUs (Intel Quick Sync, AMD VCN) or dedicated GPUs can significantly offload video decoding from the main CPU, freeing up resources. This is highly recommended, especially for multiple cameras. + +:::tip + +For commercial installations it is important to verify the number of supported concurrent streams on your GPU, many consumer GPUs max out at ~20 concurrent camera streams. + +::: + +## Hardware Considerations + +### Object Detection + +There are many different hardware options for object detection depending on priorities and available hardware. See [the recommended hardware page](./hardware.md#detectors) for more specifics on what hardware is recommended for object detection. + +### Storage + +Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings. + + +#### SSDs (Solid State Drives) + +SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs. + +- Longevity: Modern SSDs are designed with advanced wear-leveling algorithms and significantly higher "Terabytes Written" (TBW) ratings than earlier models. For typical home NVR use, a good quality SSD will likely outlast the useful life of your NVR hardware itself. +- Performance: SSDs excel at handling the numerous small write operations that occur during continuous video recording and can significantly improve the responsiveness of the Frigate UI and clip retrieval. +- Silence and Efficiency: SSDs produce no noise and consume less power than traditional HDDs. + +#### HDDs (Hard Disk Drives) + +Traditional Hard Disk Drives (HDDs) remain a great and often more cost-effective option for long-term video storage, especially for larger setups where raw capacity is prioritized. + +- Cost-Effectiveness: HDDs offer the best cost per gigabyte, making them ideal for storing many days, weeks, or months of continuous footage. +- Capacity: HDDs are available in much larger capacities than most consumer SSDs, which is beneficial for extensive video archives. +- NVR-Rated Drives: If choosing an HDD, consider drives specifically designed for surveillance (NVR) use, such as Western Digital Purple or Seagate SkyHawk. These drives are engineered for 24/7 operation and continuous write workloads, offering improved reliability compared to standard desktop drives. + +Determining Your Storage Needs +The amount of storage you need will depend on several factors: + +- Number of Cameras: More cameras naturally require more space. +- Resolution and Framerate: Higher resolution (e.g., 4K) and higher framerate (e.g., 30fps) streams consume significantly more storage. +- Recording Method: Continuous recording uses the most space. motion-only recording or object-triggered recording can save space, but may miss some footage. +- Retention Period: How many days, weeks, or months of footage do you want to keep? + +#### Network Storage (NFS/SMB) + +While supported, using network-attached storage (NAS) for recordings can introduce latency and network dependency considerations. For optimal performance and reliability, it is generally recommended to have local storage for your Frigate recordings. If using a NAS, ensure your network connection to it is robust and fast (Gigabit Ethernet at minimum) and that the NAS itself can handle the continuous write load. + +### RAM (Memory) + +- **Basic Minimum: 4GB RAM**: This is generally sufficient for a very basic Frigate setup with a few cameras and a dedicated object detection accelerator, without running any enrichments. Performance might be tight, especially with higher resolution streams or numerous detections. +- **Minimum for Enrichments: 8GB RAM**: If you plan to utilize Frigate's enrichment features (e.g., facial recognition, license plate recognition, or other AI models that run alongside standard object detection), 8GB of RAM should be considered the minimum. Enrichments require additional memory to load and process their respective models and data. +- **Recommended: 16GB RAM**: For most users, especially those with many cameras (8+) or who plan to heavily leverage enrichments, 16GB of RAM is highly recommended. This provides ample headroom for smooth operation, reduces the likelihood of swapping to disk (which can impact performance), and allows for future expansion. \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/updating.md b/sam2-cpu/frigate-dev/docs/docs/frigate/updating.md new file mode 100644 index 0000000..d95ae83 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/updating.md @@ -0,0 +1,119 @@ +--- +id: updating +title: Updating +--- + +# Updating Frigate + +The current stable version of Frigate is **0.16.2**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.16.2). + +Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant Addon, etc.). Below are instructions for the most common setups. + +## Before You Begin + +- **Stop Frigate**: For most methods, you’ll need to stop the running Frigate instance before backing up and updating. +- **Backup Your Configuration**: Always back up your `/config` directory (e.g., `config.yml` and `frigate.db`, the SQLite database) before updating. This ensures you can roll back if something goes wrong. +- **Check Release Notes**: Carefully review the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases) for breaking changes or configuration updates that might affect your setup. + +## Updating with Docker + +If you’re running Frigate via Docker (recommended method), follow these steps: + +1. **Stop the Container**: + + - If using Docker Compose: + ```bash + docker compose down frigate + ``` + - If using `docker run`: + ```bash + docker stop frigate + ``` + +2. **Update and Pull the Latest Image**: + + - If using Docker Compose: + - Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.16.2` instead of `0.15.2`). For example: + ```yaml + services: + frigate: + image: ghcr.io/blakeblackshear/frigate:0.16.2 + ``` + - Then pull the image: + ```bash + docker pull ghcr.io/blakeblackshear/frigate:0.16.2 + ``` + - **Note for `stable` Tag Users**: If your `docker-compose.yml` uses the `stable` tag (e.g., `ghcr.io/blakeblackshear/frigate:stable`), you don’t need to update the tag manually. The `stable` tag always points to the latest stable release after pulling. + - If using `docker run`: + - Pull the image with the appropriate tag (e.g., `0.16.2`, `0.16.2-tensorrt`, or `stable`): + ```bash + docker pull ghcr.io/blakeblackshear/frigate:0.16.2 + ``` + +3. **Start the Container**: + + - If using Docker Compose: + ```bash + docker compose up -d + ``` + - If using `docker run`, re-run your original command (e.g., from the [Installation](./installation.md#docker) section) with the updated image tag. + +4. **Verify the Update**: + - Check the container logs to ensure Frigate starts successfully: + ```bash + docker logs frigate + ``` + - Visit the Frigate Web UI (default: `http://:5000`) to confirm the new version is running. The version number is displayed at the top of the System Metrics page. + +### Notes + +- If you’ve customized other settings (e.g., `shm-size`), ensure they’re still appropriate after the update. +- Docker will automatically use the updated image when you restart the container, as long as you pulled the correct version. + +## Updating the Home Assistant Addon + +For users running Frigate as a Home Assistant Addon: + +1. **Check for Updates**: + + - Navigate to **Settings > Add-ons** in Home Assistant. + - Find your installed Frigate addon (e.g., "Frigate NVR" or "Frigate NVR (Full Access)"). + - If an update is available, you’ll see an "Update" button. + +2. **Update the Addon**: + + - Click the "Update" button next to the Frigate addon. + - Wait for the process to complete. Home Assistant will handle downloading and installing the new version. + +3. **Restart the Addon**: + + - After updating, go to the addon’s page and click "Restart" to apply the changes. + +4. **Verify the Update**: + - Check the addon logs (under the "Log" tab) to ensure Frigate starts without errors. + - Access the Frigate Web UI to confirm the new version is running. + +### Notes + +- Ensure your `/config/frigate.yml` is compatible with the new version by reviewing the [Release notes](https://github.com/blakeblackshear/frigate/releases). +- If using custom hardware (e.g., Coral or GPU), verify that configurations still work, as addon updates don’t modify your hardware settings. + +## Rolling Back + +If an update causes issues: + +1. Stop Frigate. +2. Restore your backed-up config file and database. +3. Revert to the previous image version: + - For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.15.2`) in your `docker run` command. + - For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.15.2`), and re-run `docker compose up -d`. + - For Home Assistant: Reinstall the previous addon version manually via the repository if needed and restart the addon. +4. Verify the old version is running again. + +## Troubleshooting + +- **Container Fails to Start**: Check logs (`docker logs frigate`) for errors. +- **UI Not Loading**: Ensure ports (e.g., 5000, 8971) are still mapped correctly and the service is running. +- **Hardware Issues**: Revisit hardware-specific setup (e.g., Coral, GPU) if detection or decoding fails post-update. + +Common questions are often answered in the [FAQ](https://github.com/blakeblackshear/frigate/discussions), pinned at the top of the support discussions. diff --git a/sam2-cpu/frigate-dev/docs/docs/frigate/video_pipeline.md b/sam2-cpu/frigate-dev/docs/docs/frigate/video_pipeline.md new file mode 100644 index 0000000..ba93656 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/frigate/video_pipeline.md @@ -0,0 +1,67 @@ +--- +id: video_pipeline +title: Video pipeline +--- + +Frigate uses a sophisticated video pipeline that starts with the camera feed and progressively applies transformations to it (e.g. decoding, motion detection, etc.). + +This guide provides an overview to help users understand some of the key Frigate concepts. + +## Overview + +At a high level, there are five processing steps that could be applied to a camera feed + +```mermaid +%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%% + +flowchart LR + Feed(Feed acquisition) --> Decode(Video decoding) + Decode --> Motion(Motion detection) + Motion --> Object(Object detection) + Feed --> Recording(Recording and visualization) + Motion --> Recording + Object --> Recording +``` + +As the diagram shows, all feeds first need to be acquired. Depending on the data source, it may be as simple as using FFmpeg to connect to an RTSP source via TCP or something more involved like connecting to an Apple Homekit camera using go2rtc. A single camera can produce a main (i.e. high resolution) and a sub (i.e. lower resolution) video feed. + +Typically, the sub-feed will be decoded to produce full-frame images. As part of this process, the resolution may be downscaled and an image sampling frequency may be imposed (e.g. keep 5 frames per second). + +These frames will then be compared over time to detect movement areas (a.k.a. motion boxes). These motion boxes are combined into motion regions and are analyzed by a machine learning model to detect known objects. Finally, the snapshot and recording retention config will decide what video clips and events should be saved. + +## Detailed view of the video pipeline + +The following diagram adds a lot more detail than the simple view explained before. The goal is to show the detailed data paths between the processing steps. + +```mermaid +%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%% + +flowchart TD + RecStore[(Recording\nstore)] + SnapStore[(Snapshot\nstore)] + + subgraph Acquisition + Cam["Camera"] -->|FFmpeg supported| Stream + Cam -->|"Other streaming\nprotocols"| go2rtc + go2rtc("go2rtc") --> Stream + Stream[Capture main and\nsub streams] --> |detect stream|Decode(Decode and\ndownscale) + end + subgraph Motion + Decode --> MotionM(Apply\nmotion masks) + MotionM --> MotionD(Motion\ndetection) + end + subgraph Detection + MotionD --> |motion regions| ObjectD(Object detection) + Decode --> ObjectD + ObjectD --> ObjectFilter(Apply object filters & zones) + ObjectFilter --> ObjectZ(Track objects) + end + Decode --> |decoded frames|Birdseye + MotionD --> |motion event|Birdseye + ObjectZ --> |object event|Birdseye + + MotionD --> |"video segments\n(retain motion)"|RecStore + ObjectZ --> |detection clip|RecStore + Stream -->|"video segments\n(retain all)"| RecStore + ObjectZ --> |detection snapshot|SnapStore +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/guides/configuring_go2rtc.md b/sam2-cpu/frigate-dev/docs/docs/guides/configuring_go2rtc.md new file mode 100644 index 0000000..ca50a90 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/guides/configuring_go2rtc.md @@ -0,0 +1,120 @@ +--- +id: configuring_go2rtc +title: Configuring go2rtc +--- + +Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect directly to your cameras. However, adding go2rtc to your configuration is required for the following features: + +- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio +- Live stream support for cameras in Home Assistant Integration +- RTSP relay for use with other consumers to reduce the number of connections to your camera streams + +## Setup a go2rtc stream + +First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#module-streams), not just rtsp. + +:::tip + +For the best experience, you should set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera. + +See [the live view docs](../configuration/live.md#setting-stream-for-live-ui) for more information. + +::: + +```yaml +go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 +``` + +After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream. + +### What if my video doesn't play? + +- Check Logs: + + - Access the go2rtc logs in the Frigate UI under Logs in the sidebar. + - If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. + +- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface. + + - Navigate to port 1984 in your browser to access go2rtc's web interface. + - If using Frigate through Home Assistant, enable the web interface at port 1984. + - If using Docker, forward port 1984 before accessing the web interface. + - Click `stream` for the specific camera to see if the camera's stream is being received. + +- Check Video Codec: + + - If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported. + - If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#codecs-madness) in go2rtc documentation. + - If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#hardware" + ``` + +- Switch to FFmpeg if needed: + + - Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types. + + ```yaml + go2rtc: + streams: + back: + - ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + ``` + + - If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC. + - If possible, update your camera's audio settings to AAC in your camera's firmware. + - If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows: + + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#audio=aac" + ``` + + If you need to convert **both** the audio and video streams, you can use the following: + + ```yaml + go2rtc: + streams: + back: + - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 + - "ffmpeg:back#video=h264#audio=aac#hardware" + ``` + + When using the ffmpeg module, you would add AAC audio like this: + + ```yaml + go2rtc: + streams: + back: + - "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware" + ``` + +:::warning + +To access the go2rtc stream externally when utilizing the Frigate Add-On (for +instance through VLC), you must first enable the RTSP Restream port. +You can do this by visiting the Frigate Add-On configuration page within Home +Assistant and revealing the hidden options under the "Show disabled ports" +section. + +::: + +### Next steps + +1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera). +2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router. +3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation. + +## Homekit Configuration + +To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`. diff --git a/sam2-cpu/frigate-dev/docs/docs/guides/getting_started.md b/sam2-cpu/frigate-dev/docs/docs/guides/getting_started.md new file mode 100644 index 0000000..89176ad --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/guides/getting_started.md @@ -0,0 +1,318 @@ +--- +id: getting_started +title: Getting started +--- + +# Getting Started + +:::tip + +If you already have an environment with Linux and Docker installed, you can continue to [Installing Frigate](#installing-frigate) below. + +If you already have Frigate installed through Docker or through a Home Assistant Add-on, you can continue to [Configuring Frigate](#configuring-frigate) below. + +::: + +## Setting up hardware + +This section guides you through setting up a server with Debian Bookworm and Docker. + +### Install Debian 12 (Bookworm) + +There are many guides on how to install Debian Server, so this will be an abbreviated guide. Connect a temporary monitor and keyboard to your device so you can install a minimal server without a desktop environment. + +#### Prepare installation media + +1. Download the small installation image from the [Debian website](https://www.debian.org/distrib/netinst) +1. Flash the ISO to a USB device (popular tool is [balena Etcher](https://etcher.balena.io/)) +1. Boot your device from USB + +#### Install and setup Debian for remote access + +1. Ensure your device is connected to the network so updates and software options can be installed +1. Choose the non-graphical install option if you don't have a mouse connected, but either install method works fine +1. You will be prompted to set the root user password and create a user with a password +1. Install the minimum software. Fewer dependencies result in less maintenance. + 1. Uncheck "Debian desktop environment" and "GNOME" + 1. Check "SSH server" + 1. Keep "standard system utilities" checked +1. After reboot, login as root at the command prompt to add user to sudoers + 1. Install sudo + ```bash + apt update && apt install -y sudo + ``` + 1. Add the user you created to the sudo group (change `blake` to your own user) + ```bash + usermod -aG sudo blake + ``` +1. Shutdown by running `poweroff` + +At this point, you can install the device in a permanent location. The remaining steps can be performed via SSH from another device. If you don't have an SSH client, you can install one of the options listed in the [Visual Studio Code documentation](https://code.visualstudio.com/docs/remote/troubleshooting#_installing-a-supported-ssh-client). + +#### Finish setup via SSH + +1. Connect via SSH and login with your non-root user created during install +1. Setup passwordless sudo so you don't have to type your password for each sudo command (change `blake` in the command below to your user) + + ```bash + echo 'blake ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/user + ``` + +1. Logout and login again to activate passwordless sudo +1. Setup automatic security updates for the OS (optional) + 1. Ensure everything is up to date by running + ```bash + sudo apt update && sudo apt upgrade -y + ``` + 1. Install unattended upgrades + ```bash + sudo apt install -y unattended-upgrades + echo unattended-upgrades unattended-upgrades/enable_auto_updates boolean true | sudo debconf-set-selections + sudo dpkg-reconfigure -f noninteractive unattended-upgrades + ``` + +Now you have a minimal Debian server that requires very little maintenance. + +### Install Docker + +1. Install Docker Engine (not Docker Desktop) using the [official docs](https://docs.docker.com/engine/install/debian/) + 1. Specifically, follow the steps in the [Install using the apt repository](https://docs.docker.com/engine/install/debian/#install-using-the-repository) section +2. Add your user to the docker group as described in the [Linux postinstall steps](https://docs.docker.com/engine/install/linux-postinstall/) + +## Installing Frigate + +This section shows how to create a minimal directory structure for a Docker installation on Debian. If you have installed Frigate as a Home Assistant Add-on or another way, you can continue to [Configuring Frigate](#configuring-frigate). + +### Setup directories + +Frigate will create a config file if one does not exist on the initial startup. The following directory structure is the bare minimum to get started. Once Frigate is running, you can use the built-in config editor which supports config validation. + +``` +. +├── docker-compose.yml +├── config/ +└── storage/ +``` + +This will create the above structure: + +```bash +mkdir storage config && touch docker-compose.yml +``` + +If you are setting up Frigate on a Linux device via SSH, you can use [nano](https://itsfoss.com/nano-editor-guide/) to edit the following files. If you prefer to edit remote files with a full editor instead of a terminal, I recommend using [Visual Studio Code](https://code.visualstudio.com/) with the [Remote SSH extension](https://code.visualstudio.com/docs/remote/ssh-tutorial). + +:::note + +This `docker-compose.yml` file is just a starter for amd64 devices. You will need to customize it for your setup as detailed in the [Installation docs](/frigate/installation#docker). + +::: +`docker-compose.yml` + +```yaml +services: + frigate: + container_name: frigate + restart: unless-stopped + stop_grace_period: 30s + image: ghcr.io/blakeblackshear/frigate:stable + volumes: + - ./config:/config + - ./storage:/media/frigate + - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "8971:8971" + - "8554:8554" # RTSP feeds +``` + +Now you should be able to start Frigate by running `docker compose up -d` from within the folder containing `docker-compose.yml`. On startup, an admin user and password will be created and outputted in the logs. You can see this by running `docker logs frigate`. Frigate should now be accessible at `https://server_ip:8971` where you can login with the `admin` user and finish the configuration using the built-in configuration editor. + +## Configuring Frigate + +This section assumes that you already have an environment setup as described in [Installation](../frigate/installation.md). You should also configure your cameras according to the [camera setup guide](/frigate/camera_setup). Pay particular attention to the section on choosing a detect resolution. + +### Step 1: Add a detect stream + +First we will add the detect stream for the camera: + +```yaml +mqtt: + enabled: False + +cameras: + name_of_your_camera: # <------ Name the camera + enabled: True + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection + roles: + - detect +``` + +### Step 2: Start Frigate + +At this point you should be able to start Frigate and see the video feed in the UI. + +If you get an error image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with H264 RTSP cameras that support TCP connections. + +FFmpeg arguments for other types of cameras can be found [here](../configuration/camera_specific.md). + +### Step 3: Configure hardware acceleration (recommended) + +Now that you have a working camera configuration, you want to setup hardware acceleration to minimize the CPU required to decode your video streams. See the [hardware acceleration](../configuration/hardware_acceleration_video.md) config reference for examples applicable to your hardware. + +Here is an example configuration with hardware acceleration configured to work with most Intel processors with an integrated GPU using the [preset](../configuration/ffmpeg_presets.md): + +`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes) + +```yaml +services: + frigate: + ... + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware + ... +``` + +`config.yml` + +```yaml +mqtt: ... + +cameras: + name_of_your_camera: + ffmpeg: + inputs: ... + hwaccel_args: preset-vaapi + detect: ... +``` + +### Step 4: Configure detectors + +By default, Frigate will use a single CPU detector. If you have a USB Coral, you will need to add a detectors section to your config. + +`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes) + +```yaml +services: + frigate: + ... + devices: + - /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions + - /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux + ... +``` + +```yaml +mqtt: ... + +detectors: # <---- add detectors + coral: + type: edgetpu + device: usb + +cameras: + name_of_your_camera: + ffmpeg: ... + detect: + enabled: True # <---- turn on detection + ... +``` + +More details on available detectors can be found [here](../configuration/object_detectors.md). + +Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they will need to be added according to the [configuration file reference](../configuration/reference.md). + +### Step 5: Setup motion masks + +Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. To do this, navigate to the camera in the UI, select "Debug" at the top, and enable "Motion boxes" in the options below the video feed. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas. + +Now that you know where you need to mask, use the "Mask & Zone creator" in the options pane to generate the coordinates needed for your config file. More information about masks can be found [here](../configuration/masks.md). + +:::warning + +Note that motion masks should not be used to mark out areas where you do not want objects to be detected or to reduce false positives. They do not alter the image sent to object detection, so you can still get tracked objects, alerts, and detections in areas with motion masks. These only prevent motion in these areas from initiating object detection. + +::: + +Your configuration should look similar to this now. + +```yaml +mqtt: + enabled: False + +detectors: + coral: + type: edgetpu + device: usb + +cameras: + name_of_your_camera: + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + motion: + mask: + - 0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432 +``` + +### Step 6: Enable recordings + +In order to review activity in the Frigate UI, recordings need to be enabled. + +To enable recording video, add the `record` role to a stream and enable it in the config. If record is disabled in the config, it won't be possible to enable it in the UI. + +```yaml +mqtt: ... + +detectors: ... + +cameras: + name_of_your_camera: + ffmpeg: + inputs: + - path: rtsp://10.0.10.10:554/rtsp + roles: + - detect + - path: rtsp://10.0.10.10:554/high_res_stream # <----- Add stream you want to record from + roles: + - record + detect: ... + record: # <----- Enable recording + enabled: True + motion: ... +``` + +If you don't have separate streams for detect and record, you would just add the record role to the list on the first input. + +:::note + +If you only define one stream in your `inputs` and do not assign a `detect` role to it, Frigate will automatically assign it the `detect` role. Frigate will always decode a stream to support motion detection, Birdseye, the API image endpoints, and other features, even if you have disabled object detection with `enabled: False` in your config's `detect` section. + +If you only plan to use Frigate for recording, it is still recommended to define a `detect` role for a low resolution stream to minimize resource usage from the required stream decoding. + +::: + +By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/reference.md). + +### Step 7: Complete config + +At this point you have a complete config with basic functionality. + +- View [common configuration examples](../configuration/index.md#common-configuration-examples) for a list of common configuration examples. +- View [full config reference](../configuration/reference.md) for a complete list of configuration options. + +### Follow up + +Now that you have a working install, you can use the following documentation for additional features: + +1. [Configuring go2rtc](configuring_go2rtc.md) - Additional live view options and RTSP relay +2. [Zones](../configuration/zones.md) +3. [Review](../configuration/review.md) +4. [Masks](../configuration/masks.md) +5. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant diff --git a/sam2-cpu/frigate-dev/docs/docs/guides/ha_network_storage.md b/sam2-cpu/frigate-dev/docs/docs/guides/ha_network_storage.md new file mode 100644 index 0000000..78cdddd --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/guides/ha_network_storage.md @@ -0,0 +1,40 @@ +--- +id: ha_network_storage +title: Home Assistant network storage +--- + +As of Home Assistant 2023.6, Network Mounted Storage is supported for Add-ons. + +## Setting Up Remote Storage For Frigate + +### Prerequisites + +- Home Assistant 2023.6 or newer is installed +- Running Home Assistant Operating System 10.2 or newer OR Running Supervised with latest os-agent installed (this is required for supervised install) + +### Initial Setup + +1. Stop the Frigate Add-on + +### Move current data + +Keeping the current data is optional, but the data will need to be moved regardless so the share can be created successfully. + +#### If you want to keep the current data + +1. Move the frigate.db, frigate.db-shm, frigate.db-wal files to the /config directory +2. Rename the /media/frigate folder to /media/frigate_tmp + +#### If you don't want to keep the current data + +1. Delete the /media/frigate folder and all of its contents + +### Create the media share + +1. Go to **Settings -> System -> Storage -> Add Network Storage** +2. Name the share `frigate` (this is required) +3. Choose type `media` +4. Fill out the additional required info for your particular NAS +5. Connect +6. Move files from `/media/frigate_tmp` to `/media/frigate` if they were kept in previous step +7. Start the Frigate Add-on diff --git a/sam2-cpu/frigate-dev/docs/docs/guides/ha_notifications.md b/sam2-cpu/frigate-dev/docs/docs/guides/ha_notifications.md new file mode 100644 index 0000000..a92dab1 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/guides/ha_notifications.md @@ -0,0 +1,50 @@ +--- +id: ha_notifications +title: Home Assistant notifications +--- + +The best way to get started with notifications for Frigate is to use the [Blueprint](https://community.home-assistant.io/t/frigate-mobile-app-notifications-2-0/559732). You can use the yaml generated from the Blueprint as a starting point and customize from there. + +It is generally recommended to trigger notifications based on the `frigate/reviews` mqtt topic. This provides the event_id(s) needed to fetch [thumbnails/snapshots/clips](../integrations/home-assistant.md#notification-api) and other useful information to customize when and where you want to receive alerts. The data is published in the form of a change feed, which means you can reference the "previous state" of the object in the `before` section and the "current state" of the object in the `after` section. You can see an example [here](../integrations/mqtt.md#frigateevents). + +Here is a simple example of a notification automation of tracked objects which will update the existing notification for each change. This means the image you see in the notification will update as Frigate finds a "better" image. + +```yaml +automation: + - alias: Notify of tracked object + trigger: + platform: mqtt + topic: frigate/events + action: + - service: notify.mobile_app_pixel_3 + data: + message: 'A {{trigger.payload_json["after"]["label"]}} was detected.' + data: + image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android' + tag: '{{trigger.payload_json["after"]["id"]}}' + when: '{{trigger.payload_json["after"]["start_time"]|int}}' +``` + +Note that iOS devices support live previews of cameras by adding a camera entity id to the message data. + +```yaml +automation: + - alias: Security_Frigate_Notifications + description: "" + trigger: + - platform: mqtt + topic: frigate/reviews + payload: alert + value_template: "{{ value_json['after']['severity'] }}" + action: + - service: notify.mobile_app_iphone + data: + message: 'A {{trigger.payload_json["after"]["data"]["objects"] | sort | join(", ") | title}} was detected.' + data: + image: >- + https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["data"]["detections"][0]}}/thumbnail.jpg + tag: '{{trigger.payload_json["after"]["id"]}}' + when: '{{trigger.payload_json["after"]["start_time"]|int}}' + entity_id: camera.{{trigger.payload_json["after"]["camera"] | replace("-","_") | lower}} + mode: single +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/guides/reverse_proxy.md b/sam2-cpu/frigate-dev/docs/docs/guides/reverse_proxy.md new file mode 100644 index 0000000..5edfb5c --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/guides/reverse_proxy.md @@ -0,0 +1,211 @@ +--- +id: reverse_proxy +title: Setting up a reverse proxy +--- + +This guide outlines the basic configuration steps needed to set up a reverse proxy in front of your Frigate instance. + +A reverse proxy is typically needed if you want to set up Frigate on a custom URL, on a subdomain, or on a host serving multiple sites. It could also be used to set up your own authentication provider or for more advanced HTTP routing. + +Before setting up a reverse proxy, check if any of the built-in functionality in Frigate suits your needs: +|Topic|Docs| +|-|-| +|TLS|Please see the `tls` [configuration option](../configuration/tls.md)| +|Authentication|Please see the [authentication](../configuration/authentication.md) documentation| +|IPv6|[Enabling IPv6](../configuration/advanced.md#enabling-ipv6) + +**Note about TLS** +When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port." +To disable TLS, set the following in your Frigate configuration: +```yml +tls: + enabled: false +``` + +:::warning +A reverse proxy can be used to secure access to an internal web server, but the user will be entirely reliant on the steps they have taken. You must ensure you are following security best practices. +This page does not attempt to outline the specific steps needed to secure your internal website. +Please use your own knowledge to assess and vet the reverse proxy software before you install anything on your system. +::: + +## Proxies + +There are many solutions available to implement reverse proxies and the community is invited to help out documenting others through a contribution to this page. + +* [Apache2](#apache2-reverse-proxy) +* [Nginx](#nginx-reverse-proxy) +* [Traefik](#traefik-reverse-proxy) +* [Caddy](#caddy-reverse-proxy) + +## Apache2 Reverse Proxy + +In the configuration examples below, only the directives relevant to the reverse proxy approach above are included. +On Debian Apache2 the configuration file will be named along the lines of `/etc/apache2/sites-available/cctv.conf` + +### Step 1: Configure the Apache2 Reverse Proxy + +Make life easier for yourself by presenting your Frigate interface as a DNS sub-domain rather than as a sub-folder of your main domain. +Here we access Frigate via https://cctv.mydomain.co.uk + +```xml + + ServerName cctv.mydomain.co.uk + + ProxyPreserveHost On + ProxyPass "/" "http://frigatepi.local:8971/" + ProxyPassReverse "/" "http://frigatepi.local:8971/" + + ProxyPass /ws ws://frigatepi.local:8971/ws + ProxyPassReverse /ws ws://frigatepi.local:8971/ws + + ProxyPass /live/ ws://frigatepi.local:8971/live/ + ProxyPassReverse /live/ ws://frigatepi.local:8971/live/ + + RewriteEngine on + RewriteCond %{HTTP:Upgrade} =websocket [NC] + RewriteRule /(.*) ws://frigatepi.local:8971/$1 [P,L] + RewriteCond %{HTTP:Upgrade} !=websocket [NC] + RewriteRule /(.*) http://frigatepi.local:8971/$1 [P,L] + +``` + +### Step 2: Use SSL to encrypt access to your Frigate instance + +Whilst this won't, on its own, prevent access to your Frigate webserver it will encrypt all content (such as login credentials). +Installing SSL is beyond the scope of this document but [Let's Encrypt](https://letsencrypt.org/) is a widely used approach. +This Apache2 configuration snippet then results in unencrypted requests being redirected to the webserver SSL port + +```xml + +ServerName cctv.mydomain.co.uk +RewriteEngine on +RewriteCond %{SERVER_NAME} =cctv.mydomain.co.uk +RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] + +``` + +### Step 3: Authenticate users at the proxy + +There are many ways to authenticate a website but a straightforward approach is to use [Apache2 password files](https://httpd.apache.org/docs/2.4/howto/auth.html). + +```xml + + + AuthType Basic + AuthName "Restricted Files" + AuthUserFile "/var/www/passwords" + Require user paul + + +``` + +## Nginx Reverse Proxy + +This method shows a working example for subdomain type reverse proxy with SSL enabled. + +### Setup server and port to reverse proxy + +This is set in `$server` and `$port` this should match your ports you have exposed to your docker container. Optionally you listen on port `443` and enable `SSL` + +``` +# ------------------------------------------------------------ +# frigate.domain.com +# ------------------------------------------------------------ + +server { + set $forward_scheme http; + set $server "192.168.100.2"; # FRIGATE SERVER LOCATION + set $port 8971; + + listen 80; + listen 443 ssl; + http2 on; + + server_name frigate.domain.com; +} +``` + +### Setup SSL (optional) + +This section points to your SSL files, the example below shows locations to a default Lets Encrypt SSL certificate. + +``` + # Let's Encrypt SSL + include conf.d/include/letsencrypt-acme-challenge.conf; + include conf.d/include/ssl-ciphers.conf; + ssl_certificate /etc/letsencrypt/live/npm-1/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/npm-1/privkey.pem; +``` + +### Setup reverse proxy settings + +The settings below enabled connection upgrade, sets up logging (optional) and proxies everything from the `/` context to the docker host and port specified earlier in the configuration + +``` + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + + access_log /data/logs/proxy-host-40_access.log proxy; + error_log /data/logs/proxy-host-40_error.log warn; + + location / { + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + } + +``` + +## Traefik Reverse Proxy + +This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance. +Before using the example below, you must first set up Traefik with the [Docker provider](https://doc.traefik.io/traefik/providers/docker/) + +```yml +services: + frigate: + container_name: frigate + image: ghcr.io/blakeblackshear/frigate:stable + ... + ... + labels: + - "traefik.enable=true" + - "traefik.http.services.frigate.loadbalancer.server.port=8971" + - "traefik.http.routers.frigate.rule=Host(`traefik.example.com`)" +``` + +The above configuration will create a "service" in Traefik, automatically adding your container's IP on port 8971 as a backend. +It will also add a router, routing requests to "traefik.example.com" to your local container. + +Note that with this approach, you don't need to expose any ports for the Frigate instance since all traffic will be routed over the internal Docker network. + +## Caddy Reverse Proxy + +This example shows Frigate running under a subdomain with logging and a tls cert (in this case a wildcard domain cert obtained independently of caddy) handled via imports + +```caddy +(logging) { + log { + output file /var/log/caddy/{args[0]}.log { + roll_size 10MiB + roll_keep 5 + roll_keep_for 10d + } + format json + level INFO + } +} + + +(tls) { + tls /var/lib/caddy/wildcard.YOUR_DOMAIN.TLD.fullchain.pem /var/lib/caddy/wildcard.YOUR_DOMAIN.TLD.privkey.pem +} + +frigate.YOUR_DOMAIN.TLD { + reverse_proxy http://localhost:8971 + import tls + import logging frigate.YOUR_DOMAIN.TLD +} + +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/api.md b/sam2-cpu/frigate-dev/docs/docs/integrations/api.md new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/home-assistant.md b/sam2-cpu/frigate-dev/docs/docs/integrations/home-assistant.md new file mode 100644 index 0000000..169a7ad --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/integrations/home-assistant.md @@ -0,0 +1,332 @@ +--- +id: home-assistant +title: Home Assistant Integration +--- + +The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). + +## Installation + +### Preparation + +The Frigate integration requires the `mqtt` integration to be installed and +manually configured first. + +See the [MQTT integration +documentation](https://www.home-assistant.io/integrations/mqtt/) for more +details. + +In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function. + +### Integration installation + +Available via HACS as a default repository. To install: + +- Use [HACS](https://hacs.xyz/) to install the integration: + +``` +Home Assistant > HACS > Click in the Search bar and type "Frigate" > Frigate +``` + +- Restart Home Assistant. +- Then add/configure the integration: + +``` +Home Assistant > Settings > Devices & Services > Add Integration > Frigate +``` + +Note: You will also need +[media_source](https://www.home-assistant.io/integrations/media_source/) enabled +in your Home Assistant configuration for the Media Browser to appear. + +### (Optional) Lovelace Card Installation + +To install the optional companion Lovelace card, please see the [separate +installation instructions](https://github.com/dermotduffy/frigate-hass-card) for +that card. + +## Configuration + +When configuring the integration, you will be asked for the `URL` of your Frigate instance which can be pointed at the internal unauthenticated port (`5000`) or the authenticated port (`8971`) for your instance. This may look like `http://:5000/`. + +### Docker Compose Examples + +If you are running Home Assistant and Frigate with Docker Compose on the same device, here are some examples. + +#### Home Assistant running with host networking + +It is not recommended to run Frigate in host networking mode. In this example, you would use `http://172.17.0.1:5000` or `http://172.17.0.1:8971` when configuring the integration. + +```yaml +services: + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + network_mode: host + ... + + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - "172.17.0.1:5000:5000" + ... +``` + +#### Home Assistant _not_ running with host networking or in a separate compose file + +In this example, it is recommended to connect to the authenticated port, for example, `http://frigate:8971` when configuring the integration. There is no need to map the port for the Frigate container. + +```yaml +services: + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + # network_mode: host + ... + + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + # - "172.17.0.1:5000:5000" + ... +``` + +### Home Assistant Add-on + +If you are using Home Assistant Add-on, the URL should be one of the following depending on which Add-on variant you are using. Note that if you are using the Proxy Add-on, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network. + +| Add-on Variant | URL | +| -------------------------- | ----------------------------------------- | +| Frigate | `http://ccab4aaf-frigate:5000` | +| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` | +| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` | +| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` | + +### Frigate running on a separate machine + +If you run Frigate on a separate device within your local network, Home Assistant will need access to port 8971. + +#### Local network + +Use `http://:8971` as the URL for the integration so that authentication is required. + +:::tip + +The above URL assumes you have [disabled TLS](../configuration/tls). +By default, TLS is enabled and Frigate will be using a self-signed certificate. HomeAssistant will fail to connect HTTPS to port 8971 since it fails to verify the self-signed certificate. +Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be acessible with a valid certificate. + +::: + +```yaml +services: + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - "8971:8971" + ... +``` + +#### Tailscale or other private networking + +Use `http://:5000` as the URL for the integration. + +```yaml +services: + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + ... + ports: + - ":5000:5000" + ... +``` + +## Options + +``` +Home Assistant > Configuration > Integrations > Frigate > Options +``` + +| Option | Description | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| RTSP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTSP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTSP streams](#rtsp-stream) below. | + +## Entities Provided + +| Platform | Description | +| --------------- | ------------------------------------------------------------------------------- | +| `camera` | Live camera stream (requires RTSP). | +| `image` | Image of the latest detected object for each camera. | +| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. | +| `switch` | Switch entities to toggle detection, recordings and snapshots. | +| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. | + +## Media Browser Support + +The integration provides: + +- Browsing tracked object recordings with thumbnails +- Browsing snapshots +- Browsing recordings by month, day, camera, time + +This is accessible via "Media Browser" on the left menu panel in Home Assistant. + +## Casting Clips To Media Devices + +The integration supports casting clips and camera streams to supported media devices. + +:::tip +For clips to be castable to media devices, audio is required and may need to be [enabled for recordings](../troubleshooting/faqs.md#audio-in-recordings). + +**NOTE: Even if you camera does not support audio, audio will need to be enabled for Casting to be accepted.** + +::: + + + +## Camera API + +To disable a camera dynamically + +``` +action: camera.turn_off +data: {} +target: + entity_id: camera.back_deck_cam # your Frigate camera entity ID +``` + +To enable a camera that has been disabled dynamically + +``` +action: camera.turn_on +data: {} +target: + entity_id: camera.back_deck_cam # your Frigate camera entity ID +``` + +## Notification API + +Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications. + +To load a thumbnail for a tracked object: + +``` +https://HA_URL/api/frigate/notifications//thumbnail.jpg +``` + +To load a snapshot for a tracked object: + +``` +https://HA_URL/api/frigate/notifications//snapshot.jpg +``` + +To load a video clip of a tracked object using an Android device: + +``` +https://HA_URL/api/frigate/notifications//clip.mp4 +``` + +To load a video clip of a tracked object using an iOS device: + +``` +https://HA_URL/api/frigate/notifications//master.m3u8 +``` + +To load a preview gif of a tracked object: + +``` +https://HA_URL/api/frigate/notifications//event_preview.gif +``` + +To load a preview gif of a review item: + +``` +https://HA_URL/api/frigate/notifications//review_preview.gif +``` + + + +## RTSP stream + +In order for the live streams to function they need to be accessible on the RTSP +port (default: `8554`) at `:8554`. Home Assistant will directly +connect to that streaming port when the live camera is viewed. + +#### RTSP URL Template + +For advanced usecases, this behavior can be changed with the [RTSP URL +template](#options) option. When set, this string will override the default stream +address that is derived from the default behavior described above. This option supports +[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict +variables from [Frigate API](../integrations/api) +available for the template. Note that no Home Assistant state is available to the +template, only the camera dict from Frigate. + +This is potentially useful when Frigate is behind a reverse proxy, and/or when +the default stream port is otherwise not accessible to Home Assistant (e.g. +firewall rules). + +###### RTSP URL Template Examples + +Use a different port number: + +``` +rtsp://:2000/front_door +``` + +Use the camera name in the stream URL: + +``` +rtsp://:2000/{{ name }} +``` + +Use the camera name in the stream URL, converting it to lowercase first: + +``` +rtsp://:2000/{{ name|lower }} +``` + +## Multiple Instance Support + +The Frigate integration seamlessly supports the use of multiple Frigate servers. + +### Requirements for Multiple Instances + +In order for multiple Frigate instances to function correctly, the +`topic_prefix` and `client_id` parameters must be set differently per server. +See [MQTT +configuration](mqtt) +for how to set these. + +#### API URLs + +When multiple Frigate instances are configured, [API](#notification-api) URLs should include an +identifier to tell Home Assistant which Frigate instance to refer to. The +identifier used is the MQTT `client_id` parameter included in the configuration, +and is used like so: + +``` +https://HA_URL/api/frigate//notifications//thumbnail.jpg +``` + +``` +https://HA_URL/api/frigate//clips/front_door-1624599978.427826-976jaa.mp4 +``` + +#### Default Treatment + +When a single Frigate instance is configured, the `client-id` parameter need not +be specified in URLs/identifiers -- that single instance is assumed. When +multiple Frigate instances are configured, the user **must** explicitly specify +which server they are referring to. + +## FAQ + +#### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit? + +The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera. + +#### I have set up automations based on the occupancy sensors. Sometimes the automation runs because the sensors are turned on, but then I look at Frigate I can't find the object that triggered the sensor. Is this a bug? + +No. The occupancy sensors have fewer checks in place because they are often used for things like turning the lights on where latency needs to be as low as possible. So false positives can sometimes trigger these sensors. If you want false positive filtering, you should use an mqtt sensor on the `frigate/events` or `frigate/reviews` topic. diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/homekit.md b/sam2-cpu/frigate-dev/docs/docs/integrations/homekit.md new file mode 100644 index 0000000..5954af4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/integrations/homekit.md @@ -0,0 +1,37 @@ +--- +id: homekit +title: HomeKit +--- + +Frigate cameras can be integrated with Apple HomeKit through go2rtc. This allows you to view your camera streams directly in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices. + +## Overview + +HomeKit integration is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server to expose your cameras to HomeKit. + +## Setup + +All HomeKit configuration and pairing should be done through the **go2rtc WebUI**. + +### Accessing the go2rtc WebUI + +The go2rtc WebUI is available at: + +``` +http://:1984 +``` + +Replace `` with the IP address or hostname of your Frigate server. + +### Pairing Cameras + +1. Navigate to the go2rtc WebUI at `http://:1984` +2. Use the `add` section to add a new camera to HomeKit +3. Follow the on-screen instructions to generate pairing codes for your cameras + +## Requirements + +- Frigate must be accessible on your local network using host network_mode +- Your iOS device must be on the same network as Frigate +- Port 1984 must be accessible for the go2rtc WebUI +- For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc) diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/mqtt.md b/sam2-cpu/frigate-dev/docs/docs/integrations/mqtt.md new file mode 100644 index 0000000..809c8c8 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/integrations/mqtt.md @@ -0,0 +1,527 @@ +--- +id: mqtt +title: MQTT +--- + +These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file. + +## General Frigate Topics + +### `frigate/available` + +Designed to be used as an availability topic with Home Assistant. Possible message are: +"online": published when Frigate is running (on startup) +"offline": published after Frigate has stopped + +### `frigate/restart` + +Causes Frigate to exit. Docker should be configured to automatically restart the container on exit. + +### `frigate/events` + +Message published for each changed tracked object. The first message is published when the tracked object is no longer marked as a false_positive. When Frigate finds a better snapshot of the tracked object or when a zone change occurs, it will publish a message with the same id. When the tracked object ends, a final message is published with `end_time` set. + +```json +{ + "type": "update", // new, update, end + "before": { + "id": "1607123955.475377-mxklsc", + "camera": "front_door", + "frame_time": 1607123961.837752, + "snapshot": { + "frame_time": 1607123965.975463, + "box": [415, 489, 528, 700], + "area": 12728, + "region": [260, 446, 660, 846], + "score": 0.77546, + "attributes": [] + }, + "label": "person", + "sub_label": null, + "top_score": 0.958984375, + "false_positive": false, + "start_time": 1607123955.475377, + "end_time": null, + "score": 0.7890625, + "box": [424, 500, 536, 712], + "area": 23744, + "ratio": 2.113207, + "region": [264, 450, 667, 853], + "current_zones": ["driveway"], + "entered_zones": ["yard", "driveway"], + "thumbnail": null, + "has_snapshot": false, + "has_clip": false, + "active": true, // convenience attribute, this is strictly opposite of "stationary" + "stationary": false, // whether or not the object is considered stationary + "motionless_count": 0, // number of frames the object has been motionless + "position_changes": 2, // number of times the object has moved from a stationary position + "attributes": { + "face": 0.64 + }, // attributes with top score that have been identified on the object at any point + "current_attributes": [], // detailed data about the current attributes in this frame + "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "average_estimated_speed": 14.3, // average estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 + }, + "after": { + "id": "1607123955.475377-mxklsc", + "camera": "front_door", + "frame_time": 1607123962.082975, + "snapshot": { + "frame_time": 1607123965.975463, + "box": [415, 489, 528, 700], + "area": 12728, + "region": [260, 446, 660, 846], + "score": 0.77546, + "attributes": [] + }, + "label": "person", + "sub_label": ["John Smith", 0.79], + "top_score": 0.958984375, + "false_positive": false, + "start_time": 1607123955.475377, + "end_time": null, + "score": 0.87890625, + "box": [432, 496, 544, 854], + "area": 40096, + "ratio": 1.251397, + "region": [218, 440, 693, 915], + "current_zones": ["yard", "driveway"], + "entered_zones": ["yard", "driveway"], + "thumbnail": null, + "has_snapshot": false, + "has_clip": false, + "active": true, // convenience attribute, this is strictly opposite of "stationary" + "stationary": false, // whether or not the object is considered stationary + "motionless_count": 0, // number of frames the object has been motionless + "position_changes": 2, // number of times the object has changed position + "attributes": { + "face": 0.86 + }, // attributes with top score that have been identified on the object at any point + "current_attributes": [ + // detailed data about the current attributes in this frame + { + "label": "face", + "box": [442, 506, 534, 524], + "score": 0.86 + } + ], + "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "average_estimated_speed": 14.31, // average estimated speed (mph or kph) for objects moving through zones with speed estimation enabled + "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled + "recognized_license_plate": "ABC12345", // a recognized license plate for car objects + "recognized_license_plate_score": 0.933451 + } +} +``` + +### `frigate/tracked_object_update` + +Message published for updates to tracked object metadata, for example: + +#### Generative AI Description Update + +```json +{ + "type": "description", + "id": "1607123955.475377-mxklsc", + "description": "The car is a red sedan moving away from the camera." +} +``` + +#### Face Recognition Update + +```json +{ + "type": "face", + "id": "1607123955.475377-mxklsc", + "name": "John", + "score": 0.95, + "camera": "front_door_cam", + "timestamp": 1607123958.748393 +} +``` + +#### License Plate Recognition Update + +```json +{ + "type": "lpr", + "id": "1607123955.475377-mxklsc", + "name": "John's Car", + "plate": "123ABC", + "score": 0.95, + "camera": "driveway_cam", + "timestamp": 1607123958.748393 +} +``` + +#### Object Classification Update + +Message published when [object classification](/configuration/custom_classification/object_classification) reaches consensus on a classification result. + +**Sub label type:** + +```json +{ + "type": "classification", + "id": "1607123955.475377-mxklsc", + "camera": "front_door_cam", + "timestamp": 1607123958.748393, + "model": "person_classifier", + "sub_label": "delivery_person", + "score": 0.87 +} +``` + +**Attribute type:** + +```json +{ + "type": "classification", + "id": "1607123955.475377-mxklsc", + "camera": "front_door_cam", + "timestamp": 1607123958.748393, + "model": "helmet_detector", + "attribute": "yes", + "score": 0.92 +} +``` + +### `frigate/reviews` + +Message published for each changed review item. The first message is published when the `detection` or `alert` is initiated. + +An `update` with the same ID will be published when: + +- The severity changes from `detection` to `alert` +- Additional objects are detected +- An object is recognized via face, lpr, etc. + +When the review activity has ended a final `end` message is published. + +```json +{ + "type": "update", // new, update, end + "before": { + "id": "1718987129.308396-fqk5ka", // review_id + "camera": "front_cam", + "start_time": 1718987129.308396, + "end_time": null, + "severity": "detection", + "thumb_path": "/media/frigate/clips/review/thumb-front_cam-1718987129.308396-fqk5ka.webp", + "data": { + "detections": [ + // list of event IDs + "1718987128.947436-g92ztx", + "1718987148.879516-d7oq7r", + "1718987126.934663-q5ywpt" + ], + "objects": ["person", "car"], + "sub_labels": [], + "zones": [], + "audio": [] + } + }, + "after": { + "id": "1718987129.308396-fqk5ka", + "camera": "front_cam", + "start_time": 1718987129.308396, + "end_time": null, + "severity": "alert", + "thumb_path": "/media/frigate/clips/review/thumb-front_cam-1718987129.308396-fqk5ka.webp", + "data": { + "detections": [ + "1718987128.947436-g92ztx", + "1718987148.879516-d7oq7r", + "1718987126.934663-q5ywpt" + ], + "objects": ["person", "car"], + "sub_labels": ["Bob"], + "zones": ["front_yard"], + "audio": [] + } + } +} +``` + +### `frigate/triggers` + +Message published when a trigger defined in a camera's `semantic_search` configuration fires. + +```json +{ + "name": "car_trigger", + "camera": "driveway", + "event_id": "1751565549.853251-b69j73", + "type": "thumbnail", + "score": 0.85 +} +``` + +### `frigate/stats` + +Same data available at `/api/stats` published at a configurable interval. + +### `frigate/camera_activity` + +Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publising to `frigate/onConnect` + +### `frigate/notifications/set` + +Topic to turn notifications on and off. Expected values are `ON` and `OFF`. + +### `frigate/notifications/state` + +Topic with current state of notifications. Published values are `ON` and `OFF`. + +## Frigate Camera Topics + +### `frigate///status` + +Publishes the current health status of each role that is enabled (`audio`, `detect`, `record`). Possible values are: + +- `online`: Stream is running and being processed +- `offline`: Stream is offline and is being restarted +- `disabled`: Camera is currently disabled + +### `frigate//` + +Publishes the count of objects for the camera for use as a sensor in Home Assistant. +`all` can be used as the object_name for the count of all objects for the camera. + +### `frigate///active` + +Publishes the count of active objects for the camera for use as a sensor in Home +Assistant. `all` can be used as the object_name for the count of all active objects +for the camera. + +### `frigate//` + +Publishes the count of objects for the zone for use as a sensor in Home Assistant. +`all` can be used as the object_name for the count of all objects for the zone. + +### `frigate///active` + +Publishes the count of active objects for the zone for use as a sensor in Home +Assistant. `all` can be used as the object_name for the count of all objects for the +zone. + +### `frigate///snapshot` + +Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image +is published again. + +The height and crop of snapshots can be configured in the config. + +### `frigate//audio/` + +Publishes "ON" when a type of audio is detected and "OFF" when it is not for the camera for use as a sensor in Home Assistant. + +`all` can be used as the audio_type for the status of all audio types. + +### `frigate//audio/dBFS` + +Publishes the dBFS value for audio detected on this camera. + +**NOTE:** Requires audio detection to be enabled + +### `frigate//audio/rms` + +Publishes the rms value for audio detected on this camera. + +**NOTE:** Requires audio detection to be enabled + +### `frigate//audio/transcription` + +Publishes transcribed text for audio detected on this camera. + +**NOTE:** Requires audio detection and transcription to be enabled + +### `frigate//classification/` + +Publishes the current state detected by a state classification model for the camera. The topic name includes the model name as configured in your classification settings. +The published value is the detected state class name (e.g., `open`, `closed`, `on`, `off`). The state is only published when it changes, helping to reduce unnecessary MQTT traffic. + +### `frigate//enabled/set` + +Topic to turn Frigate's processing of a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//enabled/state` + +Topic with current state of processing for a camera. Published values are `ON` and `OFF`. + +### `frigate//detect/set` + +Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//detect/state` + +Topic with current state of object detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//audio/set` + +Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//audio/state` + +Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//recordings/set` + +Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//recordings/state` + +Topic with current state of recordings for a camera. Published values are `ON` and `OFF`. + +### `frigate//snapshots/set` + +Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//snapshots/state` + +Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`. + +### `frigate//motion/set` + +Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`. +NOTE: Turning off motion detection will fail if detection is not disabled. + +### `frigate//motion` + +Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`. +NOTE: After motion is initially detected, `ON` will be set until no motion has +been detected for `mqtt_off_delay` seconds (30 by default). + +### `frigate//motion/state` + +Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`. + +### `frigate//improve_contrast/set` + +Topic to turn improve_contrast for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//improve_contrast/state` + +Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`. + +### `frigate//motion_threshold/set` + +Topic to adjust motion threshold for a camera. Expected value is an integer. + +### `frigate//motion_threshold/state` + +Topic with current motion threshold for a camera. Published value is an integer. + +### `frigate//motion_contour_area/set` + +Topic to adjust motion contour area for a camera. Expected value is an integer. + +### `frigate//motion_contour_area/state` + +Topic with current motion contour area for a camera. Published value is an integer. + +### `frigate//review_status` + +Topic with current activity status of the camera. Possible values are `NONE`, `DETECTION`, or `ALERT`. + +### `frigate//ptz` + +Topic to send PTZ commands to camera. + +| Command | Description | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `preset_` | send command to move to preset with name `` | +| `MOVE_` | send command to continuously move in ``, possible values are [UP, DOWN, LEFT, RIGHT] | +| `ZOOM_` | send command to continuously zoom ``, possible values are [IN, OUT] | +| `STOP` | send command to stop moving | + +### `frigate//ptz_autotracker/set` + +Topic to turn the PTZ autotracker for a camera on and off. Expected values are `ON` and `OFF`. + +### `frigate//ptz_autotracker/state` + +Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`. + +### `frigate//ptz_autotracker/active` + +Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`. + +### `frigate//review_alerts/set` + +Topic to turn review alerts for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_alerts/state` + +Topic with current state of review alerts for a camera. Published values are `ON` and `OFF`. + +### `frigate//review_detections/set` + +Topic to turn review detections for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_detections/state` + +Topic with current state of review detections for a camera. Published values are `ON` and `OFF`. + +### `frigate//object_descriptions/set` + +Topic to turn generative AI object descriptions for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//object_descriptions/state` + +Topic with current state of generative AI object descriptions for a camera. Published values are `ON` and `OFF`. + +### `frigate//review_descriptions/set` + +Topic to turn generative AI review descriptions for a camera on or off. Expected values are `ON` and `OFF`. + +### `frigate//review_descriptions/state` + +Topic with current state of generative AI review descriptions for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye/set` + +Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode +must be enabled in the configuration. + +### `frigate//birdseye/state` + +Topic with current state of Birdseye for a camera. Published values are `ON` and `OFF`. + +### `frigate//birdseye_mode/set` + +Topic to set Birdseye mode for a camera. Birdseye offers different modes to customize under which circumstances the camera is shown. + +_Note: Changing the value from `CONTINUOUS` -> `MOTION | OBJECTS` will take up to 30 seconds for +the camera to be removed from the view._ + +| Command | Description | +| ------------ | ----------------------------------------------------------------- | +| `CONTINUOUS` | Always included | +| `MOTION` | Show when detected motion within the last 30 seconds are included | +| `OBJECTS` | Shown if an active object tracked within the last 30 seconds | + +### `frigate//birdseye_mode/state` + +Topic with current state of the Birdseye mode for a camera. Published values are `CONTINUOUS`, `MOTION`, `OBJECTS`. + +### `frigate//notifications/set` + +Topic to turn notifications on and off. Expected values are `ON` and `OFF`. + +### `frigate//notifications/state` + +Topic with current state of notifications. Published values are `ON` and `OFF`. + +### `frigate//notifications/suspend` + +Topic to suspend notifications for a certain number of minutes. Expected value is an integer. + +### `frigate//notifications/suspended` + +Topic with timestamp that notifications are suspended until. Published value is a UNIX timestamp, or 0 if notifications are not suspended. diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/plus.md b/sam2-cpu/frigate-dev/docs/docs/integrations/plus.md new file mode 100644 index 0000000..961d6e9 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/integrations/plus.md @@ -0,0 +1,78 @@ +--- +id: plus +title: Frigate+ +--- + +For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/). + +## Setup + +### Create an account + +Free accounts can be created at [https://plus.frigate.video](https://plus.frigate.video). + +### Generate an API key + +Once logged in, you can generate an API key for Frigate in Settings. + +![API key](/img/plus-api-key-min.png) + +### Set your API key + +In Frigate, you can use an environment variable or a docker secret named `PLUS_API_KEY` to enable the `Frigate+` buttons on the Explore page. Home Assistant Addon users can set it under Settings > Add-ons > Frigate > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch). + +:::warning + +You cannot use the `environment_vars` section of your Frigate configuration file to set this environment variable. It must be defined as an environment variable in the docker config or Home Assistant Add-on config. + +::: + +## Submit examples + +Once your API key is configured, you can submit examples directly from the Explore page in Frigate. From the More Filters menu, select "Has a Snapshot - Yes" and "Submitted to Frigate+ - No", and press Apply at the bottom of the pane. Then, click on a thumbnail and select the Snapshot tab. + +You can use your keyboard's left and right arrow keys to quickly navigate between the tracked object snapshots. + +:::note + +Snapshots must be enabled to be able to submit examples to Frigate+ + +::: + +![Submit To Plus](/img/plus/submit-to-plus.jpg) + +### Annotate and verify + +You can view all of your submitted images at [https://plus.frigate.video](https://plus.frigate.video). Annotations can be added by clicking an image. For more detailed information about labeling, see the documentation on [annotating](../plus/annotating.md). + +![Annotate](/img/annotate.png) + +## Use Models + +Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically. + +You can either choose the new model from the Frigate+ pane in the Settings page of the Frigate UI, or manually set the model at the root level in your config: + +```yaml +model: + path: plus:// +``` + +:::note + +Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. + +::: + +Models are downloaded into the `/config/model_cache` folder and only downloaded if needed. + +If needed, you can override the labelmap for Frigate+ models. This is not recommended as renaming labels will break the Submit to Frigate+ feature if the labels are not available in Frigate+. + +```yaml +model: + path: plus:// + labelmap: + 3: animal + 4: animal + 5: animal +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/integrations/third_party_extensions.md b/sam2-cpu/frigate-dev/docs/docs/integrations/third_party_extensions.md new file mode 100644 index 0000000..adaee27 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/integrations/third_party_extensions.md @@ -0,0 +1,40 @@ +--- +id: third_party_extensions +title: Third Party Extensions +--- + +Being open source, others have the possibility to modify and extend the rich functionality Frigate already offers. +This page is meant to be an overview over additions one can make to the home NVR setup. The list is not exhaustive and can be extended via PR to the Frigate docs. Most of these services are designed to interface with Frigate's unauthenticated api over port 5000. + +:::warning + +This page does not recommend or rate the presented projects. +Please use your own knowledge to assess and vet them before you install anything on your system. + +::: + +## [Advanced Camera Card (formerly known as Frigate Card](https://card.camera/#/README) + +The [Advanced Camera Card](https://card.camera/#/README) is a Home Assistant dashboard card with deep Frigate integration. + +## [Double Take](https://github.com/skrashevich/double-take) + +[Double Take](https://github.com/skrashevich/double-take) provides an unified UI and API for processing and training images for facial recognition. +It supports automatically setting the sub labels in Frigate for person objects that are detected and recognized. +This is a fork (with fixed errors and new features) of [original Double Take](https://github.com/jakowenko/double-take) project which, unfortunately, isn't being maintained by author. + +## [Frigate Notify](https://github.com/0x2142/frigate-notify) + +[Frigate Notify](https://github.com/0x2142/frigate-notify) is a simple app designed to send notifications from Frigate to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended. + +## [Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) + +[Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) is a program that works in tandem with Frigate. It responds to Frigate when a snapshot or a review is made (and more can be added), and uploads them to one or more remote server(s) of your choice. + +## [Frigate telegram](https://github.com/OldTyT/frigate-telegram) + +[Frigate telegram](https://github.com/OldTyT/frigate-telegram) makes it possible to send events from Frigate to Telegram. Events are sent as a message with a text description, video, and thumbnail. + +## [Periscope](https://github.com/maksz42/periscope) + +[Periscope](https://github.com/maksz42/periscope) is a lightweight Android app that turns old devices into live viewers for Frigate. It works on Android 2.2 and above, including Android TV. It supports authentication and HTTPS. diff --git a/sam2-cpu/frigate-dev/docs/docs/mdx.md b/sam2-cpu/frigate-dev/docs/docs/mdx.md new file mode 100644 index 0000000..f0210fb --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/mdx.md @@ -0,0 +1,17 @@ +--- +id: mdx +title: Powered by MDX +--- + +You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). + +export const Highlight = ({children, color}) => ( {children} ); + +Docusaurus green and Facebook blue are my favorite colors. + +I can write **Markdown** alongside my _JSX_! diff --git a/sam2-cpu/frigate-dev/docs/docs/plus/annotating.md b/sam2-cpu/frigate-dev/docs/docs/plus/annotating.md new file mode 100644 index 0000000..dc8e571 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/plus/annotating.md @@ -0,0 +1,53 @@ +--- +id: annotating +title: Annotating your images +--- + +For the best results, follow these guidelines. You may also want to review the documentation on [improving your model](./index.md#improving-your-model). + +**Label every object in the image**: It is important that you label all objects in each image before verifying. If you don't label a car for example, the model will be taught that part of the image is _not_ a car and it will start to get confused. You can exclude labels that you don't want detected on any of your cameras. + +**Make tight bounding boxes**: Tighter bounding boxes improve the recognition and ensure that accurate bounding boxes are predicted at runtime. + +**Label the full object even when occluded**: If you have a person standing behind a car, label the full person even though a portion of their body may be hidden behind the car. This helps predict accurate bounding boxes and improves zone accuracy and filters at runtime. If an object is partly out of frame, label it only when a person would reasonably be able to recognize the object from the visible parts. + +**Label objects hard to identify as difficult**: When objects are truly difficult to make out, such as a car barely visible through a bush, or a dog that is hard to distinguish from the background at night, flag it as 'difficult'. This is not used in the model training as of now, but will in the future. + +**Delivery logos such as `amazon`, `ups`, and `fedex` should label the logo**: For a Fedex truck, label the truck as a `car` and make a different bounding box just for the Fedex logo. If there are multiple logos, label each of them. + +![Fedex Logo](/img/plus/fedex-logo.jpg) + +## AI suggested labels + +If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for you camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer. + +![Suggestions](/img/plus/suggestions.webp) + +Suggestions are converted to labels when saving, so you should remove any errant suggestions. There is already some logic designed to avoid duplicate labels, but you may still occasionally see some duplicate suggestions. You should keep the most accurate bounding box and delete any duplicates so that you have just one label per object remaining. + +## False positive labels + +False positives will be shown with a red box and the label will have a strike through. These can't be adjusted, but they can be deleted if you accidentally submit a true positive as a false positive from Frigate. +![false positive](/img/plus/false-positive.jpg) + +Misidentified objects should have a correct label added. For example, if a person was mistakenly detected as a cat, you should submit it as a false positive in Frigate and add a label for the person. The boxes will overlap. + +![add image](/img/plus/false-positive-overlap.jpg) + +## Shortcuts for a faster workflow + +| Shortcut Key | Description | +| ----------------- | ----------------------------- | +| `?` | Show all keyboard shortcuts | +| `w` | Add box | +| `d` | Toggle difficult | +| `s` | Switch to the next label | +| `Shift + s` | Switch to the previous label | +| `tab` | Select next largest box | +| `del` | Delete current box | +| `esc` | Deselect/Cancel | +| `← ↑ → ↓` | Move box | +| `Shift + ← ↑ → ↓` | Resize box | +| `scrollwheel` | Zoom in/out | +| `f` | Hide/show all but current box | +| `spacebar` | Verify and save | diff --git a/sam2-cpu/frigate-dev/docs/docs/plus/faq.md b/sam2-cpu/frigate-dev/docs/docs/plus/faq.md new file mode 100644 index 0000000..151eb3f --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/plus/faq.md @@ -0,0 +1,34 @@ +--- +id: faq +title: FAQ +--- + +### Are my models trained just on my image uploads? How are they built? + +Frigate+ models are built by fine tuning a base model with the images you have annotated and verified. The base model is trained from scratch from a sampling of images across all Frigate+ user submissions and takes weeks of expensive GPU resources to train. If the models were built using your image uploads alone, you would need to provide tens of thousands of examples and it would take more than a week (and considerable cost) to train. Diversity helps the model generalize. + +### Are my video feeds sent to the cloud for analysis when using Frigate+ models? + +No. Frigate+ models are a drop in replacement for the default model. All processing is performed locally as always. The only images sent to Frigate+ are the ones you specifically submit via the `Send to Frigate+` button or upload directly. + +### Can I label anything I want and train the model to recognize something custom for me? + +Not currently. At the moment, the set of labels will be consistent for all users. The focus will be on expanding that set of labels before working on completely custom user labels. + +### Can Frigate+ models be used offline? + +Yes. Models and metadata are stored in the `model_cache` directory within the config folder. Frigate will only attempt to download a model if it does not exist in the cache. This means you can backup the directory and/or use it completely offline. + +### Can I keep using my Frigate+ models even if I do not renew my subscription? + +Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models trained with your subscription are yours to keep and use forever. However, do note that the terms and conditions prohibit you from sharing, reselling, or creating derivative products from the models. + +### Why can't I submit images to Frigate+? + +If you've configured your API key and the Frigate+ Settings page in the UI shows that the key is active, you need to ensure that you've enabled both snapshots and `clean_copy` snapshots for the cameras you'd like to submit images for. Note that `clean_copy` is enabled by default when snapshots are enabled. + +```yaml +snapshots: + enabled: true + clean_copy: true +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/plus/first_model.md b/sam2-cpu/frigate-dev/docs/docs/plus/first_model.md new file mode 100644 index 0000000..adec174 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/plus/first_model.md @@ -0,0 +1,75 @@ +--- +id: first_model +title: Requesting your first model +--- + +## Step 1: Upload and annotate your images + +Before requesting your first model, you will need to upload and verify at least 10 images to Frigate+. The more images you upload, annotate, and verify the better your results will be. Most users start to see very good results once they have at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. Refer to the [integration docs](../integrations/plus.md#generate-an-api-key) for instructions on how to easily submit images to Frigate+ directly from Frigate. + +It is recommended to submit **both** true positives and false positives. This will help the model differentiate between what is and isn't correct. You should aim for a target of 80% true positive submissions and 20% false positives across all of your images. If you are experiencing false positives in a specific area, submitting true positives for any object type near that area in similar lighting conditions will help teach the model what that area looks like when no objects are present. + +For more detailed recommendations, you can refer to the docs on [annotating](./annotating.md). + +## Step 2: Submit a model request + +Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours. +![Plus Models Page](/img/plus/plus-models.jpg) + +## Step 3: Set your model id in the config + +You will receive an email notification when your Frigate+ model is ready. +![Model Ready Email](/img/plus/model-ready-email.jpg) + +Models available in Frigate+ can be used with a special model path. No other information needs to be configured because it fetches the remaining config from Frigate+ automatically. + +```yaml +model: + path: plus:// +``` + +:::note + +Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. + +::: + +:::tip + +When setting the plus model id, all other fields should be removed as these are configured automatically with the Frigate+ model config + +::: + +## Step 4: Adjust your object filters for higher scores + +Frigate+ models generally have much higher scores than the default model provided in Frigate. You will likely need to increase your `threshold` and `min_score` values. Here is an example of how these values can be refined, but you should expect these to evolve as your model improves. For more information about how `threshold` and `min_score` are related, see the docs on [object filters](../configuration/object_filters.md#object-scores). + +```yaml +objects: + filters: + dog: + min_score: .7 + threshold: .9 + cat: + min_score: .65 + threshold: .8 + face: + min_score: .7 + package: + min_score: .65 + threshold: .9 + license_plate: + min_score: .6 + amazon: + min_score: .75 + ups: + min_score: .75 + fedex: + min_score: .75 + person: + min_score: .65 + threshold: .85 + car: + min_score: .65 + threshold: .85 +``` diff --git a/sam2-cpu/frigate-dev/docs/docs/plus/index.md b/sam2-cpu/frigate-dev/docs/docs/plus/index.md new file mode 100644 index 0000000..fa8f86f --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/plus/index.md @@ -0,0 +1,117 @@ +--- +id: index +title: Models +--- + +Frigate+ offers models trained on images submitted by Frigate+ users from their security cameras and is specifically designed for the way Frigate NVR analyzes video footage. These models offer higher accuracy with less resources. The images you upload are used to fine tune a base model trained from images uploaded by all Frigate+ users. This fine tuning process results in a model that is optimized for accuracy in your specific conditions. + +With a subscription, 12 model trainings to fine tune your model per year are included. In addition, you will have access to any base models published while your subscription is active. If you cancel your subscription, you will retain access to any trained and base models in your account. An active subscription is required to submit model requests or purchase additional trainings. New base models are published quarterly with target dates of January 15th, April 15th, July 15th, and October 15th. + +Information on how to integrate Frigate+ with Frigate can be found in the [integration docs](../integrations/plus.md). + +## Available model types + +There are three model types offered in Frigate+, `mobiledet`, `yolonas`, and `yolov9`. All of these models are object detection models and are trained to detect the same set of labels [listed below](#available-label-types). + +Not all model types are supported by all detectors, so it's important to choose a model type to match your detector as shown in the table under [supported detector types](#supported-detector-types). You can test model types for compatibility and speed on your hardware by using the base models. + +| Model Type | Description | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mobiledet` | Based on the same architecture as the default model included with Frigate. Runs on Google Coral devices and CPUs. | +| `yolonas` | A newer architecture that offers slightly higher accuracy and improved detection of small objects. Runs on Intel, NVidia GPUs, and AMD GPUs. | +| `yolov9` | A leading SOTA (state of the art) object detection model with similar performance to yolonas, but on a wider range of hardware options. Runs on Intel, NVidia GPUs, AMD GPUs, Hailo, MemryX\*, Apple Silicon\*, and Rockchip NPUs. | + +_\* Support coming in 0.17_ + +### YOLOv9 Details + +YOLOv9 models are available in `s` and `t` sizes. When requesting a `yolov9` model, you will be prompted to choose a size. If you are unsure what size to choose, you should perform some tests with the base models to find the performance level that suits you. The `s` size is most similar to the current `yolonas` models in terms of inference times and accuracy, and a good place to start is the `320x320` resolution model for `yolov9s`. + +:::info + +When switching to YOLOv9, you may need to adjust your thresholds for some objects. + +::: + +#### Hailo Support + +If you have a Hailo device, you will need to specify the hardware you have when submitting a model request because they are not cross compatible. Please test using the available base models before submitting your model request. + +#### Rockchip (RKNN) Support + +For 0.16, YOLOv9 onnx models will need to be manually converted. First, you will need to configure Frigate to use the model id for your YOLOv9 onnx model so it downloads the model to your `model_cache` directory. From there, you can follow the [documentation](/configuration/object_detectors.md#converting-your-own-onnx-model-to-rknn-format) to convert it. Automatic conversion is coming in 0.17. + +## Supported detector types + +Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo8l`), and Rockchip\* (`rknn`) detectors. + +| Hardware | Recommended Detector Type | Recommended Model Type | +| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- | +| [CPU](/configuration/object_detectors.md#cpu-detector-not-recommended) | `cpu` | `mobiledet` | +| [Coral (all form factors)](/configuration/object_detectors.md#edge-tpu-detector) | `edgetpu` | `mobiledet` | +| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` | +| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` | +| [AMD ROCm GPU](/configuration/object_detectors#amdrocm-gpu-detector) | `onnx` | `yolov9` | +| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo-8) | `hailo8l` | `yolov9` | +| [Rockchip NPU](/configuration/object_detectors#rockchip-platform)\* | `rknn` | `yolov9` | + +_\* Requires manual conversion in 0.16. Automatic conversion coming in 0.17._ + +## Improving your model + +Some users may find that Frigate+ models result in more false positives initially, but by submitting true and false positives, the model will improve. With all the new images now being submitted by subscribers, future base models will improve as more and more examples are incorporated. Note that only images with at least one verified label will be used when training your model. Submitting an image from Frigate as a true or false positive will not verify the image. You still must verify the image in Frigate+ in order for it to be used in training. + +- **Submit both true positives and false positives**. This will help the model differentiate between what is and isn't correct. You should aim for a target of 80% true positive submissions and 20% false positives across all of your images. If you are experiencing false positives in a specific area, submitting true positives for any object type near that area in similar lighting conditions will help teach the model what that area looks like when no objects are present. +- **Lower your thresholds a little in order to generate more false/true positives near the threshold value**. For example, if you have some false positives that are scoring at 68% and some true positives scoring at 72%, you can try lowering your threshold to 65% and submitting both true and false positives within that range. This will help the model learn and widen the gap between true and false positive scores. +- **Submit diverse images**. For the best results, you should provide at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. As circumstances change, you may need to submit new examples to address new types of false positives. For example, the change from summer days to snowy winter days or other changes such as a new grill or patio furniture may require additional examples and training. + +## Available label types + +Frigate+ models support a more relevant set of objects for security cameras. The labels for annotation in Frigate+ are configurable by editing the camera in the Cameras section of Frigate+. Currently, the following objects are supported: + +- **People**: `person`, `face` +- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `license_plate` +- **Delivery Logos**: `amazon`, `usps`, `ups`, `fedex`, `dhl`, `an_post`, `purolator`, `postnl`, `nzpost`, `postnord`, `gls`, `dpd`, `canada_post`, `royal_mail` +- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo` +- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella` + +Other object types available in the default Frigate model are not available. Additional object types will be added in future releases. + +### Candidate labels + +Candidate labels are also available for annotation. These labels don't have enough data to be included in the model yet, but using them will help add support sooner. You can enable these labels by editing the camera settings. + +Where possible, these labels are mapped to existing labels during training. For example, any `baby` labels are mapped to `person` until support for new labels is added. + +The candidate labels are: `baby`, `bpost`, `badger`, `possum`, `rodent`, `chicken`, `groundhog`, `boar`, `hedgehog`, `tractor`, `golf cart`, `garbage truck`, `bus`, `sports ball` + +Candidate labels are not available for automatic suggestions. + +### Label attributes + +Frigate has special handling for some labels when using Frigate+ models. `face`, `license_plate`, and delivery logos such as `amazon`, `ups`, and `fedex` are considered attribute labels which are not tracked like regular objects and do not generate review items directly. In addition, the `threshold` filter will have no effect on these labels. You should adjust the `min_score` and other filter values as needed. + +In order to have Frigate start using these attribute labels, you will need to add them to the list of objects to track: + +```yaml +objects: + track: + - person + - face + - license_plate + - dog + - cat + - car + - amazon + - fedex + - ups + - package +``` + +When using Frigate+ models, Frigate will choose the snapshot of a person object that has the largest visible face. For cars, the snapshot with the largest visible license plate will be selected. This aids in secondary processing such as facial and license plate recognition for person and car objects. + +![Face Attribute](/img/plus/attribute-example-face.jpg) + +Delivery logos such as `amazon`, `ups`, and `fedex` labels are used to automatically assign a sub label to car objects. + +![Fedex Attribute](/img/plus/attribute-example-fedex.jpg) diff --git a/sam2-cpu/frigate-dev/docs/docs/troubleshooting/edgetpu.md b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/edgetpu.md new file mode 100644 index 0000000..f5cb358 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/edgetpu.md @@ -0,0 +1,95 @@ +--- +id: edgetpu +title: Troubleshooting EdgeTPU +--- + +## USB Coral Not Detected + +There are many possible causes for a USB coral not being detected and some are OS specific. It is important to understand how the USB coral works: + +1. When the device is first plugged in and has not initialized it will appear as `1a6e:089a Global Unichip Corp.` when running `lsusb` or checking the hardware page in HA OS. +2. Once initialized, the device will appear as `18d1:9302 Google Inc.` when running `lsusb` or checking the hardware page in HA OS. + +:::tip + +Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inferance using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral. + +::: + +If the coral does not initialize then Frigate can not interface with it. Some common reasons for the USB based Coral not initializing are: + +### Not Enough Power + +The USB coral can draw up to 900mA and this can be too much for some on-device USB ports, especially for small board computers like the RPi. If the coral is not initializing then some recommended steps are: + +1. Try a different port, some ports are capable of providing more power than others. +2. Make sure the port is USB3, this is important for power and to ensure the coral runs at max speed. +3. Try a different cable, some users have found the included cable to not work well. +4. Use an externally powered USB hub. + +### Incorrect Device Access + +The USB coral has different IDs when it is uninitialized and initialized. + +- When running Frigate in a VM, Proxmox lxc, etc. you must ensure both device IDs are mapped. +- When running through the Home Assistant OS you may need to run the Full Access variant of the Frigate Add-on with the _Protection mode_ switch disabled so that the coral can be accessed. + +### Synology 716+II running DSM 7.2.1-69057 Update 5 + +Some users have reported that this older device runs an older kernel causing issues with the coral not being detected. The following steps allowed it to be detected correctly: + +1. Plug in the coral TPU in any of the USB ports on the NAS +2. Open the control panel - info screen. The coral TPU would be shown as a generic device. +3. Start the docker container with Coral TPU enabled in the config +4. The TPU would be detected but a few moments later it would disconnect. +5. While leaving the TPU device plugged in, restart the NAS using the reboot command in the UI. Do NOT unplug the NAS/power it off etc. +6. Open the control panel - info scree. The coral TPU will now be recognised as a USB Device - google inc +7. Start the frigate container. Everything should work now! + +### QNAP NAS + +QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features—`facial recognition`, `object recognition`, or `similar photo recognition`—are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use. +To allow the Coral TPU device to be discovered, the you must either: + +1. [Disable the AI recognition features in QuMagie](https://docs.qnap.com/application/qumagie/2.x/en-us/configuring-qnap-ai-core-settings-FB13CE03.html), +2. Remove the QNAP AI Core extension or +3. Manually start the QNAP AI Core extension after Frigate has fully started (not recommended). + +It is also recommended to restart the NAS once the changes have been made. + +## USB Coral Detection Appears to be Stuck + +The USB Coral can become stuck and need to be restarted, this can happen for a number of reasons depending on hardware and software setup. Some common reasons are: + +1. Some users have found the cable included with the coral to cause this problem and that switching to a different cable fixed it entirely. +2. Running Frigate in a VM may cause communication with the device to be lost and need to be reset. + +## PCIe Coral Not Detected + +The most common reason for the PCIe Coral not being detected is that the driver has not been installed. This process varies based on what OS and kernel that is being run. + +- In most cases [the Coral docs](https://coral.ai/docs/m2/get-started/#2-install-the-pcie-driver-and-edge-tpu-runtime) show how to install the driver for the PCIe based Coral. +- For some newer Linux distros (for example, Ubuntu 22.04+), https://github.com/jnicolson/gasket-builder can be used to build and install the latest version of the driver. + +## Attempting to load TPU as pci & Fatal Python error: Illegal instruction + +This is an issue due to outdated gasket driver when being used with new linux kernels. Installing an updated driver from https://github.com/jnicolson/gasket-builder has been reported to fix the issue. + +### Not detected on Raspberry Pi5 + +A kernel update to the RPi5 means an upate to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25) + +Specifically, add the following to config.txt + +``` +dtoverlay=pciex1-compat-pi5,no-mip +dtoverlay=pcie-32bit-dma-pi5 +``` + +## Only One PCIe Coral Is Detected With Coral Dual EdgeTPU + +Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has it's own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work. + +E-key slot implemented to full m.2 electromechanical specification has two PCIe busses. Most motherboard manufacturers implement only one PCIe bus in m.2 E-key connector (this is why only one TPU is working). Some SBCs can have only USB bus on m.2 connector, ie none of TPUs will work. + +In this case it is recommended to use a Dual EdgeTPU Adapter [like the one from MagicBlueSmoke](https://github.com/magic-blue-smoke/Dual-Edge-TPU-Adapter) diff --git a/sam2-cpu/frigate-dev/docs/docs/troubleshooting/faqs.md b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/faqs.md new file mode 100644 index 0000000..ff2379e --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/faqs.md @@ -0,0 +1,112 @@ +--- +id: faqs +title: Frequently Asked Questions +--- + +### Fatal Python error: Bus error + +This error message is due to a shm-size that is too small. Try updating your shm-size according to [this guide](../frigate/installation.md#calculating-required-shm-size). + +### How can I get sound or audio in my recordings? {#audio-in-recordings} + +By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to set a [FFmpeg preset](/configuration/ffmpeg_presets) that supports audio: + +```yaml +ffmpeg: + output_args: + record: preset-record-generic-audio-aac +``` + +### How can I get sound in live view? + +Audio is only supported for live view when go2rtc is configured, see [the live docs](../configuration/live.md) for more information. + +### I can't view recordings in the Web UI. + +Ensure your cameras send h264 encoded video, or [transcode them](/configuration/restream.md). + +You can open `chrome://media-internals/` in another tab and then try to playback, the media internals page will give information about why playback is failing. + +### What do I do if my cameras sub stream is not good enough? + +Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that a suitable resolution, the main stream can be resized. + +To do this efficiently the following setup is required: + +1. A GPU or iGPU must be available to do the scaling. +2. [ffmpeg presets for hwaccel](/configuration/hardware_acceleration_video.md) must be used +3. Set the desired detection resolution for `detect -> width` and `detect -> height`. + +When this is done correctly, the GPU will do the decoding and scaling which will result in a small increase in CPU usage but with better results. + +### My mjpeg stream or snapshots look green and crazy + +This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards. + +![mismatched-resolution](/img/mismatched-resolution-min.jpg) + +### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found" + +These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically. + +### "On connect called" + +If you see repeated "On connect called" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`. + +### Error: Database Is Locked + +SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced.md#database) should be used to move the database to a location on the internal drive. + +### Unable to publish to MQTT: client is not connected + +If MQTT isn't working in docker try using the IP of the device hosting the MQTT server instead of `localhost`, `127.0.0.1`, or `mosquitto.ix-mosquitto.svc.cluster.local`. + +This is because Frigate does not run in host mode so localhost points to the Frigate container and not the host device's network. + +### How do I know if my camera is offline + +A camera being offline can be detected via MQTT or /api/stats, the camera_fps for any offline camera will be 0. + +Also, Home Assistant will mark any offline camera as being unavailable when the camera is offline. + +### How can I view the Frigate log files without using the Web UI? + +Frigate manages logs internally as well as outputs directly to Docker via standard output. To view these logs using the CLI, follow these steps: + +- Open a terminal or command prompt on the host running your Frigate container. +- Type the following command and press Enter: + ``` + docker logs -f frigate + ``` + This command tells Docker to show you the logs from the Frigate container. + Note: If you've given your Frigate container a different name, replace "frigate" in the command with your container's actual name. The "-f" option means the logs will continue to update in real-time as new entries are added. To stop viewing the logs, press `Ctrl+C`. If you'd like to learn more about using Docker logs, including additional options and features, you can explore Docker's [official documentation](https://docs.docker.com/engine/reference/commandline/logs/). + +Alternatively, when you create the Frigate Docker container, you can bind a directory on the host to the mountpoint `/dev/shm/logs` to not only be able to persist the logs to disk, but also to be able to query them directly from the host using your favorite log parsing/query utility. + +``` +docker run -d \ + --name frigate \ + --restart=unless-stopped \ + --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ + --device /dev/bus/usb:/dev/bus/usb \ + --device /dev/dri/renderD128 \ + --shm-size=64m \ + -v /path/to/your/storage:/media/frigate \ + -v /path/to/your/config:/config \ + -v /etc/localtime:/etc/localtime:ro \ + -v /path/to/local/log/dir:/dev/shm/logs \ + -e FRIGATE_RTSP_PASSWORD='password' \ + -p 5000:5000 \ + -p 8554:8554 \ + -p 8555:8555/tcp \ + -p 8555:8555/udp \ + ghcr.io/blakeblackshear/frigate:stable +``` + +### My RTSP stream works fine in VLC, but it does not work when I put the same URL in my Frigate config. Is this a bug? + +No. Frigate uses the TCP protocol to connect to your camera's RTSP URL. VLC automatically switches between UDP and TCP depending on network conditions and stream availability. So a stream that works in VLC but not in Frigate is likely due to VLC selecting UDP as the transfer protocol. + +TCP ensures that all data packets arrive in the correct order. This is crucial for video recording, decoding, and stream processing, which is why Frigate enforces a TCP connection. UDP is faster but less reliable, as it does not guarantee packet delivery or order, and VLC does not have the same requirements as Frigate. + +You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation. diff --git a/sam2-cpu/frigate-dev/docs/docs/troubleshooting/gpu.md b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/gpu.md new file mode 100644 index 0000000..a5b4824 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/gpu.md @@ -0,0 +1,13 @@ +--- +id: gpu +title: Troubleshooting GPU +--- + +## OpenVINO + +### Can't get OPTIMIZATION_CAPABILITIES property as no supported devices found. + +Some users have reported issues using some Intel iGPUs with OpenVINO, where the GPU would not be detected. This error can be caused by various problems, so it is important to ensure the configuration is setup correctly. Some solutions users have noted: + +- In some cases users have noted that an HDMI dummy plug was necessary to be plugged into the motherboard's HDMI port. +- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container. \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docs/docs/troubleshooting/memory.md b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/memory.md new file mode 100644 index 0000000..b8ef536 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/memory.md @@ -0,0 +1,129 @@ +--- +id: memory +title: Memory Troubleshooting +--- + +Frigate includes built-in memory profiling using [memray](https://bloomberg.github.io/memray/) to help diagnose memory issues. This feature allows you to profile specific Frigate modules to identify memory leaks, excessive allocations, or other memory-related problems. + +## Enabling Memory Profiling + +Memory profiling is controlled via the `FRIGATE_MEMRAY_MODULES` environment variable. Set it to a comma-separated list of module names you want to profile: + +```bash +export FRIGATE_MEMRAY_MODULES="frigate.review_segment_manager,frigate.capture" +``` + +### Module Names + +Frigate processes are named using a module-based naming scheme. Common module names include: + +- `frigate.review_segment_manager` - Review segment processing +- `frigate.recording_manager` - Recording management +- `frigate.capture` - Camera capture processes (all cameras with this module name) +- `frigate.process` - Camera processing/tracking (all cameras with this module name) +- `frigate.output` - Output processing +- `frigate.audio_manager` - Audio processing +- `frigate.embeddings` - Embeddings processing + +You can also specify the full process name (including camera-specific identifiers) if you want to profile a specific camera: + +```bash +export FRIGATE_MEMRAY_MODULES="frigate.capture:front_door" +``` + +When you specify a module name (e.g., `frigate.capture`), all processes with that module prefix will be profiled. For example, `frigate.capture` will profile all camera capture processes. + +## How It Works + +1. **Binary File Creation**: When profiling is enabled, memray creates a binary file (`.bin`) in `/config/memray_reports/` that is updated continuously in real-time as the process runs. + +2. **Automatic HTML Generation**: On normal process exit, Frigate automatically: + + - Stops memray tracking + - Generates an HTML flamegraph report + - Saves it to `/config/memray_reports/.html` + +3. **Crash Recovery**: If a process crashes (SIGKILL, segfault, etc.), the binary file is preserved with all data up to the crash point. You can manually generate the HTML report from the binary file. + +## Viewing Reports + +### Automatic Reports + +After a process exits normally, you'll find HTML reports in `/config/memray_reports/`. Open these files in a web browser to view interactive flamegraphs showing memory usage patterns. + +### Manual Report Generation + +If a process crashes or you want to generate a report from an existing binary file, you can manually create the HTML report: + +```bash +memray flamegraph /config/memray_reports/.bin +``` + +This will generate an HTML file that you can open in your browser. + +## Understanding the Reports + +Memray flamegraphs show: + +- **Memory allocations over time**: See where memory is being allocated in your code +- **Call stacks**: Understand the full call chain leading to allocations +- **Memory hotspots**: Identify functions or code paths that allocate the most memory +- **Memory leaks**: Spot patterns where memory is allocated but not freed + +The interactive HTML reports allow you to: + +- Zoom into specific time ranges +- Filter by function names +- View detailed allocation information +- Export data for further analysis + +## Best Practices + +1. **Profile During Issues**: Enable profiling when you're experiencing memory issues, not all the time, as it adds some overhead. + +2. **Profile Specific Modules**: Instead of profiling everything, focus on the modules you suspect are causing issues. + +3. **Let Processes Run**: Allow processes to run for a meaningful duration to capture representative memory usage patterns. + +4. **Check Binary Files**: If HTML reports aren't generated automatically (e.g., after a crash), check for `.bin` files in `/config/memray_reports/` and generate reports manually. + +5. **Compare Reports**: Generate reports at different times to compare memory usage patterns and identify trends. + +## Troubleshooting + +### No Reports Generated + +- Check that the environment variable is set correctly +- Verify the module name matches exactly (case-sensitive) +- Check logs for memray-related errors +- Ensure `/config/memray_reports/` directory exists and is writable + +### Process Crashed Before Report Generation + +- Look for `.bin` files in `/config/memray_reports/` +- Manually generate HTML reports using: `memray flamegraph .bin` +- The binary file contains all data up to the crash point + +### Reports Show No Data + +- Ensure the process ran long enough to generate meaningful data +- Check that memray is properly installed (included by default in Frigate) +- Verify the process actually started and ran (check process logs) + +## Example Usage + +```bash +# Enable profiling for review and capture modules +export FRIGATE_MEMRAY_MODULES="frigate.review_segment_manager,frigate.capture" + +# Start Frigate +# ... let it run for a while ... + +# Check for reports +ls -lh /config/memray_reports/ + +# If a process crashed, manually generate report +memray flamegraph /config/memray_reports/frigate_capture_front_door.bin +``` + +For more information about memray and interpreting reports, see the [official memray documentation](https://bloomberg.github.io/memray/). diff --git a/sam2-cpu/frigate-dev/docs/docs/troubleshooting/recordings.md b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/recordings.md new file mode 100644 index 0000000..d26a361 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docs/troubleshooting/recordings.md @@ -0,0 +1,82 @@ +--- +id: recordings +title: Troubleshooting Recordings +--- + +## I have Frigate configured for motion recording only, but it still seems to be recording even with no motion. Why? + +You'll want to: + +- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion. +- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion. +- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner. + +## I see the message: WARNING : Unable to keep up with recording segments in cache for camera. Keeping the 5 most recent segments out of 6 and discarding the rest... + +This error can be caused by a number of different issues. The first step in troubleshooting is to enable debug logging for recording. This will enable logging showing how long it takes for recordings to be moved from RAM cache to the disk. + +```yaml +logger: + logs: + frigate.record.maintainer: debug +``` + +This will include logs like: + +``` +DEBUG : Copied /media/frigate/recordings/{segment_path} in 0.2 seconds. +``` + +It is important to let this run until the errors begin to happen, to confirm that there is not a slow down in the disk at the time of the error. + +#### Copy Times > 1 second + +If the storage is too slow to keep up with the recordings then the maintainer will fall behind and purge the oldest recordings to ensure the cache does not fill up causing a crash. In this case it is important to diagnose why the copy times are slow. + +##### Check RAM, swap, cache utilization, and disk utilization + +If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside frigate will help. It is important to review each aspect of available system resources. + +On linux, some helpful tools/commands in diagnosing would be: + +- docker stats +- htop +- iotop -o +- iostat -sxy --human 1 1 +- vmstat 1 + +On modern linux kernels, the system will utilize some swap if enabled. Setting vm.swappiness=1 no longer means that the kernel will only swap in order to avoid OOM. To prevent any swapping inside a container, set allocations memory and memory+swap to be the same and disable swapping by setting the following docker/podman run parameters: + +**Docker Compose example** + +```yaml +services: + frigate: + ... + mem_swappiness: 0 + memswap_limit: + deploy: + resources: + limits: + memory: +``` + +**Run command example** + +``` +--memory= --memory-swap= --memory-swappiness=0 +``` + +NOTE: These are hard-limits for the container, be sure there is enough headroom above what is shown by `docker stats` for your container. It will immediately halt if it hits ``. In general, running all cache and tmp filespace in RAM is preferable to disk I/O where possible. + +##### Check Storage Type + +Mounting a network share is a popular option for storing Recordings, but this can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased the copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. + +##### Check mount options + +Some users found that mounting a drive via `fstab` with the `sync` option caused dramatically reduce performance and led to this issue. Using `async` instead greatly reduced copy times. + +#### Copy Times < 1 second + +If the storage is working quickly then this error may be caused by CPU load on the machine being too high for Frigate to have the resources to keep up. Try temporarily shutting down other services to see if the issue improves. diff --git a/sam2-cpu/frigate-dev/docs/docusaurus.config.ts b/sam2-cpu/frigate-dev/docs/docusaurus.config.ts new file mode 100644 index 0000000..8ab9d85 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/docusaurus.config.ts @@ -0,0 +1,223 @@ +import type * as Preset from "@docusaurus/preset-classic"; +import * as path from "node:path"; +import type { Config, PluginConfig } from "@docusaurus/types"; +import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs"; + +const config: Config = { + title: "Frigate", + tagline: "NVR With Realtime Object Detection for IP Cameras", + url: "https://docs.frigate.video", + baseUrl: "/", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", + favicon: "img/branding/favicon.ico", + organizationName: "blakeblackshear", + projectName: "frigate", + themes: [ + "@docusaurus/theme-mermaid", + "docusaurus-theme-openapi-docs", + "@inkeep/docusaurus/chatButton", + "@inkeep/docusaurus/searchBar", + ], + markdown: { + mermaid: true, + }, + i18n: { + defaultLocale: 'en', + locales: ['en'], + localeConfigs: { + en: { + label: 'English', + } + }, + }, + themeConfig: { + announcementBar: { + id: 'frigate_plus', + content: ` + 🚀 + Get more relevant and accurate detections with Frigate+ models. + Learn more + + `, + backgroundColor: '#005f73', + textColor: '#e0fbfc', + isCloseable: false, + }, + docs: { + sidebar: { + hideable: true, + }, + }, + inkeepConfig: { + baseSettings: { + apiKey: "b1a4c4d73c9b48aa5b3cdae6e4c81f0bb3d1134eeb5a7100", + integrationId: "cm6xmhn9h000gs601495fkkdx", + organizationId: "org_map2JQEOco8U1ZYY", + primaryBrandColor: "#010101", + }, + aiChatSettings: { + chatSubjectName: "Frigate", + botAvatarSrcUrl: "https://frigate.video/images/favicon.png", + getHelpCallToActions: [ + { + name: "GitHub", + url: "https://github.com/blakeblackshear/frigate", + icon: { + builtIn: "FaGithub", + }, + }, + ], + quickQuestions: [ + "How to configure and setup camera settings?", + "How to setup notifications?", + "Supported builtin detectors?", + "How to restream video feed?", + "How can I get sound or audio in my recordings?", + ], + }, + }, + prism: { + additionalLanguages: ["bash", "json"], + }, + languageTabs: [ + { + highlight: "python", + language: "python", + logoClass: "python", + }, + { + highlight: "javascript", + language: "nodejs", + logoClass: "nodejs", + }, + { + highlight: "javascript", + language: "javascript", + logoClass: "javascript", + }, + { + highlight: "bash", + language: "curl", + logoClass: "curl", + }, + { + highlight: "rust", + language: "rust", + logoClass: "rust", + }, + ], + navbar: { + title: "Frigate", + logo: { + alt: "Frigate", + src: "img/branding/logo.svg", + srcDark: "img/branding/logo-dark.svg", + }, + items: [ + { + to: "/", + activeBasePath: "docs", + label: "Docs", + position: "left", + }, + { + href: "https://frigate.video", + label: "Website", + position: "right", + }, + { + href: "http://demo.frigate.video", + label: "Demo", + position: "right", + }, + { + type: 'localeDropdown', + position: 'right', + dropdownItemsAfter: [ + { + label: '简体中文(社区翻译)', + href: 'https://docs.frigate-cn.video', + } + ] + }, + { + href: 'https://github.com/blakeblackshear/frigate', + label: 'GitHub', + position: 'right', + }, + ], + }, + footer: { + style: "dark", + links: [ + { + title: "Community", + items: [ + { + label: "GitHub", + href: "https://github.com/blakeblackshear/frigate", + }, + { + label: "Discussions", + href: "https://github.com/blakeblackshear/frigate/discussions", + }, + ], + }, + ], + copyright: `Copyright © ${new Date().getFullYear()} Frigate LLC`, + }, + }, + plugins: [ + path.resolve(__dirname, "plugins", "raw-loader"), + [ + "docusaurus-plugin-openapi-docs", + { + id: "openapi", + docsPluginId: "classic", // configured for preset-classic + config: { + frigateApi: { + specPath: "static/frigate-api.yaml", + outputDir: "docs/integrations/api", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "tag", + sidebarCollapsible: true, + sidebarCollapsed: true, + }, + showSchemas: true, + } satisfies OpenApiPlugin.Options, + }, + }, + ], + ] as PluginConfig[], + presets: [ + [ + "classic", + { + docs: { + routeBasePath: "/", + sidebarPath: "./sidebars.ts", + // Please change this to your repo. + editUrl: + "https://github.com/blakeblackshear/frigate/edit/master/docs/", + sidebarCollapsible: false, + docItemComponent: "@theme/ApiItem", // Derived from docusaurus-theme-openapi + }, + + theme: { + customCss: "./src/css/custom.css", + }, + } satisfies Preset.Options, + ], + ], +}; + +export default async function createConfig() { + return config; +} diff --git a/sam2-cpu/frigate-dev/docs/package-lock.json b/sam2-cpu/frigate-dev/docs/package-lock.json new file mode 100644 index 0000000..4d59547 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/package-lock.json @@ -0,0 +1,23362 @@ +{ + "name": "docs", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "0.0.0", + "dependencies": { + "@docusaurus/core": "^3.7.0", + "@docusaurus/plugin-content-docs": "^3.7.0", + "@docusaurus/preset-classic": "^3.7.0", + "@docusaurus/theme-mermaid": "^3.7.0", + "@inkeep/docusaurus": "^2.0.16", + "@mdx-js/react": "^3.1.0", + "clsx": "^2.1.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.1", + "prism-react-renderer": "^2.4.1", + "raw-loader": "^4.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/types": "^3.7.0", + "@types/react": "^18.3.27" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.20.tgz", + "integrity": "sha512-0DKAZP9SiphUHuT/HmCYrv0uNyHfqn4gT3e5LsL+y1n3mMhWrrKNS2QYn+ysVd7yOmrLyv30gzrCCdbjnN+vtw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@vercel/oidc": "3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.19.tgz", + "integrity": "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/react": { + "version": "2.0.113", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.113.tgz", + "integrity": "sha512-jAwWxIHrzRAP5Lwv+Z9be54Uxogd0QhUyfDAx/apVCyhszinotN7ABrQMXBQsbqXUmgvlncMvEEXxWQbpOT6iA==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider-utils": "3.0.19", + "ai": "5.0.111", + "swr": "^2.2.5", + "throttleit": "2.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1", + "zod": "^3.25.76 || ^4.1.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.0.tgz", + "integrity": "sha512-EfW0bfxjPs+C7ANkJDw2TATntfBKsFiy7APh+KO0pQ8A6HYa5I0NjFuCGCXWfzzzLXNZta3QUl3n5Kmm6aJo9Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.0.tgz", + "integrity": "sha512-eG5xV8rujK4ZIHXrRshvv9O13NmU/k42Rnd3w43iKH5RaQ2zWuZO6Q7XjaoJjAFVCsJWqRbXzbYyPGrbF3wGNg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.0.tgz", + "integrity": "sha512-AYh2uL8IUW9eZrbbT+wZElyb7QkkeV3US2NEKY7doqMlyPWE8lErNfkVN1NvZdVcY4/SVic5GDbeDz2ft8YIiQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.0.tgz", + "integrity": "sha512-0emZTaYOeI9WzJi0TcNd2k3SxiN6DZfdWc2x2gHt855Jl9jPUOzfVTL6gTvCCrOlT4McvpDGg5nGO+9doEjjig==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.0.tgz", + "integrity": "sha512-wrBJ8fE+M0TDG1As4DDmwPn2TXajrvmvAN72Qwpuv8e2JOKNohF7+JxBoF70ZLlvP1A1EiH8DBu+JpfhBbNphQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.0.tgz", + "integrity": "sha512-LnkeX4p0ENt0DoftDJJDzQQJig/sFQmD1eQifl/iSjhUOGUIKC/7VTeXRcKtQB78naS8njUAwpzFvxy1CDDXDQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.0.tgz", + "integrity": "sha512-aF9tc4ex/smypXw+W3lBPB1jjKoaGHpZezTqofvDOI/oK1dR2sdTpFpK2Ru+7IRzYgwtRqHF3znmTlyoNs9dpA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.0.tgz", + "integrity": "sha512-22SHEEVNjZfFWkFks3P6HilkR3rS7a6GjnCIqR22Zz4HNxdfT0FG+RE7efTcFVfLUkTTMQQybvaUcwMrHXYa7Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.0.tgz", + "integrity": "sha512-2LT0/Z+/sFwEpZLH6V17WSZ81JX2uPjgvv5eNlxgU7rPyup4NXXfuMbtCJ+6uc4RO/LQpEJd3Li59ke3wtyAsA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.0.tgz", + "integrity": "sha512-uivZ9wSWZ8mz2ZU0dgDvQwvVZV8XBv6lYBXf8UtkQF3u7WeTqBPeU8ZoeTyLpf0jAXCYOvc1mAVmK0xPLuEwOQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.0.tgz", + "integrity": "sha512-O2BB8DuySuddgOAbhyH4jsGbL+KyDGpzJRtkDZkv091OMomqIA78emhhMhX9d/nIRrzS1wNLWB/ix7Hb2eV5rg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.0.tgz", + "integrity": "sha512-eW6xyHCyYrJD0Kjk9Mz33gQ40LfWiEA51JJTVfJy3yeoRSw/NXhAL81Pljpa0qslTs6+LO/5DYPZddct6HvISQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.0.tgz", + "integrity": "sha512-Vn2+TukMGHy4PIxmdvP667tN/MhS7MPT8EEvEhS6JyFLPx3weLcxSa1F9gVvrfHWCUJhLWoMVJVB2PT8YfRGcw==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.0.tgz", + "integrity": "sha512-xaqXyna5yBZ+r1SJ9my/DM6vfTqJg9FJgVydRJ0lnO+D5NhqGW/qaRG/iBGKr/d4fho34el6WakV7BqJvrl/HQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.43.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz", + "integrity": "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz", + "integrity": "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz", + "integrity": "sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz", + "integrity": "sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz", + "integrity": "sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==", + "license": "MIT", + "dependencies": { + "@ai-sdk/react": "^2.0.30", + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.3.1", + "@docsearch/css": "4.3.2", + "ai": "^5.0.30", + "algoliasearch": "^5.28.0", + "marked": "^16.3.0", + "zod": "^4.1.8" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.9.2.tgz", + "integrity": "sha512-GEANdi/SgER+L7Japs25YiGil/AUDnFFHaCGPBbundxoWtCkA2lmy7/tFmgED4y1htAy6Oi4wkJEQdGssnw9MA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/runtime-corejs3": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.9.2.tgz", + "integrity": "sha512-ZOVi6GYgTcsZcUzjblpzk3wH1Fya2VNpd5jtHoCCFcJlMQ1EYXZetfAnRHLcyiFeBABaI1ltTYbOBtH/gahGVA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.9.2", + "@docusaurus/cssnano-preset": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", + "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.9.2", + "@docusaurus/bundler": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.1", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.6", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.9.2.tgz", + "integrity": "sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.9.2.tgz", + "integrity": "sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.9.2.tgz", + "integrity": "sha512-wiYoGwF9gdd6rev62xDU8AAM8JuLI/hlwOtCzMmYcspEkzecKrP8J8X+KpYnTlACBUUtXNJpSoCwFWJhLRevzQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.9.2.tgz", + "integrity": "sha512-8qVe2QA9hVLzvnxP46ysuofJUIc/yYQ82tvA/rBTrnpXtCjNSFLxEZfd5U8cYZuJIVlkPxamsIgwd5tGZXfvew==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.9.2.tgz", + "integrity": "sha512-3I2HXy3L1QcjLJLGAoTvoBnpOwa6DPUa3Q0dMK19UTY9mhPkKQg/DYhAGTiBUKcTR0f08iw7kLPqOhIgdV3eVQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", + "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.9.2.tgz", + "integrity": "sha512-s4849w/p4noXUrGpPUF0BPqIAfdAe76BLaRGAGKZ1gTDNiGxGcpsLcwJ9OTi1/V8A+AzvsmI9pkjie2zjIQZKA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.9.2.tgz", + "integrity": "sha512-w1s3+Ss+eOQbscGM4cfIFBlVg/QKxyYgj26k5AnakuHkKxH6004ZtuLe5awMBotIYF2bbGDoDhpgQ4r/kcj4rQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.9.2.tgz", + "integrity": "sha512-j7a5hWuAFxyQAkilZwhsQ/b3T7FfHZ+0dub6j/GxKNFJp2h9qk/P1Bp7vrGASnvA9KNQBBL1ZXTe7jlh4VdPdA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.9.2.tgz", + "integrity": "sha512-mAwwQJ1Us9jL/lVjXtErXto4p4/iaLlweC54yDUK1a97WfkC6Z2k5/769JsFgwOwOP+n5mUQGACXOEQ0XDuVUw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.9.2.tgz", + "integrity": "sha512-YJ4lDCphabBtw19ooSlc1MnxtYGpjFV9rEdzjLsUnBCeis2djUyCozZaFhCg6NGEwOn7HDDyMh0yzcdRpnuIvA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@types/gtag.js": "^0.0.12", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.9.2.tgz", + "integrity": "sha512-LJtIrkZN/tuHD8NqDAW1Tnw0ekOwRTfobWPsdO15YxcicBo2ykKF0/D6n0vVBfd3srwr9Z6rzrIWYrMzBGrvNw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.9.2.tgz", + "integrity": "sha512-WLh7ymgDXjG8oPoM/T4/zUP7KcSuFYRZAUTl8vR6VzYkfc18GBM4xLhcT+AKOwun6kBivYKUJf+vlqYJkm+RHw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.9.2.tgz", + "integrity": "sha512-n+1DE+5b3Lnf27TgVU5jM1d4x5tUh2oW5LTsBxJX4PsAPV0JGcmI6p3yLYtEY0LRVEIJh+8RsdQmRE66wSV8mw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.9.2.tgz", + "integrity": "sha512-IgyYO2Gvaigi21LuDIe+nvmN/dfGXAiMcV/murFqcpjnZc7jxFAxW+9LEjdPt61uZLxG4ByW/oUmX/DDK9t/8w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/plugin-css-cascade-layers": "3.9.2", + "@docusaurus/plugin-debug": "3.9.2", + "@docusaurus/plugin-google-analytics": "3.9.2", + "@docusaurus/plugin-google-gtag": "3.9.2", + "@docusaurus/plugin-google-tag-manager": "3.9.2", + "@docusaurus/plugin-sitemap": "3.9.2", + "@docusaurus/plugin-svgr": "3.9.2", + "@docusaurus/theme-classic": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-search-algolia": "3.9.2", + "@docusaurus/types": "3.9.2" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.9.2.tgz", + "integrity": "sha512-IGUsArG5hhekXd7RDb11v94ycpJpFdJPkLnt10fFQWOVxAtq5/D7hT6lzc2fhyQKaaCE62qVajOMKL7OiAFAIA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/plugin-content-blog": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/plugin-content-pages": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", + "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", + "integrity": "sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "^3.9.0 || ^4.1.0", + "@docusaurus/core": "3.9.2", + "@docusaurus/logger": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/theme-translations": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.9.2.tgz", + "integrity": "sha512-vIryvpP18ON9T9rjgMRFLr2xJVDpw1rtagEGf8Ccce4CkTrvM/fRB8N2nyWYOW5u3DdjkwKw5fBa+3tbn9P4PA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/types": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.9.2.tgz", + "integrity": "sha512-Ux1JUNswg+EfUEmajJjyhIohKceitY/yzjRUpu04WXgvVz+fbhVC0p+R0JhvEu4ytw8zIAys2hrdpQPBHRIa8Q==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", + "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "gray-matter": "^4.0.3", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.9.2.tgz", + "integrity": "sha512-I53UC1QctruA6SWLvbjbhCpAw7+X7PePoe5pYcwTOEXD/PxeP8LnECAhTHHwWCblyUX5bMi4QLRkxvyZ+IT8Aw==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.9.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", + "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.9.2", + "@docusaurus/utils": "3.9.2", + "@docusaurus/utils-common": "3.9.2", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "deprecated": "Please update to a newer version.", + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@hookform/error-message": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz", + "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@inkeep/docusaurus": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@inkeep/docusaurus/-/docusaurus-2.0.16.tgz", + "integrity": "sha512-dQhjlvFnl3CVr0gWeJ/V/qLnDy1XYrCfkdVSa2D3gJTxI9/vOf9639Y1aPxTxO88DiXuW9CertLrZLB6SoJ2yg==", + "license": "MIT" + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@redocly/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", + "integrity": "sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==", + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz", + "integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==", + "license": "MIT", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz", + "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vercel/oidc": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ai": { + "version": "5.0.111", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.111.tgz", + "integrity": "sha512-kD1eBl3ZbSYIz9lZe0HvQpO23HruBFfqxUl0S/MtoDF4DCmfCtKhsGGGIvoIcMpjiLlJjtF//ZWcYu+v/3YRzg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "2.0.20", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.19", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.0.tgz", + "integrity": "sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.12.0", + "@algolia/client-abtesting": "5.46.0", + "@algolia/client-analytics": "5.46.0", + "@algolia/client-common": "5.46.0", + "@algolia/client-insights": "5.46.0", + "@algolia/client-personalization": "5.46.0", + "@algolia/client-query-suggestions": "5.46.0", + "@algolia/client-search": "5.46.0", + "@algolia/ingestion": "1.46.0", + "@algolia/monitoring": "1.46.0", + "@algolia/recommend": "5.46.0", + "@algolia/requester-browser-xhr": "5.46.0", + "@algolia/requester-fetch": "5.46.0", + "@algolia/requester-node-http": "5.46.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz", + "integrity": "sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/allof-merge": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/allof-merge/-/allof-merge-0.6.7.tgz", + "integrity": "sha512-slvjkM56OdeVkm1tllrnaumtSHwqyHrepXkAe6Am+CW4WdbHkNqdOKPF6cvY3/IouzvXk1BoLICT5LY7sCoFGw==", + "license": "MIT", + "dependencies": { + "json-crawl": "^0.5.3" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "dependencies": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", + "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.5.2.tgz", + "integrity": "sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-package-manager": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-3.0.2.tgz", + "integrity": "sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/detect-port": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", + "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", + "license": "MIT", + "dependencies": { + "address": "^1.0.1", + "debug": "4" + }, + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-plugin-openapi-docs": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.5.1.tgz", + "integrity": "sha512-3I6Sjz19D/eM86a24/nVkYfqNkl/zuXSP04XVo7qm/vlPeCpHVM4li2DLj7PzElr6dlS9RbaS4HVIQhEOPGBRQ==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.4", + "@redocly/openapi-core": "^1.10.5", + "allof-merge": "^0.6.6", + "chalk": "^4.1.2", + "clsx": "^1.1.1", + "fs-extra": "^9.0.1", + "json-pointer": "^0.6.2", + "json5": "^2.2.3", + "lodash": "^4.17.20", + "mustache": "^4.2.0", + "openapi-to-postmanv2": "^4.21.0", + "postman-collection": "^4.4.0", + "slugify": "^1.6.5", + "swagger2openapi": "^7.0.8", + "xml-formatter": "^2.6.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "^3.5.0", + "@docusaurus/utils": "^3.5.0", + "@docusaurus/utils-validation": "^3.5.0", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/docusaurus-plugin-openapi-docs/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-plugin-openapi-docs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docusaurus-plugin-sass": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.6.tgz", + "integrity": "sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "sass-loader": "^16.0.2" + }, + "peerDependencies": { + "@docusaurus/core": "^2.0.0-beta || ^3.0.0-alpha", + "sass": "^1.30.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.5.1.tgz", + "integrity": "sha512-C7mYh9JC3l9jjRtqJVu0EIyOgxHB08jE0Tp5NSkNkrrBak4A13SrXCisNjvt1eaNjS+tsz7qD0bT3aI5hsRvWA==", + "license": "MIT", + "dependencies": { + "@hookform/error-message": "^2.0.1", + "@reduxjs/toolkit": "^1.7.1", + "allof-merge": "^0.6.6", + "buffer": "^6.0.3", + "clsx": "^1.1.1", + "copy-text-to-clipboard": "^3.1.0", + "crypto-js": "^4.1.1", + "file-saver": "^2.0.5", + "lodash": "^4.17.20", + "pako": "^2.1.0", + "postman-code-generators": "^1.10.1", + "postman-collection": "^4.4.0", + "prism-react-renderer": "^2.3.0", + "process": "^0.11.10", + "react-hook-form": "^7.43.8", + "react-live": "^4.0.0", + "react-magic-dropzone": "^1.0.1", + "react-markdown": "^8.0.1", + "react-modal": "^3.15.1", + "react-redux": "^7.2.0", + "rehype-raw": "^6.1.1", + "remark-gfm": "3.0.1", + "sass": "^1.80.4", + "sass-loader": "^16.0.2", + "unist-util-visit": "^5.0.0", + "url": "^0.11.1", + "xml-formatter": "^2.6.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@docusaurus/theme-common": "^3.5.0", + "docusaurus-plugin-openapi-docs": "^4.0.0", + "docusaurus-plugin-sass": "^0.2.3", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-raw/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/docusaurus-theme-openapi-docs/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "license": "MIT" + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", + "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-reasons": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", + "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", + "license": "Apache-2.0" + }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "license": "MIT" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-crawl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/json-crawl/-/json-crawl-0.5.3.tgz", + "integrity": "sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "license": "MIT", + "dependencies": { + "foreach": "^2.0.4" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4" + } + }, + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "license": "MIT", + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.27", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", + "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/liquid-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", + "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-definitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-format": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.1.tgz", + "integrity": "sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==", + "license": "Apache-2.0", + "dependencies": { + "charset": "^1.0.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/neotraverse": { + "version": "0.6.15", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.15.tgz", + "integrity": "sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver-browser": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz", + "integrity": "sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "path-browserify": "^1.0.1", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-to-postmanv2": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-4.25.0.tgz", + "integrity": "sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "8.11.0", + "ajv-draft-04": "1.0.0", + "ajv-formats": "2.1.1", + "async": "3.2.4", + "commander": "2.20.3", + "graphlib": "2.1.8", + "js-yaml": "4.1.0", + "json-pointer": "0.6.2", + "json-schema-merge-allof": "0.8.1", + "lodash": "4.17.21", + "neotraverse": "0.6.15", + "oas-resolver-browser": "2.5.6", + "object-hash": "3.0.0", + "path-browserify": "1.0.1", + "postman-collection": "^4.4.0", + "swagger2openapi": "7.0.8", + "yaml": "1.10.2" + }, + "bin": { + "openapi2postmanv2": "bin/openapi2postmanv2.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openapi-to-postmanv2/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/openapi-to-postmanv2/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.5.0.tgz", + "integrity": "sha512-xgxFQPAPxeWmsgy8cR7GM1PGAL/smA5E9qU7K//D4vucS01es3M0fDujhDJn3kY8Ip7/vVYcecbe1yY+vBo3qQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.22", + "browserslist": "^4.28.0", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.5.2", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postman-code-generators": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/postman-code-generators/-/postman-code-generators-1.14.2.tgz", + "integrity": "sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "async": "3.2.2", + "detect-package-manager": "3.0.2", + "lodash": "4.17.21", + "path": "0.12.7", + "postman-collection": "^4.4.0", + "shelljs": "0.8.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postman-code-generators/node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "license": "MIT" + }, + "node_modules/postman-collection": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.5.0.tgz", + "integrity": "sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==", + "license": "Apache-2.0", + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.6.3", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/postman-url-encoder": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", + "integrity": "sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==", + "license": "Apache-2.0", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/raw-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/raw-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/raw-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/raw-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.68.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", + "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-live": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/react-live/-/react-live-4.1.8.tgz", + "integrity": "sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==", + "license": "MIT", + "dependencies": { + "prism-react-renderer": "^2.4.0", + "sucrase": "^3.35.0", + "use-editable": "^2.3.3" + }, + "engines": { + "node": ">= 0.12.0", + "npm": ">= 2.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", + "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-magic-dropzone": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz", + "integrity": "sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-markdown/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/react-markdown/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/react-markdown/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/react-markdown/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown/node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-markdown/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "license": "MIT", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "license": "MIT", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.96.0.tgz", + "integrity": "sha512-8u4xqqUeugGNCYwr9ARNtQKTOj4KmYiJAVKXf2CTIivTCR51j96htbMKWDru8H5SaQWpyVgTfOF8Ylyf5pun1Q==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.6", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.6.tgz", + "integrity": "sha512-sglGzId5gmlfxNs4gK2U3h7HlVRfx278YK6Ono5lwzuvi1jxig80YiuHkaDBVsYIKFhx8wN7XSCI0M2IDS/3qA==", + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", + "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", + "license": "BlueOak-1.0.0" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.2", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", + "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-js/node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/style-to-js/node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swr": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.7.tgz", + "integrity": "sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/use-editable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/use-editable/-/use-editable-2.3.3.tgz", + "integrity": "sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==", + "license": "MIT" + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" + }, + "node_modules/validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "dependencies": { + "validate.io-number": "^1.0.3" + } + }, + "node_modules/validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "node_modules/validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==" + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpackbar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", + "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "consola": "^3.2.3", + "figures": "^3.2.0", + "markdown-table": "^2.0.0", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "webpack": "3 || 4 || 5" + } + }, + "node_modules/webpackbar/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/webpackbar/node_modules/markdown-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", + "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpackbar/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpackbar/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-formatter": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.1.tgz", + "integrity": "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==", + "license": "MIT", + "dependencies": { + "xml-parser-xo": "^3.2.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xml-parser-xo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", + "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/docs/package.json b/sam2-cpu/frigate-dev/docs/package.json new file mode 100644 index 0000000..0ff76c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/package.json @@ -0,0 +1,54 @@ +{ + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "npm run regen-docs && docusaurus start --host 0.0.0.0", + "build": "npm run regen-docs && docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "gen-api-docs": "docusaurus gen-api-docs all", + "clear-api-docs": "docusaurus clean-api-docs all", + "regen-docs": "npm run clear-api-docs && npm run gen-api-docs", + "serve": "docusaurus serve --host 0.0.0.0", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "^3.7.0", + "@docusaurus/plugin-content-docs": "^3.7.0", + "@docusaurus/preset-classic": "^3.7.0", + "@docusaurus/theme-mermaid": "^3.7.0", + "@inkeep/docusaurus": "^2.0.16", + "@mdx-js/react": "^3.1.0", + "clsx": "^2.1.1", + "docusaurus-plugin-openapi-docs": "^4.5.1", + "docusaurus-theme-openapi-docs": "^4.5.1", + "prism-react-renderer": "^2.4.1", + "raw-loader": "^4.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/types": "^3.7.0", + "@types/react": "^18.3.27" + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/sam2-cpu/frigate-dev/docs/plugins/raw-loader.js b/sam2-cpu/frigate-dev/docs/plugins/raw-loader.js new file mode 100644 index 0000000..693d099 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/plugins/raw-loader.js @@ -0,0 +1,12 @@ +module.exports = function (context, options) { + return { + name: 'labelmap', + configureWebpack(config, isServer, utils) { + return { + module: { + rules: [{ test: /\.txt$/, use: 'raw-loader' }], + }, + }; + }, + }; +}; diff --git a/sam2-cpu/frigate-dev/docs/sidebars.ts b/sam2-cpu/frigate-dev/docs/sidebars.ts new file mode 100644 index 0000000..1f5d057 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/sidebars.ts @@ -0,0 +1,143 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; +import { PropSidebarItemLink } from "@docusaurus/plugin-content-docs"; +import frigateHttpApiSidebar from "./docs/integrations/api/sidebar"; + +const sidebars: SidebarsConfig = { + docs: { + Frigate: [ + "frigate/index", + "frigate/hardware", + "frigate/planning_setup", + "frigate/installation", + "frigate/updating", + "frigate/camera_setup", + "frigate/video_pipeline", + "frigate/glossary", + ], + Guides: [ + "guides/getting_started", + "guides/configuring_go2rtc", + "guides/ha_notifications", + "guides/ha_network_storage", + "guides/reverse_proxy", + ], + Configuration: { + "Configuration Files": [ + "configuration/index", + "configuration/reference", + { + type: "link", + label: "Go2RTC Configuration Reference", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.10#configuration", + } as PropSidebarItemLink, + ], + Detectors: [ + "configuration/object_detectors", + "configuration/audio_detectors", + ], + Enrichments: [ + "configuration/semantic_search", + "configuration/face_recognition", + "configuration/license_plate_recognition", + "configuration/bird_classification", + { + type: "category", + label: "Custom Classification", + link: { + type: "generated-index", + title: "Custom Classification", + description: "Configuration for custom classification models", + }, + items: [ + "configuration/custom_classification/state_classification", + "configuration/custom_classification/object_classification", + ], + }, + { + type: "category", + label: "Generative AI", + link: { + type: "generated-index", + title: "Generative AI", + description: "Generative AI Features", + }, + items: [ + "configuration/genai/genai_config", + "configuration/genai/genai_review", + "configuration/genai/genai_objects", + ], + }, + ], + Cameras: [ + "configuration/cameras", + "configuration/review", + "configuration/record", + "configuration/snapshots", + "configuration/motion_detection", + "configuration/birdseye", + "configuration/live", + "configuration/restream", + "configuration/autotracking", + "configuration/camera_specific", + ], + Objects: [ + "configuration/object_filters", + "configuration/masks", + "configuration/zones", + "configuration/objects", + "configuration/stationary_objects", + ], + "Hardware Acceleration": [ + "configuration/hardware_acceleration_video", + "configuration/hardware_acceleration_enrichments", + ], + "Extra Configuration": [ + "configuration/authentication", + "configuration/notifications", + "configuration/ffmpeg_presets", + "configuration/pwa", + "configuration/tls", + "configuration/advanced", + ], + }, + Integrations: [ + "integrations/plus", + "integrations/home-assistant", + // This is the HTTP API generated by OpenAPI + { + type: "category", + label: "HTTP API", + link: { + type: "generated-index", + title: "Frigate HTTP API", + description: "HTTP API", + slug: "/integrations/api/frigate-http-api", + }, + items: frigateHttpApiSidebar, + }, + "integrations/mqtt", + "integrations/homekit", + "configuration/metrics", + "integrations/third_party_extensions", + ], + "Frigate+": [ + "plus/index", + "plus/annotating", + "plus/first_model", + "plus/faq", + ], + Troubleshooting: [ + "troubleshooting/faqs", + "troubleshooting/recordings", + "troubleshooting/gpu", + "troubleshooting/edgetpu", + "troubleshooting/memory", + ], + Development: [ + "development/contributing", + "development/contributing-boards", + ], + }, +}; + +export default sidebars; diff --git a/sam2-cpu/frigate-dev/docs/src/components/CommunityBadge/index.jsx b/sam2-cpu/frigate-dev/docs/src/components/CommunityBadge/index.jsx new file mode 100644 index 0000000..67b9a9e --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/src/components/CommunityBadge/index.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +export default function CommunityBadge() { + return ( + + Community Supported + + ); +} diff --git a/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/index.jsx b/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/index.jsx new file mode 100644 index 0000000..b786c8a --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/index.jsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from 'react'; +import { useLocation } from '@docusaurus/router'; +import styles from './styles.module.css'; + +export default function LanguageAlert() { + const [showAlert, setShowAlert] = useState(false); + const { pathname } = useLocation(); + + useEffect(() => { + const userLanguage = navigator?.language || 'en'; + const isChineseUser = userLanguage.includes('zh'); + setShowAlert(isChineseUser); + + }, [pathname]); + + if (!showAlert) return null; + + return ( +
+ 检测到您的主要语言为中文,您可以访问由中文社区翻译的 + 中文文档 + 以获得更好的体验 +
+ ); +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/styles.module.css b/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/styles.module.css new file mode 100644 index 0000000..d415849 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/src/components/LanguageAlert/styles.module.css @@ -0,0 +1,18 @@ +.alert { + padding: 12px; + background: #fff8e6; + border-bottom: 1px solid #ffd166; + text-align: center; + font-size: 15px; +} + +[data-theme="dark"] .alert { + background: #3b2f0b; + border-bottom: 1px solid #665c22; +} + +.alert a { + color: #1890ff; + font-weight: 500; + margin-left: 6px; +} diff --git a/sam2-cpu/frigate-dev/docs/src/css/custom.css b/sam2-cpu/frigate-dev/docs/src/css/custom.css new file mode 100644 index 0000000..9a572ec --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/src/css/custom.css @@ -0,0 +1,236 @@ +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #3b82f7; + --ifm-color-primary-dark: #1d4ed8; + --ifm-color-primary-darker: #1e40af; + --ifm-color-primary-darkest: #1e3a8a; + --ifm-color-primary-light: #60a5fa; + --ifm-color-primary-lighter: #93c5fd; + --ifm-color-primary-lightest: #dbeafe; + --ifm-code-font-size: 95%; +} + +.docusaurus-highlight-code-line { + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +/** + Custom CSS for OpenAPI Specification. Based of openapi https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/main/demo +*/ + +/* Sidebar Method labels */ +.api-method > .menu__link, +.schema > .menu__link { + align-items: center; + justify-content: start; +} + +.api-method > .menu__link::before, +.schema > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + border: 1px solid; + margin-right: var(--ifm-spacing-horizontal); + text-align: center; + flex-shrink: 0; + border-color: transparent; + color: white; +} + +.get > .menu__link::before { + content: "get"; + background-color: var(--ifm-color-primary); +} + +.post > .menu__link::before { + content: "post"; + background-color: var(--ifm-color-success); +} + +.delete > .menu__link::before { + content: "del"; + background-color: var(--openapi-code-red); +} + +.put > .menu__link::before { + content: "put"; + background-color: var(--openapi-code-blue); +} + +.patch > .menu__link::before { + content: "patch"; + background-color: var(--openapi-code-orange); +} + +.head > .menu__link::before { + content: "head"; + background-color: var(--ifm-color-secondary-darkest); +} + +.event > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-darkest); +} + +.schema > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-darkest); +} + +.menu__list-item--deprecated > .menu__link, +.menu__list-item--deprecated > .menu__link:hover { + text-decoration: line-through; +} +/* Sidebar Method labels High Contrast */ +.api-method-contrast > .menu__link, +.schema-contrast > .menu__link { + align-items: center; + justify-content: start; +} + +.api-method-contrast > .menu__link::before, +.schema-contrast > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + border: 1px solid; + border-inline-start-width: 5px; + margin-right: var(--ifm-spacing-horizontal); + text-align: center; + flex-shrink: 0; +} + +.get-contrast > .menu__link::before { + content: "get"; + background-color: var(--ifm-color-info-contrast-background); + color: var(--ifm-color-info-contrast-foreground); + border-color: var(--ifm-color-info-dark); +} + +.post-contrast > .menu__link::before { + content: "post"; + background-color: var(--ifm-color-success-contrast-background); + color: var(--ifm-color-success-contrast-foreground); + border-color: var(--ifm-color-success-dark); +} + +.delete-contrast > .menu__link::before { + content: "del"; + background-color: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-contrast-foreground); + border-color: var(--ifm-color-danger-dark); +} + +.put-contrast > .menu__link::before { + content: "put"; + background-color: var(--ifm-color-warning-contrast-background); + color: var(--ifm-color-warning-contrast-foreground); + border-color: var(--ifm-color-warning-dark); +} + +.patch-contrast > .menu__link::before { + content: "patch"; + background-color: var(--ifm-color-success-contrast-background); + color: var(--ifm-color-success-contrast-foreground); + border-color: var(--ifm-color-success-dark); +} + +.head-contrast > .menu__link::before { + content: "head"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +.event-contrast > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +.schema-contrast > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +/* Simple */ +.api-method-simple > .menu__link { + align-items: center; + justify-content: start; +} +.api-method-simple > .menu__link::before { + width: 55px; + height: 20px; + font-size: 12px; + line-height: 20px; + text-transform: uppercase; + font-weight: 600; + border-radius: 0.25rem; + align-content: start; + margin-right: var(--ifm-spacing-horizontal); + text-align: right; + flex-shrink: 0; + border-color: transparent; +} + +.get-simple > .menu__link::before { + content: "get"; + color: var(--ifm-color-info); +} + +.post-simple > .menu__link::before { + content: "post"; + color: var(--ifm-color-success); +} + +.delete-simple > .menu__link::before { + content: "del"; + color: var(--ifm-color-danger); +} + +.put-simple > .menu__link::before { + content: "put"; + color: var(--ifm-color-warning); +} + +.patch-simple > .menu__link::before { + content: "patch"; + color: var(--ifm-color-warning); +} + +.head-simple > .menu__link::before { + content: "head"; + color: var(--ifm-color-secondary-contrast-foreground); +} + +.event-simple > .menu__link::before { + content: "event"; + color: var(--ifm-color-secondary-contrast-foreground); +} + +.schema-simple > .menu__link::before { + content: "schema"; + color: var(--ifm-color-secondary-contrast-foreground); +} diff --git a/sam2-cpu/frigate-dev/docs/src/theme/Navbar/index.js b/sam2-cpu/frigate-dev/docs/src/theme/Navbar/index.js new file mode 100644 index 0000000..4dd3aee --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/src/theme/Navbar/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import NavbarLayout from '@theme/Navbar/Layout'; +import NavbarContent from '@theme/Navbar/Content'; +import LanguageAlert from '../../components/LanguageAlert'; + +export default function Navbar() { + return ( + <> + + + + + + ); +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/docs/static/.nojekyll b/sam2-cpu/frigate-dev/docs/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/docs/static/frigate-api.yaml b/sam2-cpu/frigate-dev/docs/static/frigate-api.yaml new file mode 100644 index 0000000..6246889 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/static/frigate-api.yaml @@ -0,0 +1,5349 @@ +openapi: 3.1.0 +info: + # To avoid the introduction page we set the title to empty string + # https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/4e771d309f6defe395449b26cc3c65814d72cbcc/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts#L92-L129 + title: "" + version: 0.1.0 + +servers: + - url: https://demo.frigate.video/api + - url: http://localhost:5001/api + +paths: + /auth: + get: + tags: + - Auth + summary: Authenticate request + description: |- + Authenticates the current request based on proxy headers or JWT token. + This endpoint verifies authentication credentials and manages JWT token refresh. + On success, no JSON body is returned; authentication state is communicated via response headers and cookies. + operationId: auth_auth_get + responses: + "202": + description: Authentication Accepted (no response body, different headers depending on auth method) + headers: + remote-user: + description: Authenticated username or "anonymous" in proxy-only mode + schema: + type: string + remote-role: + description: Resolved role (e.g., admin, viewer, or custom) + schema: + type: string + Set-Cookie: + description: May include refreshed JWT cookie ("frigate-token") when applicable + schema: + type: string + "401": + description: Authentication Failed + /profile: + get: + tags: + - Auth + summary: Get user profile + description: |- + Returns the current authenticated user's profile including username, role, and allowed cameras. + This endpoint requires authentication and returns information about the user's permissions. + operationId: profile_profile_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "401": + description: Unauthorized + /logout: + get: + tags: + - Auth + summary: Logout user + description: |- + Logs out the current user by clearing the session cookie. + After logout, subsequent requests will require re-authentication. + operationId: logout_logout_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "303": + description: See Other (redirects to login page) + /login: + post: + tags: + - Auth + summary: Login with credentials + description: |- + Authenticates a user with username and password. + Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. + The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header. + + Example using Bearer token: + ``` + curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile + ``` + operationId: login_login_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPostLoginBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "401": + description: Login Failed - Invalid credentials + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /users: + get: + tags: + - Auth + summary: Get all users + description: |- + Returns a list of all users with their usernames and roles. + Requires admin role. Each user object contains the username and assigned role. + operationId: get_users_users_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "403": + description: Forbidden - Admin role required + post: + tags: + - Auth + summary: Create new user + description: |- + Creates a new user with the specified username, password, and role. + Requires admin role. Password must meet strength requirements: + - Minimum 8 characters + - At least one uppercase letter + - At least one digit + - At least one special character (!@#$%^&*(),.?":{}\|<>) + operationId: create_user_users_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPostUsersBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "400": + description: Bad Request - Invalid username or role + content: + application/json: + schema: {} + "403": + description: Forbidden - Admin role required + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /users/{username}: + delete: + tags: + - Auth + summary: Delete user + description: |- + Deletes a user by username. The built-in admin user cannot be deleted. + Requires admin role. Returns success message or error if user not found. + operationId: delete_user_users__username__delete + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + description: The username of the user to delete + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "403": + description: Forbidden - Cannot delete admin user or admin role required + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /users/{username}/password: + put: + tags: + - Auth + summary: Update user password + description: |- + Updates a user's password. Users can only change their own password unless they have admin role. + Requires the current password to verify identity for non-admin users. + Password must meet strength requirements: + - Minimum 8 characters + - At least one uppercase letter + - At least one digit + - At least one special character (!@#$%^&*(),.?":{}\|<>) + + If user changes their own password, a new JWT cookie is automatically issued. + operationId: update_password_users__username__password_put + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + description: The username of the user whose password to update + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPutPasswordBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "400": + description: Bad Request - Current password required or password doesn't meet requirements + "401": + description: Unauthorized - Current password is incorrect + "403": + description: Forbidden - Viewers can only update their own password + "404": + description: Not Found - User not found + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /users/{username}/role: + put: + tags: + - Auth + summary: Update user role + description: |- + Updates a user's role. The built-in admin user's role cannot be modified. + Requires admin role. Valid roles are defined in the configuration. + operationId: update_role_users__username__role_put + parameters: + - name: username + in: path + required: true + schema: + type: string + title: Username + description: The username of the user whose role to update + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPutRoleBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "400": + description: Bad Request - Invalid role + "403": + description: Forbidden - Cannot modify admin user's role or admin role required + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces: + get: + tags: + - Classification + summary: Get all registered faces + description: |- + Returns a dictionary mapping face names to lists of image filenames. + Each key represents a registered face name, and the value is a list of image + files associated with that face. Supported image formats include .webp, .png, + .jpg, and .jpeg. + operationId: get_faces_faces_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/FacesResponse" + /faces/reprocess: + post: + tags: + - Classification + summary: Reprocess a face training image + description: |- + Reprocesses a face training image to update the prediction. + Requires face recognition to be enabled in the configuration. The training file + must exist in the faces/train directory. Returns a success response or an error + message if face recognition is not enabled or the training file is invalid. + operationId: reclassify_face_faces_reprocess_post + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/train/{name}/classify: + post: + tags: + - Classification + summary: Classify and save a face training image + description: |- + Adds a training image to a specific face name for face recognition. + Accepts either a training file from the train directory or an event_id to extract + the face from. The image is saved to the face's directory and the face classifier + is cleared to incorporate the new training data. Returns a success message with + the new filename or an error if face recognition is not enabled, the file/event + is invalid, or the face cannot be extracted. + operationId: train_face_faces_train__name__classify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/{name}/create: + post: + tags: + - Classification + summary: Create a new face name + description: |- + Creates a new folder for a face name in the faces directory. + This is used to organize face training images. The face name is sanitized and + spaces are replaced with underscores. Returns a success message or an error if + face recognition is not enabled. + operationId: create_face_faces__name__create_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/{name}/register: + post: + tags: + - Classification + summary: Register a face image + description: >- + Registers a face image for a specific face name by uploading an image + file. + The uploaded image is processed and added to the face recognition system. Returns a + success response with details about the registration, or an error if face recognition + is not enabled or the image cannot be processed. + operationId: register_face_faces__name__register_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: >- + #/components/schemas/Body_register_face_faces__name__register_post + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/recognize: + post: + tags: + - Classification + summary: Recognize a face from an uploaded image + description: |- + Recognizes a face from an uploaded image file by comparing it against + registered faces in the system. Returns the recognized face name and confidence score, + or an error if face recognition is not enabled or the image cannot be processed. + operationId: recognize_face_faces_recognize_post + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/Body_recognize_face_faces_recognize_post" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/FaceRecognitionResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/{name}/delete: + post: + tags: + - Classification + summary: Delete face images + description: >- + Deletes specific face images for a given face name. The image IDs must + belong + to the specified face folder. To delete an entire face folder, all image IDs in that + folder must be sent. Returns a success message or an error if face recognition is not enabled. + operationId: deregister_faces_faces__name__delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DeleteFaceImagesBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /faces/{old_name}/rename: + put: + tags: + - Classification + summary: Rename a face name + description: |- + Renames a face name in the system. The old name must exist and the new + name must be valid. Returns a success message or an error if face recognition is not enabled. + operationId: rename_face_faces__old_name__rename_put + parameters: + - name: old_name + in: path + required: true + schema: + type: string + title: Old Name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RenameFaceBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /lpr/reprocess: + put: + tags: + - Classification + summary: Reprocess a license plate + description: |- + Reprocesses a license plate image to update the plate. + Requires license plate recognition to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if license plate + recognition is not enabled or the event_id is invalid. + operationId: reprocess_license_plate_lpr_reprocess_put + parameters: + - name: event_id + in: query + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /reindex: + put: + tags: + - Classification + summary: Reindex embeddings + description: |- + Reindexes the embeddings for all tracked objects. + Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled. + operationId: reindex_embeddings_reindex_put + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + /audio/transcribe: + put: + tags: + - Classification + summary: Transcribe audio + description: |- + Transcribes audio from a specific event. + Requires audio transcription to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid. + operationId: transcribe_audio_audio_transcribe_put + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AudioTranscriptionBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /classification/{name}/dataset: + get: + tags: + - Classification + summary: Get classification dataset + description: |- + Gets the dataset for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: get_classification_dataset_classification__name__dataset_get + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /classification/{name}/train: + get: + tags: + - Classification + summary: Get classification train images + description: |- + Gets the train images for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: get_classification_images_classification__name__train_get + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + post: + tags: + - Classification + summary: Train a classification model + description: |- + Trains a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid. + operationId: train_configured_model_classification__name__train_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /classification/{name}/dataset/{category}/delete: + post: + tags: + - Classification + summary: Delete classification dataset images + description: >- + Deletes specific dataset images for a given classification model and + category. + The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid. + operationId: >- + delete_classification_dataset_images_classification__name__dataset__category__delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: category + in: path + required: true + schema: + type: string + title: Category + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /classification/{name}/dataset/categorize: + post: + tags: + - Classification + summary: Categorize a classification image + description: >- + Categorizes a specific classification image for a given classification + model and category. + The image must exist in the specified category. Returns a success message or an error if the name or category is invalid. + operationId: >- + categorize_classification_image_classification__name__dataset_categorize_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /classification/{name}/train/delete: + post: + tags: + - Classification + summary: Delete classification train images + description: |- + Deletes specific train images for a given classification model. + The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid. + operationId: >- + delete_classification_train_images_classification__name__train_delete_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review: + get: + tags: + - Review + summary: Review + operationId: review_review_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + type: string + default: all + title: Labels + - name: zones + in: query + required: false + schema: + type: string + default: all + title: Zones + - name: reviewed + in: query + required: false + schema: + type: integer + default: 0 + title: Reviewed + - name: limit + in: query + required: false + schema: + type: integer + title: Limit + - name: severity + in: query + required: false + schema: + $ref: "#/components/schemas/SeverityEnum" + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReviewSegmentResponse" + title: Response Review Review Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review_ids: + get: + tags: + - Review + summary: Review Ids + operationId: review_ids_review_ids_get + parameters: + - name: ids + in: query + required: true + schema: + type: string + title: Ids + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReviewSegmentResponse" + title: Response Review Ids Review Ids Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/summary: + get: + tags: + - Review + summary: Review Summary + operationId: review_summary_review_summary_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + type: string + default: all + title: Labels + - name: zones + in: query + required: false + schema: + type: string + default: all + title: Zones + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/ReviewSummaryResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /reviews/viewed: + post: + tags: + - Review + summary: Set Multiple Reviewed + operationId: set_multiple_reviewed_reviews_viewed_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ReviewModifyMultipleBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /reviews/delete: + post: + tags: + - Review + summary: Delete Reviews + operationId: delete_reviews_reviews_delete_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ReviewModifyMultipleBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/activity/motion: + get: + tags: + - Review + summary: Motion Activity + description: Get motion and audio activity. + operationId: motion_activity_review_activity_motion_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + - name: scale + in: query + required: false + schema: + type: integer + default: 30 + title: Scale + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ReviewActivityMotionResponse" + title: Response Motion Activity Review Activity Motion Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/event/{event_id}: + get: + tags: + - Review + summary: Get Review From Event + operationId: get_review_from_event_review_event__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/ReviewSegmentResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/{review_id}: + get: + tags: + - Review + summary: Get Review + operationId: get_review_review__review_id__get + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/ReviewSegmentResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/{review_id}/viewed: + delete: + tags: + - Review + summary: Set Not Reviewed + operationId: set_not_reviewed_review__review_id__viewed_delete + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/summarize/start/{start_ts}/end/{end_ts}: + post: + tags: + - Review + summary: Generate Review Summary + description: Use GenAI to summarize review items over a period of time. + operationId: >- + generate_review_summary_review_summarize_start__start_ts__end__end_ts__post + parameters: + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /: + get: + tags: + - App + summary: Is Healthy + operationId: is_healthy__get + responses: + "200": + description: Successful Response + content: + text/plain: + schema: + type: string + /config/schema.json: + get: + tags: + - App + summary: Config Schema + operationId: config_schema_config_schema_json_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /go2rtc/streams: + get: + tags: + - App + summary: Go2Rtc Streams + operationId: go2rtc_streams_go2rtc_streams_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /go2rtc/streams/{camera_name}: + get: + tags: + - App + summary: Go2Rtc Camera Stream + operationId: go2rtc_camera_stream_go2rtc_streams__camera_name__get + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /version: + get: + tags: + - App + summary: Version + operationId: version_version_get + responses: + "200": + description: Successful Response + content: + text/plain: + schema: + type: string + /stats: + get: + tags: + - App + summary: Stats + operationId: stats_stats_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /stats/history: + get: + tags: + - App + summary: Stats History + operationId: stats_history_stats_history_get + parameters: + - name: keys + in: query + required: false + schema: + type: string + title: Keys + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /metrics: + get: + tags: + - App + summary: Metrics + description: Expose Prometheus metrics endpoint and update metrics with latest stats + operationId: metrics_metrics_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /config: + get: + tags: + - App + summary: Config + operationId: config_config_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /config/raw: + get: + tags: + - App + summary: Config Raw + operationId: config_raw_config_raw_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /config/save: + post: + tags: + - App + summary: Config Save + operationId: config_save_config_save_post + parameters: + - name: save_option + in: query + required: true + schema: + type: string + title: Save Option + requestBody: + required: true + content: + text/plain: + schema: + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /config/set: + put: + tags: + - App + summary: Config Set + operationId: config_set_config_set_put + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppConfigSetBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /ffprobe: + get: + tags: + - App + summary: Ffprobe + operationId: ffprobe_ffprobe_get + parameters: + - name: paths + in: query + required: false + schema: + type: string + default: "" + title: Paths + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /vainfo: + get: + tags: + - App + summary: Vainfo + operationId: vainfo_vainfo_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /nvinfo: + get: + tags: + - App + summary: Nvinfo + operationId: nvinfo_nvinfo_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /logs/{service}: + get: + tags: + - App + - Logs + summary: Logs + description: Get logs for the requested service (frigate/nginx/go2rtc) + operationId: logs_logs__service__get + parameters: + - name: service + in: path + required: true + schema: + type: string + enum: + - frigate + - nginx + - go2rtc + title: Service + - name: download + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Download + - name: stream + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + default: false + title: Stream + - name: start + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 0 + title: Start + - name: end + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: End + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /restart: + post: + tags: + - App + summary: Restart + operationId: restart_restart_post + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /labels: + get: + tags: + - App + summary: Get Labels + operationId: get_labels_labels_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: "" + title: Camera + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /sub_labels: + get: + tags: + - App + summary: Get Sub Labels + operationId: get_sub_labels_sub_labels_get + parameters: + - name: split_joined + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Split Joined + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /plus/models: + get: + tags: + - App + summary: Plusmodels + operationId: plusModels_plus_models_get + parameters: + - name: filterByCurrentModelDetector + in: query + required: false + schema: + type: boolean + default: false + title: Filterbycurrentmodeldetector + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /recognized_license_plates: + get: + tags: + - App + summary: Get Recognized License Plates + operationId: get_recognized_license_plates_recognized_license_plates_get + parameters: + - name: split_joined + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Split Joined + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /timeline: + get: + tags: + - App + summary: Timeline + operationId: timeline_timeline_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: all + title: Camera + - name: limit + in: query + required: false + schema: + type: integer + default: 100 + title: Limit + - name: source_id + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Source Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /timeline/hourly: + get: + tags: + - App + summary: Hourly Timeline + description: Get hourly summary for timeline. + operationId: hourly_timeline_timeline_hourly_get + parameters: + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Labels + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Before + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 200 + title: Limit + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: utc + title: Timezone + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /preview/{camera_name}/start/{start_ts}/end/{end_ts}: + get: + tags: + - Preview + summary: Get preview clips for time range + description: |- + Gets all preview clips for a specified camera and time range. + Returns a list of preview video clips that overlap with the requested time period, + ordered by start time. Use camera_name='all' to get previews from all cameras. + Returns an error if no previews are found. + operationId: preview_ts_preview__camera_name__start__start_ts__end__end_ts__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PreviewModel" + title: >- + Response Preview Ts Preview Camera Name Start Start Ts + End End Ts Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: + get: + tags: + - Preview + summary: Get preview clips for specific hour + description: |- + Gets all preview clips for a specific hour in a given timezone. + Converts the provided date/time from the specified timezone to UTC and retrieves + all preview clips for that hour. Use camera_name='all' to get previews from all cameras. + The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes). + operationId: >- + preview_hour_preview__year_month___day___hour___camera_name___tz_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: tz_name + in: path + required: true + schema: + type: string + title: Tz Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PreviewModel" + title: >- + Response Preview Hour Preview Year Month Day Hour + Camera Name Tz Name Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames: + get: + tags: + - Preview + summary: Get cached preview frame filenames + description: >- + Gets a list of cached preview frame filenames for a specific camera and + time range. + Returns an array of filenames for preview frames that fall within the specified time period, + sorted in chronological order. These are individual frame images cached for quick preview display. + operationId: >- + get_preview_frames_from_cache_preview__camera_name__start__start_ts__end__end_ts__frames_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + type: string + title: >- + Response Get Preview Frames From Cache Preview Camera Name + Start Start Ts End End Ts Frames Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /notifications/pubkey: + get: + tags: + - Notifications + summary: Get VAPID public key + description: |- + Gets the VAPID public key for the notifications. + Returns the public key or an error if notifications are not enabled. + operationId: get_vapid_pub_key_notifications_pubkey_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /notifications/register: + post: + tags: + - Notifications + summary: Register notifications + description: |- + Registers a notifications subscription. + Returns a success message or an error if the subscription is not provided. + operationId: register_notifications_notifications_register_post + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /exports: + get: + tags: + - Export + summary: Get exports + description: |- + Gets all exports from the database for cameras the user has access to. + Returns a list of exports ordered by date (most recent first). + operationId: get_exports_exports_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ExportModel" + title: Response Get Exports Exports Get + /export/{camera_name}/start/{start_time}/end/{end_time}: + post: + tags: + - Export + summary: Start recording export + description: |- + Starts an export of a recording for the specified time range. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range. + operationId: >- + export_recording_export__camera_name__start__start_time__end__end_time__post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_time + in: path + required: true + schema: + type: number + title: Start Time + - name: end_time + in: path + required: true + schema: + type: number + title: End Time + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExportRecordingsBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/StartExportResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /export/{event_id}/rename: + patch: + tags: + - Export + summary: Rename export + description: |- + Renames an export. + NOTE: This changes the friendly name of the export, not the filename. + operationId: export_rename_export__event_id__rename_patch + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExportRenameBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /export/{event_id}: + delete: + tags: + - Export + summary: Delete export + operationId: export_delete_export__event_id__delete + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /exports/{export_id}: + get: + tags: + - Export + summary: Get a single export + description: |- + Gets a specific export by ID. The user must have access to the camera + associated with the export. + operationId: get_export_exports__export_id__get + parameters: + - name: export_id + in: path + required: true + schema: + type: string + title: Export Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/ExportModel" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events: + get: + tags: + - Events + summary: Get events + description: Returns a list of events. + operationId: events_events_get + parameters: + - name: camera + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Camera + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Cameras + - name: label + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Label + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Labels + - name: sub_label + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Sub Label + - name: sub_labels + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Sub Labels + - name: zone + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Zone + - name: zones + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Zones + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 100 + title: Limit + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Before + - name: time_range + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: 00:00,24:00 + title: Time Range + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Has Snapshot + - name: in_progress + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: In Progress + - name: include_thumbnails + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 1 + title: Include Thumbnails + - name: favorites + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Favorites + - name: min_score + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Min Score + - name: max_score + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Max Score + - name: min_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Min Speed + - name: max_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Max Speed + - name: recognized_license_plate + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Recognized License Plate + - name: is_submitted + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Is Submitted + - name: min_length + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Min Length + - name: max_length + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Max Length + - name: event_id + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Event Id + - name: sort + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Sort + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: utc + title: Timezone + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EventResponse" + title: Response Events Events Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/explore: + get: + tags: + - Events + summary: Get summary of objects + description: |- + Gets a summary of objects from the database. + Returns a list of objects with a max of `limit` objects for each label. + operationId: events_explore_events_explore_get + parameters: + - name: limit + in: query + required: false + schema: + type: integer + default: 10 + title: Limit + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EventResponse" + title: Response Events Explore Events Explore Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /event_ids: + get: + tags: + - Events + summary: Get events by ids + description: |- + Gets events by a list of ids. + Returns a list of events. + operationId: event_ids_event_ids_get + parameters: + - name: ids + in: query + required: true + schema: + type: string + title: Ids + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EventResponse" + title: Response Event Ids Event Ids Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/search: + get: + tags: + - Events + summary: Search events + description: |- + Searches for events in the database. + Returns a list of events. + operationId: events_search_events_search_get + parameters: + - name: query + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Query + - name: event_id + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Event Id + - name: search_type + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: thumbnail + title: Search Type + - name: include_thumbnails + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 1 + title: Include Thumbnails + - name: limit + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 50 + title: Limit + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Cameras + - name: labels + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Labels + - name: zones + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Zones + - name: after + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: After + - name: before + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Before + - name: time_range + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: 00:00,24:00 + title: Time Range + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + title: Has Snapshot + - name: is_submitted + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + title: Is Submitted + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: utc + title: Timezone + - name: min_score + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Min Score + - name: max_score + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Max Score + - name: min_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Min Speed + - name: max_speed + in: query + required: false + schema: + anyOf: + - type: number + - type: "null" + title: Max Speed + - name: recognized_license_plate + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Recognized License Plate + - name: sort + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + title: Sort + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/summary: + get: + tags: + - Events + summary: Events Summary + operationId: events_summary_events_summary_get + parameters: + - name: timezone + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: utc + title: Timezone + - name: has_clip + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Has Clip + - name: has_snapshot + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Has Snapshot + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}: + get: + tags: + - Events + summary: Get event by id + description: Gets an event by its id. + operationId: event_events__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/EventResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + delete: + tags: + - Events + summary: Delete event + description: |- + Deletes an event from the database. + Returns a success message or an error if the event is not found. + operationId: delete_event_events__event_id__delete + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/retain: + post: + tags: + - Events + summary: Set event retain indefinitely + description: |- + Sets an event to retain indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + operationId: set_retain_events__event_id__retain_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + delete: + tags: + - Events + summary: Stop event from being retained indefinitely + description: |- + Stops an event from being retained indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + operationId: delete_retain_events__event_id__retain_delete + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/plus: + post: + tags: + - Events + summary: Send event to Frigate+ + description: |- + Sends an event to Frigate+. + Returns a success message or an error if the event is not found. + operationId: send_to_plus_events__event_id__plus_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SubmitPlusBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/EventUploadPlusResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/false_positive: + put: + tags: + - Events + summary: Submit false positive to Frigate+ + description: |- + Submit an event as a false positive to Frigate+. + This endpoint is the same as the standard Frigate+ submission endpoint, + but is specifically for marking an event as a false positive. + operationId: false_positive_events__event_id__false_positive_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/EventUploadPlusResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/sub_label: + post: + tags: + - Events + summary: Set event sub label + description: |- + Sets an event's sub label. + Returns a success message or an error if the event is not found. + operationId: set_sub_label_events__event_id__sub_label_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsSubLabelBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/recognized_license_plate: + post: + tags: + - Events + summary: Set event license plate + description: |- + Sets an event's license plate. + Returns a success message or an error if the event is not found. + operationId: set_plate_events__event_id__recognized_license_plate_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsLPRBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/description: + post: + tags: + - Events + summary: Set event description + description: |- + Sets an event's description. + Returns a success message or an error if the event is not found. + operationId: set_description_events__event_id__description_post + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsDescriptionBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/description/regenerate: + put: + tags: + - Events + summary: Regenerate event description + description: |- + Regenerates an event's description. + Returns a success message or an error if the event is not found. + operationId: regenerate_description_events__event_id__description_regenerate_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: source + in: query + required: false + schema: + anyOf: + - $ref: "#/components/schemas/RegenerateDescriptionEnum" + - type: "null" + default: thumbnails + title: Source + - name: force + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + default: false + title: Force + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /description/generate: + post: + tags: + - Events + summary: Generate description embedding + description: |- + Generates an embedding for an event's description. + Returns a success message or an error if the event is not found. + operationId: generate_description_embedding_description_generate_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsDescriptionBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/: + delete: + tags: + - Events + summary: Delete events + description: |- + Deletes a list of events from the database. + Returns a success message or an error if the events are not found. + operationId: delete_events_events__delete + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsDeleteBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/EventMultiDeleteResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{camera_name}/{label}/create: + post: + tags: + - Events + summary: Create manual event + description: |- + Creates a manual event in the database. + Returns a success message or an error if the event is not found. + NOTES: + - Creating a manual event does not trigger an update to /events MQTT topic. + - If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end. + operationId: create_event_events__camera_name___label__create_post + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EventsCreateBody" + default: + score: 0 + duration: 30 + include_recording: true + draw: {} + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/EventCreateResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/end: + put: + tags: + - Events + summary: End manual event + description: |- + Ends a manual event. + Returns a success message or an error if the event is not found. + NOTE: This should only be used for manual events. + operationId: end_event_events__event_id__end_put + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventsEndBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + $ref: "#/components/schemas/GenericResponse" + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /trigger/embedding: + post: + tags: + - Events + summary: Create trigger embedding + description: |- + Creates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: create_trigger_embedding_trigger_embedding_post + parameters: + - name: camera_name + in: query + required: true + schema: + type: string + title: Camera Name + - name: name + in: query + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TriggerEmbeddingBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Create Trigger Embedding Trigger Embedding Post + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /trigger/embedding/{camera_name}/{name}: + put: + tags: + - Events + summary: Update trigger embedding + description: |- + Updates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: update_trigger_embedding_trigger_embedding__camera_name___name__put + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TriggerEmbeddingBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: object + title: >- + Response Update Trigger Embedding Trigger Embedding Camera + Name Name Put + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + delete: + tags: + - Events + summary: Delete trigger embedding + description: |- + Deletes a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + operationId: delete_trigger_embedding_trigger_embedding__camera_name___name__delete + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + - name: name + in: path + required: true + schema: + type: string + title: Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: object + title: >- + Response Delete Trigger Embedding Trigger Embedding Camera + Name Name Delete + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /triggers/status/{camera_name}: + get: + tags: + - Events + summary: Get triggers status + description: |- + Gets the status of all triggers for a specific camera. + Returns a success message or an error if the camera is not found. + operationId: get_triggers_status_triggers_status__camera_name__get + parameters: + - name: camera_name + in: path + required: true + schema: + type: string + title: Camera Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: object + title: Response Get Triggers Status Triggers Status Camera Name Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}: + get: + tags: + - Media + summary: Mjpeg Feed + operationId: mjpeg_feed__camera_name__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: fps + in: query + required: false + schema: + type: integer + default: 3 + title: Fps + - name: height + in: query + required: false + schema: + type: integer + default: 360 + title: Height + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Bbox + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Timestamp + - name: zones + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Zones + - name: mask + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Mask + - name: motion + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Motion + - name: regions + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Regions + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/ptz/info: + get: + tags: + - Media + summary: Camera Ptz Info + operationId: camera_ptz_info__camera_name__ptz_info_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/latest.{extension}: + get: + tags: + - Media + summary: Latest Frame + operationId: latest_frame__camera_name__latest__extension__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: extension + in: path + required: true + schema: + $ref: "#/components/schemas/Extension" + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Bbox + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Timestamp + - name: zones + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Zones + - name: mask + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Mask + - name: motion + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Motion + - name: paths + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Paths + - name: regions + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Regions + - name: quality + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 70 + title: Quality + - name: height + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Height + - name: store + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Store + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/recordings/{frame_time}/snapshot.{format}: + get: + tags: + - Media + summary: Get Snapshot From Recording + operationId: >- + get_snapshot_from_recording__camera_name__recordings__frame_time__snapshot__format__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: frame_time + in: path + required: true + schema: + type: number + title: Frame Time + - name: format + in: path + required: true + schema: + type: string + enum: + - png + - jpg + title: Format + - name: height + in: query + required: false + schema: + type: integer + title: Height + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/plus/{frame_time}: + post: + tags: + - Media + summary: Submit Recording Snapshot To Plus + operationId: submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: frame_time + in: path + required: true + schema: + type: string + title: Frame Time + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /recordings/storage: + get: + tags: + - Media + summary: Get Recordings Storage Usage + operationId: get_recordings_storage_usage_recordings_storage_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /recordings/summary: + get: + tags: + - Media + summary: All Recordings Summary + description: Returns true/false by day indicating if recordings exist + operationId: all_recordings_summary_recordings_summary_get + parameters: + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + - name: cameras + in: query + required: false + schema: + anyOf: + - type: string + - type: "null" + default: all + title: Cameras + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/recordings/summary: + get: + tags: + - Media + summary: Recordings Summary + description: Returns hourly summary for recordings of given camera + operationId: recordings_summary__camera_name__recordings_summary_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: timezone + in: query + required: false + schema: + type: string + default: utc + title: Timezone + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/recordings: + get: + tags: + - Media + summary: Recordings + description: >- + Return specific camera recordings between the given 'after'/'end' times. + If not provided the last hour will be used + operationId: recordings__camera_name__recordings_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: after + in: query + required: false + schema: + type: number + default: 1759932070.40171 + title: After + - name: before + in: query + required: false + schema: + type: number + default: 1759935670.40172 + title: Before + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /recordings/unavailable: + get: + tags: + - Media + summary: No Recordings + description: Get time ranges with no recordings. + operationId: no_recordings_recordings_unavailable_get + parameters: + - name: cameras + in: query + required: false + schema: + type: string + default: all + title: Cameras + - name: before + in: query + required: false + schema: + type: number + title: Before + - name: after + in: query + required: false + schema: + type: number + title: After + - name: scale + in: query + required: false + schema: + type: integer + default: 30 + title: Scale + responses: + "200": + description: Successful Response + content: + application/json: + schema: + type: array + items: + type: object + title: Response No Recordings Recordings Unavailable Get + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4: + get: + tags: + - Media + summary: Recording Clip + description: >- + For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. + Safari does not reliably process progressive mp4 files. + operationId: recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /vod/{camera_name}/start/{start_ts}/end/{end_ts}: + get: + tags: + - Media + summary: Vod Ts + description: >- + Returns an HLS playlist for the specified timestamp-range on the + specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: vod_ts_vod__camera_name__start__start_ts__end__end_ts__get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /vod/{year_month}/{day}/{hour}/{camera_name}: + get: + tags: + - Media + summary: Vod Hour No Timezone + description: >- + Returns an HLS playlist for the specified date-time on the specified + camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: + get: + tags: + - Media + summary: Vod Hour + description: >- + Returns an HLS playlist for the specified date-time (with timezone) on + the specified camera. Append /master.m3u8 or /index.m3u8 for HLS + playback. + operationId: vod_hour_vod__year_month___day___hour___camera_name___tz_name__get + parameters: + - name: year_month + in: path + required: true + schema: + type: string + title: Year Month + - name: day + in: path + required: true + schema: + type: integer + title: Day + - name: hour + in: path + required: true + schema: + type: integer + title: Hour + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: tz_name + in: path + required: true + schema: + type: string + title: Tz Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /vod/event/{event_id}: + get: + tags: + - Media + summary: Vod Event + description: >- + Returns an HLS playlist for the specified object. Append /master.m3u8 or + /index.m3u8 for HLS playback. + operationId: vod_event_vod_event__event_id__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to the vod. + default: 0 + title: Padding + description: Padding to apply to the vod. + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/snapshot.jpg: + get: + tags: + - Media + summary: Event Snapshot + description: >- + Returns a snapshot image for the specified object id. NOTE: The query + params only take affect while the event is in-progress. Once the event + has ended the snapshot configuration is used. + operationId: event_snapshot_events__event_id__snapshot_jpg_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: download + in: query + required: false + schema: + anyOf: + - type: boolean + - type: "null" + default: false + title: Download + - name: timestamp + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Timestamp + - name: bbox + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Bbox + - name: crop + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Crop + - name: height + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + title: Height + - name: quality + in: query + required: false + schema: + anyOf: + - type: integer + - type: "null" + default: 70 + title: Quality + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/thumbnail.{extension}: + get: + tags: + - Media + summary: Event Thumbnail + operationId: event_thumbnail_events__event_id__thumbnail__extension__get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: extension + in: path + required: true + schema: + $ref: "#/components/schemas/Extension" + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 30 days in seconds. + default: 2592000 + title: Max Cache Age + description: Max cache age in seconds. Default 30 days in seconds. + - name: format + in: query + required: false + schema: + type: string + enum: + - ios + - android + default: ios + title: Format + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/grid.jpg: + get: + tags: + - Media + summary: Grid Snapshot + operationId: grid_snapshot__camera_name__grid_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: color + in: query + required: false + schema: + type: string + default: green + title: Color + - name: font_scale + in: query + required: false + schema: + type: number + default: 0.5 + title: Font Scale + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/snapshot-clean.webp: + get: + tags: + - Media + summary: Event Snapshot Clean + operationId: event_snapshot_clean_events__event_id__snapshot_clean_png_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: download + in: query + required: false + schema: + type: boolean + default: false + title: Download + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/clip.mp4: + get: + tags: + - Media + summary: Event Clip + operationId: event_clip_events__event_id__clip_mp4_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to clip. + default: 0 + title: Padding + description: Padding to apply to clip. + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /events/{event_id}/preview.gif: + get: + tags: + - Media + summary: Event Preview + operationId: event_preview_events__event_id__preview_gif_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif: + get: + tags: + - Media + summary: Preview Gif + operationId: preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 30 days in seconds. + default: 2592000 + title: Max Cache Age + description: Max cache age in seconds. Default 30 days in seconds. + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4: + get: + tags: + - Media + summary: Preview Mp4 + operationId: preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: start_ts + in: path + required: true + schema: + type: number + title: Start Ts + - name: end_ts + in: path + required: true + schema: + type: number + title: End Ts + - name: max_cache_age + in: query + required: false + schema: + type: integer + description: Max cache age in seconds. Default 7 days in seconds. + default: 604800 + title: Max Cache Age + description: Max cache age in seconds. Default 7 days in seconds. + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /review/{event_id}/preview: + get: + tags: + - Media + summary: Review Preview + operationId: review_preview_review__event_id__preview_get + parameters: + - name: event_id + in: path + required: true + schema: + type: string + title: Event Id + - name: format + in: query + required: false + schema: + type: string + enum: + - gif + - mp4 + default: gif + title: Format + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /preview/{file_name}/thumbnail.webp: + get: + tags: + - Media + summary: Preview Thumbnail + description: Get a thumbnail from the cached preview frames. + operationId: preview_thumbnail_preview__file_name__thumbnail_webp_get + parameters: + - name: file_name + in: path + required: true + schema: + type: string + title: File Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /preview/{file_name}/thumbnail.jpg: + get: + tags: + - Media + summary: Preview Thumbnail + description: Get a thumbnail from the cached preview frames. + operationId: preview_thumbnail_preview__file_name__thumbnail_jpg_get + parameters: + - name: file_name + in: path + required: true + schema: + type: string + title: File Name + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/{label}/thumbnail.jpg: + get: + tags: + - Media + summary: Label Thumbnail + operationId: label_thumbnail__camera_name___label__thumbnail_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/{label}/best.jpg: + get: + tags: + - Media + summary: Label Thumbnail + operationId: label_thumbnail__camera_name___label__best_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/{label}/clip.mp4: + get: + tags: + - Media + summary: Label Clip + operationId: label_clip__camera_name___label__clip_mp4_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" + /{camera_name}/{label}/snapshot.jpg: + get: + tags: + - Media + summary: Label Snapshot + description: >- + Returns the snapshot image from the latest event for the given camera + and label combo + operationId: label_snapshot__camera_name___label__snapshot_jpg_get + parameters: + - name: camera_name + in: path + required: true + schema: + anyOf: + - type: string + - type: "null" + title: Camera Name + - name: label + in: path + required: true + schema: + type: string + title: Label + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" +components: + schemas: + AppConfigSetBody: + properties: + requires_restart: + type: integer + title: Requires Restart + default: 1 + update_topic: + anyOf: + - type: string + - type: "null" + title: Update Topic + config_data: + anyOf: + - type: object + - type: "null" + title: Config Data + type: object + title: AppConfigSetBody + AppPostLoginBody: + properties: + user: + type: string + title: User + password: + type: string + title: Password + type: object + required: + - user + - password + title: AppPostLoginBody + AppPostUsersBody: + properties: + username: + type: string + title: Username + password: + type: string + title: Password + role: + anyOf: + - type: string + - type: "null" + title: Role + default: viewer + type: object + required: + - username + - password + title: AppPostUsersBody + AppPutPasswordBody: + properties: + password: + type: string + title: Password + type: object + required: + - password + title: AppPutPasswordBody + AppPutRoleBody: + properties: + role: + type: string + title: Role + type: object + required: + - role + title: AppPutRoleBody + AudioTranscriptionBody: + properties: + event_id: + type: string + title: Event Id + type: object + required: + - event_id + title: AudioTranscriptionBody + Body_recognize_face_faces_recognize_post: + properties: + file: + type: string + format: binary + title: File + type: object + required: + - file + title: Body_recognize_face_faces_recognize_post + Body_register_face_faces__name__register_post: + properties: + file: + type: string + format: binary + title: File + type: object + required: + - file + title: Body_register_face_faces__name__register_post + DayReview: + properties: + day: + type: string + format: date-time + title: Day + reviewed_alert: + type: integer + title: Reviewed Alert + reviewed_detection: + type: integer + title: Reviewed Detection + total_alert: + type: integer + title: Total Alert + total_detection: + type: integer + title: Total Detection + type: object + required: + - day + - reviewed_alert + - reviewed_detection + - total_alert + - total_detection + title: DayReview + DeleteFaceImagesBody: + properties: + ids: + items: + type: string + type: array + title: Ids + description: List of image filenames to delete from the face folder + type: object + required: + - ids + title: DeleteFaceImagesBody + EventCreateResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + event_id: + type: string + title: Event Id + type: object + required: + - success + - message + - event_id + title: EventCreateResponse + EventMultiDeleteResponse: + properties: + success: + type: boolean + title: Success + deleted_events: + items: + type: string + type: array + title: Deleted Events + not_found_events: + items: + type: string + type: array + title: Not Found Events + type: object + required: + - success + - deleted_events + - not_found_events + title: EventMultiDeleteResponse + EventResponse: + properties: + id: + type: string + title: Id + label: + type: string + title: Label + sub_label: + anyOf: + - type: string + - type: "null" + title: Sub Label + camera: + type: string + title: Camera + start_time: + type: number + title: Start Time + end_time: + anyOf: + - type: number + - type: "null" + title: End Time + false_positive: + anyOf: + - type: boolean + - type: "null" + title: False Positive + zones: + items: + type: string + type: array + title: Zones + thumbnail: + anyOf: + - type: string + - type: "null" + title: Thumbnail + has_clip: + type: boolean + title: Has Clip + has_snapshot: + type: boolean + title: Has Snapshot + retain_indefinitely: + type: boolean + title: Retain Indefinitely + plus_id: + anyOf: + - type: string + - type: "null" + title: Plus Id + model_hash: + anyOf: + - type: string + - type: "null" + title: Model Hash + detector_type: + anyOf: + - type: string + - type: "null" + title: Detector Type + model_type: + anyOf: + - type: string + - type: "null" + title: Model Type + data: + type: object + title: Data + type: object + required: + - id + - label + - sub_label + - camera + - start_time + - end_time + - false_positive + - zones + - thumbnail + - has_clip + - has_snapshot + - retain_indefinitely + - plus_id + - model_hash + - detector_type + - model_type + - data + title: EventResponse + EventUploadPlusResponse: + properties: + success: + type: boolean + title: Success + plus_id: + type: string + title: Plus Id + type: object + required: + - success + - plus_id + title: EventUploadPlusResponse + EventsCreateBody: + properties: + sub_label: + anyOf: + - type: string + - type: "null" + title: Sub Label + score: + anyOf: + - type: number + - type: "null" + title: Score + default: 0 + duration: + anyOf: + - type: integer + - type: "null" + title: Duration + default: 30 + include_recording: + anyOf: + - type: boolean + - type: "null" + title: Include Recording + default: true + draw: + anyOf: + - type: object + - type: "null" + title: Draw + default: {} + type: object + title: EventsCreateBody + EventsDeleteBody: + properties: + event_ids: + items: + type: string + type: array + title: The event IDs to delete + type: object + required: + - event_ids + title: EventsDeleteBody + EventsDescriptionBody: + properties: + description: + anyOf: + - type: string + - type: "null" + title: The description of the event + type: object + required: + - description + title: EventsDescriptionBody + EventsEndBody: + properties: + end_time: + anyOf: + - type: number + - type: "null" + title: End Time + type: object + title: EventsEndBody + EventsLPRBody: + properties: + recognizedLicensePlate: + type: string + maxLength: 100 + title: Recognized License Plate + recognizedLicensePlateScore: + anyOf: + - type: number + maximum: 1 + exclusiveMinimum: 0 + - type: "null" + title: Score for recognized license plate + type: object + required: + - recognizedLicensePlate + title: EventsLPRBody + EventsSubLabelBody: + properties: + subLabel: + type: string + maxLength: 100 + title: Sub label + subLabelScore: + anyOf: + - type: number + maximum: 1 + exclusiveMinimum: 0 + - type: "null" + title: Score for sub label + camera: + anyOf: + - type: string + - type: "null" + title: Camera this object is detected on. + type: object + required: + - subLabel + title: EventsSubLabelBody + ExportModel: + properties: + id: + type: string + title: Id + description: Unique identifier for the export + camera: + type: string + title: Camera + description: Camera name associated with this export + name: + type: string + title: Name + description: Friendly name of the export + date: + type: number + title: Date + description: Unix timestamp when the export was created + video_path: + type: string + title: Video Path + description: File path to the exported video + thumb_path: + type: string + title: Thumb Path + description: File path to the export thumbnail + in_progress: + type: boolean + title: In Progress + description: Whether the export is currently being processed + type: object + required: + - id + - camera + - name + - date + - video_path + - thumb_path + - in_progress + title: ExportModel + description: Model representing a single export. + ExportRecordingsBody: + properties: + playback: + $ref: "#/components/schemas/PlaybackFactorEnum" + title: Playback factor + default: realtime + source: + $ref: "#/components/schemas/PlaybackSourceEnum" + title: Playback source + default: recordings + name: + type: string + maxLength: 256 + title: Friendly name + image_path: + type: string + title: Image Path + type: object + title: ExportRecordingsBody + ExportRenameBody: + properties: + name: + type: string + maxLength: 256 + title: Friendly name + type: object + required: + - name + title: ExportRenameBody + Extension: + type: string + enum: + - webp + - png + - jpg + - jpeg + title: Extension + FaceRecognitionResponse: + properties: + success: + type: boolean + title: Success + description: Whether the face recognition was successful + score: + anyOf: + - type: number + - type: "null" + title: Score + description: Confidence score of the recognition (0-1) + face_name: + anyOf: + - type: string + - type: "null" + title: Face Name + description: The recognized face name if successful + type: object + required: + - success + title: FaceRecognitionResponse + description: >- + Response model for face recognition endpoint. + + + Returns the result of attempting to recognize a face from an uploaded + image. + FacesResponse: + additionalProperties: + items: + type: string + type: array + type: object + title: FacesResponse + description: |- + Response model for the get_faces endpoint. + + Returns a mapping of face names to lists of image filenames. + Each face name corresponds to a directory in the faces folder, + and the list contains the names of image files for that face. + + Example: + { + "john_doe": ["face1.webp", "face2.jpg"], + "jane_smith": ["face3.png"] + } + GenericResponse: + properties: + success: + type: boolean + title: Success + message: + type: string + title: Message + type: object + required: + - success + - message + title: GenericResponse + HTTPValidationError: + properties: + detail: + items: + $ref: "#/components/schemas/ValidationError" + type: array + title: Detail + type: object + title: HTTPValidationError + Last24HoursReview: + properties: + reviewed_alert: + type: integer + title: Reviewed Alert + reviewed_detection: + type: integer + title: Reviewed Detection + total_alert: + type: integer + title: Total Alert + total_detection: + type: integer + title: Total Detection + type: object + required: + - reviewed_alert + - reviewed_detection + - total_alert + - total_detection + title: Last24HoursReview + PlaybackFactorEnum: + type: string + enum: + - realtime + - timelapse_25x + title: PlaybackFactorEnum + PlaybackSourceEnum: + type: string + enum: + - recordings + - preview + title: PlaybackSourceEnum + PreviewModel: + properties: + camera: + type: string + title: Camera + description: Camera name for this preview + src: + type: string + title: Src + description: Path to the preview video file + type: + type: string + title: Type + description: MIME type of the preview video (video/mp4) + start: + type: number + title: Start + description: Unix timestamp when the preview starts + end: + type: number + title: End + description: Unix timestamp when the preview ends + type: object + required: + - camera + - src + - type + - start + - end + title: PreviewModel + description: Model representing a single preview clip. + RegenerateDescriptionEnum: + type: string + enum: + - thumbnails + - snapshot + title: RegenerateDescriptionEnum + RenameFaceBody: + properties: + new_name: + type: string + title: New Name + type: object + required: + - new_name + title: RenameFaceBody + ReviewActivityMotionResponse: + properties: + start_time: + type: integer + title: Start Time + motion: + type: number + title: Motion + camera: + type: string + title: Camera + type: object + required: + - start_time + - motion + - camera + title: ReviewActivityMotionResponse + ReviewModifyMultipleBody: + properties: + ids: + items: + type: string + minLength: 1 + type: array + minItems: 1 + title: Ids + type: object + required: + - ids + title: ReviewModifyMultipleBody + ReviewSegmentResponse: + properties: + id: + type: string + title: Id + camera: + type: string + title: Camera + start_time: + type: string + format: date-time + title: Start Time + end_time: + type: string + format: date-time + title: End Time + has_been_reviewed: + type: boolean + title: Has Been Reviewed + severity: + $ref: "#/components/schemas/SeverityEnum" + thumb_path: + type: string + title: Thumb Path + data: + title: Data + type: object + required: + - id + - camera + - start_time + - end_time + - has_been_reviewed + - severity + - thumb_path + - data + title: ReviewSegmentResponse + ReviewSummaryResponse: + properties: + last24Hours: + $ref: "#/components/schemas/Last24HoursReview" + root: + additionalProperties: + $ref: "#/components/schemas/DayReview" + type: object + title: Root + type: object + required: + - last24Hours + - root + title: ReviewSummaryResponse + SeverityEnum: + type: string + enum: + - alert + - detection + title: SeverityEnum + StartExportResponse: + properties: + success: + type: boolean + title: Success + description: Whether the export was started successfully + message: + type: string + title: Message + description: Status or error message + export_id: + anyOf: + - type: string + - type: "null" + title: Export Id + description: The export ID if successfully started + type: object + required: + - success + - message + title: StartExportResponse + description: Response model for starting an export. + SubmitPlusBody: + properties: + include_annotation: + type: integer + title: Include Annotation + default: 1 + type: object + title: SubmitPlusBody + TriggerEmbeddingBody: + properties: + type: + $ref: "#/components/schemas/TriggerType" + data: + type: string + title: Data + threshold: + type: number + maximum: 1 + minimum: 0 + title: Threshold + default: 0.5 + type: object + required: + - type + - data + title: TriggerEmbeddingBody + TriggerType: + type: string + enum: + - thumbnail + - description + title: TriggerType + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + type: object + required: + - loc + - msg + - type + title: ValidationError diff --git a/sam2-cpu/frigate-dev/docs/static/img/annotate.png b/sam2-cpu/frigate-dev/docs/static/img/annotate.png new file mode 100644 index 0000000..c36ec94 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/annotate.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/autotracking-debug.gif b/sam2-cpu/frigate-dev/docs/static/img/autotracking-debug.gif new file mode 100644 index 0000000..d3bb202 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/autotracking-debug.gif differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/bottom-center-mask.jpg b/sam2-cpu/frigate-dev/docs/static/img/bottom-center-mask.jpg new file mode 100644 index 0000000..103875a Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/bottom-center-mask.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/bottom-center.jpg b/sam2-cpu/frigate-dev/docs/static/img/bottom-center.jpg new file mode 100644 index 0000000..e11c4c4 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/bottom-center.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/branding/LICENSE.md b/sam2-cpu/frigate-dev/docs/static/img/branding/LICENSE.md new file mode 100644 index 0000000..4975f03 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/static/img/branding/LICENSE.md @@ -0,0 +1,30 @@ +# COPYRIGHT AND TRADEMARK NOTICE + +The images, logos, and icons contained in this directory (the "Brand Assets") are +proprietary to Frigate LLC and are NOT covered by the MIT License governing the +rest of this repository. + +1. TRADEMARK STATUS + The "Frigate" name and the accompanying logo are common law trademarks™ of + Frigate LLC. Frigate LLC reserves all rights to these marks. + +2. LIMITED PERMISSION FOR USE + Permission is hereby granted to display these Brand Assets strictly for the + following purposes: + a. To execute the software interface on a local machine. + b. To identify the software in documentation or reviews (nominative use). + +3. RESTRICTIONS + You may NOT: + a. Use these Brand Assets to represent a derivative work (fork) as an official + product of Frigate LLC. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate LLC. + c. Modify or alter the Brand Assets. + +If you fork this repository with the intent to distribute a modified or competing +version of the software, you must replace these Brand Assets with your own +original content. + +ALL RIGHTS RESERVED. +Copyright (c) 2025 Frigate LLC. diff --git a/sam2-cpu/frigate-dev/docs/static/img/branding/favicon.ico b/sam2-cpu/frigate-dev/docs/static/img/branding/favicon.ico new file mode 100644 index 0000000..1de8ec8 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/branding/favicon.ico differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/branding/frigate.png b/sam2-cpu/frigate-dev/docs/static/img/branding/frigate.png new file mode 100644 index 0000000..1156bc6 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/branding/frigate.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/branding/logo-dark.svg b/sam2-cpu/frigate-dev/docs/static/img/branding/logo-dark.svg new file mode 100644 index 0000000..16cd275 --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/static/img/branding/logo-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/sam2-cpu/frigate-dev/docs/static/img/branding/logo.svg b/sam2-cpu/frigate-dev/docs/static/img/branding/logo.svg new file mode 100644 index 0000000..3d01f2a --- /dev/null +++ b/sam2-cpu/frigate-dev/docs/static/img/branding/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/sam2-cpu/frigate-dev/docs/static/img/camera-ui.png b/sam2-cpu/frigate-dev/docs/static/img/camera-ui.png new file mode 100644 index 0000000..55418e8 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/camera-ui.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/diagram.png b/sam2-cpu/frigate-dev/docs/static/img/diagram.png new file mode 100644 index 0000000..1ee8659 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/diagram.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/driveway_zones-min.png b/sam2-cpu/frigate-dev/docs/static/img/driveway_zones-min.png new file mode 100644 index 0000000..39e1954 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/driveway_zones-min.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/driveway_zones.png b/sam2-cpu/frigate-dev/docs/static/img/driveway_zones.png new file mode 100644 index 0000000..67ed8f4 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/driveway_zones.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/events-ui.png b/sam2-cpu/frigate-dev/docs/static/img/events-ui.png new file mode 100644 index 0000000..13ee205 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/events-ui.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly-min.png b/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly-min.png new file mode 100644 index 0000000..62a78cd Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly-min.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly.png b/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly.png new file mode 100644 index 0000000..e0e7d49 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/example-mask-poly.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/frigate-autotracking-example.gif b/sam2-cpu/frigate-dev/docs/static/img/frigate-autotracking-example.gif new file mode 100644 index 0000000..b0bc424 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/frigate-autotracking-example.gif differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/ground-plane.jpg b/sam2-cpu/frigate-dev/docs/static/img/ground-plane.jpg new file mode 100644 index 0000000..f7ea4db Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/ground-plane.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/home-ui.png b/sam2-cpu/frigate-dev/docs/static/img/home-ui.png new file mode 100644 index 0000000..efdce3a Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/home-ui.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/live-view.png b/sam2-cpu/frigate-dev/docs/static/img/live-view.png new file mode 100644 index 0000000..6bc5eed Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/live-view.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/media_browser-min.png b/sam2-cpu/frigate-dev/docs/static/img/media_browser-min.png new file mode 100644 index 0000000..39229d0 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/media_browser-min.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/media_browser.png b/sam2-cpu/frigate-dev/docs/static/img/media_browser.png new file mode 100644 index 0000000..9c449d0 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/media_browser.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution-min.jpg b/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution-min.jpg new file mode 100644 index 0000000..ef5037f Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution-min.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution.jpg b/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution.jpg new file mode 100644 index 0000000..662a20f Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/mismatched-resolution.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/notification-min.png b/sam2-cpu/frigate-dev/docs/static/img/notification-min.png new file mode 100644 index 0000000..0d49cdf Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/notification-min.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/notification.png b/sam2-cpu/frigate-dev/docs/static/img/notification.png new file mode 100644 index 0000000..0df060e Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/notification.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus-api-key-min.png b/sam2-cpu/frigate-dev/docs/static/img/plus-api-key-min.png new file mode 100644 index 0000000..3e9c7f4 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus-api-key-min.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-face.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-face.jpg new file mode 100644 index 0000000..a99c878 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-face.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-fedex.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-fedex.jpg new file mode 100644 index 0000000..156b507 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/attribute-example-fedex.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive-overlap.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive-overlap.jpg new file mode 100644 index 0000000..feedc3b Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive-overlap.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive.jpg new file mode 100644 index 0000000..1e5cc3d Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/false-positive.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/fedex-logo.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/fedex-logo.jpg new file mode 100644 index 0000000..42c36dd Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/fedex-logo.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/model-ready-email.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/model-ready-email.jpg new file mode 100644 index 0000000..7787237 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/model-ready-email.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/plus-models.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/plus-models.jpg new file mode 100644 index 0000000..702391d Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/plus-models.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/send-to-plus.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/send-to-plus.jpg new file mode 100644 index 0000000..cffd7e5 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/send-to-plus.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/submit-to-plus.jpg b/sam2-cpu/frigate-dev/docs/static/img/plus/submit-to-plus.jpg new file mode 100644 index 0000000..fd90254 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/submit-to-plus.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/plus/suggestions.webp b/sam2-cpu/frigate-dev/docs/static/img/plus/suggestions.webp new file mode 100644 index 0000000..274eda7 Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/plus/suggestions.webp differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/resolutions-min.jpg b/sam2-cpu/frigate-dev/docs/static/img/resolutions-min.jpg new file mode 100644 index 0000000..19169ac Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/resolutions-min.jpg differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/resolutions.png b/sam2-cpu/frigate-dev/docs/static/img/resolutions.png new file mode 100644 index 0000000..59d8c8b Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/resolutions.png differ diff --git a/sam2-cpu/frigate-dev/docs/static/img/review-items.png b/sam2-cpu/frigate-dev/docs/static/img/review-items.png new file mode 100644 index 0000000..641813b Binary files /dev/null and b/sam2-cpu/frigate-dev/docs/static/img/review-items.png differ diff --git a/sam2-cpu/frigate-dev/frigate/__init__.py b/sam2-cpu/frigate-dev/frigate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/__main__.py b/sam2-cpu/frigate-dev/frigate/__main__.py new file mode 100644 index 0000000..f3181e4 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/__main__.py @@ -0,0 +1,136 @@ +import argparse +import faulthandler +import multiprocessing as mp +import signal +import sys +import threading +from typing import Union + +import ruamel.yaml +from pydantic import ValidationError + +from frigate.app import FrigateApp +from frigate.config import FrigateConfig +from frigate.log import setup_logging +from frigate.util.config import find_config_file + + +def main() -> None: + manager = mp.Manager() + faulthandler.enable() + + # Setup the logging thread + setup_logging(manager) + + threading.current_thread().name = "frigate" + stop_event = mp.Event() + + # send stop event on SIGINT + signal.signal(signal.SIGINT, lambda sig, frame: stop_event.set()) + + # Make sure we exit cleanly on SIGTERM. + signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit()) + + # Parse the cli arguments. + parser = argparse.ArgumentParser( + prog="Frigate", + description="An NVR with realtime local object detection for IP cameras.", + ) + parser.add_argument("--validate-config", action="store_true") + args = parser.parse_args() + + # Load the configuration. + try: + config = FrigateConfig.load(install=True) + except ValidationError as e: + print("*************************************************************") + print("*************************************************************") + print("*** Your config file is not valid! ***") + print("*** Please check the docs at ***") + print("*** https://docs.frigate.video/configuration/ ***") + print("*************************************************************") + print("*************************************************************") + print("*** Config Validation Errors ***") + print("*************************************************************\n") + # Attempt to get the original config file for line number tracking + config_path = find_config_file() + with open(config_path, "r") as f: + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(f) + + for error in e.errors(): + error_path = error["loc"] + + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key: Union[int, str] = ( + int(part) if isinstance(part, str) and part.isdigit() else part + ) + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + if isinstance(key, int): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception as traverse_error: + print(f"Could not determine exact line number: {traverse_error}") + + if current != full_config: + print(f"Line # : {line_number}") + print(f"Key : {' -> '.join(map(str, error_path))}") + print(f"Value : {error.get('input', '-')}") + print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") + + print("*************************************************************") + print("*** End Config Validation Errors ***") + print("*************************************************************") + + # attempt to start Frigate in recovery mode + try: + config = FrigateConfig.load(install=True, safe_load=True) + print("Starting Frigate in safe mode.") + except ValidationError: + print("Unable to start Frigate in safe mode.") + sys.exit(1) + if args.validate_config: + print("*************************************************************") + print("*** Your config file is valid. ***") + print("*************************************************************") + sys.exit(0) + + # Run the main application. + FrigateApp(config, manager, stop_event).start() + + +if __name__ == "__main__": + mp.set_forkserver_preload( + [ + # Standard library and core dependencies + "sqlite3", + # Third-party libraries commonly used in Frigate + "numpy", + "cv2", + "peewee", + "zmq", + "ruamel.yaml", + # Frigate core modules + "frigate.camera.maintainer", + ] + ) + mp.set_start_method("forkserver", force=True) + main() diff --git a/sam2-cpu/frigate-dev/frigate/api/__init__.py b/sam2-cpu/frigate-dev/frigate/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/api/app.py b/sam2-cpu/frigate-dev/frigate/api/app.py new file mode 100644 index 0000000..c87e929 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/app.py @@ -0,0 +1,841 @@ +"""Main api runner.""" + +import asyncio +import copy +import json +import logging +import os +import traceback +import urllib +from datetime import datetime, timedelta +from functools import reduce +from io import StringIO +from pathlib import Path as FilePath +from typing import Any, Dict, List, Optional + +import aiofiles +import ruamel.yaml +from fastapi import APIRouter, Body, Path, Request, Response +from fastapi.encoders import jsonable_encoder +from fastapi.params import Depends +from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse +from markupsafe import escape +from peewee import SQL, fn, operator +from pydantic import ValidationError + +from frigate.api.auth import allow_any_authenticated, allow_public, require_role +from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters +from frigate.api.defs.request.app_body import AppConfigSetBody +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateTopic, +) +from frigate.models import Event, Timeline +from frigate.stats.prometheus import get_metrics, update_metrics +from frigate.util.builtin import ( + clean_camera_user_pass, + flatten_config_data, + process_config_query_string, + update_yaml_file_bulk, +) +from frigate.util.config import find_config_file +from frigate.util.services import ( + get_nvidia_driver_info, + process_logs, + restart_frigate, + vainfo_hwaccel, +) +from frigate.util.time import get_tz_modifiers +from frigate.version import VERSION + +logger = logging.getLogger(__name__) + + +router = APIRouter(tags=[Tags.app]) + + +@router.get( + "/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) +def is_healthy(): + return "Frigate is running. Alive and healthy!" + + +@router.get("/config/schema.json", dependencies=[Depends(allow_public())]) +def config_schema(request: Request): + return Response( + content=request.app.frigate_config.schema_json(), media_type="application/json" + ) + + +@router.get( + "/version", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] +) +def version(): + return VERSION + + +@router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) +def stats(request: Request): + return JSONResponse(content=request.app.stats_emitter.get_latest_stats()) + + +@router.get("/stats/history", dependencies=[Depends(allow_any_authenticated())]) +def stats_history(request: Request, keys: str = None): + if keys: + keys = keys.split(",") + + return JSONResponse(content=request.app.stats_emitter.get_stats_history(keys)) + + +@router.get("/metrics", dependencies=[Depends(allow_any_authenticated())]) +def metrics(request: Request): + """Expose Prometheus metrics endpoint and update metrics with latest stats""" + # Retrieve the latest statistics and update the Prometheus metrics + stats = request.app.stats_emitter.get_latest_stats() + # query DB for count of events by camera, label + event_counts: List[Dict[str, Any]] = ( + Event.select(Event.camera, Event.label, fn.Count()) + .group_by(Event.camera, Event.label) + .dicts() + ) + + update_metrics(stats=stats, event_counts=event_counts) + content, content_type = get_metrics() + return Response(content=content, media_type=content_type) + + +@router.get("/config", dependencies=[Depends(allow_any_authenticated())]) +def config(request: Request): + config_obj: FrigateConfig = request.app.frigate_config + config: dict[str, dict[str, Any]] = config_obj.model_dump( + mode="json", warnings="none", exclude_none=True + ) + + # remove the mqtt password + config["mqtt"].pop("password", None) + + # remove the proxy secret + config["proxy"].pop("auth_secret", None) + + for camera_name, camera in request.app.frigate_config.cameras.items(): + camera_dict = config["cameras"][camera_name] + + # clean paths + for input in camera_dict.get("ffmpeg", {}).get("inputs", []): + input["path"] = clean_camera_user_pass(input["path"]) + + # add clean ffmpeg_cmds + camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds) + for cmd in camera_dict["ffmpeg_cmds"]: + cmd["cmd"] = clean_camera_user_pass(" ".join(cmd["cmd"])) + + # ensure that zones are relative + for zone_name, zone in config_obj.cameras[camera_name].zones.items(): + camera_dict["zones"][zone_name]["color"] = zone.color + + # remove go2rtc stream passwords + go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump( + mode="json", warnings="none", exclude_none=True + ) + for stream_name, stream in go2rtc.get("streams", {}).items(): + if stream is None: + continue + if isinstance(stream, str): + cleaned = clean_camera_user_pass(stream) + else: + cleaned = [] + + for item in stream: + cleaned.append(clean_camera_user_pass(item)) + + config["go2rtc"]["streams"][stream_name] = cleaned + + config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} + config["model"]["colormap"] = config_obj.model.colormap + config["model"]["all_attributes"] = config_obj.model.all_attributes + config["model"]["non_logo_attributes"] = config_obj.model.non_logo_attributes + + # Add model plus data if plus is enabled + if config["plus"]["enabled"]: + model_path = config.get("model", {}).get("path") + if model_path: + model_json_path = FilePath(model_path).with_suffix(".json") + try: + with open(model_json_path, "r") as f: + model_plus_data = json.load(f) + config["model"]["plus"] = model_plus_data + except FileNotFoundError: + config["model"]["plus"] = None + except json.JSONDecodeError: + config["model"]["plus"] = None + else: + config["model"]["plus"] = None + + # use merged labelamp + for detector_config in config["detectors"].values(): + detector_config["model"]["labelmap"] = ( + request.app.frigate_config.model.merged_labelmap + ) + + return JSONResponse(content=config) + + +@router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))]) +def config_raw_paths(request: Request): + """Admin-only endpoint that returns camera paths and go2rtc streams without credential masking.""" + config_obj: FrigateConfig = request.app.frigate_config + + raw_paths = {"cameras": {}, "go2rtc": {"streams": {}}} + + # Extract raw camera ffmpeg input paths + for camera_name, camera in config_obj.cameras.items(): + raw_paths["cameras"][camera_name] = { + "ffmpeg": { + "inputs": [ + {"path": input.path, "roles": input.roles} + for input in camera.ffmpeg.inputs + ] + } + } + + # Extract raw go2rtc stream URLs + go2rtc_config = config_obj.go2rtc.model_dump( + mode="json", warnings="none", exclude_none=True + ) + for stream_name, stream in go2rtc_config.get("streams", {}).items(): + if stream is None: + continue + raw_paths["go2rtc"]["streams"][stream_name] = stream + + return JSONResponse(content=raw_paths) + + +@router.get("/config/raw", dependencies=[Depends(allow_any_authenticated())]) +def config_raw(): + config_file = find_config_file() + + if not os.path.isfile(config_file): + return JSONResponse( + content=({"success": False, "message": "Could not find file"}), + status_code=404, + ) + + with open(config_file, "r") as f: + raw_config = f.read() + f.close() + + return JSONResponse( + content=raw_config, media_type="text/plain", status_code=200 + ) + + +@router.post("/config/save", dependencies=[Depends(require_role(["admin"]))]) +def config_save(save_option: str, body: Any = Body(media_type="text/plain")): + new_config = body.decode() + if not new_config: + return JSONResponse( + content=( + {"success": False, "message": "Config with body param is required"} + ), + status_code=400, + ) + + # Validate the config schema + try: + # Use ruamel to parse and preserve line numbers + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(StringIO(new_config)) + + FrigateConfig.parse_yaml(new_config) + + except ValidationError as e: + error_message = [] + + for error in e.errors(): + error_path = error["loc"] + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key = int(part) if part.isdigit() else part + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception: + line_number = "Unable to determine" + + error_message.append( + f"Line {line_number}: {' -> '.join(map(str, error_path))} - {error.get('msg', error.get('type', 'Unknown'))}" + ) + + return JSONResponse( + content=( + { + "success": False, + "message": "Your configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n" + + "\n".join(error_message), + } + ), + status_code=400, + ) + + except Exception: + return JSONResponse( + content=( + { + "success": False, + "message": f"\nYour configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n{escape(str(traceback.format_exc()))}", + } + ), + status_code=400, + ) + + # Save the config to file + try: + config_file = find_config_file() + + with open(config_file, "w") as f: + f.write(new_config) + f.close() + except Exception: + return JSONResponse( + content=( + { + "success": False, + "message": "Could not write config file, be sure that Frigate has write permission on the config file.", + } + ), + status_code=400, + ) + + if save_option == "restart": + try: + restart_frigate() + except Exception as e: + logging.error(f"Error restarting Frigate: {e}") + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully saved, unable to restart Frigate", + } + ), + status_code=200, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully saved, restarting (this can take up to one minute)...", + } + ), + status_code=200, + ) + else: + return JSONResponse( + content=({"success": True, "message": "Config successfully saved."}), + status_code=200, + ) + + +@router.put("/config/set", dependencies=[Depends(require_role(["admin"]))]) +def config_set(request: Request, body: AppConfigSetBody): + config_file = find_config_file() + + with open(config_file, "r") as f: + old_raw_config = f.read() + + try: + updates = {} + + # process query string parameters (takes precedence over body.config_data) + parsed_url = urllib.parse.urlparse(str(request.url)) + query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True) + + # Filter out empty keys but keep blank values for non-empty keys + query_string = {k: v for k, v in query_string.items() if k} + + if query_string: + updates = process_config_query_string(query_string) + elif body.config_data: + updates = flatten_config_data(body.config_data) + + if not updates: + return JSONResponse( + content=( + {"success": False, "message": "No configuration data provided"} + ), + status_code=400, + ) + + # apply all updates in a single operation + update_yaml_file_bulk(config_file, updates) + + # validate the updated config + with open(config_file, "r") as f: + new_raw_config = f.read() + + try: + config = FrigateConfig.parse(new_raw_config) + except Exception: + with open(config_file, "w") as f: + f.write(old_raw_config) + f.close() + logger.error(f"\nConfig Error:\n\n{str(traceback.format_exc())}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error parsing config. Check logs for error message.", + } + ), + status_code=400, + ) + except Exception as e: + logging.error(f"Error updating config: {e}") + return JSONResponse( + content=({"success": False, "message": "Error updating config"}), + status_code=500, + ) + + if body.requires_restart == 0 or body.update_topic: + old_config: FrigateConfig = request.app.frigate_config + request.app.frigate_config = config + + if body.update_topic: + if body.update_topic.startswith("config/cameras/"): + _, _, camera, field = body.update_topic.split("/") + + if field == "add": + settings = config.cameras[camera] + elif field == "remove": + settings = old_config.cameras[camera] + else: + settings = config.get_nested_object(body.update_topic) + + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera), + settings, + ) + else: + # Generic handling for global config updates + settings = config.get_nested_object(body.update_topic) + + # Publish None for removal, actual config for add/update + request.app.config_publisher.publisher.publish( + body.update_topic, settings + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Config successfully updated, restart to apply", + } + ), + status_code=200, + ) + + +@router.get("/vainfo", dependencies=[Depends(allow_any_authenticated())]) +def vainfo(): + vainfo = vainfo_hwaccel() + return JSONResponse( + content={ + "return_code": vainfo.returncode, + "stderr": ( + vainfo.stderr.decode("unicode_escape").strip() + if vainfo.returncode != 0 + else "" + ), + "stdout": ( + vainfo.stdout.decode("unicode_escape").strip() + if vainfo.returncode == 0 + else "" + ), + } + ) + + +@router.get("/nvinfo", dependencies=[Depends(allow_any_authenticated())]) +def nvinfo(): + return JSONResponse(content=get_nvidia_driver_info()) + + +@router.get( + "/logs/{service}", + tags=[Tags.logs], + dependencies=[Depends(allow_any_authenticated())], +) +async def logs( + service: str = Path(enum=["frigate", "nginx", "go2rtc"]), + download: Optional[str] = None, + stream: Optional[bool] = False, + start: Optional[int] = 0, + end: Optional[int] = None, +): + """Get logs for the requested service (frigate/nginx/go2rtc)""" + + def download_logs(service_location: str): + try: + file = open(service_location, "r") + contents = file.read() + file.close() + return JSONResponse(jsonable_encoder(contents)) + except FileNotFoundError as e: + logger.error(e) + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, + ) + + async def stream_logs(file_path: str): + """Asynchronously stream log lines.""" + buffer = "" + try: + async with aiofiles.open(file_path, "r") as file: + await file.seek(0, 2) + while True: + line = await file.readline() + if line: + buffer += line + # Process logs only when there are enough lines in the buffer + if "\n" in buffer: + _, processed_lines = process_logs(buffer, service) + buffer = "" + for processed_line in processed_lines: + yield f"{processed_line}\n" + else: + await asyncio.sleep(0.1) + except FileNotFoundError: + yield "Log file not found.\n" + + log_locations = { + "frigate": "/dev/shm/logs/frigate/current", + "go2rtc": "/dev/shm/logs/go2rtc/current", + "nginx": "/dev/shm/logs/nginx/current", + } + service_location = log_locations.get(service) + + if not service_location: + return JSONResponse( + content={"success": False, "message": "Not a valid service"}, + status_code=404, + ) + + if download: + return download_logs(service_location) + + if stream: + return StreamingResponse(stream_logs(service_location), media_type="text/plain") + + # For full logs initially + try: + async with aiofiles.open(service_location, "r") as file: + contents = await file.read() + + total_lines, log_lines = process_logs(contents, service, start, end) + return JSONResponse( + content={"totalLines": total_lines, "lines": log_lines}, + status_code=200, + ) + except FileNotFoundError as e: + logger.error(e) + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, + ) + + +@router.post("/restart", dependencies=[Depends(require_role(["admin"]))]) +def restart(): + try: + restart_frigate() + except Exception as e: + logging.error(f"Error restarting Frigate: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to restart Frigate.", + } + ), + status_code=500, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Restarting (this can take up to one minute)...", + } + ), + status_code=200, + ) + + +@router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) +def get_labels(camera: str = ""): + try: + if camera: + events = Event.select(Event.label).where(Event.camera == camera).distinct() + else: + events = Event.select(Event.label).distinct() + except Exception as e: + logger.error(e) + return JSONResponse( + content=({"success": False, "message": "Failed to get labels"}), + status_code=404, + ) + + labels = sorted([e.label for e in events]) + return JSONResponse(content=labels) + + +@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) +def get_sub_labels(split_joined: Optional[int] = None): + try: + events = Event.select(Event.sub_label).distinct() + except Exception: + return JSONResponse( + content=({"success": False, "message": "Failed to get sub_labels"}), + status_code=404, + ) + + sub_labels = [e.sub_label for e in events] + + if None in sub_labels: + sub_labels.remove(None) + + if split_joined: + original_labels = sub_labels.copy() + + for label in original_labels: + if "," in label: + sub_labels.remove(label) + parts = label.split(",") + + for part in parts: + if part.strip() not in sub_labels: + sub_labels.append(part.strip()) + + sub_labels.sort() + return JSONResponse(content=sub_labels) + + +@router.get("/plus/models", dependencies=[Depends(allow_any_authenticated())]) +def plusModels(request: Request, filterByCurrentModelDetector: bool = False): + if not request.app.frigate_config.plus_api.is_active(): + return JSONResponse( + content=({"success": False, "message": "Frigate+ is not enabled"}), + status_code=400, + ) + + models: dict[Any, Any] = request.app.frigate_config.plus_api.get_models() + + if not models["list"]: + return JSONResponse( + content=({"success": False, "message": "No models found"}), + status_code=400, + ) + + modelList = models["list"] + + # current model type + modelType = request.app.frigate_config.model.model_type + + # current detectorType for comparing to supportedDetectors + detectorType = list(request.app.frigate_config.detectors.values())[0].type + + validModels = [] + + for model in sorted( + filter( + lambda m: ( + not filterByCurrentModelDetector + or (detectorType in m["supportedDetectors"] and modelType in m["type"]) + ), + modelList, + ), + key=(lambda m: m["trainDate"]), + reverse=True, + ): + validModels.append(model) + + return JSONResponse(content=validModels) + + +@router.get( + "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] +) +def get_recognized_license_plates(split_joined: Optional[int] = None): + try: + query = ( + Event.select( + SQL("json_extract(data, '$.recognized_license_plate') AS plate") + ) + .where(SQL("json_extract(data, '$.recognized_license_plate') IS NOT NULL")) + .distinct() + ) + recognized_license_plates = [row[0] for row in query.tuples()] + except Exception: + return JSONResponse( + content=( + {"success": False, "message": "Failed to get recognized license plates"} + ), + status_code=404, + ) + + if split_joined: + original_recognized_license_plates = recognized_license_plates.copy() + for recognized_license_plate in original_recognized_license_plates: + if recognized_license_plate and "," in recognized_license_plate: + recognized_license_plates.remove(recognized_license_plate) + parts = recognized_license_plate.split(",") + for part in parts: + if part.strip() not in recognized_license_plates: + recognized_license_plates.append(part.strip()) + + recognized_license_plates = list(set(recognized_license_plates)) + recognized_license_plates.sort() + return JSONResponse(content=recognized_license_plates) + + +@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())]) +def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None): + clauses = [] + + selected_columns = [ + Timeline.timestamp, + Timeline.camera, + Timeline.source, + Timeline.source_id, + Timeline.class_type, + Timeline.data, + ] + + if camera != "all": + clauses.append((Timeline.camera == camera)) + + if source_id: + source_ids = [sid.strip() for sid in source_id.split(",")] + if len(source_ids) == 1: + clauses.append((Timeline.source_id == source_ids[0])) + else: + clauses.append((Timeline.source_id.in_(source_ids))) + + if len(clauses) == 0: + clauses.append((True)) + + timeline = ( + Timeline.select(*selected_columns) + .where(reduce(operator.and_, clauses)) + .order_by(Timeline.timestamp.asc()) + .limit(limit) + .dicts() + ) + + return JSONResponse(content=[t for t in timeline]) + + +@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) +def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()): + """Get hourly summary for timeline.""" + cameras = params.cameras + labels = params.labels + before = params.before + after = params.after + limit = params.limit + tz_name = params.timezone + + _, minute_modifier, _ = get_tz_modifiers(tz_name) + minute_offset = int(minute_modifier.split(" ")[0]) + + clauses = [] + + if cameras != "all": + camera_list = cameras.split(",") + clauses.append((Timeline.camera << camera_list)) + + if labels != "all": + label_list = labels.split(",") + clauses.append((Timeline.data["label"] << label_list)) + + if before: + clauses.append((Timeline.timestamp < before)) + + if after: + clauses.append((Timeline.timestamp > after)) + + if len(clauses) == 0: + clauses.append((True)) + + timeline = ( + Timeline.select( + Timeline.camera, + Timeline.timestamp, + Timeline.data, + Timeline.class_type, + Timeline.source_id, + Timeline.source, + ) + .where(reduce(operator.and_, clauses)) + .order_by(Timeline.timestamp.desc()) + .limit(limit) + .dicts() + .iterator() + ) + + count = 0 + start = 0 + end = 0 + hours: dict[str, list[dict[str, Any]]] = {} + + for t in timeline: + if count == 0: + start = t["timestamp"] + else: + end = t["timestamp"] + + count += 1 + + hour = ( + datetime.fromtimestamp(t["timestamp"]).replace( + minute=0, second=0, microsecond=0 + ) + + timedelta( + minutes=minute_offset, + ) + ).timestamp() + if hour not in hours: + hours[hour] = [t] + else: + hours[hour].insert(0, t) + + return JSONResponse( + content={ + "start": start, + "end": end, + "count": count, + "hours": hours, + } + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/auth.py b/sam2-cpu/frigate-dev/frigate/api/auth.py new file mode 100644 index 0000000..d3b5006 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/auth.py @@ -0,0 +1,1020 @@ +"""Auth apis.""" + +import base64 +import hashlib +import ipaddress +import json +import logging +import os +import re +import secrets +import time +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, Response +from fastapi.responses import JSONResponse, RedirectResponse +from joserfc import jwt +from peewee import DoesNotExist +from slowapi import Limiter + +from frigate.api.defs.request.app_body import ( + AppPostLoginBody, + AppPostUsersBody, + AppPutPasswordBody, + AppPutRoleBody, +) +from frigate.api.defs.tags import Tags +from frigate.config import AuthConfig, ProxyConfig +from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM +from frigate.models import User + +logger = logging.getLogger(__name__) + + +def require_admin_by_default(): + """ + Global admin requirement dependency for all endpoints by default. + + This is set as the default dependency on the FastAPI app to ensure all + endpoints require admin access unless explicitly overridden with + allow_public(), allow_any_authenticated(), or require_role(). + + Port 5000 (internal) always has admin role set by the /auth endpoint, + so this check passes automatically for internal requests. + + Certain paths are exempted from the global admin check because they must + be accessible before authentication (login, auth) or they have their own + route-level authorization dependencies that handle access control. + """ + # Paths that have route-level auth dependencies and should bypass global admin check + # These paths still have authorization - it's handled by their route-level dependencies + EXEMPT_PATHS = { + # Public auth endpoints (allow_public) + "/auth", + "/auth/first_time_login", + "/login", + "/logout", + # Authenticated user endpoints (allow_any_authenticated) + "/profile", + # Public info endpoints (allow_public) + "/", + "/version", + "/config/schema.json", + # Authenticated user endpoints (allow_any_authenticated) + "/metrics", + "/stats", + "/stats/history", + "/config", + "/config/raw", + "/vainfo", + "/nvinfo", + "/labels", + "/sub_labels", + "/plus/models", + "/recognized_license_plates", + "/timeline", + "/timeline/hourly", + "/recordings/storage", + "/recordings/summary", + "/recordings/unavailable", + "/go2rtc/streams", + "/event_ids", + "/events", + "/exports", + } + + # Path prefixes that should be exempt (for paths with parameters) + EXEMPT_PREFIXES = ( + "/logs/", # /logs/{service} + "/review", # /review, /review/{id}, /review/summary, /review_ids, etc. + "/reviews/", # /reviews/viewed, /reviews/delete + "/events/", # /events/{id}/thumbnail, /events/summary, etc. (camera-scoped) + "/export/", # /export/{camera}/start/..., /export/{id}/rename, /export/{id} + "/go2rtc/streams/", # /go2rtc/streams/{camera} + "/users/", # /users/{username}/password (has own auth) + "/preview/", # /preview/{file}/thumbnail.jpg + "/exports/", # /exports/{export_id} + "/vod/", # /vod/{camera_name}/... + "/notifications/", # /notifications/pubkey, /notifications/register + ) + + async def admin_checker(request: Request): + path = request.url.path + + # Check exact path matches + if path in EXEMPT_PATHS: + return + + # Check prefix matches for parameterized paths + if path.startswith(EXEMPT_PREFIXES): + return + + # Dynamic camera path exemption: + # Any path whose first segment matches a configured camera name should + # bypass the global admin requirement. These endpoints enforce access + # via route-level dependencies (e.g. require_camera_access) to ensure + # per-camera authorization. This allows non-admin authenticated users + # (e.g. viewer role) to access camera-specific resources without + # needing admin privileges. + try: + if path.startswith("/"): + first_segment = path.split("/", 2)[1] + if ( + first_segment + and first_segment in request.app.frigate_config.cameras + ): + return + except Exception: + pass + + # For all other paths, require admin role + # Port 5000 (internal) requests have admin role set automatically + role = request.headers.get("remote-role") + if role == "admin": + return + + raise HTTPException( + status_code=403, + detail="Access denied. A user with the admin role is required.", + ) + + return admin_checker + + +def _is_authenticated(request: Request) -> bool: + """ + Helper to determine if a request is from an authenticated user. + + Returns True if the request has a valid authenticated user (not anonymous). + Port 5000 internal requests are considered anonymous despite having admin role. + """ + username = request.headers.get("remote-user") + return username is not None and username != "anonymous" + + +def allow_public(): + """ + Override dependency to allow unauthenticated access to an endpoint. + + Use this for endpoints that should be publicly accessible without + authentication, such as login page, health checks, or pre-auth info. + + Example: + @router.get("/public-endpoint", dependencies=[Depends(allow_public())]) + """ + + async def public_checker(request: Request): + return # Always allow + + return public_checker + + +def allow_any_authenticated(): + """ + Override dependency to allow any authenticated user (bypass admin requirement). + + Allows: + - Port 5000 internal requests (have admin role despite anonymous user) + - Any authenticated user with a real username (not "anonymous") + + Rejects: + - Port 8971 requests with anonymous user (auth disabled, no proxy auth) + + Example: + @router.get("/authenticated-endpoint", dependencies=[Depends(allow_any_authenticated())]) + """ + + async def auth_checker(request: Request): + # Port 5000 requests have admin role and should be allowed + role = request.headers.get("remote-role") + if role == "admin": + return + + # Otherwise require a real authenticated user (not anonymous) + if not _is_authenticated(request): + raise HTTPException(status_code=401, detail="Authentication required") + return + + return auth_checker + + +router = APIRouter(tags=[Tags.auth]) + + +@router.get("/auth/first_time_login", dependencies=[Depends(allow_public())]) +def first_time_login(request: Request): + """Return whether the admin first-time login help flag is set in config. + + This endpoint is intentionally unauthenticated so the login page can + query it before a user is authenticated. + """ + auth_config = request.app.frigate_config.auth + + return JSONResponse( + content={ + "admin_first_time_login": auth_config.admin_first_time_login + or auth_config.reset_admin_password + } + ) + + +class RateLimiter: + _limit = "" + + def set_limit(self, limit: str): + self._limit = limit + + def get_limit(self) -> str: + return self._limit + + +rateLimiter = RateLimiter() + + +def get_remote_addr(request: Request): + route = list(reversed(request.headers.get("x-forwarded-for").split(","))) + logger.debug(f"IP Route: {[r for r in route]}") + trusted_proxies = [] + for proxy in request.app.frigate_config.auth.trusted_proxies: + try: + network = ipaddress.ip_network(proxy) + except ValueError: + logger.warning(f"Unable to parse trusted network: {proxy}") + trusted_proxies.append(network) + + # return the first remote address that is not trusted + for addr in route: + ip = ipaddress.ip_address(addr.strip()) + logger.debug(f"Checking {ip} (v{ip.version})") + trusted = False + for trusted_proxy in trusted_proxies: + logger.debug( + f"Checking against trusted proxy: {trusted_proxy} (v{trusted_proxy.version})" + ) + if trusted_proxy.version == 4: + ipv4 = ip.ipv4_mapped if ip.version == 6 else ip + if ipv4 is not None and ipv4 in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + elif trusted_proxy.version == 6 and ip.version == 6: + if ip in trusted_proxy: + trusted = True + logger.debug(f"Trusted: {str(ip)} by {str(trusted_proxy)}") + break + if trusted: + logger.debug(f"{ip} is trusted") + continue + else: + logger.debug(f"First untrusted IP: {str(ip)}") + return str(ip) + + # if there wasn't anything in the route, just return the default + remote_addr = None + + if hasattr(request, "remote_addr"): + remote_addr = request.remote_addr + + return remote_addr or "127.0.0.1" + + +def get_jwt_secret() -> str: + jwt_secret = None + # check env var + if JWT_SECRET_ENV_VAR in os.environ: + logger.debug( + f"Using jwt secret from {JWT_SECRET_ENV_VAR} environment variable." + ) + jwt_secret = os.environ.get(JWT_SECRET_ENV_VAR) + # check docker secrets + elif os.path.isfile(os.path.join("/run/secrets", JWT_SECRET_ENV_VAR)): + logger.debug(f"Using jwt secret from {JWT_SECRET_ENV_VAR} docker secret file.") + jwt_secret = ( + Path(os.path.join("/run/secrets", JWT_SECRET_ENV_VAR)).read_text().strip() + ) + # check for the add-on options file + elif os.path.isfile("/data/options.json"): + with open("/data/options.json") as f: + raw_options = f.read() + logger.debug("Using jwt secret from Home Assistant Add-on options file.") + options = json.loads(raw_options) + jwt_secret = options.get("jwt_secret") + + if jwt_secret is None: + jwt_secret_file = os.path.join(CONFIG_DIR, ".jwt_secret") + # check .jwt_secrets file + if not os.path.isfile(jwt_secret_file): + logger.debug( + "No jwt secret found. Generating one and storing in .jwt_secret file in config directory." + ) + jwt_secret = secrets.token_hex(64) + try: + fd = os.open( + jwt_secret_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600 + ) + with os.fdopen(fd, "w") as f: + f.write(str(jwt_secret)) + except Exception: + logger.warning( + "Unable to write jwt token file to config directory. A new jwt token will be created at each startup." + ) + else: + logger.debug("Using jwt secret from .jwt_secret file in config directory.") + with open(jwt_secret_file) as f: + try: + jwt_secret = f.readline().strip() + except Exception: + logger.warning( + "Unable to read jwt token from .jwt_secret file in config directory. A new jwt token will be created at each startup." + ) + jwt_secret = secrets.token_hex(64) + + if len(jwt_secret) < 64: + logger.warning("JWT Secret is recommended to be 64 characters or more") + + return jwt_secret + + +def hash_password(password: str, salt=None, iterations=600000): + if salt is None: + salt = secrets.token_hex(16) + assert salt and isinstance(salt, str) and "$" not in salt + assert isinstance(password, str) + pw_hash = hashlib.pbkdf2_hmac( + "sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations + ) + b64_hash = base64.b64encode(pw_hash).decode("ascii").strip() + return "{}${}${}${}".format(PASSWORD_HASH_ALGORITHM, iterations, salt, b64_hash) + + +def verify_password(password, password_hash): + if (password_hash or "").count("$") != 3: + return False + algorithm, iterations, salt, b64_hash = password_hash.split("$", 3) + iterations = int(iterations) + assert algorithm == PASSWORD_HASH_ALGORITHM + compare_hash = hash_password(password, salt, iterations) + return secrets.compare_digest(password_hash, compare_hash) + + +def validate_password_strength(password: str) -> tuple[bool, Optional[str]]: + """ + Validate password strength. + + Returns a tuple of (is_valid, error_message). + """ + if not password: + return False, "Password cannot be empty" + + if len(password) < 8: + return False, "Password must be at least 8 characters long" + + if not any(c.isupper() for c in password): + return False, "Password must contain at least one uppercase letter" + + if not any(c.isdigit() for c in password): + return False, "Password must contain at least one digit" + + if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password): + return False, "Password must contain at least one special character" + + return True, None + + +def create_encoded_jwt(user, role, expiration, secret): + return jwt.encode( + {"alg": "HS256"}, + {"sub": user, "role": role, "exp": expiration, "iat": int(time.time())}, + secret, + ) + + +def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure): + # TODO: ideally this would set secure as well, but that requires TLS + response.set_cookie( + key=cookie_name, + value=encoded_jwt, + httponly=True, + expires=expiration, + secure=secure, + ) + + +async def get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + + if not username or not role: + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + + return {"username": username, "role": role} + + +def require_role(required_roles: List[str]): + async def role_checker(request: Request): + proxy_config: ProxyConfig = request.app.frigate_config.proxy + config_roles = list(request.app.frigate_config.auth.roles.keys()) + + # Get role from header (could be comma-separated) + role_header = request.headers.get("remote-role") + roles = ( + [r.strip() for r in role_header.split(proxy_config.separator)] + if role_header + else [] + ) + + # Check if we have any roles + if not roles: + raise HTTPException(status_code=403, detail="Role not provided") + + # enforce config roles + valid_roles = [r for r in roles if r in config_roles] + if not valid_roles: + raise HTTPException( + status_code=403, + detail=f"No valid roles found in {roles}. Required: {', '.join(required_roles)}. Available: {', '.join(config_roles)}", + ) + + if not any(role in required_roles for role in valid_roles): + raise HTTPException( + status_code=403, + detail=f"Role {', '.join(valid_roles)} not authorized. Required: {', '.join(required_roles)}", + ) + + return next( + (role for role in valid_roles if role in required_roles), valid_roles[0] + ) + + return role_checker + + +def resolve_role( + headers: dict, proxy_config: ProxyConfig, config_roles: set[str] +) -> str: + """ + Determine the effective role for a request based on proxy headers and configuration. + + Order of resolution: + 1. If a role header is defined in proxy_config.header_map.role: + - If a role_map is configured, treat the header as group claims + (split by proxy_config.separator) and map to roles. + - If no role_map is configured, treat the header as role names directly. + 2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'. + + Args: + headers (dict): Incoming request headers (case-insensitive). + proxy_config (ProxyConfig): Proxy configuration. + config_roles (set[str]): Set of valid roles from config. + + Returns: + str: Resolved role (one of config_roles or validated default). + """ + default_role = proxy_config.default_role + role_header = proxy_config.header_map.role + + # Validate default_role against config; fallback to 'viewer' if invalid + validated_default = default_role if default_role in config_roles else "viewer" + if not config_roles: + validated_default = "viewer" # Edge case: no roles defined + + if not role_header: + logger.debug( + "No role header configured in proxy_config.header_map. Returning validated default role '%s'.", + validated_default, + ) + return validated_default + + raw_value = headers.get(role_header, "") + logger.debug("Raw role header value from '%s': %r", role_header, raw_value) + + if not raw_value: + logger.debug( + "Role header missing or empty. Returning validated default role '%s'.", + validated_default, + ) + return validated_default + + # role_map configured, treat header as group claims + if proxy_config.header_map.role_map: + groups = [ + g.strip() for g in raw_value.split(proxy_config.separator) if g.strip() + ] + logger.debug("Parsed groups from role header: %s", groups) + + matched_roles = { + role_name + for role_name, required_groups in proxy_config.header_map.role_map.items() + if any(group in groups for group in required_groups) + } + logger.debug("Matched roles from role_map: %s", matched_roles) + + if matched_roles: + resolved = next( + (r for r in config_roles if r in matched_roles), validated_default + ) + logger.debug("Resolved role (with role_map) to '%s'.", resolved) + return resolved + + logger.debug( + "No role_map match for groups '%s'. Using validated default role '%s'.", + raw_value, + validated_default, + ) + return validated_default + + # no role_map, treat as role names directly + roles_from_header = [ + r.strip().lower() for r in raw_value.split(proxy_config.separator) if r.strip() + ] + logger.debug("Parsed roles directly from header: %s", roles_from_header) + + resolved = next( + (r for r in config_roles if r in roles_from_header), + validated_default, + ) + if resolved == validated_default and roles_from_header: + logger.debug( + "Provided proxy role header values '%s' did not contain a valid role. Using validated default role '%s'.", + raw_value, + validated_default, + ) + else: + logger.debug("Resolved role (direct header) to '%s'.", resolved) + + return resolved + + +# Endpoints +@router.get( + "/auth", + dependencies=[Depends(allow_public())], + summary="Authenticate request", + description=( + "Authenticates the current request based on proxy headers or JWT token. " + "This endpoint verifies authentication credentials and manages JWT token refresh. " + "On success, no JSON body is returned; authentication state is communicated via response headers and cookies." + ), + status_code=202, + responses={ + 202: { + "description": "Authentication Accepted (no response body)", + "headers": { + "remote-user": { + "description": 'Authenticated username or "anonymous" in proxy-only mode', + "schema": {"type": "string"}, + }, + "remote-role": { + "description": "Resolved role (e.g., admin, viewer, or custom)", + "schema": {"type": "string"}, + }, + "Set-Cookie": { + "description": "May include refreshed JWT cookie when applicable", + "schema": {"type": "string"}, + }, + }, + }, + 401: {"description": "Authentication Failed"}, + }, +) +def auth(request: Request): + auth_config: AuthConfig = request.app.frigate_config.auth + proxy_config: ProxyConfig = request.app.frigate_config.proxy + + success_response = Response("", status_code=202) + + # dont require auth if the request is on the internal port + # this header is set by Frigate's nginx proxy, so it cant be spoofed + if int(request.headers.get("x-server-port", default=0)) == 5000: + success_response.headers["remote-user"] = "anonymous" + success_response.headers["remote-role"] = "admin" + return success_response + + fail_response = Response("", status_code=401) + + # ensure the proxy secret matches if configured + if ( + proxy_config.auth_secret is not None + and request.headers.get("x-proxy-secret", "") != proxy_config.auth_secret + ): + logger.debug("X-Proxy-Secret header does not match configured secret value") + return fail_response + + # if auth is disabled, just apply the proxy header map and return success + if not auth_config.enabled: + # pass the user header value from the upstream proxy if a mapping is specified + # or use anonymous if none are specified + user_header = proxy_config.header_map.user + success_response.headers["remote-user"] = ( + request.headers.get(user_header, default="anonymous") + if user_header + else "anonymous" + ) + + # parse header and resolve a valid role + config_roles_set = set(auth_config.roles.keys()) + role = resolve_role(request.headers, proxy_config, config_roles_set) + + success_response.headers["remote-role"] = role + return success_response + + # now apply authentication + fail_response.headers["location"] = "/login" + + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_REFRESH = request.app.frigate_config.auth.refresh_time + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + + jwt_source = None + encoded_token = None + if "authorization" in request.headers and request.headers[ + "authorization" + ].startswith("Bearer "): + jwt_source = "authorization" + logger.debug("Found authorization header") + encoded_token = request.headers["authorization"].replace("Bearer ", "") + elif JWT_COOKIE_NAME in request.cookies: + jwt_source = "cookie" + logger.debug("Found jwt cookie") + encoded_token = request.cookies[JWT_COOKIE_NAME] + + if encoded_token is None: + logger.debug("No jwt token found") + return fail_response + + try: + token = jwt.decode(encoded_token, request.app.jwt_token) + if "sub" not in token.claims: + logger.debug("user not set in jwt token") + return fail_response + if "role" not in token.claims: + logger.debug("role not set in jwt token") + return fail_response + if "exp" not in token.claims: + logger.debug("exp not set in jwt token") + return fail_response + + user = token.claims.get("sub") + role = token.claims.get("role") + current_time = int(time.time()) + + # if the jwt is expired + expiration = int(token.claims.get("exp")) + logger.debug( + f"current time: {datetime.fromtimestamp(current_time).strftime('%c')}" + ) + logger.debug( + f"jwt expires at: {datetime.fromtimestamp(expiration).strftime('%c')}" + ) + logger.debug( + f"jwt refresh at: {datetime.fromtimestamp(expiration - JWT_REFRESH).strftime('%c')}" + ) + if expiration <= current_time: + logger.debug("jwt token expired") + return fail_response + + # if the jwt cookie is expiring soon + if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time: + logger.debug("jwt token expiring soon, refreshing cookie") + + # Check if password has been changed since token was issued + # If so, force re-login by rejecting the refresh + try: + user_obj = User.get_by_id(user) + if user_obj.password_changed_at is not None: + token_iat = int(token.claims.get("iat", 0)) + password_changed_timestamp = int( + user_obj.password_changed_at.timestamp() + ) + if token_iat < password_changed_timestamp: + logger.debug( + "jwt token issued before password change, rejecting refresh" + ) + return fail_response + except DoesNotExist: + logger.debug("user not found") + return fail_response + + new_expiration = current_time + JWT_SESSION_LENGTH + new_encoded_jwt = create_encoded_jwt( + user, role, new_expiration, request.app.jwt_token + ) + set_jwt_cookie( + success_response, + JWT_COOKIE_NAME, + new_encoded_jwt, + new_expiration, + JWT_COOKIE_SECURE, + ) + + success_response.headers["remote-user"] = user + success_response.headers["remote-role"] = role + return success_response + except Exception as e: + logger.error(f"Error parsing jwt: {e}") + return fail_response + + +@router.get( + "/profile", + dependencies=[Depends(allow_any_authenticated())], + summary="Get user profile", + description="Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions.", +) +def profile(request: Request): + username = request.headers.get("remote-user", "anonymous") + role = request.headers.get("remote-role", "viewer") + + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + + return JSONResponse( + content={"username": username, "role": role, "allowed_cameras": allowed_cameras} + ) + + +@router.get( + "/logout", + dependencies=[Depends(allow_public())], + summary="Logout user", + description="Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication.", +) +def logout(request: Request): + auth_config: AuthConfig = request.app.frigate_config.auth + response = RedirectResponse("/login", status_code=303) + response.delete_cookie(auth_config.cookie_name) + return response + + +limiter = Limiter(key_func=get_remote_addr) + + +@router.post( + "/login", + dependencies=[Depends(allow_public())], + summary="Login with credentials", + description='Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header.\n\nExample using Bearer token:\n```\ncurl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile\n```', +) +@limiter.limit(limit_value=rateLimiter.get_limit) +def login(request: Request, body: AppPostLoginBody): + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + user = body.user + password = body.password + + try: + db_user: User = User.get_by_id(user) + except DoesNotExist: + return JSONResponse(content={"message": "Login failed"}, status_code=401) + + password_hash = db_user.password_hash + if verify_password(password, password_hash): + role = getattr(db_user, "role", "viewer") + config_roles_set = set(request.app.frigate_config.auth.roles.keys()) + if role not in config_roles_set: + logger.warning( + f"User {db_user.username} has an invalid role {role}, falling back to 'viewer'." + ) + role = "viewer" + expiration = int(time.time()) + JWT_SESSION_LENGTH + encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token) + response = Response("", 200) + set_jwt_cookie( + response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + ) + # Clear admin_first_time_login flag after successful admin login so the + # UI stops showing the first-time login documentation link. + if role == "admin": + request.app.frigate_config.auth.admin_first_time_login = False + + return response + return JSONResponse(content={"message": "Login failed"}, status_code=401) + + +@router.get( + "/users", + dependencies=[Depends(require_role(["admin"]))], + summary="Get all users", + description="Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role.", +) +def get_users(): + exports = ( + User.select(User.username, User.role).order_by(User.username).dicts().iterator() + ) + return JSONResponse([e for e in exports]) + + +@router.post( + "/users", + dependencies=[Depends(require_role(["admin"]))], + summary="Create new user", + description='Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).', +) +def create_user( + request: Request, + body: AppPostUsersBody, +): + HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations + config_roles = list(request.app.frigate_config.auth.roles.keys()) + + if not re.match("^[A-Za-z0-9._]+$", body.username): + return JSONResponse(content={"message": "Invalid username"}, status_code=400) + + if body.role not in config_roles: + return JSONResponse( + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, + ) + role = body.role or "viewer" + password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) + User.insert( + { + User.username: body.username, + User.password_hash: password_hash, + User.role: role, + User.notification_tokens: [], + } + ).execute() + return JSONResponse(content={"username": body.username}) + + +@router.delete( + "/users/{username}", + dependencies=[Depends(require_role(["admin"]))], + summary="Delete user", + description="Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found.", +) +def delete_user(request: Request, username: str): + # Prevent deletion of the built-in admin user + if username == "admin": + return JSONResponse( + content={"message": "Cannot delete admin user"}, status_code=403 + ) + + User.delete_by_id(username) + return JSONResponse(content={"success": True}) + + +@router.put( + "/users/{username}/password", + dependencies=[Depends(allow_any_authenticated())], + summary="Update user password", + description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.", +) +async def update_password( + request: Request, + username: str, + body: AppPutPasswordBody, +): + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + # auth failed + return current_user + + current_username = current_user.get("username") + current_role = current_user.get("role") + + # viewers can only change their own password + if current_role == "viewer" and current_username != username: + raise HTTPException( + status_code=403, detail="Viewers can only update their own password" + ) + + HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations + + try: + user = User.get_by_id(username) + except DoesNotExist: + return JSONResponse(content={"message": "User not found"}, status_code=404) + + # Require old_password when non-admin user is changing any password + # Admin users changing passwords do NOT need to provide the current password + if current_role != "admin": + if not body.old_password: + return JSONResponse( + content={"message": "Current password is required"}, + status_code=400, + ) + if not verify_password(body.old_password, user.password_hash): + return JSONResponse( + content={"message": "Current password is incorrect"}, + status_code=401, + ) + + # Validate new password strength + is_valid, error_message = validate_password_strength(body.password) + if not is_valid: + return JSONResponse( + content={"message": error_message}, + status_code=400, + ) + + password_hash = hash_password(body.password, iterations=HASH_ITERATIONS) + User.update( + { + User.password_hash: password_hash, + User.password_changed_at: datetime.now(), + } + ).where(User.username == username).execute() + + response = JSONResponse(content={"success": True}) + + # If user changed their own password, issue a new JWT to keep them logged in + if current_username == username: + JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name + JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure + JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length + + expiration = int(time.time()) + JWT_SESSION_LENGTH + encoded_jwt = create_encoded_jwt( + username, current_role, expiration, request.app.jwt_token + ) + # Set new JWT cookie on response + set_jwt_cookie( + response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + ) + + return response + + +@router.put( + "/users/{username}/role", + dependencies=[Depends(require_role(["admin"]))], + summary="Update user role", + description="Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration.", +) +async def update_role( + request: Request, + username: str, + body: AppPutRoleBody, +): + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + # auth failed + return current_user + + current_role = current_user.get("role") + # viewers can't change anyone's role + if current_role == "viewer": + raise HTTPException( + status_code=403, detail="Admin role is required to change user roles" + ) + if username == "admin": + return JSONResponse( + content={"message": "Cannot modify admin user's role"}, status_code=403 + ) + config_roles = list(request.app.frigate_config.auth.roles.keys()) + if body.role not in config_roles: + return JSONResponse( + content={"message": f"Role must be one of: {', '.join(config_roles)}"}, + status_code=400, + ) + + User.set_by_id(username, {User.role: body.role}) + return JSONResponse(content={"success": True}) + + +async def require_camera_access( + camera_name: Optional[str] = None, + request: Request = None, +): + """Dependency to enforce camera access based on user role.""" + if camera_name is None: + return # For lists, filter later + + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + return current_user + + role = current_user["role"] + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names) + + # Admin or full access bypasses + if role == "admin" or not roles_dict.get(role): + return + + if camera_name not in allowed_cameras: + raise HTTPException( + status_code=403, + detail=f"Access denied to camera '{camera_name}'. Allowed: {allowed_cameras}", + ) + + +async def get_allowed_cameras_for_filter(request: Request): + """Dependency to get allowed_cameras for filtering lists.""" + current_user = await get_current_user(request) + if isinstance(current_user, JSONResponse): + return [] # Unauthorized: no cameras + + role = current_user["role"] + all_camera_names = set(request.app.frigate_config.cameras.keys()) + roles_dict = request.app.frigate_config.auth.roles + return User.get_allowed_cameras(role, roles_dict, all_camera_names) diff --git a/sam2-cpu/frigate-dev/frigate/api/camera.py b/sam2-cpu/frigate-dev/frigate/api/camera.py new file mode 100644 index 0000000..936a0bb --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/camera.py @@ -0,0 +1,1000 @@ +"""Camera apis.""" + +import json +import logging +import re +from importlib.util import find_spec +from pathlib import Path +from urllib.parse import quote_plus + +import httpx +import requests +from fastapi import APIRouter, Depends, Query, Request, Response +from fastapi.responses import JSONResponse +from onvif import ONVIFCamera, ONVIFError +from zeep.exceptions import Fault, TransportError +from zeep.transports import AsyncTransport + +from frigate.api.auth import ( + allow_any_authenticated, + require_camera_access, + require_role, +) +from frigate.api.defs.tags import Tags +from frigate.config.config import FrigateConfig +from frigate.util.builtin import clean_camera_user_pass +from frigate.util.image import run_ffmpeg_snapshot +from frigate.util.services import ffprobe_stream + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.camera]) + + +def _is_valid_host(host: str) -> bool: + """ + Validate that the host is in a valid format. + Allows private IPs since cameras are typically on local networks. + Only blocks obviously malicious input to prevent injection attacks. + """ + try: + # Remove port if present + host_without_port = host.split(":")[0] if ":" in host else host + + # Block whitespace, newlines, and control characters + if not host_without_port or re.search(r"[\s\x00-\x1f]", host_without_port): + return False + + # Allow standard hostname/IP characters: alphanumeric, dots, hyphens + if not re.match(r"^[a-zA-Z0-9.-]+$", host_without_port): + return False + + return True + except Exception: + return False + + +@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())]) +def go2rtc_streams(): + r = requests.get("http://127.0.0.1:1984/api/streams") + if not r.ok: + logger.error("Failed to fetch streams from go2rtc") + return JSONResponse( + content=({"success": False, "message": "Error fetching stream data"}), + status_code=500, + ) + stream_data = r.json() + for data in stream_data.values(): + for producer in data.get("producers") or []: + producer["url"] = clean_camera_user_pass(producer.get("url", "")) + return JSONResponse(content=stream_data) + + +@router.get( + "/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)] +) +def go2rtc_camera_stream(request: Request, camera_name: str): + r = requests.get( + f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone" + ) + if not r.ok: + camera_config = request.app.frigate_config.cameras.get(camera_name) + + if camera_config and camera_config.enabled: + logger.error("Failed to fetch streams from go2rtc") + + return JSONResponse( + content=({"success": False, "message": "Error fetching stream data"}), + status_code=500, + ) + stream_data = r.json() + for producer in stream_data.get("producers", []): + producer["url"] = clean_camera_user_pass(producer.get("url", "")) + return JSONResponse(content=stream_data) + + +@router.put( + "/go2rtc/streams/{stream_name}", dependencies=[Depends(require_role(["admin"]))] +) +def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""): + """Add or update a go2rtc stream configuration.""" + try: + params = {"name": stream_name} + if src: + params["src"] = src + + r = requests.put( + "http://127.0.0.1:1984/api/streams", + params=params, + timeout=10, + ) + if not r.ok: + logger.error(f"Failed to add go2rtc stream {stream_name}: {r.text}") + return JSONResponse( + content=( + {"success": False, "message": f"Failed to add stream: {r.text}"} + ), + status_code=r.status_code, + ) + return JSONResponse( + content={"success": True, "message": "Stream added successfully"} + ) + except requests.RequestException as e: + logger.error(f"Error communicating with go2rtc: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error communicating with go2rtc", + } + ), + status_code=500, + ) + + +@router.delete( + "/go2rtc/streams/{stream_name}", dependencies=[Depends(require_role(["admin"]))] +) +def go2rtc_delete_stream(stream_name: str): + """Delete a go2rtc stream.""" + try: + r = requests.delete( + "http://127.0.0.1:1984/api/streams", + params={"src": stream_name}, + timeout=10, + ) + if not r.ok: + logger.error(f"Failed to delete go2rtc stream {stream_name}: {r.text}") + return JSONResponse( + content=( + {"success": False, "message": f"Failed to delete stream: {r.text}"} + ), + status_code=r.status_code, + ) + return JSONResponse( + content={"success": True, "message": "Stream deleted successfully"} + ) + except requests.RequestException as e: + logger.error(f"Error communicating with go2rtc: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Error communicating with go2rtc", + } + ), + status_code=500, + ) + + +@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))]) +def ffprobe(request: Request, paths: str = "", detailed: bool = False): + path_param = paths + + if not path_param: + return JSONResponse( + content=({"success": False, "message": "Path needs to be provided."}), + status_code=404, + ) + + if path_param.startswith("camera"): + camera = path_param[7:] + + if camera not in request.app.frigate_config.cameras.keys(): + return JSONResponse( + content=( + {"success": False, "message": f"{camera} is not a valid camera."} + ), + status_code=404, + ) + + if not request.app.frigate_config.cameras[camera].enabled: + return JSONResponse( + content=({"success": False, "message": f"{camera} is not enabled."}), + status_code=404, + ) + + paths = map( + lambda input: input.path, + request.app.frigate_config.cameras[camera].ffmpeg.inputs, + ) + elif "," in clean_camera_user_pass(path_param): + paths = path_param.split(",") + else: + paths = [path_param] + + # user has multiple streams + output = [] + + for path in paths: + ffprobe = ffprobe_stream( + request.app.frigate_config.ffmpeg, path.strip(), detailed=detailed + ) + + if ffprobe.returncode != 0: + try: + stderr_decoded = ffprobe.stderr.decode("utf-8") + except UnicodeDecodeError: + try: + stderr_decoded = ffprobe.stderr.decode("unicode_escape") + except Exception: + stderr_decoded = str(ffprobe.stderr) + + stderr_lines = [ + line.strip() for line in stderr_decoded.split("\n") if line.strip() + ] + + result = { + "return_code": ffprobe.returncode, + "stderr": stderr_lines, + "stdout": "", + } + else: + result = { + "return_code": ffprobe.returncode, + "stderr": [], + "stdout": json.loads(ffprobe.stdout.decode("unicode_escape").strip()), + } + + # Add detailed metadata if requested and probe was successful + if detailed and ffprobe.returncode == 0 and result["stdout"]: + try: + probe_data = result["stdout"] + metadata = {} + + # Extract video stream information + video_stream = None + audio_stream = None + + for stream in probe_data.get("streams", []): + if stream.get("codec_type") == "video": + video_stream = stream + elif stream.get("codec_type") == "audio": + audio_stream = stream + + # Video metadata + if video_stream: + metadata["video"] = { + "codec": video_stream.get("codec_name"), + "width": video_stream.get("width"), + "height": video_stream.get("height"), + "fps": _extract_fps(video_stream.get("avg_frame_rate")), + "pixel_format": video_stream.get("pix_fmt"), + "profile": video_stream.get("profile"), + "level": video_stream.get("level"), + } + + # Calculate resolution string + if video_stream.get("width") and video_stream.get("height"): + metadata["video"]["resolution"] = ( + f"{video_stream['width']}x{video_stream['height']}" + ) + + # Audio metadata + if audio_stream: + metadata["audio"] = { + "codec": audio_stream.get("codec_name"), + "channels": audio_stream.get("channels"), + "sample_rate": audio_stream.get("sample_rate"), + "channel_layout": audio_stream.get("channel_layout"), + } + + # Container/format metadata + if probe_data.get("format"): + format_info = probe_data["format"] + metadata["container"] = { + "format": format_info.get("format_name"), + "duration": format_info.get("duration"), + "size": format_info.get("size"), + } + + result["metadata"] = metadata + + except Exception as e: + logger.warning(f"Failed to extract detailed metadata: {e}") + # Continue without metadata if parsing fails + + output.append(result) + + return JSONResponse(content=output) + + +@router.get("/ffprobe/snapshot", dependencies=[Depends(require_role(["admin"]))]) +def ffprobe_snapshot(request: Request, url: str = "", timeout: int = 10): + """Get a snapshot from a stream URL using ffmpeg.""" + if not url: + return JSONResponse( + content={"success": False, "message": "URL parameter is required"}, + status_code=400, + ) + + config: FrigateConfig = request.app.frigate_config + + image_data, error = run_ffmpeg_snapshot( + config.ffmpeg, url, "mjpeg", timeout=timeout + ) + + if image_data: + return Response( + image_data, + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + elif error == "timeout": + return JSONResponse( + content={"success": False, "message": "Timeout capturing snapshot"}, + status_code=408, + ) + else: + logger.error(f"ffmpeg failed: {error}") + return JSONResponse( + content={"success": False, "message": "Failed to capture snapshot"}, + status_code=500, + ) + + +@router.get("/reolink/detect", dependencies=[Depends(require_role(["admin"]))]) +def reolink_detect(host: str = "", username: str = "", password: str = ""): + """ + Detect Reolink camera capabilities and recommend optimal protocol. + + Queries the Reolink camera API to determine the camera's resolution + and recommends either http-flv (for 5MP and below) or rtsp (for higher resolutions). + """ + if not host: + return JSONResponse( + content={"success": False, "message": "Host parameter is required"}, + status_code=400, + ) + + if not username: + return JSONResponse( + content={"success": False, "message": "Username parameter is required"}, + status_code=400, + ) + + if not password: + return JSONResponse( + content={"success": False, "message": "Password parameter is required"}, + status_code=400, + ) + + # Validate host format to prevent injection attacks + if not _is_valid_host(host): + return JSONResponse( + content={"success": False, "message": "Invalid host format"}, + status_code=400, + ) + + try: + # URL-encode credentials to prevent injection + encoded_user = quote_plus(username) + encoded_password = quote_plus(password) + api_url = f"http://{host}/api.cgi?cmd=GetEnc&user={encoded_user}&password={encoded_password}" + + response = requests.get(api_url, timeout=5) + + if not response.ok: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": f"Failed to connect to camera API: HTTP {response.status_code}", + }, + status_code=200, + ) + + data = response.json() + enc_data = data[0] if isinstance(data, list) and len(data) > 0 else data + + stream_info = None + if isinstance(enc_data, dict): + if enc_data.get("value", {}).get("Enc"): + stream_info = enc_data["value"]["Enc"] + elif enc_data.get("Enc"): + stream_info = enc_data["Enc"] + + if not stream_info or not stream_info.get("mainStream"): + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Could not find stream information in API response", + } + ) + + main_stream = stream_info["mainStream"] + width = main_stream.get("width", 0) + height = main_stream.get("height", 0) + + if not width or not height: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Could not determine camera resolution", + } + ) + + megapixels = (width * height) / 1_000_000 + protocol = "http-flv" if megapixels <= 5.0 else "rtsp" + + return JSONResponse( + content={ + "success": True, + "protocol": protocol, + "resolution": f"{width}x{height}", + "megapixels": round(megapixels, 2), + } + ) + + except requests.exceptions.Timeout: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Connection timeout - camera did not respond", + } + ) + except requests.exceptions.RequestException: + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Failed to connect to camera", + } + ) + except Exception: + logger.exception(f"Error detecting Reolink camera at {host}") + return JSONResponse( + content={ + "success": False, + "protocol": None, + "message": "Unable to detect camera capabilities", + } + ) + + +def _extract_fps(r_frame_rate: str) -> float | None: + """Extract FPS from ffprobe avg_frame_rate / r_frame_rate string (e.g., '30/1' -> 30.0)""" + if not r_frame_rate: + return None + try: + num, den = r_frame_rate.split("/") + return round(float(num) / float(den), 2) + except (ValueError, ZeroDivisionError): + return None + + +@router.get( + "/onvif/probe", + dependencies=[Depends(require_role(["admin"]))], + summary="Probe ONVIF device", + description=( + "Probe an ONVIF device to determine capabilities and optionally test available stream URIs. " + "Query params: host (required), port (default 80), username, password, test (boolean), " + "auth_type (basic or digest, default basic)." + ), +) +async def onvif_probe( + request: Request, + host: str = Query(None), + port: int = Query(80), + username: str = Query(""), + password: str = Query(""), + test: bool = Query(False), + auth_type: str = Query("basic"), # Add auth_type parameter +): + """ + Probe a single ONVIF device to determine capabilities. + + Connects to an ONVIF device and queries for: + - Device information (manufacturer, model) + - Media profiles count + - PTZ support + - Available presets + - Autotracking support + + Query Parameters: + host: Device host/IP address (required) + port: Device port (default 80) + username: ONVIF username (optional) + password: ONVIF password (optional) + test: run ffprobe on the stream (optional) + auth_type: Authentication type - "basic" or "digest" (default "basic") + + Returns: + JSON with device capabilities information + """ + if not host: + return JSONResponse( + content={"success": False, "message": "host parameter is required"}, + status_code=400, + ) + + # Validate host format + if not _is_valid_host(host): + return JSONResponse( + content={"success": False, "message": "Invalid host format"}, + status_code=400, + ) + + # Validate auth_type + if auth_type not in ["basic", "digest"]: + return JSONResponse( + content={ + "success": False, + "message": "auth_type must be 'basic' or 'digest'", + }, + status_code=400, + ) + + onvif_camera = None + + try: + logger.debug(f"Probing ONVIF device at {host}:{port} with {auth_type} auth") + + try: + wsdl_base = None + spec = find_spec("onvif") + if spec and getattr(spec, "origin", None): + wsdl_base = str(Path(spec.origin).parent / "wsdl") + except Exception: + wsdl_base = None + + onvif_camera = ONVIFCamera( + host, port, username or "", password or "", wsdl_dir=wsdl_base + ) + + # Configure digest authentication if requested + if auth_type == "digest" and username and password: + # Create httpx client with digest auth + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + + # Replace the transport in the zeep client + transport = AsyncTransport(client=client) + + # Update the xaddr before setting transport + await onvif_camera.update_xaddrs() + + # Replace transport in all services + if hasattr(onvif_camera, "devicemgmt"): + onvif_camera.devicemgmt.zeep_client.transport = transport + if hasattr(onvif_camera, "media"): + onvif_camera.media.zeep_client.transport = transport + if hasattr(onvif_camera, "ptz"): + onvif_camera.ptz.zeep_client.transport = transport + + logger.debug("Configured digest authentication") + else: + await onvif_camera.update_xaddrs() + + # Get device information + device_info = { + "manufacturer": "Unknown", + "model": "Unknown", + "firmware_version": "Unknown", + } + try: + device_service = await onvif_camera.create_devicemgmt_service() + + # Update transport for device service if digest auth + if auth_type == "digest" and username and password: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + device_service.zeep_client.transport = transport + + device_info_resp = await device_service.GetDeviceInformation() + manufacturer = getattr(device_info_resp, "Manufacturer", None) or ( + device_info_resp.get("Manufacturer") + if isinstance(device_info_resp, dict) + else None + ) + model = getattr(device_info_resp, "Model", None) or ( + device_info_resp.get("Model") + if isinstance(device_info_resp, dict) + else None + ) + firmware = getattr(device_info_resp, "FirmwareVersion", None) or ( + device_info_resp.get("FirmwareVersion") + if isinstance(device_info_resp, dict) + else None + ) + device_info.update( + { + "manufacturer": manufacturer or "Unknown", + "model": model or "Unknown", + "firmware_version": firmware or "Unknown", + } + ) + except Exception as e: + logger.debug(f"Failed to get device info: {e}") + + # Get media profiles + profiles = [] + profiles_count = 0 + first_profile_token = None + ptz_config_token = None + try: + media_service = await onvif_camera.create_media_service() + + # Update transport for media service if digest auth + if auth_type == "digest" and username and password: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + media_service.zeep_client.transport = transport + + profiles = await media_service.GetProfiles() + profiles_count = len(profiles) if profiles else 0 + if profiles and len(profiles) > 0: + p = profiles[0] + first_profile_token = getattr(p, "token", None) or ( + p.get("token") if isinstance(p, dict) else None + ) + # Get PTZ configuration token from the profile + ptz_configuration = getattr(p, "PTZConfiguration", None) or ( + p.get("PTZConfiguration") if isinstance(p, dict) else None + ) + if ptz_configuration: + ptz_config_token = getattr(ptz_configuration, "token", None) or ( + ptz_configuration.get("token") + if isinstance(ptz_configuration, dict) + else None + ) + except Exception as e: + logger.debug(f"Failed to get media profiles: {e}") + + # Check PTZ support and capabilities + ptz_supported = False + presets_count = 0 + autotrack_supported = False + + try: + ptz_service = await onvif_camera.create_ptz_service() + + # Update transport for PTZ service if digest auth + if auth_type == "digest" and username and password: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + ptz_service.zeep_client.transport = transport + + # Check if PTZ service is available + try: + await ptz_service.GetServiceCapabilities() + ptz_supported = True + logger.debug("PTZ service is available") + except Exception as e: + logger.debug(f"PTZ service not available: {e}") + ptz_supported = False + + # Try to get presets if PTZ is supported and we have a profile + if ptz_supported and first_profile_token: + try: + presets_resp = await ptz_service.GetPresets( + {"ProfileToken": first_profile_token} + ) + presets_count = len(presets_resp) if presets_resp else 0 + logger.debug(f"Found {presets_count} presets") + except Exception as e: + logger.debug(f"Failed to get presets: {e}") + presets_count = 0 + + # Check for autotracking support - requires both FOV relative movement and MoveStatus + if ptz_supported and first_profile_token and ptz_config_token: + # First check for FOV relative movement support + pt_r_fov_supported = False + try: + config_request = ptz_service.create_type("GetConfigurationOptions") + config_request.ConfigurationToken = ptz_config_token + ptz_config = await ptz_service.GetConfigurationOptions( + config_request + ) + + if ptz_config: + # Check for pt-r-fov support + spaces = getattr(ptz_config, "Spaces", None) or ( + ptz_config.get("Spaces") + if isinstance(ptz_config, dict) + else None + ) + + if spaces: + rel_pan_tilt_space = getattr( + spaces, "RelativePanTiltTranslationSpace", None + ) or ( + spaces.get("RelativePanTiltTranslationSpace") + if isinstance(spaces, dict) + else None + ) + + if rel_pan_tilt_space: + # Look for FOV space + for i, space in enumerate(rel_pan_tilt_space): + uri = None + if isinstance(space, dict): + uri = space.get("URI") + else: + uri = getattr(space, "URI", None) + + if uri and "TranslationSpaceFov" in uri: + pt_r_fov_supported = True + logger.debug( + "FOV relative movement (pt-r-fov) supported" + ) + break + + logger.debug(f"PTZ config spaces: {ptz_config}") + except Exception as e: + logger.debug(f"Failed to check FOV relative movement: {e}") + pt_r_fov_supported = False + + # Now check for MoveStatus support via GetServiceCapabilities + if pt_r_fov_supported: + try: + service_capabilities_request = ptz_service.create_type( + "GetServiceCapabilities" + ) + service_capabilities = await ptz_service.GetServiceCapabilities( + service_capabilities_request + ) + + # Look for MoveStatus in the capabilities + move_status_capable = False + if service_capabilities: + # Try to find MoveStatus key recursively + def find_move_status(obj, key="MoveStatus"): + if isinstance(obj, dict): + if key in obj: + return obj[key] + for v in obj.values(): + result = find_move_status(v, key) + if result is not None: + return result + elif hasattr(obj, key): + return getattr(obj, key) + elif hasattr(obj, "__dict__"): + for v in vars(obj).values(): + result = find_move_status(v, key) + if result is not None: + return result + return None + + move_status_value = find_move_status(service_capabilities) + + # MoveStatus should return "true" if supported + if isinstance(move_status_value, bool): + move_status_capable = move_status_value + elif isinstance(move_status_value, str): + move_status_capable = ( + move_status_value.lower() == "true" + ) + + logger.debug(f"MoveStatus capability: {move_status_value}") + + # Autotracking is supported if both conditions are met + autotrack_supported = pt_r_fov_supported and move_status_capable + + if autotrack_supported: + logger.debug( + "Autotracking fully supported (pt-r-fov + MoveStatus)" + ) + else: + logger.debug( + f"Autotracking not fully supported - pt-r-fov: {pt_r_fov_supported}, MoveStatus: {move_status_capable}" + ) + except Exception as e: + logger.debug(f"Failed to check MoveStatus support: {e}") + autotrack_supported = False + + except Exception as e: + logger.debug(f"Failed to probe PTZ service: {e}") + + result = { + "success": True, + "host": host, + "port": port, + "manufacturer": device_info["manufacturer"], + "model": device_info["model"], + "firmware_version": device_info["firmware_version"], + "profiles_count": profiles_count, + "ptz_supported": ptz_supported, + "presets_count": presets_count, + "autotrack_supported": autotrack_supported, + } + + # Gather RTSP candidates + rtsp_candidates: list[dict] = [] + try: + media_service = await onvif_camera.create_media_service() + + # Update transport for media service if digest auth + if auth_type == "digest" and username and password: + auth = httpx.DigestAuth(username, password) + client = httpx.AsyncClient(auth=auth, timeout=10.0) + transport = AsyncTransport(client=client) + media_service.zeep_client.transport = transport + + if profiles_count and media_service: + for p in profiles or []: + token = getattr(p, "token", None) or ( + p.get("token") if isinstance(p, dict) else None + ) + if not token: + continue + try: + stream_setup = { + "Stream": "RTP-Unicast", + "Transport": {"Protocol": "RTSP"}, + } + stream_req = { + "ProfileToken": token, + "StreamSetup": stream_setup, + } + stream_uri_resp = await media_service.GetStreamUri(stream_req) + uri = ( + stream_uri_resp.get("Uri") + if isinstance(stream_uri_resp, dict) + else getattr(stream_uri_resp, "Uri", None) + ) + if uri: + logger.debug( + f"GetStreamUri returned for token {token}: {uri}" + ) + # If credentials were provided, do NOT add the unauthenticated URI. + try: + if isinstance(uri, str) and uri.startswith("rtsp://"): + if username and password and "@" not in uri: + # Inject URL-encoded credentials and add only the + # authenticated version. + cred = f"{quote_plus(username)}:{quote_plus(password)}@" + injected = uri.replace( + "rtsp://", f"rtsp://{cred}", 1 + ) + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": injected, + } + ) + else: + # No credentials provided or URI already contains + # credentials — add the URI as returned. + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": uri, + } + ) + else: + # Non-RTSP URIs (e.g., http-flv) — add as returned. + rtsp_candidates.append( + { + "source": "GetStreamUri", + "profile_token": token, + "uri": uri, + } + ) + except Exception as e: + logger.debug( + f"Skipping stream URI for token {token} due to processing error: {e}" + ) + continue + except Exception: + logger.debug( + f"GetStreamUri failed for token {token}", exc_info=True + ) + continue + + # Add common RTSP patterns as fallback + if not rtsp_candidates: + common_paths = [ + "/h264", + "/live.sdp", + "/media.amp", + "/Streaming/Channels/101", + "/Streaming/Channels/1", + "/stream1", + "/cam/realmonitor?channel=1&subtype=0", + "/11", + ] + # Use URL-encoded credentials for pattern fallback URIs when provided + auth_str = ( + f"{quote_plus(username)}:{quote_plus(password)}@" + if username and password + else "" + ) + rtsp_port = 554 + for path in common_paths: + uri = f"rtsp://{auth_str}{host}:{rtsp_port}{path}" + rtsp_candidates.append({"source": "pattern", "uri": uri}) + except Exception: + logger.debug("Failed to collect RTSP candidates") + + # Optionally test RTSP candidates using ffprobe_stream + tested_candidates = [] + if test and rtsp_candidates: + for c in rtsp_candidates: + uri = c["uri"] + to_test = [uri] + try: + if ( + username + and password + and isinstance(uri, str) + and uri.startswith("rtsp://") + and "@" not in uri + ): + cred = f"{quote_plus(username)}:{quote_plus(password)}@" + cred_uri = uri.replace("rtsp://", f"rtsp://{cred}", 1) + if cred_uri not in to_test: + to_test.append(cred_uri) + except Exception: + pass + + for test_uri in to_test: + try: + probe = ffprobe_stream( + request.app.frigate_config.ffmpeg, test_uri, detailed=False + ) + print(probe) + ok = probe is not None and getattr(probe, "returncode", 1) == 0 + tested_candidates.append( + { + "uri": test_uri, + "source": c.get("source"), + "ok": ok, + "profile_token": c.get("profile_token"), + } + ) + except Exception as e: + logger.debug(f"Unable to probe stream: {e}") + tested_candidates.append( + { + "uri": test_uri, + "source": c.get("source"), + "ok": False, + "profile_token": c.get("profile_token"), + } + ) + + result["rtsp_candidates"] = rtsp_candidates + if test: + result["rtsp_tested"] = tested_candidates + + logger.debug(f"ONVIF probe successful: {result}") + return JSONResponse(content=result) + + except ONVIFError as e: + logger.warning(f"ONVIF error probing {host}:{port}: {e}") + return JSONResponse( + content={"success": False, "message": "ONVIF error"}, + status_code=400, + ) + except (Fault, TransportError) as e: + logger.warning(f"Connection error probing {host}:{port}: {e}") + return JSONResponse( + content={"success": False, "message": "Connection error"}, + status_code=503, + ) + except Exception as e: + logger.warning(f"Error probing ONVIF device at {host}:{port}, {e}") + return JSONResponse( + content={"success": False, "message": "Probe failed"}, + status_code=500, + ) + + finally: + # Best-effort cleanup of ONVIF camera client session + if onvif_camera is not None: + try: + # Check if the camera has a close method and call it + if hasattr(onvif_camera, "close"): + await onvif_camera.close() + except Exception as e: + logger.debug(f"Error closing ONVIF camera session: {e}") diff --git a/sam2-cpu/frigate-dev/frigate/api/classification.py b/sam2-cpu/frigate-dev/frigate/api/classification.py new file mode 100644 index 0000000..deafaf9 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/classification.py @@ -0,0 +1,1029 @@ +"""Object classification APIs.""" + +import datetime +import logging +import os +import random +import shutil +import string +from typing import Any + +import cv2 +from fastapi import APIRouter, Depends, Request, UploadFile +from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filename +from peewee import DoesNotExist +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import require_role +from frigate.api.defs.request.classification_body import ( + AudioTranscriptionBody, + DeleteFaceImagesBody, + GenerateObjectExamplesBody, + GenerateStateExamplesBody, + RenameFaceBody, +) +from frigate.api.defs.response.classification_response import ( + FaceRecognitionResponse, + FacesResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.config.camera import DetectConfig +from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR +from frigate.embeddings import EmbeddingsContext +from frigate.models import Event +from frigate.util.classification import ( + collect_object_classification_examples, + collect_state_classification_examples, + get_dataset_image_count, + read_training_metadata, +) +from frigate.util.file import get_event_snapshot + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.classification]) + + +@router.get( + "/faces", + response_model=FacesResponse, + summary="Get all registered faces", + description="""Returns a dictionary mapping face names to lists of image filenames. + Each key represents a registered face name, and the value is a list of image + files associated with that face. Supported image formats include .webp, .png, + .jpg, and .jpeg.""", +) +def get_faces(): + face_dict: dict[str, list[str]] = {} + + if not os.path.exists(FACE_DIR): + return JSONResponse(status_code=200, content={}) + + for name in os.listdir(FACE_DIR): + face_dir = os.path.join(FACE_DIR, name) + + if not os.path.isdir(face_dir): + continue + + face_dict[name] = [] + + for file in filter( + lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))), + os.listdir(face_dir), + ): + face_dict[name].append(file) + + return JSONResponse(status_code=200, content=face_dict) + + +@router.post( + "/faces/reprocess", + dependencies=[Depends(require_role(["admin"]))], + summary="Reprocess a face training image", + description="""Reprocesses a face training image to update the prediction. + Requires face recognition to be enabled in the configuration. The training file + must exist in the faces/train directory. Returns a success response or an error + message if face recognition is not enabled or the training file is invalid.""", +) +def reclassify_face(request: Request, body: dict = None): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, Any] = body or {} + training_file = os.path.join( + FACE_DIR, f"train/{sanitize_filename(json.get('training_file', ''))}" + ) + + if not training_file or not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file}", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reprocess_face(training_file) + + if not isinstance(response, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request.", + }, + ) + + return JSONResponse( + status_code=200 if response.get("success", True) else 400, + content=response, + ) + + +@router.post( + "/faces/train/{name}/classify", + response_model=GenericResponse, + summary="Classify and save a face training image", + description="""Adds a training image to a specific face name for face recognition. + Accepts either a training file from the train directory or an event_id to extract + the face from. The image is saved to the face's directory and the face classifier + is cleared to incorporate the new training data. Returns a success message with + the new filename or an error if face recognition is not enabled, the file/event + is invalid, or the face cannot be extracted.""", +) +def train_face(request: Request, name: str, body: dict = None): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + json: dict[str, Any] = body or {} + training_file_name = sanitize_filename(json.get("training_file", "")) + training_file = os.path.join(FACE_DIR, f"train/{training_file_name}") + event_id = json.get("event_id") + + if not training_file_name and not event_id: + return JSONResponse( + content=( + { + "success": False, + "message": "A training file or event_id must be passed.", + } + ), + status_code=400, + ) + + if training_file_name and not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file_name}", + } + ), + status_code=404, + ) + + sanitized_name = sanitize_filename(name) + new_name = f"{sanitized_name}-{datetime.datetime.now().timestamp()}.webp" + new_file_folder = os.path.join(FACE_DIR, f"{sanitized_name}") + + os.makedirs(new_file_folder, exist_ok=True) + + if training_file_name: + shutil.move(training_file, os.path.join(new_file_folder, new_name)) + else: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid event_id or no event exists: {event_id}", + } + ), + status_code=404, + ) + + snapshot = get_event_snapshot(event) + face_box = event.data["attributes"][0]["box"] + detect_config: DetectConfig = request.app.frigate_config.cameras[ + event.camera + ].detect + + # crop onto the face box minus the bounding box itself + x1 = int(face_box[0] * detect_config.width) + 2 + y1 = int(face_box[1] * detect_config.height) + 2 + x2 = x1 + int(face_box[2] * detect_config.width) - 4 + y2 = y1 + int(face_box[3] * detect_config.height) - 4 + face = snapshot[y1:y2, x1:x2] + success = True + + if face.size > 0: + try: + cv2.imwrite(os.path.join(new_file_folder, new_name), face) + success = True + except Exception: + pass + + if not success: + return JSONResponse( + content=( + { + "success": False, + "message": "Invalid face box or no face exists", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + context.clear_face_classifier() + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully saved {training_file_name} as {new_name}.", + } + ), + status_code=200, + ) + + +@router.post( + "/faces/{name}/create", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create a new face name", + description="""Creates a new folder for a face name in the faces directory. + This is used to organize face training images. The face name is sanitized and + spaces are replaced with underscores. Returns a success message or an error if + face recognition is not enabled.""", +) +async def create_face(request: Request, name: str): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + os.makedirs( + os.path.join(FACE_DIR, sanitize_filename(name.replace(" ", "_"))), exist_ok=True + ) + return JSONResponse( + status_code=200, + content={"success": False, "message": "Successfully created face folder."}, + ) + + +@router.post( + "/faces/{name}/register", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Register a face image", + description="""Registers a face image for a specific face name by uploading an image file. + The uploaded image is processed and added to the face recognition system. Returns a + success response with details about the registration, or an error if face recognition + is not enabled or the image cannot be processed.""", +) +async def register_face(request: Request, name: str, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + result = None if context is None else context.register_face(name, await file.read()) + + if not isinstance(result, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request. Try restarting Frigate.", + }, + ) + + return JSONResponse( + status_code=200 if result.get("success", True) else 400, + content=result, + ) + + +@router.post( + "/faces/recognize", + response_model=FaceRecognitionResponse, + summary="Recognize a face from an uploaded image", + description="""Recognizes a face from an uploaded image file by comparing it against + registered faces in the system. Returns the recognized face name and confidence score, + or an error if face recognition is not enabled or the image cannot be processed.""", +) +async def recognize_face(request: Request, file: UploadFile): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + result = context.recognize_face(await file.read()) + + if not isinstance(result, dict): + return JSONResponse( + status_code=500, + content={ + "success": False, + "message": "Could not process request. Try restarting Frigate.", + }, + ) + + return JSONResponse( + status_code=200 if result.get("success", True) else 400, + content=result, + ) + + +@router.post( + "/faces/{name}/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete face images", + description="""Deletes specific face images for a given face name. The image IDs must belong + to the specified face folder. To delete an entire face folder, all image IDs in that + folder must be sent. Returns a success message or an error if face recognition is not enabled.""", +) +def deregister_faces(request: Request, name: str, body: DeleteFaceImagesBody): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + context.delete_face_ids(name, map(lambda file: sanitize_filename(file), body.ids)) + return JSONResponse( + content=({"success": True, "message": "Successfully deleted faces."}), + status_code=200, + ) + + +@router.put( + "/faces/{old_name}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename a face name", + description="""Renames a face name in the system. The old name must exist and the new + name must be valid. Returns a success message or an error if face recognition is not enabled.""", +) +def rename_face(request: Request, old_name: str, body: RenameFaceBody): + if not request.app.frigate_config.face_recognition.enabled: + return JSONResponse( + status_code=400, + content={"message": "Face recognition is not enabled.", "success": False}, + ) + + context: EmbeddingsContext = request.app.embeddings + try: + context.rename_face(old_name, body.new_name) + return JSONResponse( + content={ + "success": True, + "message": f"Successfully renamed face to {body.new_name}.", + }, + status_code=200, + ) + except ValueError as e: + logger.error(e) + return JSONResponse( + status_code=400, + content={ + "message": "Error renaming face. Check Frigate logs.", + "success": False, + }, + ) + + +@router.put( + "/lpr/reprocess", + summary="Reprocess a license plate", + description="""Reprocesses a license plate image to update the plate. + Requires license plate recognition to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if license plate + recognition is not enabled or the event_id is invalid.""", +) +def reprocess_license_plate(request: Request, event_id: str): + if not request.app.frigate_config.lpr.enabled: + message = "License plate recognition is not enabled." + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reprocess_plate(model_to_dict(event)) + + return JSONResponse( + content=response, + status_code=200, + ) + + +@router.put( + "/reindex", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Reindex embeddings", + description="""Reindexes the embeddings for all tracked objects. + Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled.""", +) +def reindex_embeddings(request: Request): + if not request.app.frigate_config.semantic_search.enabled: + message = ( + "Cannot reindex tracked object embeddings, Semantic Search is not enabled." + ) + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.reindex_embeddings() + + if response == "started": + return JSONResponse( + content={ + "success": True, + "message": "Embeddings reindexing has started.", + }, + status_code=202, # 202 Accepted + ) + elif response == "in_progress": + return JSONResponse( + content={ + "success": False, + "message": "Embeddings reindexing is already in progress.", + }, + status_code=409, # 409 Conflict + ) + else: + return JSONResponse( + content={ + "success": False, + "message": "Failed to start reindexing.", + }, + status_code=500, + ) + + +@router.put( + "/audio/transcribe", + response_model=GenericResponse, + summary="Transcribe audio", + description="""Transcribes audio from a specific event. + Requires audio transcription to be enabled in the configuration. The event_id + must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid.""", +) +def transcribe_audio(request: Request, body: AudioTranscriptionBody): + event_id = body.event_id + + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + if not request.app.frigate_config.cameras[event.camera].audio_transcription.enabled: + message = f"Audio transcription is not enabled for {event.camera}." + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + response = context.transcribe_audio(model_to_dict(event)) + + if response == "started": + return JSONResponse( + content={ + "success": True, + "message": "Audio transcription has started.", + }, + status_code=202, # 202 Accepted + ) + elif response == "in_progress": + return JSONResponse( + content={ + "success": False, + "message": "Audio transcription for a speech event is currently in progress. Try again later.", + }, + status_code=409, # 409 Conflict + ) + else: + logger.debug(f"Failed to transcribe audio, response: {response}") + return JSONResponse( + content={ + "success": False, + "message": "Failed to transcribe audio.", + }, + status_code=500, + ) + + +# custom classification training + + +@router.get( + "/classification/{name}/dataset", + summary="Get classification dataset", + description="""Gets the dataset for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +def get_classification_dataset(name: str): + dataset_dict: dict[str, list[str]] = {} + + dataset_dir = os.path.join(CLIPS_DIR, sanitize_filename(name), "dataset") + + if not os.path.exists(dataset_dir): + return JSONResponse( + status_code=200, content={"categories": {}, "training_metadata": None} + ) + + for category_name in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category_name) + + if not os.path.isdir(category_dir): + continue + + dataset_dict[category_name] = [] + + for file in filter( + lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))), + os.listdir(category_dir), + ): + dataset_dict[category_name].append(file) + + # Get training metadata + metadata = read_training_metadata(sanitize_filename(name)) + current_image_count = get_dataset_image_count(sanitize_filename(name)) + + if metadata is None: + training_metadata = { + "has_trained": False, + "last_training_date": None, + "last_training_image_count": 0, + "current_image_count": current_image_count, + "new_images_count": current_image_count, + "dataset_changed": current_image_count > 0, + } + else: + last_training_count = metadata.get("last_training_image_count", 0) + # Dataset has changed if count is different (either added or deleted images) + dataset_changed = current_image_count != last_training_count + # Only show positive count for new images (ignore deletions in the count display) + new_images_count = max(0, current_image_count - last_training_count) + training_metadata = { + "has_trained": True, + "last_training_date": metadata.get("last_training_date"), + "last_training_image_count": last_training_count, + "current_image_count": current_image_count, + "new_images_count": new_images_count, + "dataset_changed": dataset_changed, + } + + return JSONResponse( + status_code=200, + content={ + "categories": dataset_dict, + "training_metadata": training_metadata, + }, + ) + + +@router.get( + "/classification/{name}/train", + summary="Get classification train images", + description="""Gets the train images for a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +def get_classification_images(name: str): + train_dir = os.path.join(CLIPS_DIR, sanitize_filename(name), "train") + + if not os.path.exists(train_dir): + return JSONResponse(status_code=200, content=[]) + + return JSONResponse( + status_code=200, + content=list( + filter( + lambda f: (f.lower().endswith((".webp", ".png", ".jpg", ".jpeg"))), + os.listdir(train_dir), + ) + ), + ) + + +@router.post( + "/classification/{name}/train", + response_model=GenericResponse, + summary="Train a classification model", + description="""Trains a specific classification model. + The name must exist in the classification models. Returns a success message or an error if the name is invalid.""", +) +async def train_configured_model(request: Request, name: str): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + context: EmbeddingsContext = request.app.embeddings + context.start_classification_training(name) + return JSONResponse( + content={"success": True, "message": "Started classification model training."}, + status_code=200, + ) + + +@router.post( + "/classification/{name}/dataset/{category}/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete classification dataset images", + description="""Deletes specific dataset images for a given classification model and category. + The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid.""", +) +def delete_classification_dataset_images( + request: Request, name: str, category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + list_of_ids = json.get("ids", "") + folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category) + ) + + for id in list_of_ids: + file_path = os.path.join(folder, sanitize_filename(id)) + + if os.path.isfile(file_path): + os.unlink(file_path) + + if os.path.exists(folder) and not os.listdir(folder) and category.lower() != "none": + os.rmdir(folder) + + return JSONResponse( + content=({"success": True, "message": "Successfully deleted images."}), + status_code=200, + ) + + +@router.put( + "/classification/{name}/dataset/{old_category}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename a classification category", + description="""Renames a classification category for a given classification model. + The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid.""", +) +def rename_classification_category( + request: Request, name: str, old_category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + new_category = sanitize_filename(json.get("new_category", "")) + + if not new_category: + return JSONResponse( + content=( + { + "success": False, + "message": "New category name is required.", + } + ), + status_code=400, + ) + + old_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(old_category) + ) + new_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", new_category + ) + + if not os.path.exists(old_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {old_category} does not exist.", + } + ), + status_code=404, + ) + + if os.path.exists(new_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {new_category} already exists.", + } + ), + status_code=400, + ) + + try: + os.rename(old_folder, new_folder) + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully renamed category to {new_category}.", + } + ), + status_code=200, + ) + except Exception as e: + logger.error(f"Error renaming category: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": "Failed to rename category", + } + ), + status_code=500, + ) + + +@router.post( + "/classification/{name}/dataset/categorize", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Categorize a classification image", + description="""Categorizes a specific classification image for a given classification model and category. + The image must exist in the specified category. Returns a success message or an error if the name or category is invalid.""", +) +def categorize_classification_image(request: Request, name: str, body: dict = None): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + category = sanitize_filename(json.get("category", "")) + training_file_name = sanitize_filename(json.get("training_file", "")) + training_file = os.path.join( + CLIPS_DIR, sanitize_filename(name), "train", training_file_name + ) + + if training_file_name and not os.path.isfile(training_file): + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid filename or no file exists: {training_file_name}", + } + ), + status_code=404, + ) + + random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + timestamp = datetime.datetime.now().timestamp() + new_name = f"{category}-{timestamp}-{random_id}.png" + new_file_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", category + ) + + os.makedirs(new_file_folder, exist_ok=True) + + # use opencv because webp images can not be used to train + img = cv2.imread(training_file) + cv2.imwrite(os.path.join(new_file_folder, new_name), img) + os.unlink(training_file) + + return JSONResponse( + content=({"success": True, "message": "Successfully categorized image."}), + status_code=200, + ) + + +@router.post( + "/classification/{name}/dataset/{category}/create", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create an empty classification category folder", + description="""Creates an empty folder for a classification category. + This is used to create folders for categories that don't have images yet. + Returns a success message or an error if the name is invalid.""", +) +def create_classification_category(request: Request, name: str, category: str): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + category_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(category) + ) + + os.makedirs(category_folder, exist_ok=True) + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully created category folder: {category}", + } + ), + status_code=200, + ) + + +@router.post( + "/classification/{name}/train/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete classification train images", + description="""Deletes specific train images for a given classification model. + The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid.""", +) +def delete_classification_train_images(request: Request, name: str, body: dict = None): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + list_of_ids = json.get("ids", "") + folder = os.path.join(CLIPS_DIR, sanitize_filename(name), "train") + + for id in list_of_ids: + file_path = os.path.join(folder, sanitize_filename(id)) + + if os.path.isfile(file_path): + os.unlink(file_path) + + return JSONResponse( + content=({"success": True, "message": "Successfully deleted images."}), + status_code=200, + ) + + +@router.post( + "/classification/generate_examples/state", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate state classification examples", +) +async def generate_state_examples(request: Request, body: GenerateStateExamplesBody): + """Generate examples for state classification.""" + model_name = sanitize_filename(body.model_name) + cameras_normalized = { + camera_name: tuple(crop) + for camera_name, crop in body.cameras.items() + if camera_name in request.app.frigate_config.cameras + } + + collect_state_classification_examples(model_name, cameras_normalized) + + return JSONResponse( + content={"success": True, "message": "Example generation completed"}, + status_code=200, + ) + + +@router.post( + "/classification/generate_examples/object", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate object classification examples", +) +async def generate_object_examples(request: Request, body: GenerateObjectExamplesBody): + """Generate examples for object classification.""" + model_name = sanitize_filename(body.model_name) + collect_object_classification_examples(model_name, body.label) + + return JSONResponse( + content={"success": True, "message": "Example generation completed"}, + status_code=200, + ) + + +@router.delete( + "/classification/{name}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete a classification model", + description="""Deletes a specific classification model and all its associated data. + Works even if the model is not in the config (e.g., partially created during wizard). + Returns a success message.""", +) +def delete_classification_model(request: Request, name: str): + sanitized_name = sanitize_filename(name) + + # Delete the classification model's data directory in clips + data_dir = os.path.join(CLIPS_DIR, sanitized_name) + if os.path.exists(data_dir): + try: + shutil.rmtree(data_dir) + logger.info(f"Deleted classification data directory for {name}") + except Exception as e: + logger.debug(f"Failed to delete data directory for {name}: {e}") + + # Delete the classification model's files in model_cache + model_dir = os.path.join(MODEL_CACHE_DIR, sanitized_name) + if os.path.exists(model_dir): + try: + shutil.rmtree(model_dir) + logger.info(f"Deleted classification model directory for {name}") + except Exception as e: + logger.debug(f"Failed to delete model directory for {name}: {e}") + + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully deleted classification model {name}.", + } + ), + status_code=200, + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/__init__.py b/sam2-cpu/frigate-dev/frigate/api/defs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/query/app_query_parameters.py b/sam2-cpu/frigate-dev/frigate/api/defs/query/app_query_parameters.py new file mode 100644 index 0000000..e182a6a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/query/app_query_parameters.py @@ -0,0 +1,12 @@ +from typing import Optional + +from pydantic import BaseModel + + +class AppTimelineHourlyQueryParameters(BaseModel): + cameras: Optional[str] = "all" + labels: Optional[str] = "all" + after: Optional[float] = None + before: Optional[float] = None + limit: Optional[int] = 200 + timezone: Optional[str] = "utc" diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/query/events_query_parameters.py b/sam2-cpu/frigate-dev/frigate/api/defs/query/events_query_parameters.py new file mode 100644 index 0000000..187dd3f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/query/events_query_parameters.py @@ -0,0 +1,80 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +DEFAULT_TIME_RANGE = "00:00,24:00" + + +class EventsQueryParams(BaseModel): + camera: Optional[str] = "all" + cameras: Optional[str] = "all" + label: Optional[str] = "all" + labels: Optional[str] = "all" + sub_label: Optional[str] = "all" + sub_labels: Optional[str] = "all" + zone: Optional[str] = "all" + zones: Optional[str] = "all" + limit: Optional[int] = 100 + after: Optional[float] = None + before: Optional[float] = None + time_range: Optional[str] = DEFAULT_TIME_RANGE + has_clip: Optional[int] = None + has_snapshot: Optional[int] = None + in_progress: Optional[int] = None + include_thumbnails: Optional[int] = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) + favorites: Optional[int] = None + min_score: Optional[float] = None + max_score: Optional[float] = None + min_speed: Optional[float] = None + max_speed: Optional[float] = None + recognized_license_plate: Optional[str] = "all" + is_submitted: Optional[int] = None + min_length: Optional[float] = None + max_length: Optional[float] = None + event_id: Optional[str] = None + sort: Optional[str] = None + timezone: Optional[str] = "utc" + + +class EventsSearchQueryParams(BaseModel): + query: Optional[str] = None + event_id: Optional[str] = None + search_type: Optional[str] = "thumbnail" + include_thumbnails: Optional[int] = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) + limit: Optional[int] = 50 + cameras: Optional[str] = "all" + labels: Optional[str] = "all" + zones: Optional[str] = "all" + after: Optional[float] = None + before: Optional[float] = None + time_range: Optional[str] = DEFAULT_TIME_RANGE + has_clip: Optional[bool] = None + has_snapshot: Optional[bool] = None + is_submitted: Optional[bool] = None + timezone: Optional[str] = "utc" + min_score: Optional[float] = None + max_score: Optional[float] = None + min_speed: Optional[float] = None + max_speed: Optional[float] = None + recognized_license_plate: Optional[str] = "all" + sort: Optional[str] = None + + +class EventsSummaryQueryParams(BaseModel): + timezone: Optional[str] = "utc" + has_clip: Optional[int] = None + has_snapshot: Optional[int] = None diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/query/media_query_parameters.py b/sam2-cpu/frigate-dev/frigate/api/defs/query/media_query_parameters.py new file mode 100644 index 0000000..a16f0d5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/query/media_query_parameters.py @@ -0,0 +1,62 @@ +from enum import Enum +from typing import Optional, Union + +from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema + + +class Extension(str, Enum): + webp = "webp" + png = "png" + jpg = "jpg" + jpeg = "jpeg" + + def get_mime_type(self) -> str: + if self in (Extension.jpg, Extension.jpeg): + return "image/jpeg" + return f"image/{self.value}" + + +class MediaLatestFrameQueryParams(BaseModel): + bbox: Optional[int] = None + timestamp: Optional[int] = None + zones: Optional[int] = None + mask: Optional[int] = None + motion: Optional[int] = None + paths: Optional[int] = None + regions: Optional[int] = None + quality: Optional[int] = 70 + height: Optional[int] = None + store: Optional[int] = None + + +class MediaEventsSnapshotQueryParams(BaseModel): + download: Optional[bool] = False + timestamp: Optional[int] = None + bbox: Optional[int] = None + crop: Optional[int] = None + height: Optional[int] = None + quality: Optional[int] = 70 + + +class MediaMjpegFeedQueryParams(BaseModel): + fps: int = 3 + height: int = 360 + bbox: Optional[int] = None + timestamp: Optional[int] = None + zones: Optional[int] = None + mask: Optional[int] = None + motion: Optional[int] = None + regions: Optional[int] = None + + +class MediaRecordingsSummaryQueryParams(BaseModel): + timezone: str = "utc" + cameras: Optional[str] = "all" + + +class MediaRecordingsAvailabilityQueryParams(BaseModel): + cameras: str = "all" + before: Union[float, SkipJsonSchema[None]] = None + after: Union[float, SkipJsonSchema[None]] = None + scale: int = 30 diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/query/regenerate_query_parameters.py b/sam2-cpu/frigate-dev/frigate/api/defs/query/regenerate_query_parameters.py new file mode 100644 index 0000000..af50ada --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/query/regenerate_query_parameters.py @@ -0,0 +1,13 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from frigate.events.types import RegenerateDescriptionEnum + + +class RegenerateQueryParameters(BaseModel): + source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails + force: Optional[bool] = Field( + default=False, + description="Force (re)generating the description even if GenAI is disabled for this camera.", + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/query/review_query_parameters.py b/sam2-cpu/frigate-dev/frigate/api/defs/query/review_query_parameters.py new file mode 100644 index 0000000..ee9af74 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/query/review_query_parameters.py @@ -0,0 +1,31 @@ +from typing import Union + +from pydantic import BaseModel +from pydantic.json_schema import SkipJsonSchema + +from frigate.review.types import SeverityEnum + + +class ReviewQueryParams(BaseModel): + cameras: str = "all" + labels: str = "all" + zones: str = "all" + reviewed: int = 0 + limit: Union[int, SkipJsonSchema[None]] = None + severity: Union[SeverityEnum, SkipJsonSchema[None]] = None + before: Union[float, SkipJsonSchema[None]] = None + after: Union[float, SkipJsonSchema[None]] = None + + +class ReviewSummaryQueryParams(BaseModel): + cameras: str = "all" + labels: str = "all" + zones: str = "all" + timezone: str = "utc" + + +class ReviewActivityMotionQueryParams(BaseModel): + cameras: str = "all" + before: Union[float, SkipJsonSchema[None]] = None + after: Union[float, SkipJsonSchema[None]] = None + scale: int = 30 diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/__init__.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/app_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/app_body.py new file mode 100644 index 0000000..c4129d8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/app_body.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Optional + +from pydantic import BaseModel + + +class AppConfigSetBody(BaseModel): + requires_restart: int = 1 + update_topic: str | None = None + config_data: Optional[Dict[str, Any]] = None + + +class AppPutPasswordBody(BaseModel): + password: str + old_password: Optional[str] = None + + +class AppPostUsersBody(BaseModel): + username: str + password: str + role: Optional[str] = "viewer" + + +class AppPostLoginBody(BaseModel): + user: str + password: str + + +class AppPutRoleBody(BaseModel): + role: str diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/classification_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/classification_body.py new file mode 100644 index 0000000..fb6a7dd --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/classification_body.py @@ -0,0 +1,31 @@ +from typing import Dict, List, Tuple + +from pydantic import BaseModel, Field + + +class RenameFaceBody(BaseModel): + new_name: str = Field(description="New name for the face") + + +class AudioTranscriptionBody(BaseModel): + event_id: str = Field(description="ID of the event to transcribe audio for") + + +class DeleteFaceImagesBody(BaseModel): + ids: List[str] = Field( + description="List of image filenames to delete from the face folder" + ) + + +class GenerateStateExamplesBody(BaseModel): + model_name: str = Field(description="Name of the classification model") + cameras: Dict[str, Tuple[float, float, float, float]] = Field( + description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)" + ) + + +class GenerateObjectExamplesBody(BaseModel): + model_name: str = Field(description="Name of the classification model") + label: str = Field( + description="Object label to collect examples for (e.g., 'person', 'car')" + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/events_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/events_body.py new file mode 100644 index 0000000..6110e34 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/events_body.py @@ -0,0 +1,54 @@ +from typing import List, Optional, Union + +from pydantic import BaseModel, Field + +from frigate.config.classification import TriggerType + + +class EventsSubLabelBody(BaseModel): + subLabel: str = Field(title="Sub label", max_length=100) + subLabelScore: Optional[float] = Field( + title="Score for sub label", default=None, gt=0.0, le=1.0 + ) + camera: Optional[str] = Field( + title="Camera this object is detected on.", default=None + ) + + +class EventsLPRBody(BaseModel): + recognizedLicensePlate: str = Field( + title="Recognized License Plate", max_length=100 + ) + recognizedLicensePlateScore: Optional[float] = Field( + title="Score for recognized license plate", default=None, gt=0.0, le=1.0 + ) + + +class EventsDescriptionBody(BaseModel): + description: Union[str, None] = Field(title="The description of the event") + + +class EventsCreateBody(BaseModel): + sub_label: Optional[str] = None + score: Optional[float] = 0 + duration: Optional[int] = 30 + include_recording: Optional[bool] = True + draw: Optional[dict] = {} + + +class EventsEndBody(BaseModel): + end_time: Optional[float] = None + + +class EventsDeleteBody(BaseModel): + event_ids: List[str] = Field(title="The event IDs to delete") + + +class SubmitPlusBody(BaseModel): + include_annotation: int = Field(default=1) + + +class TriggerEmbeddingBody(BaseModel): + type: TriggerType + data: str + threshold: float = Field(default=0.5, ge=0.0, le=1.0) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/export_recordings_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/export_recordings_body.py new file mode 100644 index 0000000..eb6c151 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/export_recordings_body.py @@ -0,0 +1,20 @@ +from typing import Union + +from pydantic import BaseModel, Field +from pydantic.json_schema import SkipJsonSchema + +from frigate.record.export import ( + PlaybackFactorEnum, + PlaybackSourceEnum, +) + + +class ExportRecordingsBody(BaseModel): + playback: PlaybackFactorEnum = Field( + default=PlaybackFactorEnum.realtime, title="Playback factor" + ) + source: PlaybackSourceEnum = Field( + default=PlaybackSourceEnum.recordings, title="Playback source" + ) + name: str = Field(title="Friendly name", default=None, max_length=256) + image_path: Union[str, SkipJsonSchema[None]] = None diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/export_rename_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/export_rename_body.py new file mode 100644 index 0000000..dc5bc32 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/export_rename_body.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, Field + + +class ExportRenameBody(BaseModel): + name: str = Field(title="Friendly name", max_length=256) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/request/review_body.py b/sam2-cpu/frigate-dev/frigate/api/defs/request/review_body.py new file mode 100644 index 0000000..6dc7100 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/request/review_body.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, conlist, constr + + +class ReviewModifyMultipleBody(BaseModel): + # List of string with at least one element and each element with at least one char + ids: conlist(constr(min_length=1), min_length=1) + # Whether to mark items as reviewed (True) or unreviewed (False) + reviewed: bool = True diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/classification_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/classification_response.py new file mode 100644 index 0000000..92d354f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/classification_response.py @@ -0,0 +1,38 @@ +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field, RootModel + + +class FacesResponse(RootModel[Dict[str, List[str]]]): + """Response model for the get_faces endpoint. + + Returns a mapping of face names to lists of image filenames. + Each face name corresponds to a directory in the faces folder, + and the list contains the names of image files for that face. + + Example: + { + "john_doe": ["face1.webp", "face2.jpg"], + "jane_smith": ["face3.png"] + } + """ + + root: Dict[str, List[str]] = Field( + default_factory=dict, + description="Dictionary mapping face names to lists of image filenames", + ) + + +class FaceRecognitionResponse(BaseModel): + """Response model for face recognition endpoint. + + Returns the result of attempting to recognize a face from an uploaded image. + """ + + success: bool = Field(description="Whether the face recognition was successful") + score: Optional[float] = Field( + default=None, description="Confidence score of the recognition (0-1)" + ) + face_name: Optional[str] = Field( + default=None, description="The recognized face name if successful" + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/event_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/event_response.py new file mode 100644 index 0000000..0838497 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/event_response.py @@ -0,0 +1,42 @@ +from typing import Any, Optional + +from pydantic import BaseModel, ConfigDict + + +class EventResponse(BaseModel): + id: str + label: str + sub_label: Optional[str] + camera: str + start_time: float + end_time: Optional[float] + false_positive: Optional[bool] + zones: list[str] + thumbnail: Optional[str] + has_clip: bool + has_snapshot: bool + retain_indefinitely: bool + plus_id: Optional[str] + model_hash: Optional[str] + detector_type: Optional[str] + model_type: Optional[str] + data: dict[str, Any] + + model_config = ConfigDict(protected_namespaces=()) + + +class EventCreateResponse(BaseModel): + success: bool + message: str + event_id: str + + +class EventMultiDeleteResponse(BaseModel): + success: bool + deleted_events: list[str] + not_found_events: list[str] + + +class EventUploadPlusResponse(BaseModel): + success: bool + plus_id: str diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/export_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/export_response.py new file mode 100644 index 0000000..63a9e91 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/export_response.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class ExportModel(BaseModel): + """Model representing a single export.""" + + id: str = Field(description="Unique identifier for the export") + camera: str = Field(description="Camera name associated with this export") + name: str = Field(description="Friendly name of the export") + date: float = Field(description="Unix timestamp when the export was created") + video_path: str = Field(description="File path to the exported video") + thumb_path: str = Field(description="File path to the export thumbnail") + in_progress: bool = Field( + description="Whether the export is currently being processed" + ) + + +class StartExportResponse(BaseModel): + """Response model for starting an export.""" + + success: bool = Field(description="Whether the export was started successfully") + message: str = Field(description="Status or error message") + export_id: Optional[str] = Field( + default=None, description="The export ID if successfully started" + ) + + +ExportsResponse = List[ExportModel] diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/generic_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/generic_response.py new file mode 100644 index 0000000..dbf9434 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/generic_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class GenericResponse(BaseModel): + success: bool + message: str diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/preview_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/preview_response.py new file mode 100644 index 0000000..d320a86 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/preview_response.py @@ -0,0 +1,17 @@ +from typing import List + +from pydantic import BaseModel, Field + + +class PreviewModel(BaseModel): + """Model representing a single preview clip.""" + + camera: str = Field(description="Camera name for this preview") + src: str = Field(description="Path to the preview video file") + type: str = Field(description="MIME type of the preview video (video/mp4)") + start: float = Field(description="Unix timestamp when the preview starts") + end: float = Field(description="Unix timestamp when the preview ends") + + +PreviewsResponse = List[PreviewModel] +PreviewFramesResponse = List[str] diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/response/review_response.py b/sam2-cpu/frigate-dev/frigate/api/defs/response/review_response.py new file mode 100644 index 0000000..b2fed3b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/response/review_response.py @@ -0,0 +1,43 @@ +from datetime import datetime +from typing import Dict + +from pydantic import BaseModel, Json + +from frigate.review.types import SeverityEnum + + +class ReviewSegmentResponse(BaseModel): + id: str + camera: str + start_time: datetime + end_time: datetime + has_been_reviewed: bool + severity: SeverityEnum + thumb_path: str + data: Json + + +class Last24HoursReview(BaseModel): + reviewed_alert: int + reviewed_detection: int + total_alert: int + total_detection: int + + +class DayReview(BaseModel): + day: datetime + reviewed_alert: int + reviewed_detection: int + total_alert: int + total_detection: int + + +class ReviewSummaryResponse(BaseModel): + last24Hours: Last24HoursReview + root: Dict[str, DayReview] + + +class ReviewActivityMotionResponse(BaseModel): + start_time: int + motion: float + camera: str diff --git a/sam2-cpu/frigate-dev/frigate/api/defs/tags.py b/sam2-cpu/frigate-dev/frigate/api/defs/tags.py new file mode 100644 index 0000000..f804385 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/defs/tags.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class Tags(Enum): + app = "App" + camera = "Camera" + preview = "Preview" + logs = "Logs" + media = "Media" + notifications = "Notifications" + review = "Review" + export = "Export" + events = "Events" + classification = "Classification" + auth = "Auth" diff --git a/sam2-cpu/frigate-dev/frigate/api/event.py b/sam2-cpu/frigate-dev/frigate/api/event.py new file mode 100644 index 0000000..fc78ac0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/event.py @@ -0,0 +1,2131 @@ +"""Event apis.""" + +import base64 +import datetime +import json +import logging +import os +import random +import string +from functools import reduce +from pathlib import Path +from typing import List +from urllib.parse import unquote + +import cv2 +import numpy as np +from fastapi import APIRouter, Request +from fastapi.params import Depends +from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filename +from peewee import JOIN, DoesNotExist, fn, operator +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, + require_role, +) +from frigate.api.defs.query.events_query_parameters import ( + DEFAULT_TIME_RANGE, + EventsQueryParams, + EventsSearchQueryParams, + EventsSummaryQueryParams, +) +from frigate.api.defs.query.regenerate_query_parameters import ( + RegenerateQueryParameters, +) +from frigate.api.defs.request.events_body import ( + EventsCreateBody, + EventsDeleteBody, + EventsDescriptionBody, + EventsEndBody, + EventsLPRBody, + EventsSubLabelBody, + SubmitPlusBody, + TriggerEmbeddingBody, +) +from frigate.api.defs.response.event_response import ( + EventCreateResponse, + EventMultiDeleteResponse, + EventResponse, + EventUploadPlusResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.comms.event_metadata_updater import EventMetadataTypeEnum +from frigate.const import CLIPS_DIR, TRIGGER_DIR +from frigate.embeddings import EmbeddingsContext +from frigate.models import Event, ReviewSegment, Timeline, Trigger +from frigate.track.object_processing import TrackedObject +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.time import get_dst_transitions, get_tz_modifiers + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.events]) + + +@router.get( + "/events", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get events", + description="Returns a list of events.", +) +def events( + params: EventsQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + camera = params.camera + cameras = params.cameras + + # handle old camera arg + if cameras == "all" and camera != "all": + cameras = camera + + label = unquote(params.label) + labels = params.labels + + # handle old label arg + if labels == "all" and label != "all": + labels = label + + sub_label = params.sub_label + sub_labels = params.sub_labels + + # handle old sub_label arg + if sub_labels == "all" and sub_label != "all": + sub_labels = sub_label + + zone = params.zone + zones = params.zones + + # handle old label arg + if zones == "all" and zone != "all": + zones = zone + + limit = params.limit + after = params.after + before = params.before + time_range = params.time_range + has_clip = params.has_clip + has_snapshot = params.has_snapshot + in_progress = params.in_progress + include_thumbnails = params.include_thumbnails + favorites = params.favorites + min_score = params.min_score + max_score = params.max_score + min_speed = params.min_speed + max_speed = params.max_speed + is_submitted = params.is_submitted + min_length = params.min_length + max_length = params.max_length + event_id = params.event_id + recognized_license_plate = params.recognized_license_plate + + sort = params.sort + + clauses = [] + + selected_columns = [ + Event.id, + Event.camera, + Event.label, + Event.zones, + Event.start_time, + Event.end_time, + Event.has_clip, + Event.has_snapshot, + Event.plus_id, + Event.retain_indefinitely, + Event.sub_label, + Event.top_score, + Event.false_positive, + Event.box, + Event.data, + ] + + if camera != "all": + clauses.append((Event.camera == camera)) + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append((Event.camera << camera_list)) + + if labels != "all": + label_list = labels.split(",") + clauses.append((Event.label << label_list)) + + if sub_labels != "all": + # use matching so joined sub labels are included + # for example a sub label 'bob' would get events + # with sub labels 'bob' and 'bob, john' + sub_label_clauses = [] + filtered_sub_labels = sub_labels.split(",") + + if "None" in filtered_sub_labels: + filtered_sub_labels.remove("None") + sub_label_clauses.append((Event.sub_label.is_null())) + + for label in filtered_sub_labels: + sub_label_clauses.append( + (Event.sub_label.cast("text") == label) + ) # include exact matches + + # include this label when part of a list + sub_label_clauses.append((Event.sub_label.cast("text") % f"*{label},*")) + sub_label_clauses.append((Event.sub_label.cast("text") % f"*, {label}*")) + + sub_label_clause = reduce(operator.or_, sub_label_clauses) + clauses.append((sub_label_clause)) + + if recognized_license_plate != "all": + filtered_recognized_license_plates = recognized_license_plate.split(",") + + clauses_for_plates = [] + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + clauses_for_plates.append(Event.data["recognized_license_plate"].is_null()) + + # regex vs exact matching + normal_plates = [] + for plate in filtered_recognized_license_plates: + if plate.startswith("^") or any(ch in plate for ch in ".[]?+*"): + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").regexp(plate) + ) + else: + normal_plates.append(plate) + + # if there are any plain string plates, match them with IN + if normal_plates: + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").in_(normal_plates) + ) + + recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) + clauses.append(recognized_license_plate_clause) + + if zones != "all": + # use matching so events with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + if "None" in filtered_zones: + filtered_zones.remove("None") + zone_clauses.append((Event.zones.length() == 0)) + + for zone in filtered_zones: + zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + + zone_clause = reduce(operator.or_, zone_clauses) + clauses.append((zone_clause)) + + if after: + clauses.append((Event.start_time > after)) + + if before: + clauses.append((Event.start_time < before)) + + if time_range != DEFAULT_TIME_RANGE: + # get timezone arg to ensure browser times are used + tz_name = params.timezone + hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) + + times = time_range.split(",") + time_after = times[0] + time_before = times[1] + + start_hour_fun = fn.strftime( + "%H:%M", + fn.datetime(Event.start_time, "unixepoch", hour_modifier, minute_modifier), + ) + + # cases where user wants events overnight, ex: from 20:00 to 06:00 + # should use or operator + if time_after > time_before: + clauses.append( + ( + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], + ) + ) + ) + # all other cases should be and operator + else: + clauses.append((start_hour_fun > time_after)) + clauses.append((start_hour_fun < time_before)) + + if has_clip is not None: + clauses.append((Event.has_clip == has_clip)) + + if has_snapshot is not None: + clauses.append((Event.has_snapshot == has_snapshot)) + + if in_progress is not None: + clauses.append((Event.end_time.is_null(in_progress))) + + if include_thumbnails: + selected_columns.append(Event.thumbnail) + + if favorites: + clauses.append((Event.retain_indefinitely == favorites)) + + if max_score is not None: + clauses.append((Event.data["score"] <= max_score)) + + if min_score is not None: + clauses.append((Event.data["score"] >= min_score)) + + if max_speed is not None: + clauses.append((Event.data["average_estimated_speed"] <= max_speed)) + + if min_speed is not None: + clauses.append((Event.data["average_estimated_speed"] >= min_speed)) + + if min_length is not None: + clauses.append(((Event.end_time - Event.start_time) >= min_length)) + + if max_length is not None: + clauses.append(((Event.end_time - Event.start_time) <= max_length)) + + if is_submitted is not None: + if is_submitted == 0: + clauses.append((Event.plus_id.is_null())) + elif is_submitted > 0: + clauses.append((Event.plus_id != "")) + + if event_id is not None: + clauses.append((Event.id == event_id)) + + if len(clauses) == 0: + clauses.append((True)) + + if sort: + if sort == "score_asc": + order_by = Event.data["score"].asc() + elif sort == "score_desc": + order_by = Event.data["score"].desc() + elif sort == "speed_asc": + order_by = Event.data["average_estimated_speed"].asc() + elif sort == "speed_desc": + order_by = Event.data["average_estimated_speed"].desc() + elif sort == "date_asc": + order_by = Event.start_time.asc() + elif sort == "date_desc": + order_by = Event.start_time.desc() + else: + order_by = Event.start_time.desc() + else: + order_by = Event.start_time.desc() + + events = ( + Event.select(*selected_columns) + .where(reduce(operator.and_, clauses)) + .order_by(order_by) + .limit(limit) + .dicts() + .iterator() + ) + + return JSONResponse(content=list(events)) + + +@router.get( + "/events/explore", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get summary of objects", + description="""Gets a summary of objects from the database. + Returns a list of objects with a max of `limit` objects for each label. + """, +) +def events_explore( + limit: int = 10, + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + # get distinct labels for all events + distinct_labels = ( + Event.select(Event.label) + .where(Event.camera << allowed_cameras) + .distinct() + .order_by(Event.label) + ) + + label_counts = {} + + def event_generator(): + for label_obj in distinct_labels.iterator(): + label = label_obj.label + + # get most recent events for this label + label_events = ( + Event.select() + .where((Event.label == label) & (Event.camera << allowed_cameras)) + .order_by(Event.start_time.desc()) + .limit(limit) + .iterator() + ) + + # count total events for this label + label_counts[label] = ( + Event.select() + .where((Event.label == label) & (Event.camera << allowed_cameras)) + .count() + ) + + yield from label_events + + def process_events(): + for event in event_generator(): + processed_event = { + "id": event.id, + "camera": event.camera, + "label": event.label, + "zones": event.zones, + "start_time": event.start_time, + "end_time": event.end_time, + "has_clip": event.has_clip, + "has_snapshot": event.has_snapshot, + "plus_id": event.plus_id, + "retain_indefinitely": event.retain_indefinitely, + "sub_label": event.sub_label, + "top_score": event.top_score, + "false_positive": event.false_positive, + "box": event.box, + "data": { + k: v + for k, v in event.data.items() + if k + in [ + "type", + "score", + "top_score", + "description", + "sub_label_score", + "average_estimated_speed", + "velocity_angle", + "path_data", + "recognized_license_plate", + "recognized_license_plate_score", + ] + }, + "event_count": label_counts[event.label], + } + yield processed_event + + # convert iterator to list and sort + processed_events = sorted( + process_events(), + key=lambda x: (x["event_count"], x["start_time"]), + reverse=True, + ) + + return JSONResponse(content=processed_events) + + +@router.get( + "/event_ids", + response_model=list[EventResponse], + dependencies=[Depends(allow_any_authenticated())], + summary="Get events by ids", + description="""Gets events by a list of ids. + Returns a list of events. + """, +) +async def event_ids(ids: str, request: Request): + ids = ids.split(",") + + if not ids: + return JSONResponse( + content=({"success": False, "message": "Valid list of ids must be sent"}), + status_code=400, + ) + + for event_id in ids: + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + # we should not fail the entire request if an event is not found + continue + + try: + events = Event.select().where(Event.id << ids).dicts().iterator() + return JSONResponse(list(events)) + except Exception: + return JSONResponse( + content=({"success": False, "message": "Events not found"}), status_code=400 + ) + + +@router.get( + "/events/search", + dependencies=[Depends(allow_any_authenticated())], + summary="Search events", + description="""Searches for events in the database. + Returns a list of events. + """, +) +def events_search( + request: Request, + params: EventsSearchQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + query = params.query + search_type = params.search_type + include_thumbnails = params.include_thumbnails + limit = params.limit + sort = params.sort + + # Filters + cameras = params.cameras + labels = params.labels + zones = params.zones + after = params.after + before = params.before + min_score = params.min_score + max_score = params.max_score + min_speed = params.min_speed + max_speed = params.max_speed + time_range = params.time_range + has_clip = params.has_clip + has_snapshot = params.has_snapshot + is_submitted = params.is_submitted + recognized_license_plate = params.recognized_license_plate + + # for similarity search + event_id = params.event_id + + if not query and not event_id: + return JSONResponse( + content=( + { + "success": False, + "message": "A search query must be supplied", + } + ), + status_code=400, + ) + + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content=( + { + "success": False, + "message": "Semantic search is not enabled", + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + + selected_columns = [ + Event.id, + Event.camera, + Event.label, + Event.sub_label, + Event.zones, + Event.start_time, + Event.end_time, + Event.has_clip, + Event.has_snapshot, + Event.top_score, + Event.data, + Event.plus_id, + ReviewSegment.thumb_path, + ] + + if include_thumbnails: + selected_columns.append(Event.thumbnail) + + # Build the initial SQLite query filters + event_filters = [] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + event_filters.append((Event.camera << list(filtered))) + else: + event_filters.append((Event.camera << allowed_cameras)) + + if labels != "all": + event_filters.append((Event.label << labels.split(","))) + + if zones != "all": + zone_clauses = [] + filtered_zones = zones.split(",") + + if "None" in filtered_zones: + filtered_zones.remove("None") + zone_clauses.append((Event.zones.length() == 0)) + + for zone in filtered_zones: + zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + + event_filters.append((reduce(operator.or_, zone_clauses))) + + if recognized_license_plate != "all": + filtered_recognized_license_plates = recognized_license_plate.split(",") + + clauses_for_plates = [] + + if "None" in filtered_recognized_license_plates: + filtered_recognized_license_plates.remove("None") + clauses_for_plates.append(Event.data["recognized_license_plate"].is_null()) + + # regex vs exact matching + normal_plates = [] + for plate in filtered_recognized_license_plates: + if plate.startswith("^") or any(ch in plate for ch in ".[]?+*"): + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").regexp(plate) + ) + else: + normal_plates.append(plate) + + # if there are any plain string plates, match them with IN + if normal_plates: + clauses_for_plates.append( + Event.data["recognized_license_plate"].cast("text").in_(normal_plates) + ) + + recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) + event_filters.append((recognized_license_plate_clause)) + + if after: + event_filters.append((Event.start_time > after)) + + if before: + event_filters.append((Event.start_time < before)) + + if has_clip is not None: + event_filters.append((Event.has_clip == has_clip)) + + if has_snapshot is not None: + event_filters.append((Event.has_snapshot == has_snapshot)) + + if is_submitted is not None: + if is_submitted == 0: + event_filters.append((Event.plus_id.is_null())) + elif is_submitted > 0: + event_filters.append((Event.plus_id != "")) + + if min_score is not None and max_score is not None: + event_filters.append((Event.data["score"].between(min_score, max_score))) + else: + if min_score is not None: + event_filters.append((Event.data["score"] >= min_score)) + if max_score is not None: + event_filters.append((Event.data["score"] <= max_score)) + + if min_speed is not None and max_speed is not None: + event_filters.append( + (Event.data["average_estimated_speed"].between(min_speed, max_speed)) + ) + else: + if min_speed is not None: + event_filters.append((Event.data["average_estimated_speed"] >= min_speed)) + if max_speed is not None: + event_filters.append((Event.data["average_estimated_speed"] <= max_speed)) + + if time_range != DEFAULT_TIME_RANGE: + tz_name = params.timezone + hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name) + + times = time_range.split(",") + time_after, time_before = times + + start_hour_fun = fn.strftime( + "%H:%M", + fn.datetime(Event.start_time, "unixepoch", hour_modifier, minute_modifier), + ) + + # cases where user wants events overnight, ex: from 20:00 to 06:00 + # should use or operator + if time_after > time_before: + event_filters.append( + ( + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], + ) + ) + ) + # all other cases should be and operator + else: + event_filters.append((start_hour_fun > time_after)) + event_filters.append((start_hour_fun < time_before)) + + # Perform semantic search + search_results = {} + if search_type == "similarity": + try: + search_event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={ + "success": False, + "message": "Event not found", + }, + status_code=404, + ) + + thumb_result = context.search_thumbnail(search_event) + thumb_ids = {result[0]: result[1] for result in thumb_result} + search_results = { + event_id: {"distance": distance, "source": "thumbnail"} + for event_id, distance in thumb_ids.items() + } + else: + search_types = search_type.split(",") + + # only save stats for multi-modal searches + save_stats = "thumbnail" in search_types and "description" in search_types + + if "thumbnail" in search_types: + thumb_result = context.search_thumbnail(query) + + thumb_distances = context.thumb_stats.normalize( + [result[1] for result in thumb_result], save_stats + ) + + thumb_ids = dict( + zip([result[0] for result in thumb_result], thumb_distances) + ) + search_results.update( + { + event_id: {"distance": distance, "source": "thumbnail"} + for event_id, distance in thumb_ids.items() + } + ) + + if "description" in search_types: + desc_result = context.search_description(query) + + desc_distances = context.desc_stats.normalize( + [result[1] for result in desc_result], save_stats + ) + + desc_ids = dict(zip([result[0] for result in desc_result], desc_distances)) + + for event_id, distance in desc_ids.items(): + if ( + event_id not in search_results + or distance < search_results[event_id]["distance"] + ): + search_results[event_id] = { + "distance": distance, + "source": "description", + } + + if not search_results: + return JSONResponse(content=[]) + + # Fetch events in a single query + events_query = Event.select(*selected_columns).join( + ReviewSegment, + JOIN.LEFT_OUTER, + on=(fn.json_extract(ReviewSegment.data, "$.detections").contains(Event.id)), + ) + + # Apply filters, if any + if event_filters: + events_query = events_query.where(reduce(operator.and_, event_filters)) + + # If we did a similarity search, limit events to those in search_results + if search_results: + events_query = events_query.where(Event.id << list(search_results.keys())) + + # Fetch events and process them in a single pass + processed_events = [] + for event in events_query.dicts(): + processed_event = {k: v for k, v in event.items() if k != "data"} + processed_event["data"] = { + k: v + for k, v in event["data"].items() + if k + in [ + "attributes", + "type", + "score", + "top_score", + "description", + "sub_label_score", + "average_estimated_speed", + "velocity_angle", + "path_data", + "recognized_license_plate", + "recognized_license_plate_score", + ] + } + + if event["id"] in search_results: + processed_event["search_distance"] = search_results[event["id"]]["distance"] + processed_event["search_source"] = search_results[event["id"]]["source"] + + processed_events.append(processed_event) + + if (sort is None or sort == "relevance") and search_results: + processed_events.sort(key=lambda x: x.get("search_distance", float("inf"))) + elif sort == "score_asc": + processed_events.sort(key=lambda x: x["data"]["score"]) + elif sort == "score_desc": + processed_events.sort(key=lambda x: x["data"]["score"], reverse=True) + elif sort == "speed_asc": + processed_events.sort( + key=lambda x: ( + x["data"].get("average_estimated_speed") is None, + x["data"].get("average_estimated_speed"), + ) + ) + elif sort == "speed_desc": + processed_events.sort( + key=lambda x: ( + x["data"].get("average_estimated_speed") is None, + x["data"].get("average_estimated_speed", float("-inf")), + ), + reverse=True, + ) + elif sort == "date_asc": + processed_events.sort(key=lambda x: x["start_time"]) + else: + # "date_desc" default + processed_events.sort(key=lambda x: x["start_time"], reverse=True) + + # Limit the number of events returned + processed_events = processed_events[:limit] + + return JSONResponse(content=processed_events) + + +@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())]) +def events_summary( + params: EventsSummaryQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + tz_name = params.timezone + has_clip = params.has_clip + has_snapshot = params.has_snapshot + + clauses = [] + + if has_clip is not None: + clauses.append((Event.has_clip == has_clip)) + + if has_snapshot is not None: + clauses.append((Event.has_snapshot == has_snapshot)) + + if len(clauses) == 0: + clauses.append((True)) + + time_range_query = ( + Event.select( + fn.MIN(Event.start_time).alias("min_time"), + fn.MAX(Event.start_time).alias("max_time"), + ) + .where(reduce(operator.and_, clauses) & (Event.camera << allowed_cameras)) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + if min_time is None or max_time is None: + return JSONResponse(content=[]) + + dst_periods = get_dst_transitions(tz_name, min_time, max_time) + + grouped: dict[tuple, dict] = {} + + for period_start, period_end, period_offset in dst_periods: + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + period_groups = ( + Event.select( + Event.camera, + Event.label, + Event.sub_label, + Event.data, + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Event.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day"), + Event.zones, + fn.COUNT(Event.id).alias("count"), + ) + .where( + reduce(operator.and_, clauses) + & (Event.camera << allowed_cameras) + & (Event.start_time >= period_start) + & (Event.start_time <= period_end) + ) + .group_by( + Event.camera, + Event.label, + Event.sub_label, + Event.data, + (Event.start_time + period_offset).cast("int") / (3600 * 24), + Event.zones, + ) + .namedtuples() + ) + + for g in period_groups: + key = ( + g.camera, + g.label, + g.sub_label, + json.dumps(g.data, sort_keys=True) if g.data is not None else None, + g.day, + json.dumps(g.zones, sort_keys=True) if g.zones is not None else None, + ) + + if key in grouped: + grouped[key]["count"] += int(g.count or 0) + else: + grouped[key] = { + "camera": g.camera, + "label": g.label, + "sub_label": g.sub_label, + "data": g.data, + "day": g.day, + "zones": g.zones, + "count": int(g.count or 0), + } + + return JSONResponse(content=sorted(grouped.values(), key=lambda x: x["day"])) + + +@router.get( + "/events/{event_id}", + response_model=EventResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get event by id", + description="Gets an event by its id.", +) +async def event(event_id: str, request: Request): + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + return model_to_dict(event) + except DoesNotExist: + return JSONResponse(content="Event not found", status_code=404) + + +@router.post( + "/events/{event_id}/retain", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event retain indefinitely.", + description="""Sets an event to retain indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + """, +) +def set_retain(event_id: str): + try: + event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + event.retain_indefinitely = True + event.save() + + return JSONResponse( + content=({"success": True, "message": "Event " + event_id + " retained"}), + status_code=200, + ) + + +@router.post( + "/events/{event_id}/plus", + response_model=EventUploadPlusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Send event to Frigate+", + description="""Sends an event to Frigate+. + Returns a success message or an error if the event is not found. + """, +) +async def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = None): + if not request.app.frigate_config.plus_api.is_active(): + message = "PLUS_API_KEY environment variable is not set" + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + include_annotation = body.include_annotation if body is not None else None + + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + # events from before the conversion to relative dimensions cant include annotations + if event.data.get("box") is None: + include_annotation = None + + if event.end_time is None: + logger.error(f"Unable to load clean snapshot for in-progress event: {event.id}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to load clean snapshot for in-progress event", + } + ), + status_code=400, + ) + + if event.plus_id: + message = "Already submitted to plus" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + # load clean.webp or clean.png (legacy) + try: + filename_webp = f"{event.camera}-{event.id}-clean.webp" + filename_png = f"{event.camera}-{event.id}-clean.png" + + image_path = None + if os.path.exists(os.path.join(CLIPS_DIR, filename_webp)): + image_path = os.path.join(CLIPS_DIR, filename_webp) + elif os.path.exists(os.path.join(CLIPS_DIR, filename_png)): + image_path = os.path.join(CLIPS_DIR, filename_png) + + if image_path is None: + logger.error(f"Unable to find clean snapshot for event: {event.id}") + return JSONResponse( + content=( + { + "success": False, + "message": "Unable to find clean snapshot for event", + } + ), + status_code=400, + ) + + image = cv2.imread(image_path) + except Exception: + logger.error(f"Unable to load clean snapshot for event: {event.id}") + return JSONResponse( + content=( + {"success": False, "message": "Unable to load clean snapshot for event"} + ), + status_code=400, + ) + + if image is None or image.size == 0: + logger.error(f"Unable to load clean snapshot for event: {event.id}") + return JSONResponse( + content=( + {"success": False, "message": "Unable to load clean snapshot for event"} + ), + status_code=400, + ) + + try: + plus_id = request.app.frigate_config.plus_api.upload_image(image, event.camera) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading image"}), + status_code=400, + ) + + # store image id in the database + event.plus_id = plus_id + event.save() + + if include_annotation is not None: + box = event.data["box"] + + try: + request.app.frigate_config.plus_api.add_annotation( + event.plus_id, + box, + event.label, + ) + except ValueError: + message = "Error uploading annotation, unsupported label provided." + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), + status_code=400, + ) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading annotation"}), + status_code=400, + ) + + return JSONResponse( + content=({"success": True, "plus_id": plus_id}), status_code=200 + ) + + +@router.put( + "/events/{event_id}/false_positive", + response_model=EventUploadPlusResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Submit false positive to Frigate+", + description="""Submit an event as a false positive to Frigate+. + This endpoint is the same as the standard Frigate+ submission endpoint, + but is specifically for marking an event as a false positive.""", +) +async def false_positive(request: Request, event_id: str): + if not request.app.frigate_config.plus_api.is_active(): + message = "PLUS_API_KEY environment variable is not set" + logger.error(message) + return JSONResponse( + content=( + { + "success": False, + "message": message, + } + ), + status_code=400, + ) + + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + message = f"Event {event_id} not found" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=404 + ) + + # events from before the conversion to relative dimensions cant include annotations + if event.data.get("box") is None: + message = "Events prior to 0.13 cannot be submitted as false positives" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + if event.false_positive: + message = "False positive already submitted to Frigate+" + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), status_code=400 + ) + + if not event.plus_id: + plus_response = await send_to_plus(request, event_id) + if plus_response.status_code != 200: + return plus_response + # need to refetch the event now that it has a plus_id + event = Event.get(Event.id == event_id) + + region = event.data["region"] + box = event.data["box"] + + # provide top score if score is unavailable + score = ( + (event.data["top_score"] if event.data["top_score"] else event.top_score) + if event.data["score"] is None + else event.data["score"] + ) + + try: + request.app.frigate_config.plus_api.add_false_positive( + event.plus_id, + region, + box, + score, + event.label, + event.model_hash, + event.model_type, + event.detector_type, + ) + except ValueError: + message = "Error uploading false positive, unsupported label provided." + logger.error(message) + return JSONResponse( + content=({"success": False, "message": message}), + status_code=400, + ) + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error uploading false positive"}), + status_code=400, + ) + + event.false_positive = True + event.save() + + return JSONResponse( + content=({"success": True, "plus_id": event.plus_id}), status_code=200 + ) + + +@router.delete( + "/events/{event_id}/retain", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Stop event from being retained indefinitely", + description="""Stops an event from being retained indefinitely. + Returns a success message or an error if the event is not found. + NOTE: This is a legacy endpoint and is not supported in the frontend. + """, +) +async def delete_retain(event_id: str, request: Request): + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + event.retain_indefinitely = False + event.save() + + return JSONResponse( + content=({"success": True, "message": "Event " + event_id + " un-retained"}), + status_code=200, + ) + + +@router.post( + "/events/{event_id}/sub_label", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event sub label", + description="""Sets an event's sub label. + Returns a success message or an error if the event is not found. + """, +) +async def set_sub_label( + request: Request, + event_id: str, + body: EventsSubLabelBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + event = None + + if request.app.detected_frames_processor: + tracked_obj: TrackedObject = None + + for state in request.app.detected_frames_processor.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + else: + tracked_obj = None + + if not event and not tracked_obj: + return JSONResponse( + content=( + {"success": False, "message": "Event " + event_id + " not found."} + ), + status_code=404, + ) + + new_sub_label = body.subLabel + new_score = body.subLabelScore + + if new_sub_label == "": + new_sub_label = None + new_score = None + + request.app.event_metadata_updater.publish( + (event_id, new_sub_label, new_score), EventMetadataTypeEnum.sub_label.value + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Event {event_id} sub label set to {new_sub_label if new_sub_label is not None else 'None'}", + }, + status_code=200, + ) + + +@router.post( + "/events/{event_id}/recognized_license_plate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event license plate", + description="""Sets an event's license plate. + Returns a success message or an error if the event is not found. + """, +) +async def set_plate( + request: Request, + event_id: str, + body: EventsLPRBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + event = None + + if request.app.detected_frames_processor: + tracked_obj: TrackedObject = None + + for state in request.app.detected_frames_processor.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + else: + tracked_obj = None + + if not event and not tracked_obj: + return JSONResponse( + content=( + {"success": False, "message": "Event " + event_id + " not found."} + ), + status_code=404, + ) + + new_plate = body.recognizedLicensePlate + new_score = body.recognizedLicensePlateScore + + if new_plate == "": + new_plate = None + new_score = None + + request.app.event_metadata_updater.publish( + (event_id, "recognized_license_plate", new_plate, new_score), + EventMetadataTypeEnum.attribute.value, + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Event {event_id} license plate set to {new_plate if new_plate is not None else 'None'}", + }, + status_code=200, + ) + + +@router.post( + "/events/{event_id}/description", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Set event description", + description="""Sets an event's description. + Returns a success message or an error if the event is not found. + """, +) +async def set_description( + request: Request, + event_id: str, + body: EventsDescriptionBody, +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + new_description = body.description + + event.data["description"] = new_description + event.save() + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + if len(new_description) > 0: + context.update_description( + event_id, + new_description, + ) + else: + context.db.delete_embeddings_description(event_ids=[event_id]) + + response_message = ( + f"Event {event_id} description is now blank" + if new_description is None or len(new_description) == 0 + else f"Event {event_id} description set to {new_description}" + ) + + return JSONResponse( + content=( + { + "success": True, + "message": response_message, + } + ), + status_code=200, + ) + + +@router.put( + "/events/{event_id}/description/regenerate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Regenerate event description", + description="""Regenerates an event's description. + Returns a success message or an error if the event is not found. + """, +) +async def regenerate_description( + request: Request, event_id: str, params: RegenerateQueryParameters = Depends() +): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Event " + event_id + " not found"}), + status_code=404, + ) + + camera_config = request.app.frigate_config.cameras[event.camera] + + if camera_config.objects.genai.enabled or params.force: + request.app.event_metadata_updater.publish( + (event.id, params.source, params.force), + EventMetadataTypeEnum.regenerate_description.value, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Event " + + event_id + + " description regeneration has been requested using " + + params.source, + } + ), + status_code=200, + ) + + return JSONResponse( + content=( + { + "success": False, + "message": "Semantic Search and Generative AI must be enabled to regenerate a description", + } + ), + status_code=400, + ) + + +@router.post( + "/description/generate", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Generate description embedding", + description="""Generates an embedding for an event's description. + Returns a success message or an error if the event is not found. + """, +) +def generate_description_embedding( + request: Request, + body: EventsDescriptionBody, +): + new_description = body.description + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + if len(new_description) > 0: + result = context.generate_description_embedding( + new_description, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": f"Embedding for description is {result}" + if result + else "Failed to generate embedding", + } + ), + status_code=200, + ) + + +async def delete_single_event(event_id: str, request: Request) -> dict: + try: + event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + except DoesNotExist: + return {"success": False, "message": f"Event {event_id} not found"} + + media_name = f"{event.camera}-{event.id}" + if event.has_snapshot: + snapshot_paths = [ + Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg"), + Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"), + Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp"), + ] + for media in snapshot_paths: + media.unlink(missing_ok=True) + + event.delete_instance() + Timeline.delete().where(Timeline.source_id == event_id).execute() + + # If semantic search is enabled, update the index + if request.app.frigate_config.semantic_search.enabled: + context: EmbeddingsContext = request.app.embeddings + context.db.delete_embeddings_thumbnail(event_ids=[event_id]) + context.db.delete_embeddings_description(event_ids=[event_id]) + + return {"success": True, "message": f"Event {event_id} deleted"} + + +@router.delete( + "/events/{event_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete event", + description="""Deletes an event from the database. + Returns a success message or an error if the event is not found. + """, +) +async def delete_event(request: Request, event_id: str): + result = await delete_single_event(event_id, request) + status_code = 200 if result["success"] else 404 + return JSONResponse(content=result, status_code=status_code) + + +@router.delete( + "/events/", + response_model=EventMultiDeleteResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete events", + description="""Deletes a list of events from the database. + Returns a success message or an error if the events are not found. + """, +) +async def delete_events(request: Request, body: EventsDeleteBody): + if not body.event_ids: + return JSONResponse( + content=({"success": False, "message": "No event IDs provided."}), + status_code=404, + ) + + deleted_events = [] + not_found_events = [] + + for event_id in body.event_ids: + result = await delete_single_event(event_id, request) + if result["success"]: + deleted_events.append(event_id) + else: + not_found_events.append(event_id) + + response = { + "success": True, + "deleted_events": deleted_events, + "not_found_events": not_found_events, + } + return JSONResponse(content=response, status_code=200) + + +@router.post( + "/events/{camera_name}/{label}/create", + response_model=EventCreateResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Create manual event", + description="""Creates a manual event in the database. + Returns a success message or an error if the event is not found. + NOTES: + - Creating a manual event does not trigger an update to /events MQTT topic. + - If a duration is set to null, the event will need to be ended manually by calling /events/{event_id}/end. + """, +) +def create_event( + request: Request, + camera_name: str, + label: str, + body: EventsCreateBody = EventsCreateBody(), +): + if not camera_name or not request.app.frigate_config.cameras.get(camera_name): + return JSONResponse( + content=( + {"success": False, "message": f"{camera_name} is not a valid camera."} + ), + status_code=404, + ) + + if not label: + return JSONResponse( + content=({"success": False, "message": f"{label} must be set."}), + status_code=404, + ) + + now = datetime.datetime.now().timestamp() + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + event_id = f"{now}-{rand_id}" + + request.app.event_metadata_updater.publish( + ( + now, + camera_name, + label, + event_id, + body.include_recording, + body.score, + body.sub_label, + body.duration, + "api", + body.draw, + ), + EventMetadataTypeEnum.manual_event_create.value, + ) + + return JSONResponse( + content=( + { + "success": True, + "message": "Successfully created event.", + "event_id": event_id, + } + ), + status_code=200, + ) + + +@router.put( + "/events/{event_id}/end", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="End manual event", + description="""Ends a manual event. + Returns a success message or an error if the event is not found. + NOTE: This should only be used for manual events. + """, +) +async def end_event(request: Request, event_id: str, body: EventsEndBody): + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + + if body.end_time is not None and body.end_time < event.start_time: + return JSONResponse( + content=( + { + "success": False, + "message": f"end_time ({body.end_time}) cannot be before start_time ({event.start_time}).", + } + ), + status_code=400, + ) + + end_time = body.end_time or datetime.datetime.now().timestamp() + request.app.event_metadata_updater.publish( + (event_id, end_time), EventMetadataTypeEnum.manual_event_end.value + ) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": f"Event {event_id} not found."}), + status_code=404, + ) + except Exception: + return JSONResponse( + content=( + {"success": False, "message": f"{event_id} must be set and valid."} + ), + status_code=404, + ) + + return JSONResponse( + content=({"success": True, "message": "Event successfully ended."}), + status_code=200, + ) + + +@router.post( + "/trigger/embedding", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Create trigger embedding", + description="""Creates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def create_trigger_embedding( + request: Request, + body: TriggerEmbeddingBody, + camera_name: str, + name: str, +): + try: + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content={ + "success": False, + "message": "Semantic search is not enabled", + }, + status_code=400, + ) + + # Check if trigger already exists + if ( + Trigger.select() + .where(Trigger.camera == camera_name, Trigger.name == name) + .exists() + ): + return JSONResponse( + content={ + "success": False, + "message": f"Trigger {camera_name}:{name} already exists", + }, + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + # Generate embedding based on type + embedding = None + if body.type == "description": + embedding = context.generate_description_embedding(body.data) + elif body.type == "thumbnail": + try: + event: Event = Event.get(Event.id == body.data) + except DoesNotExist: + # TODO: check triggers directory for image + return JSONResponse( + content={ + "success": False, + "message": f"Failed to fetch event for {body.type} trigger", + }, + status_code=400, + ) + + # Skip the event if not an object + if event.data.get("type") != "object": + return + + # Get the thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + if thumbnail is None: + return JSONResponse( + content={ + "success": False, + "message": f"Failed to get thumbnail for {body.data} for {body.type} trigger", + }, + status_code=400, + ) + + # Try to reuse existing embedding from database + cursor = context.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [body.data], + ) + + row = cursor.fetchone() if cursor else None + + if row: + query_embedding = row[0] + embedding = np.frombuffer(query_embedding, dtype=np.float32) + else: + # Generate new embedding + embedding = context.generate_image_embedding( + body.data, (base64.b64encode(thumbnail).decode("ASCII")) + ) + + if embedding is None or ( + isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0 + ): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to generate embedding for {body.type} trigger", + }, + status_code=400, + ) + + if body.type == "thumbnail": + # Save image to the triggers directory + try: + os.makedirs( + os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)), + exist_ok=True, + ) + with open( + os.path.join( + TRIGGER_DIR, + sanitize_filename(camera_name), + f"{sanitize_filename(body.data)}.webp", + ), + "wb", + ) as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {body.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" + ) + + Trigger.create( + camera=camera_name, + name=name, + type=body.type, + data=body.data, + threshold=body.threshold, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + triggering_event_id="", + last_triggered=None, + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger created successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error creating trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error creating trigger embedding", + }, + status_code=500, + ) + + +@router.put( + "/trigger/embedding/{camera_name}/{name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Update trigger embedding", + description="""Updates a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def update_trigger_embedding( + request: Request, + camera_name: str, + name: str, + body: TriggerEmbeddingBody, +): + try: + if not request.app.frigate_config.semantic_search.enabled: + return JSONResponse( + content={ + "success": False, + "message": "Semantic search is not enabled", + }, + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + # Generate embedding based on type + embedding = None + if body.type == "description": + embedding = context.generate_description_embedding(body.data) + elif body.type == "thumbnail": + webp_file = sanitize_filename(body.data) + ".webp" + webp_path = os.path.join( + TRIGGER_DIR, sanitize_filename(camera_name), webp_file + ) + + try: + event: Event = Event.get(Event.id == body.data) + # Skip the event if not an object + if event.data.get("type") != "object": + return JSONResponse( + content={ + "success": False, + "message": f"Event {body.data} is not a tracked object for {body.type} trigger", + }, + status_code=400, + ) + # Extract valid thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + with open(webp_path, "wb") as f: + f.write(thumbnail) + except DoesNotExist: + # check triggers directory for image + if not os.path.exists(webp_path): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to fetch event for {body.type} trigger", + }, + status_code=400, + ) + else: + # Load the image from the triggers directory + with open(webp_path, "rb") as f: + thumbnail = f.read() + + embedding = context.generate_image_embedding( + body.data, (base64.b64encode(thumbnail).decode("ASCII")) + ) + + if embedding is None or ( + isinstance(embedding, (list, np.ndarray)) and len(embedding) == 0 + ): + return JSONResponse( + content={ + "success": False, + "message": f"Failed to generate embedding for {body.type} trigger", + }, + status_code=400, + ) + + # Check if trigger exists for upsert + trigger = Trigger.get_or_none( + Trigger.camera == camera_name, Trigger.name == name + ) + + if trigger: + # Update existing trigger + if trigger.data != body.data: # Delete old thumbnail only if data changes + try: + os.remove( + os.path.join( + TRIGGER_DIR, + sanitize_filename(camera_name), + f"{trigger.data}.webp", + ) + ) + logger.debug( + f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" + ) + + Trigger.update( + data=body.data, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + threshold=body.threshold, + triggering_event_id="", + last_triggered=None, + ).where(Trigger.camera == camera_name, Trigger.name == name).execute() + else: + # Create new trigger (for rename case) + Trigger.create( + camera=camera_name, + name=name, + type=body.type, + data=body.data, + threshold=body.threshold, + model=request.app.frigate_config.semantic_search.model, + embedding=np.array(embedding, dtype=np.float32).tobytes(), + triggering_event_id="", + last_triggered=None, + ) + + if body.type == "thumbnail": + # Save image to the triggers directory + try: + camera_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)) + os.makedirs(camera_path, exist_ok=True) + with open( + os.path.join(camera_path, f"{sanitize_filename(body.data)}.webp"), + "wb", + ) as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {body.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to write thumbnail for trigger with data {body.data} in {camera_name}" + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger updated successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error updating trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error updating trigger embedding", + }, + status_code=500, + ) + + +@router.delete( + "/trigger/embedding/{camera_name}/{name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete trigger embedding", + description="""Deletes a trigger embedding for a specific trigger. + Returns a success message or an error if the trigger is not found. + """, +) +def delete_trigger_embedding( + request: Request, + camera_name: str, + name: str, +): + try: + trigger = Trigger.get_or_none( + Trigger.camera == camera_name, Trigger.name == name + ) + if trigger is None: + return JSONResponse( + content={ + "success": False, + "message": f"Trigger {camera_name}:{name} not found", + }, + status_code=500, + ) + + deleted = ( + Trigger.delete() + .where(Trigger.camera == camera_name, Trigger.name == name) + .execute() + ) + if deleted == 0: + return JSONResponse( + content={ + "success": False, + "message": f"Error deleting trigger {camera_name}:{name}", + }, + status_code=401, + ) + + try: + os.remove( + os.path.join( + TRIGGER_DIR, sanitize_filename(camera_name), f"{trigger.data}.webp" + ) + ) + logger.debug( + f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}." + ) + except Exception: + logger.exception( + f"Failed to delete thumbnail for trigger with data {trigger.data} in {camera_name}" + ) + + return JSONResponse( + content={ + "success": True, + "message": f"Trigger deleted successfully for {camera_name}:{name}", + }, + status_code=200, + ) + + except Exception: + logger.exception("Error deleting trigger embedding") + return JSONResponse( + content={ + "success": False, + "message": "Error deleting trigger embedding", + }, + status_code=500, + ) + + +@router.get( + "/triggers/status/{camera_name}", + response_model=dict, + dependencies=[Depends(require_role(["admin"]))], + summary="Get triggers status", + description="""Gets the status of all triggers for a specific camera. + Returns a success message or an error if the camera is not found. + """, +) +def get_triggers_status( + camera_name: str, +): + try: + # Fetch all triggers for the specified camera + triggers = Trigger.select().where(Trigger.camera == camera_name) + + # Prepare the response with trigger status + status = { + trigger.name: { + "last_triggered": trigger.last_triggered.timestamp() + if trigger.last_triggered + else None, + "triggering_event_id": trigger.triggering_event_id + if trigger.triggering_event_id + else None, + } + for trigger in triggers + } + + if not status: + return JSONResponse( + content={ + "success": False, + "message": f"No triggers found for camera {camera_name}", + }, + status_code=404, + ) + + return {"success": True, "triggers": status} + except Exception as ex: + logger.exception(ex) + return JSONResponse( + content=({"success": False, "message": "Error fetching trigger status"}), + status_code=400, + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/export.py b/sam2-cpu/frigate-dev/frigate/api/export.py new file mode 100644 index 0000000..24fed93 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/export.py @@ -0,0 +1,291 @@ +"""Export apis.""" + +import logging +import random +import string +from pathlib import Path +from typing import List + +import psutil +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from pathvalidate import sanitize_filepath +from peewee import DoesNotExist +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, + require_role, +) +from frigate.api.defs.request.export_recordings_body import ExportRecordingsBody +from frigate.api.defs.request.export_rename_body import ExportRenameBody +from frigate.api.defs.response.export_response import ( + ExportModel, + ExportsResponse, + StartExportResponse, +) +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.tags import Tags +from frigate.const import CLIPS_DIR, EXPORT_DIR +from frigate.models import Export, Previews, Recordings +from frigate.record.export import ( + PlaybackFactorEnum, + PlaybackSourceEnum, + RecordingExporter, +) +from frigate.util.time import is_current_hour + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.export]) + + +@router.get( + "/exports", + response_model=ExportsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get exports", + description="""Gets all exports from the database for cameras the user has access to. + Returns a list of exports ordered by date (most recent first).""", +) +def get_exports( + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + exports = ( + Export.select() + .where(Export.camera << allowed_cameras) + .order_by(Export.date.desc()) + .dicts() + .iterator() + ) + return JSONResponse(content=[e for e in exports]) + + +@router.post( + "/export/{camera_name}/start/{start_time}/end/{end_time}", + response_model=StartExportResponse, + dependencies=[Depends(require_camera_access)], + summary="Start recording export", + description="""Starts an export of a recording for the specified time range. + The export can be from recordings or preview footage. Returns the export ID if + successful, or an error message if the camera is invalid or no recordings/previews + are found for the time range.""", +) +def export_recording( + request: Request, + camera_name: str, + start_time: float, + end_time: float, + body: ExportRecordingsBody, +): + if not camera_name or not request.app.frigate_config.cameras.get(camera_name): + return JSONResponse( + content=( + {"success": False, "message": f"{camera_name} is not a valid camera."} + ), + status_code=404, + ) + + playback_factor = body.playback + playback_source = body.source + friendly_name = body.name + existing_image = sanitize_filepath(body.image_path) if body.image_path else None + + # Ensure that existing_image is a valid path + if existing_image and not existing_image.startswith(CLIPS_DIR): + return JSONResponse( + content=({"success": False, "message": "Invalid image path"}), + status_code=400, + ) + + if playback_source == "recordings": + recordings_count = ( + Recordings.select() + .where( + Recordings.start_time.between(start_time, end_time) + | Recordings.end_time.between(start_time, end_time) + | ( + (start_time > Recordings.start_time) + & (end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .count() + ) + + if recordings_count <= 0: + return JSONResponse( + content=( + {"success": False, "message": "No recordings found for time range"} + ), + status_code=400, + ) + else: + previews_count = ( + Previews.select() + .where( + Previews.start_time.between(start_time, end_time) + | Previews.end_time.between(start_time, end_time) + | ((start_time > Previews.start_time) & (end_time < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .count() + ) + + if not is_current_hour(start_time) and previews_count <= 0: + return JSONResponse( + content=( + {"success": False, "message": "No previews found for time range"} + ), + status_code=400, + ) + + export_id = f"{camera_name}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" + exporter = RecordingExporter( + request.app.frigate_config, + export_id, + camera_name, + friendly_name, + existing_image, + int(start_time), + int(end_time), + ( + PlaybackFactorEnum[playback_factor] + if playback_factor in PlaybackFactorEnum.__members__.values() + else PlaybackFactorEnum.realtime + ), + ( + PlaybackSourceEnum[playback_source] + if playback_source in PlaybackSourceEnum.__members__.values() + else PlaybackSourceEnum.recordings + ), + ) + exporter.start() + return JSONResponse( + content=( + { + "success": True, + "message": "Starting export of recording.", + "export_id": export_id, + } + ), + status_code=200, + ) + + +@router.patch( + "/export/{event_id}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename export", + description="""Renames an export. + NOTE: This changes the friendly name of the export, not the filename. + """, +) +async def export_rename(event_id: str, body: ExportRenameBody, request: Request): + try: + export: Export = Export.get(Export.id == event_id) + await require_camera_access(export.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": "Export not found.", + } + ), + status_code=404, + ) + + export.name = body.name + export.save() + return JSONResponse( + content=( + { + "success": True, + "message": "Successfully renamed export.", + } + ), + status_code=200, + ) + + +@router.delete( + "/export/{event_id}", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Delete export", +) +async def export_delete(event_id: str, request: Request): + try: + export: Export = Export.get(Export.id == event_id) + await require_camera_access(export.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=( + { + "success": False, + "message": "Export not found.", + } + ), + status_code=404, + ) + + files_in_use = [] + for process in psutil.process_iter(): + try: + if process.name() != "ffmpeg": + continue + file_list = process.open_files() + if file_list: + for nt in file_list: + if nt.path.startswith(EXPORT_DIR): + files_in_use.append(nt.path.split("/")[-1]) + except psutil.Error: + continue + + if export.video_path.split("/")[-1] in files_in_use: + return JSONResponse( + content=( + {"success": False, "message": "Can not delete in progress export."} + ), + status_code=400, + ) + + Path(export.video_path).unlink(missing_ok=True) + + if export.thumb_path: + Path(export.thumb_path).unlink(missing_ok=True) + + export.delete_instance() + return JSONResponse( + content=( + { + "success": True, + "message": "Successfully deleted export.", + } + ), + status_code=200, + ) + + +@router.get( + "/exports/{export_id}", + response_model=ExportModel, + dependencies=[Depends(allow_any_authenticated())], + summary="Get a single export", + description="""Gets a specific export by ID. The user must have access to the camera + associated with the export.""", +) +async def get_export(export_id: str, request: Request): + try: + export = Export.get(Export.id == export_id) + await require_camera_access(export.camera, request=request) + return JSONResponse(content=model_to_dict(export)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Export not found"}, + status_code=404, + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/fastapi_app.py b/sam2-cpu/frigate-dev/frigate/api/fastapi_app.py new file mode 100644 index 0000000..48c97df --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/fastapi_app.py @@ -0,0 +1,163 @@ +import logging +import re +from typing import Optional + +from fastapi import Depends, FastAPI, Request +from fastapi.responses import JSONResponse +from joserfc.jwk import OctKey +from playhouse.sqliteq import SqliteQueueDatabase +from slowapi import _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from starlette_context import middleware, plugins +from starlette_context.plugins import Plugin + +from frigate.api import app as main_app +from frigate.api import ( + auth, + camera, + classification, + event, + export, + media, + notification, + preview, + review, +) +from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, +) +from frigate.config import FrigateConfig +from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.embeddings import EmbeddingsContext +from frigate.ptz.onvif import OnvifController +from frigate.stats.emitter import StatsEmitter +from frigate.storage import StorageMaintainer + +logger = logging.getLogger(__name__) + + +def check_csrf(request: Request) -> bool: + if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]: + return True + if "origin" in request.headers and "x-csrf-token" not in request.headers: + return False + + return True + + +# Used to retrieve the remote-user header: https://starlette-context.readthedocs.io/en/latest/plugins.html#easy-mode +class RemoteUserPlugin(Plugin): + key = "Remote-User" + + +def create_fastapi_app( + frigate_config: FrigateConfig, + database: SqliteQueueDatabase, + embeddings: Optional[EmbeddingsContext], + detected_frames_processor, + storage_maintainer: StorageMaintainer, + onvif: OnvifController, + stats_emitter: StatsEmitter, + event_metadata_updater: EventMetadataPublisher, + config_publisher: CameraConfigUpdatePublisher, + enforce_default_admin: bool = True, +): + logger.info("Starting FastAPI app") + app = FastAPI( + debug=False, + swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"}, + dependencies=[Depends(require_admin_by_default())] + if enforce_default_admin + else [], + ) + + # update the request_address with the x-forwarded-for header from nginx + # https://starlette-context.readthedocs.io/en/latest/plugins.html#forwarded-for + app.add_middleware( + middleware.ContextMiddleware, + plugins=(plugins.ForwardedForPlugin(),), + ) + + # Middleware to connect to DB before and close connection after request + # https://github.com/fastapi/full-stack-fastapi-template/issues/224#issuecomment-737423886 + # https://fastapi.tiangolo.com/tutorial/middleware/#before-and-after-the-response + @app.middleware("http") + async def frigate_middleware(request: Request, call_next): + # Before request + if not check_csrf(request): + return JSONResponse( + content={"success": False, "message": "Missing CSRF header"}, + status_code=401, + ) + + if database.is_closed(): + database.connect() + + response = await call_next(request) + + # After request https://stackoverflow.com/a/75487519 + if not database.is_closed(): + database.close() + return response + + @app.on_event("startup") + async def startup(): + logger.info("FastAPI started") + + # Rate limiter (used for login endpoint) + if frigate_config.auth.failed_login_rate_limit is None: + limiter.enabled = False + else: + auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit) + + app.state.limiter = limiter + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + app.add_middleware(SlowAPIMiddleware) + + # Routes + # Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters + app.include_router(auth.router) + app.include_router(camera.router) + app.include_router(classification.router) + app.include_router(review.router) + app.include_router(main_app.router) + app.include_router(preview.router) + app.include_router(notification.router) + app.include_router(export.router) + app.include_router(event.router) + app.include_router(media.router) + # App Properties + app.frigate_config = frigate_config + app.embeddings = embeddings + app.detected_frames_processor = detected_frames_processor + app.storage_maintainer = storage_maintainer + app.camera_error_image = None + app.onvif = onvif + app.stats_emitter = stats_emitter + app.event_metadata_updater = event_metadata_updater + app.config_publisher = config_publisher + + if frigate_config.auth.enabled: + secret = get_jwt_secret() + key_bytes = None + if isinstance(secret, str): + # If the secret looks like hex (e.g., generated by secrets.token_hex), use raw bytes + if len(secret) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", secret or ""): + try: + key_bytes = bytes.fromhex(secret) + except ValueError: + key_bytes = secret.encode("utf-8") + else: + key_bytes = secret.encode("utf-8") + elif isinstance(secret, (bytes, bytearray)): + key_bytes = bytes(secret) + else: + key_bytes = str(secret).encode("utf-8") + + app.jwt_token = OctKey.import_key(key_bytes) + else: + app.jwt_token = None + + return app diff --git a/sam2-cpu/frigate-dev/frigate/api/media.py b/sam2-cpu/frigate-dev/frigate/api/media.py new file mode 100644 index 0000000..783b42e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/media.py @@ -0,0 +1,1977 @@ +"""Image and video apis.""" + +import asyncio +import glob +import logging +import math +import os +import subprocess as sp +import time +from datetime import datetime, timedelta, timezone +from functools import reduce +from pathlib import Path as FilePath +from typing import Any, List +from urllib.parse import unquote + +import cv2 +import numpy as np +import pytz +from fastapi import APIRouter, Depends, Path, Query, Request, Response +from fastapi.responses import FileResponse, JSONResponse, StreamingResponse +from pathvalidate import sanitize_filename +from peewee import DoesNotExist, fn, operator +from tzlocal import get_localzone_name + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) +from frigate.api.defs.query.media_query_parameters import ( + Extension, + MediaEventsSnapshotQueryParams, + MediaLatestFrameQueryParams, + MediaMjpegFeedQueryParams, + MediaRecordingsAvailabilityQueryParams, + MediaRecordingsSummaryQueryParams, +) +from frigate.api.defs.tags import Tags +from frigate.camera.state import CameraState +from frigate.config import FrigateConfig +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + INSTALL_DIR, + MAX_SEGMENT_DURATION, + PREVIEW_FRAME_TYPE, + RECORD_DIR, +) +from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment +from frigate.track.object_processing import TrackedObjectProcessor +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import get_image_from_recording +from frigate.util.time import get_dst_transitions + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.media]) + + +@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)]) +async def mjpeg_feed( + request: Request, + camera_name: str, + params: MediaMjpegFeedQueryParams = Depends(), +): + draw_options = { + "bounding_boxes": params.bbox, + "timestamp": params.timestamp, + "zones": params.zones, + "mask": params.mask, + "motion_boxes": params.motion, + "regions": params.regions, + } + if camera_name in request.app.frigate_config.cameras: + # return a multipart response + return StreamingResponse( + imagestream( + request.app.detected_frames_processor, + camera_name, + params.fps, + params.height, + draw_options, + ), + media_type="multipart/x-mixed-replace;boundary=frame", + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +def imagestream( + detected_frames_processor: TrackedObjectProcessor, + camera_name: str, + fps: int, + height: int, + draw_options: dict[str, Any], +): + while True: + # max out at specified FPS + time.sleep(1 / fps) + frame = detected_frames_processor.get_current_frame(camera_name, draw_options) + if frame is None: + frame = np.zeros((height, int(height * 16 / 9), 3), np.uint8) + + width = int(height * frame.shape[1] / frame.shape[0]) + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR) + + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n\r\n" + bytearray(jpg.tobytes()) + b"\r\n\r\n" + ) + + +@router.get("/{camera_name}/ptz/info", dependencies=[Depends(require_camera_access)]) +async def camera_ptz_info(request: Request, camera_name: str): + if camera_name in request.app.frigate_config.cameras: + # Schedule get_camera_info in the OnvifController's event loop + future = asyncio.run_coroutine_threadsafe( + request.app.onvif.get_camera_info(camera_name), request.app.onvif.loop + ) + result = future.result() + return JSONResponse(content=result) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.get( + "/{camera_name}/latest.{extension}", dependencies=[Depends(require_camera_access)] +) +async def latest_frame( + request: Request, + camera_name: str, + extension: Extension, + params: MediaLatestFrameQueryParams = Depends(), +): + frame_processor: TrackedObjectProcessor = request.app.detected_frames_processor + draw_options = { + "bounding_boxes": params.bbox, + "timestamp": params.timestamp, + "zones": params.zones, + "mask": params.mask, + "motion_boxes": params.motion, + "paths": params.paths, + "regions": params.regions, + } + quality = params.quality + + if extension == Extension.png: + quality_params = None + elif extension == Extension.webp: + quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality] + else: # jpg or jpeg + quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality] + + if camera_name in request.app.frigate_config.cameras: + frame = frame_processor.get_current_frame(camera_name, draw_options) + retry_interval = float( + request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval + or 10 + ) + + if frame is None or datetime.now().timestamp() > ( + frame_processor.get_current_frame_time(camera_name) + retry_interval + ): + if request.app.camera_error_image is None: + error_image = glob.glob( + os.path.join(INSTALL_DIR, "frigate/images/camera-error.jpg") + ) + + if len(error_image) > 0: + request.app.camera_error_image = cv2.imread( + error_image[0], cv2.IMREAD_UNCHANGED + ) + + frame = request.app.camera_error_image + + height = int(params.height or str(frame.shape[0])) + width = int(height * frame.shape[1] / frame.shape[0]) + + if frame is None: + return JSONResponse( + content={"success": False, "message": "Unable to get valid frame"}, + status_code=500, + ) + + if height < 1 or width < 1: + return JSONResponse( + content="Invalid height / width requested :: {} / {}".format( + height, width + ), + status_code=400, + ) + + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) + return Response( + content=img.tobytes(), + media_type=extension.get_mime_type(), + headers={ + "Cache-Control": "no-store" + if not params.store + else "private, max-age=60", + }, + ) + elif ( + camera_name == "birdseye" + and request.app.frigate_config.birdseye.enabled + and request.app.frigate_config.birdseye.restream + ): + frame = cv2.cvtColor( + frame_processor.get_current_frame(camera_name), + cv2.COLOR_YUV2BGR_I420, + ) + + height = int(params.height or str(frame.shape[0])) + width = int(height * frame.shape[1] / frame.shape[0]) + + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) + return Response( + content=img.tobytes(), + media_type=extension.get_mime_type(), + headers={ + "Cache-Control": "no-store" + if not params.store + else "private, max-age=60", + }, + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.get( + "/{camera_name}/recordings/{frame_time}/snapshot.{format}", + dependencies=[Depends(require_camera_access)], +) +async def get_snapshot_from_recording( + request: Request, + camera_name: str, + frame_time: float, + format: str = Path(enum=["png", "jpg"]), + height: int = None, +): + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + recording: Recordings | None = None + + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + ( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except DoesNotExist: + # try again with a rounded frame time as it may be between + # the rounded segment start time + frame_time = math.ceil(frame_time) + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + ( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except DoesNotExist: + pass + + if recording is not None: + time_in_segment = frame_time - recording.start_time + codec = "png" if format == "png" else "mjpeg" + mime_type = "png" if format == "png" else "jpeg" + config: FrigateConfig = request.app.frigate_config + + image_data = get_image_from_recording( + config.ffmpeg, recording.path, time_in_segment, codec, height + ) + + if not image_data: + return JSONResponse( + content=( + { + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + } + ), + status_code=404, + ) + return Response(image_data, headers={"Content-Type": f"image/{mime_type}"}) + else: + return JSONResponse( + content={ + "success": False, + "message": "Recording not found at {}".format(frame_time), + }, + status_code=404, + ) + + +@router.post( + "/{camera_name}/plus/{frame_time}", dependencies=[Depends(require_camera_access)] +) +async def submit_recording_snapshot_to_plus( + request: Request, camera_name: str, frame_time: str +): + if camera_name not in request.app.frigate_config.cameras: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + frame_time = float(frame_time) + recording_query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + ( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + ) + + try: + config: FrigateConfig = request.app.frigate_config + recording: Recordings = recording_query.get() + time_in_segment = frame_time - recording.start_time + image_data = get_image_from_recording( + config.ffmpeg, recording.path, time_in_segment, "png" + ) + + if not image_data: + return JSONResponse( + content={ + "success": False, + "message": f"Unable to parse frame at time {frame_time}", + }, + status_code=404, + ) + + nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) + request.app.frigate_config.plus_api.upload_image(nd, camera_name) + + return JSONResponse( + content={ + "success": True, + "message": "Successfully submitted image.", + }, + status_code=200, + ) + except DoesNotExist: + return JSONResponse( + content={ + "success": False, + "message": "Recording not found at {}".format(frame_time), + }, + status_code=404, + ) + + +@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())]) +def get_recordings_storage_usage(request: Request): + recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ + "storage" + ][RECORD_DIR] + + if not recording_stats: + return JSONResponse({}) + + total_mb = recording_stats["total"] + + camera_usages: dict[str, dict] = ( + request.app.storage_maintainer.calculate_camera_usages() + ) + + for camera_name in camera_usages.keys(): + if camera_usages.get(camera_name, {}).get("usage"): + camera_usages[camera_name]["usage_percent"] = ( + camera_usages.get(camera_name, {}).get("usage", 0) / total_mb + ) * 100 + + return JSONResponse(content=camera_usages) + + +@router.get("/recordings/summary", dependencies=[Depends(allow_any_authenticated())]) +def all_recordings_summary( + request: Request, + params: MediaRecordingsSummaryQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + """Returns true/false by day indicating if recordings exist""" + + cameras = params.cameras + if cameras != "all": + requested = set(unquote(cameras).split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + + time_range_query = ( + Recordings.select( + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), + ) + .where(Recordings.camera << camera_list) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + if min_time is None or max_time is None: + return JSONResponse(content={}) + + dst_periods = get_dst_transitions(params.timezone, min_time, max_time) + + days: dict[str, bool] = {} + + for period_start, period_end, period_offset in dst_periods: + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + period_query = ( + Recordings.select( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day") + ) + .where( + (Recordings.camera << camera_list) + & (Recordings.end_time >= period_start) + & (Recordings.start_time <= period_end) + ) + .group_by( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ) + ) + .order_by(Recordings.start_time.desc()) + .namedtuples() + ) + + for g in period_query: + days[g.day] = True + + return JSONResponse(content=dict(sorted(days.items()))) + + +@router.get( + "/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)] +) +async def recordings_summary(camera_name: str, timezone: str = "utc"): + """Returns hourly summary for recordings of given camera""" + + time_range_query = ( + Recordings.select( + fn.MIN(Recordings.start_time).alias("min_time"), + fn.MAX(Recordings.start_time).alias("max_time"), + ) + .where(Recordings.camera == camera_name) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + days: dict[str, dict] = {} + + if min_time is None or max_time is None: + return JSONResponse(content=list(days.values())) + + dst_periods = get_dst_transitions(timezone, min_time, max_time) + + for period_start, period_end, period_offset in dst_periods: + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + recording_groups = ( + Recordings.select( + fn.strftime( + "%Y-%m-%d %H", + fn.datetime( + Recordings.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("hour"), + fn.SUM(Recordings.duration).alias("duration"), + fn.SUM(Recordings.motion).alias("motion"), + fn.SUM(Recordings.objects).alias("objects"), + ) + .where( + (Recordings.camera == camera_name) + & (Recordings.end_time >= period_start) + & (Recordings.start_time <= period_end) + ) + .group_by((Recordings.start_time + period_offset).cast("int") / 3600) + .order_by(Recordings.start_time.desc()) + .namedtuples() + ) + + event_groups = ( + Event.select( + fn.strftime( + "%Y-%m-%d %H", + fn.datetime( + Event.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("hour"), + fn.COUNT(Event.id).alias("count"), + ) + .where(Event.camera == camera_name, Event.has_clip) + .where( + (Event.start_time >= period_start) & (Event.start_time <= period_end) + ) + .group_by((Event.start_time + period_offset).cast("int") / 3600) + .namedtuples() + ) + + event_map = {g.hour: g.count for g in event_groups} + + for recording_group in recording_groups: + parts = recording_group.hour.split() + hour = parts[1] + day = parts[0] + events_count = event_map.get(recording_group.hour, 0) + hour_data = { + "hour": hour, + "events": events_count, + "motion": recording_group.motion, + "objects": recording_group.objects, + "duration": round(recording_group.duration), + } + if day in days: + # merge counts if already present (edge-case at DST boundary) + days[day]["events"] += events_count or 0 + days[day]["hours"].append(hour_data) + else: + days[day] = { + "events": events_count or 0, + "hours": [hour_data], + "day": day, + } + + return JSONResponse(content=list(days.values())) + + +@router.get("/{camera_name}/recordings", dependencies=[Depends(require_camera_access)]) +async def recordings( + camera_name: str, + after: float = (datetime.now() - timedelta(hours=1)).timestamp(), + before: float = datetime.now().timestamp(), +): + """Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used""" + recordings = ( + Recordings.select( + Recordings.id, + Recordings.start_time, + Recordings.end_time, + Recordings.segment_size, + Recordings.motion, + Recordings.objects, + Recordings.duration, + ) + .where( + Recordings.camera == camera_name, + Recordings.end_time >= after, + Recordings.start_time <= before, + ) + .order_by(Recordings.start_time) + .dicts() + .iterator() + ) + + return JSONResponse(content=list(recordings)) + + +@router.get( + "/recordings/unavailable", + response_model=list[dict], + dependencies=[Depends(allow_any_authenticated())], +) +async def no_recordings( + request: Request, + params: MediaRecordingsAvailabilityQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + """Get time ranges with no recordings.""" + cameras = params.cameras + if cameras != "all": + requested = set(unquote(cameras).split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + cameras = ",".join(filtered) + else: + cameras = allowed_cameras + + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp() + ) + scale = params.scale + + clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)] + if cameras != "all": + camera_list = cameras.split(",") + clauses.append((Recordings.camera << camera_list)) + else: + camera_list = allowed_cameras + + # Get recording start times + data: list[Recordings] = ( + Recordings.select(Recordings.start_time, Recordings.end_time) + .where(reduce(operator.and_, clauses)) + .order_by(Recordings.start_time.asc()) + .dicts() + .iterator() + ) + + # Convert recordings to list of (start, end) tuples + recordings = [(r["start_time"], r["end_time"]) for r in data] + + # Iterate through time segments and check if each has any recording + no_recording_segments = [] + current = after + current_gap_start = None + + while current < before: + segment_end = min(current + scale, before) + + # Check if this segment overlaps with any recording + has_recording = any( + rec_start < segment_end and rec_end > current + for rec_start, rec_end in recordings + ) + + if not has_recording: + # This segment has no recordings + if current_gap_start is None: + current_gap_start = current # Start a new gap + else: + # This segment has recordings + if current_gap_start is not None: + # End the current gap and append it + no_recording_segments.append( + {"start_time": int(current_gap_start), "end_time": int(current)} + ) + current_gap_start = None + + current = segment_end + + # Append the last gap if it exists + if current_gap_start is not None: + no_recording_segments.append( + {"start_time": int(current_gap_start), "end_time": int(before)} + ) + + return JSONResponse(content=no_recording_segments) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4", + dependencies=[Depends(require_camera_access)], + description="For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files.", +) +async def recording_clip( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, +): + def run_download(ffmpeg_cmd: list[str], file_path: str): + with sp.Popen( + ffmpeg_cmd, + stderr=sp.PIPE, + stdout=sp.PIPE, + text=False, + ) as ffmpeg: + while True: + data = ffmpeg.stdout.read(8192) + if data is not None and len(data) > 0: + yield data + else: + if ffmpeg.returncode and ffmpeg.returncode != 0: + logger.error( + f"Failed to generate clip, ffmpeg logs: {ffmpeg.stderr.read()}" + ) + else: + FilePath(file_path).unlink(missing_ok=True) + break + + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + (Recordings.start_time.between(start_ts, end_ts)) + | (Recordings.end_time.between(start_ts, end_ts)) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if recordings.count() == 0: + return JSONResponse( + content={ + "success": False, + "message": "No recordings found for the specified time range", + }, + status_code=400, + ) + + file_name = sanitize_filename(f"playlist_{camera_name}_{start_ts}-{end_ts}.txt") + file_path = os.path.join(CACHE_DIR, file_name) + with open(file_path, "w") as file: + clip: Recordings + for clip in recordings: + file.write(f"file '{clip.path}'\n") + + # if this is the starting clip, add an inpoint + if clip.start_time < start_ts: + file.write(f"inpoint {int(start_ts - clip.start_time)}\n") + + # if this is the ending clip, add an outpoint + if clip.end_time > end_ts: + file.write(f"outpoint {int(end_ts - clip.start_time)}\n") + + if len(file_name) > 1000: + return JSONResponse( + content={ + "success": False, + "message": "Filename exceeded max length of 1000", + }, + status_code=403, + ) + + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-protocol_whitelist", + "pipe,file", + "-f", + "concat", + "-safe", + "0", + "-i", + file_path, + "-c", + "copy", + "-movflags", + "frag_keyframe+empty_moov", + "-f", + "mp4", + "pipe:", + ] + + return StreamingResponse( + run_download(ffmpeg_cmd, file_path), + media_type="video/mp4", + ) + + +@router.get( + "/vod/{camera_name}/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_ts( + camera_name: str, + start_ts: float, + end_ts: float, + force_discontinuity: bool = False, +): + logger.debug( + "VOD: Generating VOD for %s from %s to %s with force_discontinuity=%s", + camera_name, + start_ts, + end_ts, + force_discontinuity, + ) + recordings = ( + Recordings.select( + Recordings.path, + Recordings.duration, + Recordings.end_time, + Recordings.start_time, + ) + .where( + Recordings.start_time.between(start_ts, end_ts) + | Recordings.end_time.between(start_ts, end_ts) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + .iterator() + ) + + clips = [] + durations = [] + min_duration_ms = 100 # Minimum 100ms to ensure at least one video frame + max_duration_ms = MAX_SEGMENT_DURATION * 1000 + + recording: Recordings + for recording in recordings: + logger.debug( + "VOD: processing recording: %s start=%s end=%s duration=%s", + recording.path, + recording.start_time, + recording.end_time, + recording.duration, + ) + + clip = {"type": "source", "path": recording.path} + duration = int(recording.duration * 1000) + + # adjust start offset if start_ts is after recording.start_time + if start_ts > recording.start_time: + inpoint = int((start_ts - recording.start_time) * 1000) + clip["clipFrom"] = inpoint + duration -= inpoint + logger.debug( + "VOD: applied clipFrom %sms to %s", + inpoint, + recording.path, + ) + + # adjust end if recording.end_time is after end_ts + if recording.end_time > end_ts: + duration -= int((recording.end_time - end_ts) * 1000) + + if duration < min_duration_ms: + # skip if the clip has no valid duration (too short to contain frames) + logger.debug( + "VOD: skipping recording %s - resulting duration %sms too short", + recording.path, + duration, + ) + continue + + if min_duration_ms <= duration < max_duration_ms: + clip["keyFrameDurations"] = [duration] + clips.append(clip) + durations.append(duration) + logger.debug( + "VOD: added clip %s duration_ms=%s clipFrom=%s", + recording.path, + duration, + clip.get("clipFrom"), + ) + else: + logger.warning(f"Recording clip is missing or empty: {recording.path}") + + if not clips: + logger.error( + f"No recordings found for {camera_name} during the requested time range" + ) + return JSONResponse( + content={ + "success": False, + "message": "No recordings found.", + }, + status_code=404, + ) + + hour_ago = datetime.now() - timedelta(hours=1) + return JSONResponse( + content={ + "cache": hour_ago.timestamp() > start_ts, + "discontinuity": force_discontinuity, + "consistentSequenceMediaInfo": True, + "durations": durations, + "segment_duration": max(durations), + "sequences": [{"clips": clips}], + } + ) + + +@router.get( + "/vod/{year_month}/{day}/{hour}/{camera_name}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified date-time on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_hour_no_timezone(year_month: str, day: int, hour: int, camera_name: str): + """VOD for specific hour. Uses the default timezone (UTC).""" + return await vod_hour( + year_month, day, hour, camera_name, get_localzone_name().replace("/", ",") + ) + + +@router.get( + "/vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for the specified date-time (with timezone) on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_hour( + year_month: str, day: int, hour: int, camera_name: str, tz_name: str +): + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=timezone.utc) + - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() + end_ts = end_date.timestamp() + + return await vod_ts(camera_name, start_ts, end_ts) + + +@router.get( + "/vod/event/{event_id}", + dependencies=[Depends(allow_any_authenticated())], + description="Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_event( + request: Request, + event_id: str, + padding: int = Query(0, description="Padding to apply to the vod."), +): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + logger.error(f"Event not found: {event_id}") + return JSONResponse( + content={ + "success": False, + "message": "Event not found.", + }, + status_code=404, + ) + + await require_camera_access(event.camera, request=request) + + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else (event.end_time + padding) + ) + vod_response = await vod_ts(event.camera, event.start_time - padding, end_ts) + + # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false + if ( + event.start_time < datetime.now().timestamp() - 300 + and type(vod_response) is tuple + and len(vod_response) == 2 + and vod_response[1] == 404 + ): + Event.update(has_clip=False).where(Event.id == event_id).execute() + + return vod_response + + +@router.get( + "/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_camera_access)], + description="Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback.", +) +async def vod_clip( + camera_name: str, + start_ts: float, + end_ts: float, +): + return await vod_ts(camera_name, start_ts, end_ts, force_discontinuity=True) + + +@router.get( + "/events/{event_id}/snapshot.jpg", + description="Returns a snapshot image for the specified object id. NOTE: The query params only take affect while the event is in-progress. Once the event has ended the snapshot configuration is used.", +) +async def event_snapshot( + request: Request, + event_id: str, + params: MediaEventsSnapshotQueryParams = Depends(), +): + event_complete = False + jpg_bytes = None + try: + event = Event.get(Event.id == event_id, Event.end_time != None) + event_complete = True + await require_camera_access(event.camera, request=request) + if not event.has_snapshot: + return JSONResponse( + content={"success": False, "message": "Snapshot not available"}, + status_code=404, + ) + # read snapshot from disk + with open( + os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg"), "rb" + ) as image_file: + jpg_bytes = image_file.read() + except DoesNotExist: + # see if the object is currently being tracked + try: + camera_states: list[CameraState] = ( + request.app.detected_frames_processor.camera_states.values() + ) + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + jpg_bytes = tracked_obj.get_img_bytes( + ext="jpg", + timestamp=params.timestamp, + bounding_box=params.bbox, + crop=params.crop, + height=params.height, + quality=params.quality, + ) + await require_camera_access(camera_state.name, request=request) + except Exception: + return JSONResponse( + content={"success": False, "message": "Ongoing event not found"}, + status_code=404, + ) + except Exception: + return JSONResponse( + content={"success": False, "message": "Unknown error occurred"}, + status_code=404, + ) + + if jpg_bytes is None: + return JSONResponse( + content={"success": False, "message": "Live frame not available"}, + status_code=404, + ) + + headers = { + "Content-Type": "image/jpeg", + "Cache-Control": "private, max-age=31536000" if event_complete else "no-store", + } + + if params.download: + headers["Content-Disposition"] = f"attachment; filename=snapshot-{event_id}.jpg" + + return Response( + jpg_bytes, + media_type="image/jpeg", + headers=headers, + ) + + +@router.get( + "/events/{event_id}/thumbnail.{extension}", + dependencies=[Depends(require_camera_access)], +) +async def event_thumbnail( + request: Request, + event_id: str, + extension: Extension, + max_cache_age: int = Query( + 2592000, description="Max cache age in seconds. Default 30 days in seconds." + ), + format: str = Query(default="ios", enum=["ios", "android"]), +): + thumbnail_bytes = None + event_complete = False + try: + event: Event = Event.get(Event.id == event_id) + await require_camera_access(event.camera, request=request) + if event.end_time is not None: + event_complete = True + + thumbnail_bytes = get_event_thumbnail_bytes(event) + except DoesNotExist: + thumbnail_bytes = None + + if thumbnail_bytes is None: + # see if the object is currently being tracked + try: + camera_states = request.app.detected_frames_processor.camera_states.values() + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + thumbnail_bytes = tracked_obj.get_thumbnail(extension.value) + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + + if thumbnail_bytes is None: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + + # android notifications prefer a 2:1 ratio + if format == "android": + img_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8) + img = cv2.imdecode(img_as_np, flags=1) + thumbnail = cv2.copyMakeBorder( + img, + 0, + 0, + int(img.shape[1] * 0.5), + int(img.shape[1] * 0.5), + cv2.BORDER_CONSTANT, + (0, 0, 0), + ) + + quality_params = None + if extension in (Extension.jpg, Extension.jpeg): + quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), 70] + elif extension == Extension.webp: + quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60] + + _, img = cv2.imencode(f".{extension.value}", thumbnail, quality_params) + thumbnail_bytes = img.tobytes() + + return Response( + thumbnail_bytes, + media_type=extension.get_mime_type(), + headers={ + "Cache-Control": f"private, max-age={max_cache_age}" + if event_complete + else "no-store", + }, + ) + + +@router.get("/{camera_name}/grid.jpg", dependencies=[Depends(require_camera_access)]) +def grid_snapshot( + request: Request, camera_name: str, color: str = "green", font_scale: float = 0.5 +): + if camera_name in request.app.frigate_config.cameras: + detect = request.app.frigate_config.cameras[camera_name].detect + frame_processor: TrackedObjectProcessor = request.app.detected_frames_processor + frame = frame_processor.get_current_frame(camera_name, {}) + retry_interval = float( + request.app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval + or 10 + ) + + if frame is None or datetime.now().timestamp() > ( + frame_processor.get_current_frame_time(camera_name) + retry_interval + ): + return JSONResponse( + content={"success": False, "message": "Unable to get valid frame"}, + status_code=500, + ) + + try: + grid = ( + Regions.select(Regions.grid) + .where(Regions.camera == camera_name) + .get() + .grid + ) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Unable to get region grid"}, + status_code=500, + ) + + color_arg = color.lower() + + if color_arg == "red": + draw_color = (0, 0, 255) + elif color_arg == "blue": + draw_color = (255, 0, 0) + elif color_arg == "black": + draw_color = (0, 0, 0) + elif color_arg == "white": + draw_color = (255, 255, 255) + else: + draw_color = (0, 255, 0) # green + + grid_size = len(grid) + grid_coef = 1.0 / grid_size + width = detect.width + height = detect.height + for x in range(grid_size): + for y in range(grid_size): + cell = grid[x][y] + + if len(cell["sizes"]) == 0: + continue + + std_dev = round(cell["std_dev"] * width, 2) + mean = round(cell["mean"] * width, 2) + cv2.rectangle( + frame, + (int(x * grid_coef * width), int(y * grid_coef * height)), + ( + int((x + 1) * grid_coef * width), + int((y + 1) * grid_coef * height), + ), + draw_color, + 2, + ) + cv2.putText( + frame, + f"#: {len(cell['sizes'])}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.02) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + cv2.putText( + frame, + f"std: {std_dev}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.05) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + cv2.putText( + frame, + f"avg: {mean}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.08) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=draw_color, + thickness=2, + ) + + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + else: + return JSONResponse( + content={"success": False, "message": "Camera not found"}, + status_code=404, + ) + + +@router.get( + "/events/{event_id}/snapshot-clean.webp", + dependencies=[Depends(require_camera_access)], +) +def event_snapshot_clean(request: Request, event_id: str, download: bool = False): + webp_bytes = None + try: + event = Event.get(Event.id == event_id) + snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots + if not (snapshot_config.enabled and event.has_snapshot): + return JSONResponse( + content={ + "success": False, + "message": "Snapshots and clean_copy must be enabled in the config", + }, + status_code=404, + ) + if event.end_time is None: + # see if the object is currently being tracked + try: + camera_states = ( + request.app.detected_frames_processor.camera_states.values() + ) + for camera_state in camera_states: + if event_id in camera_state.tracked_objects: + tracked_obj = camera_state.tracked_objects.get(event_id) + if tracked_obj is not None: + webp_bytes = tracked_obj.get_clean_webp() + break + except Exception: + return JSONResponse( + content={"success": False, "message": "Event not found"}, + status_code=404, + ) + elif not event.has_snapshot: + return JSONResponse( + content={"success": False, "message": "Snapshot not available"}, + status_code=404, + ) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + if webp_bytes is None: + try: + # webp + clean_snapshot_path_webp = os.path.join( + CLIPS_DIR, f"{event.camera}-{event.id}-clean.webp" + ) + # png (legacy) + clean_snapshot_path_png = os.path.join( + CLIPS_DIR, f"{event.camera}-{event.id}-clean.png" + ) + + if os.path.exists(clean_snapshot_path_webp): + with open(clean_snapshot_path_webp, "rb") as image_file: + webp_bytes = image_file.read() + elif os.path.exists(clean_snapshot_path_png): + # convert png to webp and save for future use + png_image = cv2.imread(clean_snapshot_path_png, cv2.IMREAD_UNCHANGED) + if png_image is None: + return JSONResponse( + content={ + "success": False, + "message": "Invalid png snapshot", + }, + status_code=400, + ) + + ret, webp_data = cv2.imencode( + ".webp", png_image, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + if not ret: + return JSONResponse( + content={ + "success": False, + "message": "Unable to convert png to webp", + }, + status_code=400, + ) + + webp_bytes = webp_data.tobytes() + + # save the converted webp for future requests + try: + with open(clean_snapshot_path_webp, "wb") as f: + f.write(webp_bytes) + except Exception as e: + logger.warning( + f"Failed to save converted webp for event {event.id}: {e}" + ) + # continue since we now have the data to return + else: + return JSONResponse( + content={ + "success": False, + "message": "Clean snapshot not available", + }, + status_code=404, + ) + except Exception: + logger.error(f"Unable to load clean snapshot for event: {event.id}") + return JSONResponse( + content={ + "success": False, + "message": "Unable to load clean snapshot for event", + }, + status_code=400, + ) + + headers = { + "Content-Type": "image/webp", + "Cache-Control": "private, max-age=31536000", + } + + if download: + headers["Content-Disposition"] = ( + f"attachment; filename=snapshot-{event_id}-clean.webp" + ) + + return Response( + webp_bytes, + media_type="image/webp", + headers=headers, + ) + + +@router.get( + "/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)] +) +async def event_clip( + request: Request, + event_id: str, + padding: int = Query(0, description="Padding to apply to clip."), +): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + if not event.has_clip: + return JSONResponse( + content={"success": False, "message": "Clip not available"}, status_code=404 + ) + + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else event.end_time + padding + ) + return await recording_clip( + request, event.camera, event.start_time - padding, end_ts + ) + + +@router.get( + "/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)] +) +def event_preview(request: Request, event_id: str): + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + start_ts = event.start_time + end_ts = start_ts + ( + min(event.end_time - event.start_time, 20) if event.end_time else 20 + ) + return preview_gif(request, event.camera, start_ts, end_ts) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif", + dependencies=[Depends(require_camera_access)], +) +def preview_gif( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, + max_cache_age: int = Query( + 2592000, description="Max cache age in seconds. Default 30 days in seconds." + ), +): + if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0): + # has preview mp4 + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .limit(1) + .get() + ) + + if not preview: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + diff = start_ts - preview.start_time + minutes = int(diff / 60) + seconds = int(diff % 60) + config: FrigateConfig = request.app.frigate_config + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-ss", + f"00:{minutes}:{seconds}", + "-t", + f"{end_ts - start_ts}", + "-i", + preview.path, + "-r", + "8", + "-vf", + "setpts=0.12*PTS", + "-loop", + "0", + "-c:v", + "gif", + "-f", + "gif", + "-", + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + gif_bytes = process.stdout + else: + # need to generate from existing images + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera_name}" + start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}" + selected_previews = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(f"file '{os.path.join(preview_dir, file)}'") + selected_previews.append("duration 0.12") + + if not selected_previews: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + last_file = selected_previews[-2] + selected_previews.append(last_file) + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-f", + "concat", + "-y", + "-protocol_whitelist", + "pipe,file", + "-safe", + "0", + "-i", + "/dev/stdin", + "-loop", + "0", + "-c:v", + "gif", + "-f", + "gif", + "-", + ] + + process = sp.run( + ffmpeg_cmd, + input=str.encode("\n".join(selected_previews)), + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + gif_bytes = process.stdout + + return Response( + gif_bytes, + media_type="image/gif", + headers={ + "Cache-Control": f"private, max-age={max_cache_age}", + "Content-Type": "image/gif", + }, + ) + + +@router.get( + "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4", + dependencies=[Depends(require_camera_access)], +) +def preview_mp4( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, + max_cache_age: int = Query( + 604800, description="Max cache age in seconds. Default 7 days in seconds." + ), +): + file_name = sanitize_filename(f"preview_{camera_name}_{start_ts}-{end_ts}.mp4") + + if len(file_name) > 1000: + return JSONResponse( + content=( + { + "success": False, + "message": "Filename exceeded max length of 1000 characters.", + } + ), + status_code=403, + ) + + path = os.path.join(CACHE_DIR, file_name) + + if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0): + # has preview mp4 + try: + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera == camera_name) + .limit(1) + .get() + ) + except DoesNotExist: + preview = None + + if not preview: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + diff = start_ts - preview.start_time + minutes = int(diff / 60) + seconds = int(diff % 60) + config: FrigateConfig = request.app.frigate_config + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-y", + "-ss", + f"00:{minutes}:{seconds}", + "-t", + f"{end_ts - start_ts}", + "-i", + preview.path, + "-r", + "8", + "-vf", + "setpts=0.12*PTS", + "-c:v", + "libx264", + "-movflags", + "+faststart", + path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + else: + # need to generate from existing images + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera_name}" + start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}" + selected_previews = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(f"file '{os.path.join(preview_dir, file)}'") + selected_previews.append("duration 0.12") + + if not selected_previews: + return JSONResponse( + content={"success": False, "message": "Preview not found"}, + status_code=404, + ) + + last_file = selected_previews[-2] + selected_previews.append(last_file) + config: FrigateConfig = request.app.frigate_config + + ffmpeg_cmd = [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-f", + "concat", + "-y", + "-protocol_whitelist", + "pipe,file", + "-safe", + "0", + "-i", + "/dev/stdin", + "-c:v", + "libx264", + "-movflags", + "+faststart", + path, + ] + + process = sp.run( + ffmpeg_cmd, + input=str.encode("\n".join(selected_previews)), + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return JSONResponse( + content={"success": False, "message": "Unable to create preview gif"}, + status_code=500, + ) + + headers = { + "Content-Description": "File Transfer", + "Cache-Control": f"private, max-age={max_cache_age}", + "Content-Type": "video/mp4", + "Content-Length": str(os.path.getsize(path)), + # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers + "X-Accel-Redirect": f"/cache/{file_name}", + } + + return FileResponse( + path, + media_type="video/mp4", + filename=file_name, + headers=headers, + ) + + +@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)]) +def review_preview( + request: Request, + event_id: str, + format: str = Query(default="gif", enum=["gif", "mp4"]), +): + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == event_id) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Review segment not found"}), + status_code=404, + ) + + padding = 8 + start_ts = review.start_time - padding + end_ts = ( + review.end_time + padding if review.end_time else datetime.now().timestamp() + ) + + if format == "gif": + return preview_gif(request, review.camera, start_ts, end_ts) + else: + return preview_mp4(request, review.camera, start_ts, end_ts) + + +@router.get( + "/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)] +) +@router.get( + "/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)] +) +def preview_thumbnail(file_name: str): + """Get a thumbnail from the cached preview frames.""" + if len(file_name) > 1000: + return JSONResponse( + content=( + {"success": False, "message": "Filename exceeded max length of 1000"} + ), + status_code=403, + ) + + safe_file_name_current = sanitize_filename(file_name) + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + + try: + with open( + os.path.join(preview_dir, safe_file_name_current), "rb" + ) as image_file: + jpg_bytes = image_file.read() + except FileNotFoundError: + return JSONResponse( + content=({"success": False, "message": "Image file not found"}), + status_code=404, + ) + + return Response( + jpg_bytes, + media_type="image/webp", + headers={ + "Content-Type": "image/webp", + "Cache-Control": "private, max-age=31536000", + }, + ) + + +####################### dynamic routes ########################### + + +@router.get( + "/{camera_name}/{label}/best.jpg", dependencies=[Depends(require_camera_access)] +) +@router.get( + "/{camera_name}/{label}/thumbnail.jpg", + dependencies=[Depends(require_camera_access)], +) +async def label_thumbnail(request: Request, camera_name: str, label: str): + label = unquote(label) + event_query = Event.select(fn.MAX(Event.id)).where(Event.camera == camera_name) + if label != "any": + event_query = event_query.where(Event.label == label) + + try: + event_id = event_query.scalar() + + return await event_thumbnail(request, event_id, Extension.jpg, 60) + except DoesNotExist: + frame = np.zeros((175, 175, 3), np.uint8) + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + headers={"Cache-Control": "no-store"}, + ) + + +@router.get( + "/{camera_name}/{label}/clip.mp4", dependencies=[Depends(require_camera_access)] +) +async def label_clip(request: Request, camera_name: str, label: str): + label = unquote(label) + event_query = Event.select(fn.MAX(Event.id)).where( + Event.camera == camera_name, Event.has_clip == True + ) + if label != "any": + event_query = event_query.where(Event.label == label) + + try: + event = event_query.get() + + return await event_clip(request, event.id) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Event not found"}, status_code=404 + ) + + +@router.get( + "/{camera_name}/{label}/snapshot.jpg", dependencies=[Depends(require_camera_access)] +) +async def label_snapshot(request: Request, camera_name: str, label: str): + """Returns the snapshot image from the latest event for the given camera and label combo""" + label = unquote(label) + if label == "any": + event_query = ( + Event.select(Event.id) + .where(Event.camera == camera_name) + .where(Event.has_snapshot == True) + .order_by(Event.start_time.desc()) + ) + else: + event_query = ( + Event.select(Event.id) + .where(Event.camera == camera_name) + .where(Event.label == label) + .where(Event.has_snapshot == True) + .order_by(Event.start_time.desc()) + ) + + try: + event: Event = event_query.get() + return await event_snapshot(request, event.id, MediaEventsSnapshotQueryParams()) + except DoesNotExist: + frame = np.zeros((720, 1280, 3), np.uint8) + _, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + + return Response( + jpg.tobytes(), + media_type="image/jpeg", + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/notification.py b/sam2-cpu/frigate-dev/frigate/api/notification.py new file mode 100644 index 0000000..502e76d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/notification.py @@ -0,0 +1,86 @@ +"""Notification apis.""" + +import logging +import os +from typing import Any + +from cryptography.hazmat.primitives import serialization +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse +from peewee import DoesNotExist +from py_vapid import Vapid01, utils + +from frigate.api.auth import allow_any_authenticated +from frigate.api.defs.tags import Tags +from frigate.const import CONFIG_DIR +from frigate.models import User + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.notifications]) + + +@router.get( + "/notifications/pubkey", + dependencies=[Depends(allow_any_authenticated())], + summary="Get VAPID public key", + description="""Gets the VAPID public key for the notifications. + Returns the public key or an error if notifications are not enabled. + """, +) +def get_vapid_pub_key(request: Request): + config = request.app.frigate_config + notifications_enabled = config.notifications.enabled + camera_notifications_enabled = [ + c for c in config.cameras.values() if c.enabled and c.notifications.enabled + ] + if not (notifications_enabled or camera_notifications_enabled): + return JSONResponse( + content=({"success": False, "message": "Notifications are not enabled."}), + status_code=400, + ) + + key = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) + raw_pub = key.public_key.public_bytes( + serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint + ) + return JSONResponse(content=utils.b64urlencode(raw_pub), status_code=200) + + +@router.post( + "/notifications/register", + dependencies=[Depends(allow_any_authenticated())], + summary="Register notifications", + description="""Registers a notifications subscription. + Returns a success message or an error if the subscription is not provided. + """, +) +def register_notifications(request: Request, body: dict = None): + if request.app.frigate_config.auth.enabled: + # FIXME: For FastAPI the remote-user is not being populated + username = request.headers.get("remote-user") or "admin" + else: + username = "admin" + + json: dict[str, Any] = body or {} + sub = json.get("sub") + + if not sub: + return JSONResponse( + content={"success": False, "message": "Subscription must be provided."}, + status_code=400, + ) + + try: + User.update(notification_tokens=User.notification_tokens.append(sub)).where( + User.username == username + ).execute() + return JSONResponse( + content=({"success": True, "message": "Successfully saved token."}), + status_code=200, + ) + except DoesNotExist: + return JSONResponse( + content=({"success": False, "message": "Could not find user."}), + status_code=404, + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/preview.py b/sam2-cpu/frigate-dev/frigate/api/preview.py new file mode 100644 index 0000000..a8fef20 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/preview.py @@ -0,0 +1,168 @@ +"""Preview apis.""" + +import logging +import os +from datetime import datetime, timedelta, timezone + +import pytz +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import JSONResponse + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + require_camera_access, +) +from frigate.api.defs.response.preview_response import ( + PreviewFramesResponse, + PreviewsResponse, +) +from frigate.api.defs.tags import Tags +from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE +from frigate.models import Previews + +logger = logging.getLogger(__name__) + + +router = APIRouter(tags=[Tags.preview]) + + +@router.get( + "/preview/{camera_name}/start/{start_ts}/end/{end_ts}", + response_model=PreviewsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get preview clips for time range", + description="""Gets all preview clips for a specified camera and time range. + Returns a list of preview video clips that overlap with the requested time period, + ordered by start time. Use camera_name='all' to get previews from all cameras. + Returns an error if no previews are found.""", +) +def preview_ts( + camera_name: str, + start_ts: float, + end_ts: float, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get all mp4 previews relevant for time period.""" + if camera_name != "all": + if camera_name not in allowed_cameras: + raise HTTPException(status_code=403, detail="Access denied for camera") + camera_list = [camera_name] + else: + camera_list = allowed_cameras + + if not camera_list: + return JSONResponse( + content={"success": False, "message": "No previews found."}, + status_code=404, + ) + + previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(start_ts, end_ts) + | Previews.end_time.between(start_ts, end_ts) + | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) + ) + .where(Previews.camera << camera_list) + .order_by(Previews.start_time.asc()) + .dicts() + .iterator() + ) + + clips = [] + + preview: Previews + for preview in previews: + clips.append( + { + "camera": preview["camera"], + "src": preview["path"].replace(BASE_DIR, ""), + "type": "video/mp4", + "start": preview["start_time"], + "end": preview["end_time"], + } + ) + + if not clips: + return JSONResponse( + content={ + "success": False, + "message": "No previews found.", + }, + status_code=404, + ) + + return JSONResponse(content=clips, status_code=200) + + +@router.get( + "/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}", + response_model=PreviewsResponse, + dependencies=[Depends(allow_any_authenticated())], + summary="Get preview clips for specific hour", + description="""Gets all preview clips for a specific hour in a given timezone. + Converts the provided date/time from the specified timezone to UTC and retrieves + all preview clips for that hour. Use camera_name='all' to get previews from all cameras. + The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""", +) +def preview_hour( + year_month: str, + day: int, + hour: int, + camera_name: str, + tz_name: str, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), +): + """Get all mp4 previews relevant for time period given the timezone""" + parts = year_month.split("-") + start_date = ( + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc) + - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() + ) + end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) + start_ts = start_date.timestamp() + end_ts = end_date.timestamp() + + return preview_ts(camera_name, start_ts, end_ts, allowed_cameras) + + +@router.get( + "/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames", + response_model=PreviewFramesResponse, + dependencies=[Depends(require_camera_access)], + summary="Get cached preview frame filenames", + description="""Gets a list of cached preview frame filenames for a specific camera and time range. + Returns an array of filenames for preview frames that fall within the specified time period, + sorted in chronological order. These are individual frame images cached for quick preview display.""", +) +def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: float): + """Get list of cached preview frames""" + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera_name}" + start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}" + selected_previews = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + selected_previews.append(file) + + return JSONResponse( + content=selected_previews, + status_code=200, + ) diff --git a/sam2-cpu/frigate-dev/frigate/api/review.py b/sam2-cpu/frigate-dev/frigate/api/review.py new file mode 100644 index 0000000..5f6fbc1 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/api/review.py @@ -0,0 +1,772 @@ +"""Review apis.""" + +import datetime +import logging +from functools import reduce +from pathlib import Path +from typing import List + +import pandas as pd +from fastapi import APIRouter, Request +from fastapi.params import Depends +from fastapi.responses import JSONResponse +from peewee import Case, DoesNotExist, IntegrityError, fn, operator +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import ( + allow_any_authenticated, + get_allowed_cameras_for_filter, + get_current_user, + require_camera_access, + require_role, +) +from frigate.api.defs.query.review_query_parameters import ( + ReviewActivityMotionQueryParams, + ReviewQueryParams, + ReviewSummaryQueryParams, +) +from frigate.api.defs.request.review_body import ReviewModifyMultipleBody +from frigate.api.defs.response.generic_response import GenericResponse +from frigate.api.defs.response.review_response import ( + ReviewActivityMotionResponse, + ReviewSegmentResponse, + ReviewSummaryResponse, +) +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.embeddings import EmbeddingsContext +from frigate.models import Recordings, ReviewSegment, UserReviewStatus +from frigate.review.types import SeverityEnum +from frigate.util.time import get_dst_transitions + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.review]) + + +@router.get( + "/review", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) +async def review( + params: ReviewQueryParams = Depends(), + current_user: dict = Depends(get_current_user), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + cameras = params.cameras + labels = params.labels + zones = params.zones + reviewed = params.reviewed + limit = params.limit + severity = params.severity + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + ) + + clauses = [ + (ReviewSegment.start_time < before) + & ((ReviewSegment.end_time.is_null(True)) | (ReviewSegment.end_time > after)) + ] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append((ReviewSegment.camera << camera_list)) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + | (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*') + ) + clauses.append(reduce(operator.or_, label_clauses)) + + if zones != "all": + # use matching so segments with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + for zone in filtered_zones: + zone_clauses.append( + (ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*') + ) + clauses.append(reduce(operator.or_, zone_clauses)) + + if severity: + clauses.append((ReviewSegment.severity == severity)) + + # Join with UserReviewStatus to get per-user review status + review_query = ( + ReviewSegment.select( + ReviewSegment.id, + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.thumb_path, + ReviewSegment.data, + fn.COALESCE(UserReviewStatus.has_been_reviewed, False).alias( + "has_been_reviewed" + ), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, clauses)) + ) + + # Filter unreviewed items without subquery + if reviewed == 0: + review_query = review_query.where( + (UserReviewStatus.has_been_reviewed == False) + | (UserReviewStatus.has_been_reviewed.is_null()) + ) + + # Apply ordering and limit + review_query = ( + review_query.order_by(ReviewSegment.severity.asc()) + .order_by(ReviewSegment.start_time.desc()) + .limit(limit) + .dicts() + .iterator() + ) + + return JSONResponse(content=[r for r in review_query]) + + +@router.get( + "/review_ids", + response_model=list[ReviewSegmentResponse], + dependencies=[Depends(allow_any_authenticated())], +) +async def review_ids(request: Request, ids: str): + ids = ids.split(",") + + if not ids: + return JSONResponse( + content=({"success": False, "message": "Valid list of ids must be sent"}), + status_code=400, + ) + + for review_id in ids: + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + except DoesNotExist: + return JSONResponse( + content=( + {"success": False, "message": f"Review {review_id} not found"} + ), + status_code=404, + ) + + try: + reviews = ( + ReviewSegment.select().where(ReviewSegment.id << ids).dicts().iterator() + ) + return JSONResponse(list(reviews)) + except Exception: + return JSONResponse( + content=({"success": False, "message": "Review segments not found"}), + status_code=400, + ) + + +@router.get( + "/review/summary", + response_model=ReviewSummaryResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def review_summary( + params: ReviewSummaryQueryParams = Depends(), + current_user: dict = Depends(get_current_user), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + day_ago = (datetime.datetime.now() - datetime.timedelta(hours=24)).timestamp() + + cameras = params.cameras + labels = params.labels + zones = params.zones + + clauses = [(ReviewSegment.start_time > day_ago)] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append((ReviewSegment.camera << camera_list)) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') + | (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*') + ) + clauses.append(reduce(operator.or_, label_clauses)) + if zones != "all": + # use matching so segments with multiple zones + # still match on a search where any zone matches + zone_clauses = [] + filtered_zones = zones.split(",") + + for zone in filtered_zones: + zone_clauses.append( + ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*' + ) + clauses.append(reduce(operator.or_, zone_clauses)) + + last_24_query = ( + ReviewSegment.select( + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert), + 1, + ) + ], + 0, + ) + ).alias("total_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection), + 1, + ) + ], + 0, + ) + ).alias("total_detection"), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, clauses)) + .dicts() + .get() + ) + + clauses = [] + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content={}) + camera_list = list(filtered) + else: + camera_list = allowed_cameras + clauses.append((ReviewSegment.camera << camera_list)) + + if labels != "all": + # use matching so segments with multiple labels + # still match on a search where any label matches + label_clauses = [] + filtered_labels = labels.split(",") + + for label in filtered_labels: + label_clauses.append( + ReviewSegment.data["objects"].cast("text") % f'*"{label}"*' + ) + clauses.append(reduce(operator.or_, label_clauses)) + + # Find the time range of available data + time_range_query = ( + ReviewSegment.select( + fn.MIN(ReviewSegment.start_time).alias("min_time"), + fn.MAX(ReviewSegment.start_time).alias("max_time"), + ) + .where(reduce(operator.and_, clauses) if clauses else True) + .dicts() + .get() + ) + + min_time = time_range_query.get("min_time") + max_time = time_range_query.get("max_time") + + data = { + "last24Hours": last_24_query, + } + + # If no data, return early + if min_time is None or max_time is None: + return JSONResponse(content=data) + + # Get DST transition periods + dst_periods = get_dst_transitions(params.timezone, min_time, max_time) + + day_in_seconds = 60 * 60 * 24 + + # Query each DST period separately with the correct offset + for period_start, period_end, period_offset in dst_periods: + # Calculate hour/minute modifiers for this period + hours_offset = int(period_offset / 60 / 60) + minutes_offset = int(period_offset / 60 - hours_offset * 60) + period_hour_modifier = f"{hours_offset} hour" + period_minute_modifier = f"{minutes_offset} minute" + + # Build clauses including time range for this period + period_clauses = clauses.copy() + period_clauses.append( + (ReviewSegment.start_time >= period_start) + & (ReviewSegment.start_time <= period_end) + ) + + period_query = ( + ReviewSegment.select( + fn.strftime( + "%Y-%m-%d", + fn.datetime( + ReviewSegment.start_time, + "unixepoch", + period_hour_modifier, + period_minute_modifier, + ), + ).alias("day"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection) + & (UserReviewStatus.has_been_reviewed == True), + 1, + ) + ], + 0, + ) + ).alias("reviewed_detection"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.alert), + 1, + ) + ], + 0, + ) + ).alias("total_alert"), + fn.SUM( + Case( + None, + [ + ( + (ReviewSegment.severity == SeverityEnum.detection), + 1, + ) + ], + 0, + ) + ).alias("total_detection"), + ) + .left_outer_join( + UserReviewStatus, + on=( + (ReviewSegment.id == UserReviewStatus.review_segment) + & (UserReviewStatus.user_id == user_id) + ), + ) + .where(reduce(operator.and_, period_clauses)) + .group_by( + (ReviewSegment.start_time + period_offset).cast("int") / day_in_seconds + ) + .order_by(ReviewSegment.start_time.desc()) + ) + + # Merge results from this period + for e in period_query.dicts().iterator(): + day_key = e["day"] + if day_key in data: + # Merge counts if day already exists (edge case at DST boundary) + data[day_key]["reviewed_alert"] += e["reviewed_alert"] or 0 + data[day_key]["reviewed_detection"] += e["reviewed_detection"] or 0 + data[day_key]["total_alert"] += e["total_alert"] or 0 + data[day_key]["total_detection"] += e["total_detection"] or 0 + else: + data[day_key] = e + + return JSONResponse(content=data) + + +@router.post( + "/reviews/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def set_multiple_reviewed( + request: Request, + body: ReviewModifyMultipleBody, + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + for review_id in body.ids: + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + review_status = UserReviewStatus.get( + UserReviewStatus.user_id == user_id, + UserReviewStatus.review_segment == review_id, + ) + # Update based on the reviewed parameter + if review_status.has_been_reviewed != body.reviewed: + review_status.has_been_reviewed = body.reviewed + review_status.save() + except DoesNotExist: + try: + UserReviewStatus.create( + user_id=user_id, + review_segment=ReviewSegment.get(id=review_id), + has_been_reviewed=body.reviewed, + ) + except (DoesNotExist, IntegrityError): + pass + + return JSONResponse( + content=( + { + "success": True, + "message": f"Marked multiple items as {'reviewed' if body.reviewed else 'unreviewed'}", + } + ), + status_code=200, + ) + + +@router.post( + "/reviews/delete", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], +) +def delete_reviews(body: ReviewModifyMultipleBody): + list_of_ids = body.ids + reviews = ( + ReviewSegment.select( + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ) + .where(ReviewSegment.id << list_of_ids) + .dicts() + .iterator() + ) + recording_ids = [] + + for review in reviews: + start_time = review["start_time"] + end_time = review["end_time"] + camera_name = review["camera"] + recordings = ( + Recordings.select(Recordings.id, Recordings.path) + .where( + Recordings.start_time.between(start_time, end_time) + | Recordings.end_time.between(start_time, end_time) + | ( + (start_time > Recordings.start_time) + & (end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .dicts() + .iterator() + ) + + for recording in recordings: + Path(recording["path"]).unlink(missing_ok=True) + recording_ids.append(recording["id"]) + + # delete recordings and review segments + Recordings.delete().where(Recordings.id << recording_ids).execute() + ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute() + UserReviewStatus.delete().where( + UserReviewStatus.review_segment << list_of_ids + ).execute() + + return JSONResponse( + content=({"success": True, "message": "Deleted review items."}), status_code=200 + ) + + +@router.get( + "/review/activity/motion", + response_model=list[ReviewActivityMotionResponse], + dependencies=[Depends(allow_any_authenticated())], +) +def motion_activity( + params: ReviewActivityMotionQueryParams = Depends(), + allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), +): + """Get motion and audio activity.""" + cameras = params.cameras + before = params.before or datetime.datetime.now().timestamp() + after = ( + params.after + or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp() + ) + # get scale in seconds + scale = params.scale + + clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] + clauses.append((Recordings.motion > 0)) + + if cameras != "all": + requested = set(cameras.split(",")) + filtered = requested.intersection(allowed_cameras) + if not filtered: + return JSONResponse(content=[]) + camera_list = list(filtered) + clauses.append((Recordings.camera << camera_list)) + else: + clauses.append((Recordings.camera << allowed_cameras)) + + data: list[Recordings] = ( + Recordings.select( + Recordings.camera, + Recordings.start_time, + Recordings.motion, + ) + .where(reduce(operator.and_, clauses)) + .order_by(Recordings.start_time.asc()) + .dicts() + .iterator() + ) + + # resample data using pandas to get activity on scaled basis + df = pd.DataFrame(data, columns=["start_time", "motion", "camera"]) + + if df.empty: + logger.warning("No motion data found for the requested time range") + return JSONResponse(content=[]) + + df = df.astype(dtype={"motion": "float32"}) + + # set date as datetime index + df["start_time"] = pd.to_datetime(df["start_time"], unit="s") + df.set_index(["start_time"], inplace=True) + + # normalize data + motion = ( + df["motion"] + .resample(f"{scale}s") + .apply(lambda x: max(x, key=abs, default=0.0)) + .fillna(0.0) + .to_frame() + ) + cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x))) + df = motion.join(cameras) + + length = df.shape[0] + chunk = int(60 * (60 / scale)) + + for i in range(0, length, chunk): + part = df.iloc[i : i + chunk] + min_val, max_val = part["motion"].min(), part["motion"].max() + if min_val != max_val: + df.iloc[i : i + chunk, 0] = ( + part["motion"].sub(min_val).div(max_val - min_val).mul(100).fillna(0) + ) + else: + df.iloc[i : i + chunk, 0] = 0.0 + + # change types for output + df.index = df.index.astype(int) // (10**9) + normalized = df.reset_index().to_dict("records") + return JSONResponse(content=normalized) + + +@router.get( + "/review/event/{event_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def get_review_from_event(request: Request, event_id: str): + try: + review = ReviewSegment.get( + ReviewSegment.data["detections"].cast("text") % f'*"{event_id}"*' + ) + await require_camera_access(review.camera, request=request) + return JSONResponse(model_to_dict(review)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Review item not found"}, + status_code=404, + ) + + +@router.get( + "/review/{review_id}", + response_model=ReviewSegmentResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def get_review(request: Request, review_id: str): + try: + review = ReviewSegment.get(ReviewSegment.id == review_id) + await require_camera_access(review.camera, request=request) + return JSONResponse(content=model_to_dict(review)) + except DoesNotExist: + return JSONResponse( + content={"success": False, "message": "Review item not found"}, + status_code=404, + ) + + +@router.delete( + "/review/{review_id}/viewed", + response_model=GenericResponse, + dependencies=[Depends(allow_any_authenticated())], +) +async def set_not_reviewed( + review_id: str, + current_user: dict = Depends(get_current_user), +): + if isinstance(current_user, JSONResponse): + return current_user + + user_id = current_user["username"] + + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == review_id) + except DoesNotExist: + return JSONResponse( + content=( + {"success": False, "message": "Review " + review_id + " not found"} + ), + status_code=404, + ) + + try: + user_review = UserReviewStatus.get( + UserReviewStatus.user_id == user_id, + UserReviewStatus.review_segment == review, + ) + # we could update here instead of delete if we need + user_review.delete_instance() + except DoesNotExist: + pass # Already effectively "not reviewed" + + return JSONResponse( + content=({"success": True, "message": f"Set Review {review_id} as not viewed"}), + status_code=200, + ) + + +@router.post( + "/review/summarize/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(allow_any_authenticated())], + description="Use GenAI to summarize review items over a period of time.", +) +def generate_review_summary(request: Request, start_ts: float, end_ts: float): + config: FrigateConfig = request.app.frigate_config + + if not config.genai.provider: + return JSONResponse( + content=( + { + "success": False, + "message": "GenAI must be configured to use this feature.", + } + ), + status_code=400, + ) + + context: EmbeddingsContext = request.app.embeddings + summary = context.generate_review_summary(start_ts, end_ts) + + if summary: + return JSONResponse( + content=({"success": True, "summary": summary}), status_code=200 + ) + else: + return JSONResponse( + content=({"success": False, "message": "Failed to create summary."}), + status_code=500, + ) diff --git a/sam2-cpu/frigate-dev/frigate/app.py b/sam2-cpu/frigate-dev/frigate/app.py new file mode 100644 index 0000000..30259ad --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/app.py @@ -0,0 +1,655 @@ +import datetime +import logging +import multiprocessing as mp +import os +import secrets +import shutil +from multiprocessing import Queue +from multiprocessing.managers import DictProxy, SyncManager +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Optional + +import psutil +import uvicorn +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase + +from frigate.api.auth import hash_password +from frigate.api.fastapi_app import create_fastapi_app +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.camera.maintainer import CameraMaintainer +from frigate.comms.base_communicator import Communicator +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessCommunicator +from frigate.comms.mqtt import MqttClient +from frigate.comms.object_detector_signaler import DetectorProxy +from frigate.comms.webpush import WebPushClient +from frigate.comms.ws import WebSocketClient +from frigate.comms.zmq_proxy import ZmqProxy +from frigate.config.camera.updater import CameraConfigUpdatePublisher +from frigate.config.config import FrigateConfig +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + CONFIG_DIR, + EXPORT_DIR, + FACE_DIR, + MODEL_CACHE_DIR, + RECORD_DIR, + THUMB_DIR, + TRIGGER_DIR, +) +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.embeddings import EmbeddingProcess, EmbeddingsContext +from frigate.events.audio import AudioProcessor +from frigate.events.cleanup import EventCleanup +from frigate.events.maintainer import EventProcessor +from frigate.log import _stop_logging +from frigate.models import ( + Event, + Export, + Previews, + Recordings, + RecordingsToDelete, + Regions, + ReviewSegment, + Timeline, + Trigger, + User, +) +from frigate.object_detection.base import ObjectDetectProcess +from frigate.output.output import OutputProcess +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.ptz.onvif import OnvifController +from frigate.record.cleanup import RecordingCleanup +from frigate.record.export import migrate_exports +from frigate.record.record import RecordProcess +from frigate.review.review import ReviewProcess +from frigate.stats.emitter import StatsEmitter +from frigate.stats.util import stats_init +from frigate.storage import StorageMaintainer +from frigate.timeline import TimelineProcessor +from frigate.track.object_processing import TrackedObjectProcessor +from frigate.util.builtin import empty_and_close_queue +from frigate.util.image import UntrackedSharedMemory +from frigate.util.services import set_file_limit +from frigate.version import VERSION +from frigate.watchdog import FrigateWatchdog + +logger = logging.getLogger(__name__) + + +class FrigateApp: + def __init__( + self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent + ) -> None: + self.metrics_manager = manager + self.audio_process: Optional[mp.Process] = None + self.stop_event = stop_event + self.detection_queue: Queue = mp.Queue() + self.detectors: dict[str, ObjectDetectProcess] = {} + self.detection_shms: list[mp.shared_memory.SharedMemory] = [] + self.log_queue: Queue = mp.Queue() + self.camera_metrics: DictProxy = self.metrics_manager.dict() + self.embeddings_metrics: DataProcessorMetrics | None = ( + DataProcessorMetrics( + self.metrics_manager, list(config.classification.custom.keys()) + ) + if ( + config.semantic_search.enabled + or config.lpr.enabled + or config.face_recognition.enabled + or len(config.classification.custom) > 0 + ) + else None + ) + self.ptz_metrics: dict[str, PTZMetrics] = {} + self.processes: dict[str, int] = {} + self.embeddings: Optional[EmbeddingsContext] = None + self.config = config + + def ensure_dirs(self) -> None: + dirs = [ + CONFIG_DIR, + RECORD_DIR, + THUMB_DIR, + f"{CLIPS_DIR}/cache", + CACHE_DIR, + MODEL_CACHE_DIR, + EXPORT_DIR, + ] + + if self.config.face_recognition.enabled: + dirs.append(FACE_DIR) + + if self.config.semantic_search.enabled: + dirs.append(TRIGGER_DIR) + + for d in dirs: + if not os.path.exists(d) and not os.path.islink(d): + logger.info(f"Creating directory: {d}") + os.makedirs(d) + else: + logger.debug(f"Skipping directory: {d}") + + def init_camera_metrics(self) -> None: + # create camera_metrics + for camera_name in self.config.cameras.keys(): + self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager) + self.ptz_metrics[camera_name] = PTZMetrics( + autotracker_enabled=self.config.cameras[ + camera_name + ].onvif.autotracking.enabled + ) + + def init_queues(self) -> None: + # Queue for cameras to push tracked objects to + # leaving room for 2 extra cameras to be added + self.detected_frames_queue: Queue = mp.Queue( + maxsize=( + sum( + camera.enabled_in_config == True + for camera in self.config.cameras.values() + ) + + 2 + ) + * 2 + ) + + # Queue for timeline events + self.timeline_queue: Queue = mp.Queue() + + def init_database(self) -> None: + def vacuum_db(db: SqliteExtDatabase) -> None: + logger.info("Running database vacuum") + db.execute_sql("VACUUM;") + + try: + with open(f"{CONFIG_DIR}/.vacuum", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save DB state") + + def cleanup_timeline_db(db: SqliteExtDatabase) -> None: + db.execute_sql( + "DELETE FROM timeline WHERE source_id NOT IN (SELECT id FROM event);" + ) + + try: + with open(f"{CONFIG_DIR}/.timeline", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save DB state") + + # Migrate DB schema + migrate_db = SqliteExtDatabase(self.config.database.path) + + # Run migrations + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + + if len(router.diff) > 0: + logger.info("Making backup of DB before migrations...") + shutil.copyfile( + self.config.database.path, + self.config.database.path.replace("frigate.db", "backup.db"), + ) + + router.run() + + # this is a temporary check to clean up user DB from beta + # will be removed before final release + if not os.path.exists(f"{CONFIG_DIR}/.timeline"): + cleanup_timeline_db(migrate_db) + + # check if vacuum needs to be run + if os.path.exists(f"{CONFIG_DIR}/.vacuum"): + with open(f"{CONFIG_DIR}/.vacuum") as f: + try: + timestamp = round(float(f.readline())) + except Exception: + timestamp = 0 + + if ( + timestamp + < ( + datetime.datetime.now() - datetime.timedelta(weeks=2) + ).timestamp() + ): + vacuum_db(migrate_db) + else: + vacuum_db(migrate_db) + + migrate_db.close() + + def init_go2rtc(self) -> None: + for proc in psutil.process_iter(["pid", "name"]): + if proc.info["name"] == "go2rtc": + logger.info(f"go2rtc process pid: {proc.info['pid']}") + self.processes["go2rtc"] = proc.info["pid"] + + def init_recording_manager(self) -> None: + recording_process = RecordProcess(self.config, self.stop_event) + self.recording_process = recording_process + recording_process.start() + self.processes["recording"] = recording_process.pid or 0 + logger.info(f"Recording process started: {recording_process.pid}") + + def init_review_segment_manager(self) -> None: + review_segment_process = ReviewProcess(self.config, self.stop_event) + self.review_segment_process = review_segment_process + review_segment_process.start() + self.processes["review_segment"] = review_segment_process.pid or 0 + logger.info(f"Review process started: {review_segment_process.pid}") + + def init_embeddings_manager(self) -> None: + # always start the embeddings process + embedding_process = EmbeddingProcess( + self.config, self.embeddings_metrics, self.stop_event + ) + self.embedding_process = embedding_process + embedding_process.start() + self.processes["embeddings"] = embedding_process.pid or 0 + logger.info(f"Embedding process started: {embedding_process.pid}") + + def bind_database(self) -> None: + """Bind db to the main process.""" + # NOTE: all db accessing processes need to be created before the db can be bound to the main process + self.db = SqliteVecQueueDatabase( + self.config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache, + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, + 10 + * len([c for c in self.config.cameras.values() if c.enabled_in_config]), + ), + load_vec_extension=self.config.semantic_search.enabled, + ) + models = [ + Event, + Export, + Previews, + Recordings, + RecordingsToDelete, + Regions, + ReviewSegment, + Timeline, + User, + Trigger, + ] + self.db.bind(models) + + def check_db_data_migrations(self) -> None: + # check if vacuum needs to be run + if not os.path.exists(f"{CONFIG_DIR}/.exports"): + try: + with open(f"{CONFIG_DIR}/.exports", "w") as f: + f.write(str(datetime.datetime.now().timestamp())) + except PermissionError: + logger.error("Unable to write to /config to save export state") + + migrate_exports(self.config.ffmpeg, list(self.config.cameras.keys())) + + def init_embeddings_client(self) -> None: + # Create a client for other processes to use + self.embeddings = EmbeddingsContext(self.db) + + def init_inter_process_communicator(self) -> None: + self.inter_process_communicator = InterProcessCommunicator() + self.inter_config_updater = CameraConfigUpdatePublisher() + self.event_metadata_updater = EventMetadataPublisher() + self.inter_zmq_proxy = ZmqProxy() + self.detection_proxy = DetectorProxy() + + def init_onvif(self) -> None: + self.onvif_controller = OnvifController(self.config, self.ptz_metrics) + + def init_dispatcher(self) -> None: + comms: list[Communicator] = [] + + if self.config.mqtt.enabled: + comms.append(MqttClient(self.config)) + + notification_cameras = [ + c + for c in self.config.cameras.values() + if c.enabled and c.notifications.enabled_in_config + ] + + if notification_cameras: + comms.append(WebPushClient(self.config, self.stop_event)) + + comms.append(WebSocketClient(self.config)) + comms.append(self.inter_process_communicator) + + self.dispatcher = Dispatcher( + self.config, + self.inter_config_updater, + self.onvif_controller, + self.ptz_metrics, + comms, + ) + + def start_detectors(self) -> None: + for name in self.config.cameras.keys(): + try: + largest_frame = max( + [ + det.model.height * det.model.width * 3 + if det.model is not None + else 320 + for det in self.config.detectors.values() + ] + ) + shm_in = UntrackedSharedMemory( + name=name, + create=True, + size=largest_frame, + ) + except FileExistsError: + shm_in = UntrackedSharedMemory(name=name) + + try: + shm_out = UntrackedSharedMemory( + name=f"out-{name}", create=True, size=20 * 6 * 4 + ) + except FileExistsError: + shm_out = UntrackedSharedMemory(name=f"out-{name}") + + self.detection_shms.append(shm_in) + self.detection_shms.append(shm_out) + + for name, detector_config in self.config.detectors.items(): + self.detectors[name] = ObjectDetectProcess( + name, + self.detection_queue, + list(self.config.cameras.keys()), + self.config, + detector_config, + self.stop_event, + ) + + def start_ptz_autotracker(self) -> None: + self.ptz_autotracker_thread = PtzAutoTrackerThread( + self.config, + self.onvif_controller, + self.ptz_metrics, + self.dispatcher, + self.stop_event, + ) + self.ptz_autotracker_thread.start() + + def start_detected_frames_processor(self) -> None: + self.detected_frames_processor = TrackedObjectProcessor( + self.config, + self.dispatcher, + self.detected_frames_queue, + self.ptz_autotracker_thread, + self.stop_event, + ) + self.detected_frames_processor.start() + + def start_video_output_processor(self) -> None: + output_processor = OutputProcess(self.config, self.stop_event) + self.output_processor = output_processor + output_processor.start() + logger.info(f"Output process started: {output_processor.pid}") + + def start_camera_processor(self) -> None: + self.camera_maintainer = CameraMaintainer( + self.config, + self.detection_queue, + self.detected_frames_queue, + self.camera_metrics, + self.ptz_metrics, + self.stop_event, + self.metrics_manager, + ) + self.camera_maintainer.start() + + def start_audio_processor(self) -> None: + audio_cameras = [ + c + for c in self.config.cameras.values() + if c.enabled and c.audio.enabled_in_config + ] + + if audio_cameras: + self.audio_process = AudioProcessor( + self.config, audio_cameras, self.camera_metrics, self.stop_event + ) + self.audio_process.start() + self.processes["audio_detector"] = self.audio_process.pid or 0 + + def start_timeline_processor(self) -> None: + self.timeline_processor = TimelineProcessor( + self.config, self.timeline_queue, self.stop_event + ) + self.timeline_processor.start() + + def start_event_processor(self) -> None: + self.event_processor = EventProcessor( + self.config, + self.timeline_queue, + self.stop_event, + ) + self.event_processor.start() + + def start_event_cleanup(self) -> None: + self.event_cleanup = EventCleanup(self.config, self.stop_event, self.db) + self.event_cleanup.start() + + def start_record_cleanup(self) -> None: + self.record_cleanup = RecordingCleanup(self.config, self.stop_event) + self.record_cleanup.start() + + def start_storage_maintainer(self) -> None: + self.storage_maintainer = StorageMaintainer(self.config, self.stop_event) + self.storage_maintainer.start() + + def start_stats_emitter(self) -> None: + self.stats_emitter = StatsEmitter( + self.config, + stats_init( + self.config, + self.camera_metrics, + self.embeddings_metrics, + self.detectors, + self.processes, + ), + self.stop_event, + ) + self.stats_emitter.start() + + def start_watchdog(self) -> None: + self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) + self.frigate_watchdog.start() + + def init_auth(self) -> None: + if self.config.auth.enabled: + if User.select().count() == 0: + password = secrets.token_hex(16) + password_hash = hash_password( + password, iterations=self.config.auth.hash_iterations + ) + User.insert( + { + User.username: "admin", + User.role: "admin", + User.password_hash: password_hash, + User.notification_tokens: [], + } + ).execute() + + self.config.auth.admin_first_time_login = True + + logger.info("********************************************************") + logger.info("********************************************************") + logger.info("*** Auth is enabled, but no users exist. ***") + logger.info("*** Created a default user: ***") + logger.info("*** User: admin ***") + logger.info(f"*** Password: {password} ***") + logger.info("********************************************************") + logger.info("********************************************************") + elif self.config.auth.reset_admin_password: + password = secrets.token_hex(16) + password_hash = hash_password( + password, iterations=self.config.auth.hash_iterations + ) + User.replace( + username="admin", + role="admin", + password_hash=password_hash, + notification_tokens=[], + ).execute() + + logger.info("********************************************************") + logger.info("********************************************************") + logger.info("*** Reset admin password set in the config. ***") + logger.info(f"*** Password: {password} ***") + logger.info("********************************************************") + logger.info("********************************************************") + + def start(self) -> None: + logger.info(f"Starting Frigate ({VERSION})") + + # Ensure global state. + self.ensure_dirs() + + # Set soft file limits. + set_file_limit() + + # Start frigate services. + self.init_camera_metrics() + self.init_queues() + self.init_database() + self.init_onvif() + self.init_recording_manager() + self.init_review_segment_manager() + self.init_go2rtc() + self.init_embeddings_manager() + self.bind_database() + self.check_db_data_migrations() + self.init_inter_process_communicator() + self.start_detectors() + self.init_dispatcher() + self.init_embeddings_client() + self.start_video_output_processor() + self.start_ptz_autotracker() + self.start_detected_frames_processor() + self.start_camera_processor() + self.start_audio_processor() + self.start_storage_maintainer() + self.start_stats_emitter() + self.start_timeline_processor() + self.start_event_processor() + self.start_event_cleanup() + self.start_record_cleanup() + self.start_watchdog() + + self.init_auth() + + try: + uvicorn.run( + create_fastapi_app( + self.config, + self.db, + self.embeddings, + self.detected_frames_processor, + self.storage_maintainer, + self.onvif_controller, + self.stats_emitter, + self.event_metadata_updater, + self.inter_config_updater, + ), + host="127.0.0.1", + port=5001, + log_level="error", + ) + finally: + self.stop() + + def stop(self) -> None: + logger.info("Stopping...") + + # used by the docker healthcheck + Path("/dev/shm/.frigate-is-stopping").touch() + + self.stop_event.set() + + # set an end_time on entries without an end_time before exiting + Event.update( + end_time=datetime.datetime.now().timestamp(), has_snapshot=False + ).where(Event.end_time == None).execute() + ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( + ReviewSegment.end_time == None + ).execute() + + # stop the audio process + if self.audio_process: + self.audio_process.terminate() + self.audio_process.join() + + # stop the onvif controller + if self.onvif_controller: + self.onvif_controller.close() + + # ensure the detectors are done + for detector in self.detectors.values(): + detector.stop() + + empty_and_close_queue(self.detection_queue) + logger.info("Detection queue closed") + + self.detected_frames_processor.join() + empty_and_close_queue(self.detected_frames_queue) + logger.info("Detected frames queue closed") + + self.timeline_processor.join() + self.event_processor.join() + empty_and_close_queue(self.timeline_queue) + logger.info("Timeline queue closed") + + self.output_processor.terminate() + self.output_processor.join() + + self.recording_process.terminate() + self.recording_process.join() + + self.review_segment_process.terminate() + self.review_segment_process.join() + + self.dispatcher.stop() + self.ptz_autotracker_thread.join() + + self.event_cleanup.join() + self.record_cleanup.join() + self.stats_emitter.join() + self.frigate_watchdog.join() + self.db.stop() + + # Save embeddings stats to disk + if self.embeddings: + self.embeddings.stop() + + # Stop Communicators + self.inter_process_communicator.stop() + self.inter_config_updater.stop() + self.event_metadata_updater.stop() + self.inter_zmq_proxy.stop() + self.detection_proxy.stop() + + while len(self.detection_shms) > 0: + shm = self.detection_shms.pop() + shm.close() + shm.unlink() + + _stop_logging() + self.metrics_manager.shutdown() diff --git a/sam2-cpu/frigate-dev/frigate/camera/__init__.py b/sam2-cpu/frigate-dev/frigate/camera/__init__.py new file mode 100644 index 0000000..77b1fd4 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/camera/__init__.py @@ -0,0 +1,68 @@ +import multiprocessing as mp +from multiprocessing.managers import SyncManager +from multiprocessing.sharedctypes import Synchronized +from multiprocessing.synchronize import Event + + +class CameraMetrics: + camera_fps: Synchronized + detection_fps: Synchronized + detection_frame: Synchronized + process_fps: Synchronized + skipped_fps: Synchronized + read_start: Synchronized + audio_rms: Synchronized + audio_dBFS: Synchronized + + frame_queue: mp.Queue + + process_pid: Synchronized + capture_process_pid: Synchronized + ffmpeg_pid: Synchronized + + def __init__(self, manager: SyncManager): + self.camera_fps = manager.Value("d", 0) + self.detection_fps = manager.Value("d", 0) + self.detection_frame = manager.Value("d", 0) + self.process_fps = manager.Value("d", 0) + self.skipped_fps = manager.Value("d", 0) + self.read_start = manager.Value("d", 0) + self.audio_rms = manager.Value("d", 0) + self.audio_dBFS = manager.Value("d", 0) + + self.frame_queue = manager.Queue(maxsize=2) + + self.process_pid = manager.Value("i", 0) + self.capture_process_pid = manager.Value("i", 0) + self.ffmpeg_pid = manager.Value("i", 0) + + +class PTZMetrics: + autotracker_enabled: Synchronized + + start_time: Synchronized + stop_time: Synchronized + frame_time: Synchronized + zoom_level: Synchronized + max_zoom: Synchronized + min_zoom: Synchronized + + tracking_active: Event + motor_stopped: Event + reset: Event + + def __init__(self, *, autotracker_enabled: bool): + self.autotracker_enabled = mp.Value("i", autotracker_enabled) + + self.start_time = mp.Value("d", 0) + self.stop_time = mp.Value("d", 0) + self.frame_time = mp.Value("d", 0) + self.zoom_level = mp.Value("d", 0) + self.max_zoom = mp.Value("d", 0) + self.min_zoom = mp.Value("d", 0) + + self.tracking_active = mp.Event() + self.motor_stopped = mp.Event() + self.reset = mp.Event() + + self.motor_stopped.set() diff --git a/sam2-cpu/frigate-dev/frigate/camera/activity_manager.py b/sam2-cpu/frigate-dev/frigate/camera/activity_manager.py new file mode 100644 index 0000000..c2dfa89 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/camera/activity_manager.py @@ -0,0 +1,259 @@ +"""Manage camera activity and updating listeners.""" + +import datetime +import json +import logging +import random +import string +from collections import Counter +from typing import Any, Callable + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.config import CameraConfig, FrigateConfig + +logger = logging.getLogger(__name__) + + +class CameraActivityManager: + def __init__( + self, config: FrigateConfig, publish: Callable[[str, Any], None] + ) -> None: + self.config = config + self.publish = publish + self.last_camera_activity: dict[str, dict[str, Any]] = {} + self.camera_all_object_counts: dict[str, Counter] = {} + self.camera_active_object_counts: dict[str, Counter] = {} + self.zone_all_object_counts: dict[str, Counter] = {} + self.zone_active_object_counts: dict[str, Counter] = {} + self.all_zone_labels: dict[str, set[str]] = {} + + for camera_config in config.cameras.values(): + if not camera_config.enabled_in_config: + continue + + self.__init_camera(camera_config) + + def __init_camera(self, camera_config: CameraConfig) -> None: + self.last_camera_activity[camera_config.name] = {} + self.camera_all_object_counts[camera_config.name] = Counter() + self.camera_active_object_counts[camera_config.name] = Counter() + + for zone, zone_config in camera_config.zones.items(): + if zone not in self.all_zone_labels: + self.zone_all_object_counts[zone] = Counter() + self.zone_active_object_counts[zone] = Counter() + self.all_zone_labels[zone] = set() + + self.all_zone_labels[zone].update( + zone_config.objects + if zone_config.objects + else camera_config.objects.track + ) + + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + all_objects: list[dict[str, Any]] = [] + + for camera in new_activity.keys(): + # handle cameras that were added dynamically + if camera not in self.camera_all_object_counts: + self.__init_camera(self.config.cameras[camera]) + + new_objects = new_activity[camera].get("objects", []) + all_objects.extend(new_objects) + + if self.last_camera_activity.get(camera, {}).get("objects") != new_objects: + self.compare_camera_activity(camera, new_objects) + + # run through every zone, getting a count of objects in that zone right now + for zone, labels in self.all_zone_labels.items(): + all_zone_objects = Counter( + obj["label"].replace("-verified", "") + for obj in all_objects + if zone in obj["current_zones"] + ) + active_zone_objects = Counter( + obj["label"].replace("-verified", "") + for obj in all_objects + if zone in obj["current_zones"] and not obj["stationary"] + ) + any_changed = False + + # run through each object and check what topics need to be updated for this zone + for label in labels: + new_count = all_zone_objects[label] + new_active_count = active_zone_objects[label] + + if ( + new_count != self.zone_all_object_counts[zone][label] + or label not in self.zone_all_object_counts[zone] + ): + any_changed = True + self.publish(f"{zone}/{label}", new_count) + self.zone_all_object_counts[zone][label] = new_count + + if ( + new_active_count != self.zone_active_object_counts[zone][label] + or label not in self.zone_active_object_counts[zone] + ): + any_changed = True + self.publish(f"{zone}/{label}/active", new_active_count) + self.zone_active_object_counts[zone][label] = new_active_count + + if any_changed: + self.publish(f"{zone}/all", sum(list(all_zone_objects.values()))) + self.publish( + f"{zone}/all/active", sum(list(active_zone_objects.values())) + ) + + self.last_camera_activity = new_activity + + def compare_camera_activity( + self, camera: str, new_activity: dict[str, Any] + ) -> None: + all_objects = Counter( + obj["label"].replace("-verified", "") for obj in new_activity + ) + active_objects = Counter( + obj["label"].replace("-verified", "") + for obj in new_activity + if not obj["stationary"] + ) + any_changed = False + + # run through each object and check what topics need to be updated + for label in self.config.cameras[camera].objects.track: + if label in self.config.model.non_logo_attributes: + continue + + new_count = all_objects[label] + new_active_count = active_objects[label] + + if ( + new_count != self.camera_all_object_counts[camera][label] + or label not in self.camera_all_object_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{label}", new_count) + self.camera_all_object_counts[camera][label] = new_count + + if ( + new_active_count != self.camera_active_object_counts[camera][label] + or label not in self.camera_active_object_counts[camera] + ): + any_changed = True + self.publish(f"{camera}/{label}/active", new_active_count) + self.camera_active_object_counts[camera][label] = new_active_count + + if any_changed: + self.publish(f"{camera}/all", sum(list(all_objects.values()))) + self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) + + +class AudioActivityManager: + def __init__( + self, config: FrigateConfig, publish: Callable[[str, Any], None] + ) -> None: + self.config = config + self.publish = publish + self.current_audio_detections: dict[str, dict[str, dict[str, Any]]] = {} + self.event_metadata_publisher = EventMetadataPublisher() + + for camera_config in config.cameras.values(): + if not camera_config.audio.enabled_in_config: + continue + + self.__init_camera(camera_config) + + def __init_camera(self, camera_config: CameraConfig) -> None: + self.current_audio_detections[camera_config.name] = {} + + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + now = datetime.datetime.now().timestamp() + + for camera in new_activity.keys(): + # handle cameras that were added dynamically + if camera not in self.current_audio_detections: + self.__init_camera(self.config.cameras[camera]) + + new_detections = new_activity[camera].get("detections", []) + if self.compare_audio_activity(camera, new_detections, now): + logger.debug(f"Audio detections for {camera}: {new_activity}") + self.publish( + f"{camera}/audio/all", + "ON" if len(self.current_audio_detections[camera]) > 0 else "OFF", + ) + self.publish( + "audio_detections", + json.dumps(self.current_audio_detections), + ) + + def compare_audio_activity( + self, camera: str, new_detections: list[tuple[str, float]], now: float + ) -> None: + max_not_heard = self.config.cameras[camera].audio.max_not_heard + current = self.current_audio_detections[camera] + + any_changed = False + + for label, score in new_detections: + any_changed = True + if label in current: + current[label]["last_detection"] = now + current[label]["score"] = score + else: + rand_id = "".join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + event_id = f"{now}-{rand_id}" + self.publish(f"{camera}/audio/{label}", "ON") + + self.event_metadata_publisher.publish( + ( + now, + camera, + label, + event_id, + True, + score, + None, + None, + "audio", + {}, + ), + EventMetadataTypeEnum.manual_event_create.value, + ) + current[label] = { + "id": event_id, + "score": score, + "last_detection": now, + } + + # expire detections + for label in list(current.keys()): + if now - current[label]["last_detection"] > max_not_heard: + any_changed = True + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] + + return any_changed + + def expire_all(self, camera: str) -> None: + now = datetime.datetime.now().timestamp() + current = self.current_audio_detections.get(camera, {}) + + for label in list(current.keys()): + self.publish(f"{camera}/audio/{label}", "OFF") + + self.event_metadata_publisher.publish( + (current[label]["id"], now), + EventMetadataTypeEnum.manual_event_end.value, + ) + del current[label] diff --git a/sam2-cpu/frigate-dev/frigate/camera/maintainer.py b/sam2-cpu/frigate-dev/frigate/camera/maintainer.py new file mode 100644 index 0000000..815e650 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/camera/maintainer.py @@ -0,0 +1,225 @@ +"""Create and maintain camera processes / management.""" + +import logging +import multiprocessing as mp +import threading +from multiprocessing import Queue +from multiprocessing.managers import DictProxy, SyncManager +from multiprocessing.synchronize import Event as MpEvent + +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.config import FrigateConfig +from frigate.config.camera import CameraConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.models import Regions +from frigate.util.builtin import empty_and_close_queue +from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory +from frigate.util.object import get_camera_regions_grid +from frigate.util.services import calculate_shm_requirements +from frigate.video import CameraCapture, CameraTracker + +logger = logging.getLogger(__name__) + + +class CameraMaintainer(threading.Thread): + def __init__( + self, + config: FrigateConfig, + detection_queue: Queue, + detected_frames_queue: Queue, + camera_metrics: DictProxy, + ptz_metrics: dict[str, PTZMetrics], + stop_event: MpEvent, + metrics_manager: SyncManager, + ): + super().__init__(name="camera_processor") + self.config = config + self.detection_queue = detection_queue + self.detected_frames_queue = detected_frames_queue + self.stop_event = stop_event + self.camera_metrics = camera_metrics + self.ptz_metrics = ptz_metrics + self.frame_manager = SharedMemoryFrameManager() + self.region_grids: dict[str, list[list[dict[str, int]]]] = {} + self.update_subscriber = CameraConfigUpdateSubscriber( + self.config, + {}, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.remove, + ], + ) + self.shm_count = self.__calculate_shm_frame_count() + self.camera_processes: dict[str, mp.Process] = {} + self.capture_processes: dict[str, mp.Process] = {} + self.metrics_manager = metrics_manager + + def __init_historical_regions(self) -> None: + # delete region grids for removed or renamed cameras + cameras = list(self.config.cameras.keys()) + Regions.delete().where(~(Regions.camera << cameras)).execute() + + # create or update region grids for each camera + for camera in self.config.cameras.values(): + assert camera.name is not None + self.region_grids[camera.name] = get_camera_regions_grid( + camera.name, + camera.detect, + max(self.config.model.width, self.config.model.height), + ) + + def __calculate_shm_frame_count(self) -> int: + shm_stats = calculate_shm_requirements(self.config) + + if not shm_stats: + # /dev/shm not available + return 0 + + logger.debug( + f"Calculated total camera size {shm_stats['available']} / " + f"{shm_stats['camera_frame_size']} :: {shm_stats['shm_frame_count']} " + f"frames for each camera in SHM" + ) + + if shm_stats["shm_frame_count"] < 20: + logger.warning( + f"The current SHM size of {shm_stats['total']}MB is too small, " + f"recommend increasing it to at least {shm_stats['min_shm']}MB." + ) + + return shm_stats["shm_frame_count"] + + def __start_camera_processor( + self, name: str, config: CameraConfig, runtime: bool = False + ) -> None: + if not config.enabled_in_config: + logger.info(f"Camera processor not started for disabled camera {name}") + return + + if runtime: + self.camera_metrics[name] = CameraMetrics(self.metrics_manager) + self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) + self.region_grids[name] = get_camera_regions_grid( + name, + config.detect, + max(self.config.model.width, self.config.model.height), + ) + + try: + largest_frame = max( + [ + det.model.height * det.model.width * 3 + if det.model is not None + else 320 + for det in self.config.detectors.values() + ] + ) + UntrackedSharedMemory(name=f"out-{name}", create=True, size=20 * 6 * 4) + UntrackedSharedMemory( + name=name, + create=True, + size=largest_frame, + ) + except FileExistsError: + pass + + camera_process = CameraTracker( + config, + self.config.model, + self.config.model.merged_labelmap, + self.detection_queue, + self.detected_frames_queue, + self.camera_metrics[name], + self.ptz_metrics[name], + self.region_grids[name], + self.stop_event, + self.config.logger, + ) + self.camera_processes[config.name] = camera_process + camera_process.start() + self.camera_metrics[config.name].process_pid.value = camera_process.pid + logger.info(f"Camera processor started for {config.name}: {camera_process.pid}") + + def __start_camera_capture( + self, name: str, config: CameraConfig, runtime: bool = False + ) -> None: + if not config.enabled_in_config: + logger.info(f"Capture process not started for disabled camera {name}") + return + + # pre-create shms + count = 10 if runtime else self.shm_count + for i in range(count): + frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1] + self.frame_manager.create(f"{config.name}_frame{i}", frame_size) + + capture_process = CameraCapture( + config, + count, + self.camera_metrics[name], + self.stop_event, + self.config.logger, + ) + capture_process.daemon = True + self.capture_processes[name] = capture_process + capture_process.start() + self.camera_metrics[name].capture_process_pid.value = capture_process.pid + logger.info(f"Capture process started for {name}: {capture_process.pid}") + + def __stop_camera_capture_process(self, camera: str) -> None: + capture_process = self.capture_processes[camera] + if capture_process is not None: + logger.info(f"Waiting for capture process for {camera} to stop") + capture_process.terminate() + capture_process.join() + + def __stop_camera_process(self, camera: str) -> None: + camera_process = self.camera_processes[camera] + if camera_process is not None: + logger.info(f"Waiting for process for {camera} to stop") + camera_process.terminate() + camera_process.join() + logger.info(f"Closing frame queue for {camera}") + empty_and_close_queue(self.camera_metrics[camera].frame_queue) + + def run(self): + self.__init_historical_regions() + + # start camera processes + for camera, config in self.config.cameras.items(): + self.__start_camera_processor(camera, config) + self.__start_camera_capture(camera, config) + + while not self.stop_event.wait(1): + updates = self.update_subscriber.check_for_updates() + + for update_type, updated_cameras in updates.items(): + if update_type == CameraConfigUpdateEnum.add.name: + for camera in updated_cameras: + self.__start_camera_processor( + camera, + self.update_subscriber.camera_configs[camera], + runtime=True, + ) + self.__start_camera_capture( + camera, + self.update_subscriber.camera_configs[camera], + runtime=True, + ) + elif update_type == CameraConfigUpdateEnum.remove.name: + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + + # ensure the capture processes are done + for camera in self.camera_processes.keys(): + self.__stop_camera_capture_process(camera) + + # ensure the camera processors are done + for camera in self.capture_processes.keys(): + self.__stop_camera_process(camera) + + self.update_subscriber.stop() + self.frame_manager.cleanup() diff --git a/sam2-cpu/frigate-dev/frigate/camera/state.py b/sam2-cpu/frigate-dev/frigate/camera/state.py new file mode 100644 index 0000000..97c7153 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/camera/state.py @@ -0,0 +1,584 @@ +"""Maintains state of camera.""" + +import datetime +import logging +import os +import threading +from collections import defaultdict +from typing import Any, Callable + +import cv2 +import numpy as np + +from frigate.config import ( + FrigateConfig, + ZoomingModeEnum, +) +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.track.tracked_object import TrackedObject +from frigate.util.image import ( + SharedMemoryFrameManager, + draw_box_with_label, + draw_timestamp, + is_better_thumbnail, + is_label_printable, +) + +logger = logging.getLogger(__name__) + + +class CameraState: + def __init__( + self, + name, + config: FrigateConfig, + frame_manager: SharedMemoryFrameManager, + ptz_autotracker_thread: PtzAutoTrackerThread, + ): + self.name = name + self.config = config + self.camera_config = config.cameras[name] + self.frame_manager = frame_manager + self.best_objects: dict[str, TrackedObject] = {} + self.tracked_objects: dict[str, TrackedObject] = {} + self.frame_cache = {} + self.zone_objects = defaultdict(list) + self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) + self.current_frame_lock = threading.Lock() + self.current_frame_time = 0.0 + self.motion_boxes = [] + self.regions = [] + self.previous_frame_id = None + self.callbacks = defaultdict(list) + self.ptz_autotracker_thread = ptz_autotracker_thread + self.prev_enabled = self.camera_config.enabled + + def get_current_frame(self, draw_options: dict[str, Any] = {}) -> np.ndarray: + with self.current_frame_lock: + frame_copy = np.copy(self._current_frame) + frame_time = self.current_frame_time + tracked_objects = {k: v.to_dict() for k, v in self.tracked_objects.items()} + motion_boxes = self.motion_boxes.copy() + regions = self.regions.copy() + + frame_copy = cv2.cvtColor(frame_copy, cv2.COLOR_YUV2BGR_I420) + # draw on the frame + if draw_options.get("mask"): + mask_overlay = np.where(self.camera_config.motion.mask == [0]) + frame_copy[mask_overlay] = [0, 0, 0] + + if draw_options.get("bounding_boxes"): + # draw the bounding boxes on the frame + for obj in tracked_objects.values(): + if obj["frame_time"] == frame_time: + if obj["stationary"]: + color = (220, 220, 220) + thickness = 1 + else: + thickness = 2 + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + else: + thickness = 1 + color = (255, 0, 0) + + # draw thicker box around ptz autotracked object + if ( + self.camera_config.onvif.autotracking.enabled + and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init[ + self.name + ] + and self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ + self.name + ] + is not None + and obj["id"] + == self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ + self.name + ].obj_data["id"] + and obj["frame_time"] == frame_time + ): + thickness = 5 + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + + # debug autotracking zooming - show the zoom factor box + if ( + self.camera_config.onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[ + self.name + ]["max_target_box"] + side_length = max_target_box * ( + max( + self.camera_config.detect.width, + self.camera_config.detect.height, + ) + ) + + centroid_x = (obj["box"][0] + obj["box"][2]) // 2 + centroid_y = (obj["box"][1] + obj["box"][3]) // 2 + top_left = ( + int(centroid_x - side_length // 2), + int(centroid_y - side_length // 2), + ) + bottom_right = ( + int(centroid_x + side_length // 2), + int(centroid_y + side_length // 2), + ) + cv2.rectangle( + frame_copy, + top_left, + bottom_right, + (255, 255, 0), + 2, + ) + + # draw the bounding boxes on the frame + box = obj["box"] + text = ( + obj["sub_label"][0] + if ( + obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) + ) + else obj.get("recognized_license_plate", [None])[0] + if ( + obj.get("recognized_license_plate") + and obj["recognized_license_plate"][0] + ) + else obj["label"] + ) + draw_box_with_label( + frame_copy, + box[0], + box[1], + box[2], + box[3], + text, + f"{obj['score']:.0%} {int(obj['area'])}" + + ( + f" {float(obj['current_estimated_speed']):.1f}" + if obj["current_estimated_speed"] != 0 + else "" + ), + thickness=thickness, + color=color, + ) + + # draw any attributes + for attribute in obj["current_attributes"]: + box = attribute["box"] + box_area = int((box[2] - box[0]) * (box[3] - box[1])) + draw_box_with_label( + frame_copy, + box[0], + box[1], + box[2], + box[3], + attribute["label"], + f"{attribute['score']:.0%} {str(box_area)}", + thickness=thickness, + color=color, + ) + + if draw_options.get("regions"): + for region in regions: + cv2.rectangle( + frame_copy, + (region[0], region[1]), + (region[2], region[3]), + (0, 255, 0), + 2, + ) + + if draw_options.get("zones"): + for name, zone in self.camera_config.zones.items(): + thickness = ( + 8 + if any( + name in obj["current_zones"] for obj in tracked_objects.values() + ) + else 2 + ) + cv2.drawContours(frame_copy, [zone.contour], -1, zone.color, thickness) + + if draw_options.get("motion_boxes"): + for m_box in motion_boxes: + cv2.rectangle( + frame_copy, + (m_box[0], m_box[1]), + (m_box[2], m_box[3]), + (0, 0, 255), + 2, + ) + + if draw_options.get("timestamp"): + color = self.camera_config.timestamp_style.color + draw_timestamp( + frame_copy, + frame_time, + self.camera_config.timestamp_style.format, + font_effect=self.camera_config.timestamp_style.effect, + font_thickness=self.camera_config.timestamp_style.thickness, + font_color=(color.blue, color.green, color.red), + position=self.camera_config.timestamp_style.position, + ) + + if draw_options.get("paths"): + for obj in tracked_objects.values(): + if obj["frame_time"] == frame_time and obj["path_data"]: + color = self.config.model.colormap.get( + obj["label"], (255, 255, 255) + ) + + path_points = [ + ( + int(point[0][0] * self.camera_config.detect.width), + int(point[0][1] * self.camera_config.detect.height), + ) + for point in obj["path_data"] + ] + + for point in path_points: + cv2.circle(frame_copy, point, 5, color, -1) + + for i in range(1, len(path_points)): + cv2.line( + frame_copy, + path_points[i - 1], + path_points[i], + color, + 2, + ) + + bottom_center = ( + int((obj["box"][0] + obj["box"][2]) / 2), + int(obj["box"][3]), + ) + cv2.line( + frame_copy, + path_points[-1], + bottom_center, + color, + 2, + ) + + return frame_copy + + def finished(self, obj_id): + del self.tracked_objects[obj_id] + + def on(self, event_type: str, callback: Callable): + self.callbacks[event_type].append(callback) + + def update( + self, + frame_name: str, + frame_time: float, + current_detections: dict[str, dict[str, Any]], + motion_boxes: list[tuple[int, int, int, int]], + regions: list[tuple[int, int, int, int]], + ): + current_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + + tracked_objects = self.tracked_objects.copy() + current_ids = set(current_detections.keys()) + previous_ids = set(tracked_objects.keys()) + removed_ids = previous_ids.difference(current_ids) + new_ids = current_ids.difference(previous_ids) + updated_ids = current_ids.intersection(previous_ids) + + for id in new_ids: + logger.debug(f"{self.name}: New tracked object ID: {id}") + new_obj = tracked_objects[id] = TrackedObject( + self.config.model, + self.camera_config, + self.config.ui, + self.frame_cache, + current_detections[id], + ) + + # add initial frame to frame cache + logger.debug( + f"{self.name}: New object, adding {frame_time} to frame cache for {id}" + ) + self.frame_cache[frame_time] = { + "frame": np.copy(current_frame), + "object_id": id, + } + + # save initial thumbnail data and best object + thumbnail_data = { + "frame_time": frame_time, + "box": new_obj.obj_data["box"], + "area": new_obj.obj_data["area"], + "region": new_obj.obj_data["region"], + "score": new_obj.obj_data["score"], + "attributes": new_obj.obj_data["attributes"], + "current_estimated_speed": 0, + "velocity_angle": 0, + "path_data": [], + "recognized_license_plate": None, + "recognized_license_plate_score": None, + } + new_obj.thumbnail_data = thumbnail_data + tracked_objects[id].thumbnail_data = thumbnail_data + object_type = new_obj.obj_data["label"] + + # call event handlers + self.send_mqtt_snapshot(new_obj, object_type) + + for c in self.callbacks["start"]: + c(self.name, new_obj, frame_name) + + for id in updated_ids: + updated_obj = tracked_objects[id] + thumb_update, significant_update, path_update, autotracker_update = ( + updated_obj.update( + frame_time, current_detections[id], current_frame is not None + ) + ) + + if autotracker_update or significant_update: + for c in self.callbacks["autotrack"]: + c(self.name, updated_obj, frame_name) + + if thumb_update and current_frame is not None: + # ensure this frame is stored in the cache + if ( + updated_obj.thumbnail_data["frame_time"] == frame_time + and frame_time not in self.frame_cache + ): + logger.debug( + f"{self.name}: Existing object, adding {frame_time} to frame cache for {id}" + ) + self.frame_cache[frame_time] = { + "frame": np.copy(current_frame), + "object_id": id, + } + + updated_obj.last_updated = frame_time + + # if it has been more than 5 seconds since the last thumb update + # and the last update is greater than the last publish or + # the object has changed significantly or + # the object moved enough to update the path + if ( + ( + frame_time - updated_obj.last_published > 5 + and updated_obj.last_updated > updated_obj.last_published + ) + or significant_update + or path_update + ): + # call event handlers + for c in self.callbacks["update"]: + c(self.name, updated_obj, frame_name) + updated_obj.last_published = frame_time + + for id in removed_ids: + # publish events to mqtt + removed_obj = tracked_objects[id] + if "end_time" not in removed_obj.obj_data: + removed_obj.obj_data["end_time"] = frame_time + logger.debug(f"{self.name}: end callback for object {id}") + for c in self.callbacks["end"]: + c(self.name, removed_obj, frame_name) + + # TODO: can i switch to looking this up and only changing when an event ends? + # maintain best objects + camera_activity: dict[str, list[Any]] = { + "motion": len(motion_boxes) > 0, + "objects": [], + } + + for obj in tracked_objects.values(): + object_type = obj.obj_data["label"] + active = obj.is_active() + + if not obj.false_positive: + label = object_type + sub_label = None + + if obj.obj_data.get("sub_label"): + if ( + obj.obj_data.get("sub_label")[0] + in self.config.model.all_attributes + ): + label = obj.obj_data["sub_label"][0] + else: + label = f"{object_type}-verified" + sub_label = obj.obj_data["sub_label"][0] + + camera_activity["objects"].append( + { + "id": obj.obj_data["id"], + "label": label, + "stationary": not active, + "area": obj.obj_data["area"], + "ratio": obj.obj_data["ratio"], + "score": obj.obj_data["score"], + "sub_label": sub_label, + "current_zones": obj.current_zones, + } + ) + + # if we don't have access to the current frame or + # if the object's thumbnail is not from the current frame, skip + if ( + current_frame is None + or obj.thumbnail_data is None + or obj.false_positive + or obj.thumbnail_data["frame_time"] != frame_time + ): + continue + + if object_type in self.best_objects: + current_best = self.best_objects[object_type] + now = datetime.datetime.now().timestamp() + # if the object is a higher score than the current best score + # or the current object is older than desired, use the new object + if ( + is_better_thumbnail( + object_type, + current_best.thumbnail_data, + obj.thumbnail_data, + self.camera_config.frame_shape, + ) + or (now - current_best.thumbnail_data["frame_time"]) + > self.camera_config.best_image_timeout + ): + self.send_mqtt_snapshot(obj, object_type) + else: + self.send_mqtt_snapshot(obj, object_type) + + for c in self.callbacks["camera_activity"]: + c(self.name, camera_activity) + + # cleanup thumbnail frame cache + current_thumb_frames = { + obj.thumbnail_data["frame_time"] + for obj in tracked_objects.values() + if obj.thumbnail_data is not None + } + current_best_frames = { + obj.thumbnail_data["frame_time"] for obj in self.best_objects.values() + } + thumb_frames_to_delete = [ + t + for t in self.frame_cache.keys() + if t not in current_thumb_frames and t not in current_best_frames + ] + if len(thumb_frames_to_delete) > 0: + logger.debug(f"{self.name}: Current frame cache contents:") + for k, v in self.frame_cache.items(): + logger.debug(f" frame time: {k}, object id: {v['object_id']}") + for obj_id, obj in tracked_objects.items(): + thumb_time = ( + obj.thumbnail_data["frame_time"] if obj.thumbnail_data else None + ) + logger.debug( + f"{self.name}: Tracked object {obj_id} thumbnail frame_time: {thumb_time}, false positive: {obj.false_positive}" + ) + for t in thumb_frames_to_delete: + object_id = self.frame_cache[t].get("object_id", "unknown") + logger.debug(f"{self.name}: Deleting {t} from frame cache for {object_id}") + del self.frame_cache[t] + + with self.current_frame_lock: + self.tracked_objects = tracked_objects + self.motion_boxes = motion_boxes + self.regions = regions + + if current_frame is not None: + self.current_frame_time = frame_time + self._current_frame = np.copy(current_frame) + + if self.previous_frame_id is not None: + self.frame_manager.close(self.previous_frame_id) + + self.previous_frame_id = frame_name + + def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None: + for c in self.callbacks["snapshot"]: + updated = c(self.name, new_obj) + + # if the snapshot was not updated, then this object is not a best object + # but all new objects should be considered the next best object + # so we remove the label from the best objects + if updated: + self.best_objects[object_type] = new_obj + else: + if object_type in self.best_objects: + self.best_objects.pop(object_type) + break + + def save_manual_event_image( + self, + frame: np.ndarray | None, + event_id: str, + label: str, + draw: dict[str, list[dict]], + ) -> None: + img_frame = frame if frame is not None else self.get_current_frame() + + # write clean snapshot if enabled + if self.camera_config.snapshots.clean_copy: + ret, webp = cv2.imencode( + ".webp", img_frame, [int(cv2.IMWRITE_WEBP_QUALITY), 80] + ) + + if ret: + with open( + os.path.join( + CLIPS_DIR, + f"{self.camera_config.name}-{event_id}-clean.webp", + ), + "wb", + ) as p: + p.write(webp.tobytes()) + + # write jpg snapshot with optional annotations + if draw.get("boxes") and isinstance(draw.get("boxes"), list): + for box in draw.get("boxes"): + x = int(box["box"][0] * self.camera_config.detect.width) + y = int(box["box"][1] * self.camera_config.detect.height) + width = int(box["box"][2] * self.camera_config.detect.width) + height = int(box["box"][3] * self.camera_config.detect.height) + + draw_box_with_label( + img_frame, + x, + y, + x + width, + y + height, + label, + f"{box.get('score', '-')}% {int(width * height)}", + thickness=2, + color=box.get("color", (255, 0, 0)), + ) + + ret, jpg = cv2.imencode(".jpg", img_frame) + with open( + os.path.join(CLIPS_DIR, f"{self.camera_config.name}-{event_id}.jpg"), + "wb", + ) as j: + j.write(jpg.tobytes()) + + # create thumbnail with max height of 175 and save + width = int(175 * img_frame.shape[1] / img_frame.shape[0]) + thumb = cv2.resize(img_frame, dsize=(width, 175), interpolation=cv2.INTER_AREA) + thumb_path = os.path.join(THUMB_DIR, self.camera_config.name) + os.makedirs(thumb_path, exist_ok=True) + cv2.imwrite(os.path.join(thumb_path, f"{event_id}.webp"), thumb) + + def shutdown(self) -> None: + for obj in self.tracked_objects.values(): + if not obj.obj_data.get("end_time"): + obj.write_thumbnail_to_disk() diff --git a/sam2-cpu/frigate-dev/frigate/comms/base_communicator.py b/sam2-cpu/frigate-dev/frigate/comms/base_communicator.py new file mode 100644 index 0000000..5dfbf11 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/base_communicator.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod +from typing import Any, Callable + + +class Communicator(ABC): + """pub/sub model via specific protocol.""" + + @abstractmethod + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Send data via specific protocol.""" + pass + + @abstractmethod + def subscribe(self, receiver: Callable) -> None: + """Pass receiver so communicators can pass commands.""" + pass + + @abstractmethod + def stop(self) -> None: + """Stop the communicator.""" + pass diff --git a/sam2-cpu/frigate-dev/frigate/comms/config_updater.py b/sam2-cpu/frigate-dev/frigate/comms/config_updater.py new file mode 100644 index 0000000..447089a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/config_updater.py @@ -0,0 +1,59 @@ +"""Facilitates communication between processes.""" + +import multiprocessing as mp +from _pickle import UnpicklingError +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import zmq + +SOCKET_PUB_SUB = "ipc:///tmp/cache/config" + + +class ConfigPublisher: + """Publishes config changes to different processes.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.bind(SOCKET_PUB_SUB) + self.stop_event: MpEvent = mp.Event() + + def publish(self, topic: str, payload: Any) -> None: + """There is no communication back to the processes.""" + self.socket.send_string(topic, flags=zmq.SNDMORE) + self.socket.send_pyobj(payload) + + def stop(self) -> None: + self.stop_event.set() + self.socket.close() + self.context.destroy() + + +class ConfigSubscriber: + """Simplifies receiving an updated config.""" + + def __init__(self, topic: str, exact: bool = False) -> None: + self.topic = topic + self.exact = exact + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, topic) + self.socket.connect(SOCKET_PUB_SUB) + + def check_for_update(self) -> tuple[str, Any] | tuple[None, None]: + """Returns updated config or None if no update.""" + try: + topic = self.socket.recv_string(flags=zmq.NOBLOCK) + obj = self.socket.recv_pyobj() + + if not self.exact or self.topic == topic: + return (topic, obj) + else: + return (None, None) + except (zmq.ZMQError, UnicodeDecodeError, UnpicklingError): + return (None, None) + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/sam2-cpu/frigate-dev/frigate/comms/detections_updater.py b/sam2-cpu/frigate-dev/frigate/comms/detections_updater.py new file mode 100644 index 0000000..dff61c8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/detections_updater.py @@ -0,0 +1,42 @@ +"""Facilitates communication between processes.""" + +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + + +class DetectionTypeEnum(str, Enum): + all = "" + api = "api" + video = "video" + audio = "audio" + lpr = "lpr" + + +class DetectionPublisher(Publisher): + """Simplifies receiving video and audio detections.""" + + topic_base = "detection/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + +class DetectionSubscriber(Subscriber): + """Simplifies receiving video and audio detections.""" + + topic_base = "detection/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + def check_for_update( + self, timeout: float | None = None + ) -> tuple[str, Any] | tuple[None, None] | None: + return super().check_for_update(timeout) + + def _return_object(self, topic: str, payload: Any) -> Any: + if payload is None: + return (None, None) + return (topic[len(self.topic_base) :], payload) diff --git a/sam2-cpu/frigate-dev/frigate/comms/dispatcher.py b/sam2-cpu/frigate-dev/frigate/comms/dispatcher.py new file mode 100644 index 0000000..6e45ac1 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/dispatcher.py @@ -0,0 +1,843 @@ +"""Handle communication between Frigate and other applications.""" + +import datetime +import json +import logging +from typing import Any, Callable, Optional, cast + +from frigate.camera import PTZMetrics +from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager +from frigate.comms.base_communicator import Communicator +from frigate.comms.webpush import WebPushClient +from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdatePublisher, + CameraConfigUpdateTopic, +) +from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, + EXPIRE_AUDIO_ACTIVITY, + INSERT_MANY_RECORDINGS, + INSERT_PREVIEW, + NOTIFICATION_TEST, + REQUEST_REGION_GRID, + UPDATE_AUDIO_ACTIVITY, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + UPDATE_BIRDSEYE_LAYOUT, + UPDATE_CAMERA_ACTIVITY, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_EVENT_DESCRIPTION, + UPDATE_MODEL_STATE, + UPDATE_REVIEW_DESCRIPTION, + UPSERT_REVIEW_SEGMENT, +) +from frigate.models import Event, Previews, Recordings, ReviewSegment +from frigate.ptz.onvif import OnvifCommandEnum, OnvifController +from frigate.types import ModelStatusTypesEnum, TrackedObjectUpdateTypesEnum +from frigate.util.object import get_camera_regions_grid +from frigate.util.services import restart_frigate + +logger = logging.getLogger(__name__) + + +class Dispatcher: + """Handle communication between Frigate and communicators.""" + + def __init__( + self, + config: FrigateConfig, + config_updater: CameraConfigUpdatePublisher, + onvif: OnvifController, + ptz_metrics: dict[str, PTZMetrics], + communicators: list[Communicator], + ) -> None: + self.config = config + self.config_updater = config_updater + self.onvif = onvif + self.ptz_metrics = ptz_metrics + self.comms = communicators + self.camera_activity = CameraActivityManager(config, self.publish) + self.audio_activity = AudioActivityManager(config, self.publish) + self.model_state: dict[str, ModelStatusTypesEnum] = {} + self.embeddings_reindex: dict[str, Any] = {} + self.birdseye_layout: dict[str, Any] = {} + self.audio_transcription_state: str = "idle" + self._camera_settings_handlers: dict[str, Callable] = { + "audio": self._on_audio_command, + "audio_transcription": self._on_audio_transcription_command, + "detect": self._on_detect_command, + "enabled": self._on_enabled_command, + "improve_contrast": self._on_motion_improve_contrast_command, + "ptz_autotracker": self._on_ptz_autotracker_command, + "motion": self._on_motion_command, + "motion_contour_area": self._on_motion_contour_area_command, + "motion_threshold": self._on_motion_threshold_command, + "notifications": self._on_camera_notification_command, + "recordings": self._on_recordings_command, + "snapshots": self._on_snapshots_command, + "birdseye": self._on_birdseye_command, + "birdseye_mode": self._on_birdseye_mode_command, + "review_alerts": self._on_alerts_command, + "review_detections": self._on_detections_command, + "object_descriptions": self._on_object_description_command, + "review_descriptions": self._on_review_description_command, + } + self._global_settings_handlers: dict[str, Callable] = { + "notifications": self._on_global_notification_command, + } + + for comm in self.comms: + comm.subscribe(self._receive) + + self.web_push_client = next( + (comm for comm in communicators if isinstance(comm, WebPushClient)), None + ) + + def _receive(self, topic: str, payload: Any) -> Optional[Any]: + """Handle receiving of payload from communicators.""" + + def handle_camera_command( + command_type: str, camera_name: str, command: str, payload: str + ) -> None: + try: + if command_type == "set": + self._camera_settings_handlers[command](camera_name, payload) + elif command_type == "ptz": + self._on_ptz_command(camera_name, payload) + except KeyError: + logger.error(f"Invalid command type or handler: {command_type}") + + def handle_restart() -> None: + restart_frigate() + + def handle_insert_many_recordings() -> None: + Recordings.insert_many(payload).execute() + + def handle_request_region_grid() -> Any: + camera = payload + grid = get_camera_regions_grid( + camera, + self.config.cameras[camera].detect, + max(self.config.model.width, self.config.model.height), + ) + return grid + + def handle_insert_preview() -> None: + Previews.insert(payload).execute() + + def handle_upsert_review_segment() -> None: + ReviewSegment.insert(payload).on_conflict( + conflict_target=[ReviewSegment.id], + update=payload, + ).execute() + + def handle_clear_ongoing_review_segments() -> None: + ReviewSegment.update(end_time=datetime.datetime.now().timestamp()).where( + ReviewSegment.end_time.is_null(True) + ).execute() + + def handle_update_camera_activity() -> None: + self.camera_activity.update_activity(payload) + + def handle_update_audio_activity() -> None: + self.audio_activity.update_activity(payload) + + def handle_expire_audio_activity() -> None: + self.audio_activity.expire_all(payload) + + def handle_update_event_description() -> None: + event: Event = Event.get(Event.id == payload["id"]) + cast(dict, event.data)["description"] = payload["description"] + event.save() + self.publish( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event.id, + "description": event.data["description"], + "camera": event.camera, + } + ), + ) + + def handle_update_review_description() -> None: + final_data = payload["after"] + ReviewSegment.insert(final_data).on_conflict( + conflict_target=[ReviewSegment.id], + update=final_data, + ).execute() + self.publish("reviews", json.dumps(payload)) + + def handle_update_model_state() -> None: + if payload: + model = payload["model"] + state = payload["state"] + self.model_state[model] = ModelStatusTypesEnum[state] + self.publish("model_state", json.dumps(self.model_state)) + + def handle_model_state() -> None: + self.publish("model_state", json.dumps(self.model_state.copy())) + + def handle_update_audio_transcription_state() -> None: + if payload: + self.audio_transcription_state = payload + self.publish( + "audio_transcription_state", + json.dumps(self.audio_transcription_state), + ) + + def handle_audio_transcription_state() -> None: + self.publish( + "audio_transcription_state", json.dumps(self.audio_transcription_state) + ) + + def handle_update_embeddings_reindex_progress() -> None: + self.embeddings_reindex = payload + self.publish( + "embeddings_reindex_progress", + json.dumps(payload), + ) + + def handle_embeddings_reindex_progress() -> None: + self.publish( + "embeddings_reindex_progress", + json.dumps(self.embeddings_reindex.copy()), + ) + + def handle_update_birdseye_layout() -> None: + if payload: + self.birdseye_layout = payload + self.publish("birdseye_layout", json.dumps(self.birdseye_layout)) + + def handle_birdseye_layout() -> None: + self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) + + def handle_on_connect() -> None: + camera_status = self.camera_activity.last_camera_activity.copy() + audio_detections = self.audio_activity.current_audio_detections.copy() + cameras_with_status = camera_status.keys() + + for camera in self.config.cameras.keys(): + if camera not in cameras_with_status: + camera_status[camera] = {} + + camera_status[camera]["config"] = { + "detect": self.config.cameras[camera].detect.enabled, + "enabled": self.config.cameras[camera].enabled, + "snapshots": self.config.cameras[camera].snapshots.enabled, + "record": self.config.cameras[camera].record.enabled, + "audio": self.config.cameras[camera].audio.enabled, + "audio_transcription": self.config.cameras[ + camera + ].audio_transcription.live_enabled, + "notifications": self.config.cameras[camera].notifications.enabled, + "notifications_suspended": int( + self.web_push_client.suspended_cameras.get(camera, 0) + ) + if self.web_push_client + and camera in self.web_push_client.suspended_cameras + else 0, + "autotracking": self.config.cameras[ + camera + ].onvif.autotracking.enabled, + "alerts": self.config.cameras[camera].review.alerts.enabled, + "detections": self.config.cameras[camera].review.detections.enabled, + "object_descriptions": self.config.cameras[ + camera + ].objects.genai.enabled, + "review_descriptions": self.config.cameras[ + camera + ].review.genai.enabled, + } + + self.publish("camera_activity", json.dumps(camera_status)) + self.publish("model_state", json.dumps(self.model_state.copy())) + self.publish( + "embeddings_reindex_progress", + json.dumps(self.embeddings_reindex.copy()), + ) + self.publish("birdseye_layout", json.dumps(self.birdseye_layout.copy())) + self.publish("audio_detections", json.dumps(audio_detections)) + + def handle_notification_test() -> None: + self.publish("notification_test", "Test notification") + + # Dictionary mapping topic to handlers + topic_handlers = { + INSERT_MANY_RECORDINGS: handle_insert_many_recordings, + REQUEST_REGION_GRID: handle_request_region_grid, + INSERT_PREVIEW: handle_insert_preview, + UPSERT_REVIEW_SEGMENT: handle_upsert_review_segment, + CLEAR_ONGOING_REVIEW_SEGMENTS: handle_clear_ongoing_review_segments, + UPDATE_CAMERA_ACTIVITY: handle_update_camera_activity, + UPDATE_AUDIO_ACTIVITY: handle_update_audio_activity, + EXPIRE_AUDIO_ACTIVITY: handle_expire_audio_activity, + UPDATE_EVENT_DESCRIPTION: handle_update_event_description, + UPDATE_REVIEW_DESCRIPTION: handle_update_review_description, + UPDATE_MODEL_STATE: handle_update_model_state, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress, + UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout, + UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state, + NOTIFICATION_TEST: handle_notification_test, + "restart": handle_restart, + "embeddingsReindexProgress": handle_embeddings_reindex_progress, + "modelState": handle_model_state, + "audioTranscriptionState": handle_audio_transcription_state, + "birdseyeLayout": handle_birdseye_layout, + "onConnect": handle_on_connect, + } + + if topic.endswith("set") or topic.endswith("ptz") or topic.endswith("suspend"): + try: + parts = topic.split("/") + if len(parts) == 3 and topic.endswith("set"): + # example /cam_name/detect/set payload=ON|OFF + camera_name = parts[-3] + command = parts[-2] + handle_camera_command("set", camera_name, command, payload) + elif len(parts) == 2 and topic.endswith("set"): + command = parts[-2] + self._global_settings_handlers[command](payload) + elif len(parts) == 2 and topic.endswith("ptz"): + # example /cam_name/ptz payload=MOVE_UP|MOVE_DOWN|STOP... + camera_name = parts[-2] + handle_camera_command("ptz", camera_name, "", payload) + elif len(parts) == 3 and topic.endswith("suspend"): + # example /cam_name/notifications/suspend payload=duration + camera_name = parts[-3] + command = parts[-2] + self._on_camera_notification_suspend(camera_name, payload) + except IndexError: + logger.error( + f"Received invalid {topic.split('/')[-1]} command: {topic}" + ) + return None + elif topic in topic_handlers: + return topic_handlers[topic]() + else: + self.publish(topic, payload, retain=False) + return None + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Handle publishing to communicators.""" + for comm in self.comms: + comm.publish(topic, payload, retain) + + def stop(self) -> None: + for comm in self.comms: + comm.stop() + + def _on_detect_command(self, camera_name: str, payload: str) -> None: + """Callback for detect topic.""" + detect_settings = self.config.cameras[camera_name].detect + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not detect_settings.enabled: + logger.info(f"Turning on detection for {camera_name}") + detect_settings.enabled = True + + if not motion_settings.enabled: + logger.info( + f"Turning on motion for {camera_name} due to detection being enabled." + ) + motion_settings.enabled = True + self.config_updater.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.motion, camera_name + ), + motion_settings, + ) + self.publish(f"{camera_name}/motion/state", payload, retain=True) + elif payload == "OFF": + if detect_settings.enabled: + logger.info(f"Turning off detection for {camera_name}") + detect_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.detect, camera_name), + detect_settings, + ) + self.publish(f"{camera_name}/detect/state", payload, retain=True) + + def _on_enabled_command(self, camera_name: str, payload: str) -> None: + """Callback for camera topic.""" + camera_settings = self.config.cameras[camera_name] + + if payload == "ON": + if not self.config.cameras[camera_name].enabled_in_config: + logger.error( + "Camera must be enabled in the config to be turned on via MQTT." + ) + return + if not camera_settings.enabled: + logger.info(f"Turning on camera {camera_name}") + camera_settings.enabled = True + elif payload == "OFF": + if camera_settings.enabled: + logger.info(f"Turning off camera {camera_name}") + camera_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, camera_name), + camera_settings.enabled, + ) + self.publish(f"{camera_name}/enabled/state", payload, retain=True) + + def _on_motion_command(self, camera_name: str, payload: str) -> None: + """Callback for motion topic.""" + detect_settings = self.config.cameras[camera_name].detect + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not motion_settings.enabled: + logger.info(f"Turning on motion for {camera_name}") + motion_settings.enabled = True + elif payload == "OFF": + if detect_settings.enabled: + logger.error( + "Turning off motion is not allowed when detection is enabled." + ) + return + + if motion_settings.enabled: + logger.info(f"Turning off motion for {camera_name}") + motion_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion/state", payload, retain=True) + + def _on_motion_improve_contrast_command( + self, camera_name: str, payload: str + ) -> None: + """Callback for improve_contrast topic.""" + motion_settings = self.config.cameras[camera_name].motion + + if payload == "ON": + if not motion_settings.improve_contrast: + logger.info(f"Turning on improve contrast for {camera_name}") + motion_settings.improve_contrast = True + elif payload == "OFF": + if motion_settings.improve_contrast: + logger.info(f"Turning off improve contrast for {camera_name}") + motion_settings.improve_contrast = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/improve_contrast/state", payload, retain=True) + + def _on_ptz_autotracker_command(self, camera_name: str, payload: str) -> None: + """Callback for ptz_autotracker topic.""" + ptz_autotracker_settings = self.config.cameras[camera_name].onvif.autotracking + + if payload == "ON": + if not self.config.cameras[ + camera_name + ].onvif.autotracking.enabled_in_config: + logger.error( + "Autotracking must be enabled in the config to be turned on via MQTT." + ) + return + if not self.ptz_metrics[camera_name].autotracker_enabled.value: + logger.info(f"Turning on ptz autotracker for {camera_name}") + self.ptz_metrics[camera_name].autotracker_enabled.value = True + self.ptz_metrics[camera_name].start_time.value = 0 + ptz_autotracker_settings.enabled = True + elif payload == "OFF": + if self.ptz_metrics[camera_name].autotracker_enabled.value: + logger.info(f"Turning off ptz autotracker for {camera_name}") + self.ptz_metrics[camera_name].autotracker_enabled.value = False + self.ptz_metrics[camera_name].start_time.value = 0 + ptz_autotracker_settings.enabled = False + + self.publish(f"{camera_name}/ptz_autotracker/state", payload, retain=True) + + def _on_motion_contour_area_command(self, camera_name: str, payload: int) -> None: + """Callback for motion contour topic.""" + try: + payload = int(payload) + except ValueError: + f"Received unsupported value for motion contour area: {payload}" + return + + motion_settings = self.config.cameras[camera_name].motion + logger.info(f"Setting motion contour area for {camera_name}: {payload}") + motion_settings.contour_area = payload + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion_contour_area/state", payload, retain=True) + + def _on_motion_threshold_command(self, camera_name: str, payload: int) -> None: + """Callback for motion threshold topic.""" + try: + payload = int(payload) + except ValueError: + f"Received unsupported value for motion threshold: {payload}" + return + + motion_settings = self.config.cameras[camera_name].motion + logger.info(f"Setting motion threshold for {camera_name}: {payload}") + motion_settings.threshold = payload + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.motion, camera_name), + motion_settings, + ) + self.publish(f"{camera_name}/motion_threshold/state", payload, retain=True) + + def _on_global_notification_command(self, payload: str) -> None: + """Callback for global notification topic.""" + if payload != "ON" and payload != "OFF": + f"Received unsupported value for all notification: {payload}" + return + + notification_settings = self.config.notifications + logger.info(f"Setting all notifications: {payload}") + notification_settings.enabled = payload == "ON" + self.config_updater.publisher.publish( + "config/notifications", notification_settings + ) + self.publish("notifications/state", payload, retain=True) + + def _on_audio_command(self, camera_name: str, payload: str) -> None: + """Callback for audio topic.""" + audio_settings = self.config.cameras[camera_name].audio + + if payload == "ON": + if not self.config.cameras[camera_name].audio.enabled_in_config: + logger.error( + "Audio detection must be enabled in the config to be turned on via MQTT." + ) + return + + if not audio_settings.enabled: + logger.info(f"Turning on audio detection for {camera_name}") + audio_settings.enabled = True + elif payload == "OFF": + if audio_settings.enabled: + logger.info(f"Turning off audio detection for {camera_name}") + audio_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.audio, camera_name), + audio_settings, + ) + self.publish(f"{camera_name}/audio/state", payload, retain=True) + + def _on_audio_transcription_command(self, camera_name: str, payload: str) -> None: + """Callback for live audio transcription topic.""" + audio_transcription_settings = self.config.cameras[ + camera_name + ].audio_transcription + + if payload == "ON": + if not self.config.cameras[ + camera_name + ].audio_transcription.enabled_in_config: + logger.error( + "Audio transcription must be enabled in the config to be turned on via MQTT." + ) + return + + if not audio_transcription_settings.live_enabled: + logger.info(f"Turning on live audio transcription for {camera_name}") + audio_transcription_settings.live_enabled = True + elif payload == "OFF": + if audio_transcription_settings.live_enabled: + logger.info(f"Turning off live audio transcription for {camera_name}") + audio_transcription_settings.live_enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.audio_transcription, camera_name + ), + audio_transcription_settings, + ) + self.publish(f"{camera_name}/audio_transcription/state", payload, retain=True) + + def _on_recordings_command(self, camera_name: str, payload: str) -> None: + """Callback for recordings topic.""" + record_settings = self.config.cameras[camera_name].record + + if payload == "ON": + if not self.config.cameras[camera_name].record.enabled_in_config: + logger.error( + "Recordings must be enabled in the config to be turned on via MQTT." + ) + return + + if not record_settings.enabled: + logger.info(f"Turning on recordings for {camera_name}") + record_settings.enabled = True + elif payload == "OFF": + if record_settings.enabled: + logger.info(f"Turning off recordings for {camera_name}") + record_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.record, camera_name), + record_settings, + ) + self.publish(f"{camera_name}/recordings/state", payload, retain=True) + + def _on_snapshots_command(self, camera_name: str, payload: str) -> None: + """Callback for snapshots topic.""" + snapshots_settings = self.config.cameras[camera_name].snapshots + + if payload == "ON": + if not snapshots_settings.enabled: + logger.info(f"Turning on snapshots for {camera_name}") + snapshots_settings.enabled = True + elif payload == "OFF": + if snapshots_settings.enabled: + logger.info(f"Turning off snapshots for {camera_name}") + snapshots_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.snapshots, camera_name), + snapshots_settings, + ) + self.publish(f"{camera_name}/snapshots/state", payload, retain=True) + + def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None: + """Callback for ptz topic.""" + try: + preset: str = ( + payload.decode("utf-8") if isinstance(payload, bytes) else payload + ).lower() + + if "preset" in preset: + command = OnvifCommandEnum.preset + param = preset[preset.index("_") + 1 :] + elif "move_relative" in preset: + command = OnvifCommandEnum.move_relative + param = preset[preset.index("_") + 1 :] + else: + command = OnvifCommandEnum[preset] + param = "" + + self.onvif.handle_command(camera_name, command, param) + logger.info(f"Setting ptz command to {command} for {camera_name}") + except KeyError as k: + logger.error(f"Invalid PTZ command {preset}: {k}") + + def _on_birdseye_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye topic.""" + birdseye_settings = self.config.cameras[camera_name].birdseye + + if payload == "ON": + if not birdseye_settings.enabled: + logger.info(f"Turning on birdseye for {camera_name}") + birdseye_settings.enabled = True + + elif payload == "OFF": + if birdseye_settings.enabled: + logger.info(f"Turning off birdseye for {camera_name}") + birdseye_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.birdseye, camera_name), + birdseye_settings, + ) + self.publish(f"{camera_name}/birdseye/state", payload, retain=True) + + def _on_birdseye_mode_command(self, camera_name: str, payload: str) -> None: + """Callback for birdseye mode topic.""" + + if payload not in ["CONTINUOUS", "MOTION", "OBJECTS"]: + logger.info(f"Invalid birdseye_mode command: {payload}") + return + + birdseye_settings = self.config.cameras[camera_name].birdseye + + if not birdseye_settings.enabled: + logger.info(f"Birdseye mode not enabled for {camera_name}") + return + + birdseye_settings.mode = BirdseyeModeEnum(payload.lower()) + logger.info( + f"Setting birdseye mode for {camera_name} to {birdseye_settings.mode}" + ) + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.birdseye, camera_name), + birdseye_settings, + ) + self.publish(f"{camera_name}/birdseye_mode/state", payload, retain=True) + + def _on_camera_notification_command(self, camera_name: str, payload: str) -> None: + """Callback for camera level notifications topic.""" + notification_settings = self.config.cameras[camera_name].notifications + + if payload == "ON": + if not self.config.cameras[camera_name].notifications.enabled_in_config: + logger.error( + "Notifications must be enabled in the config to be turned on via MQTT." + ) + return + + if not notification_settings.enabled: + logger.info(f"Turning on notifications for {camera_name}") + notification_settings.enabled = True + if ( + self.web_push_client + and camera_name in self.web_push_client.suspended_cameras + ): + self.web_push_client.suspended_cameras[camera_name] = 0 + elif payload == "OFF": + if notification_settings.enabled: + logger.info(f"Turning off notifications for {camera_name}") + notification_settings.enabled = False + if ( + self.web_push_client + and camera_name in self.web_push_client.suspended_cameras + ): + self.web_push_client.suspended_cameras[camera_name] = 0 + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.notifications, camera_name), + notification_settings, + ) + self.publish(f"{camera_name}/notifications/state", payload, retain=True) + self.publish(f"{camera_name}/notifications/suspended", "0", retain=True) + + def _on_camera_notification_suspend(self, camera_name: str, payload: str) -> None: + """Callback for camera level notifications suspend topic.""" + try: + duration = int(payload) + except ValueError: + logger.error(f"Invalid suspension duration: {payload}") + return + + if self.web_push_client is None: + logger.error("WebPushClient not available for suspension") + return + + notification_settings = self.config.cameras[camera_name].notifications + + if not notification_settings.enabled: + logger.error(f"Notifications are not enabled for {camera_name}") + return + + if duration != 0: + self.web_push_client.suspend_notifications(camera_name, duration) + else: + self.web_push_client.unsuspend_notifications(camera_name) + + self.publish( + f"{camera_name}/notifications/suspended", + str( + int(self.web_push_client.suspended_cameras.get(camera_name, 0)) + if camera_name in self.web_push_client.suspended_cameras + else 0 + ), + retain=True, + ) + + def _on_alerts_command(self, camera_name: str, payload: str) -> None: + """Callback for alerts topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.alerts.enabled_in_config: + logger.error( + "Alerts must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.alerts.enabled: + logger.info(f"Turning on alerts for {camera_name}") + review_settings.alerts.enabled = True + elif payload == "OFF": + if review_settings.alerts.enabled: + logger.info(f"Turning off alerts for {camera_name}") + review_settings.alerts.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review, camera_name), + review_settings, + ) + self.publish(f"{camera_name}/review_alerts/state", payload, retain=True) + + def _on_detections_command(self, camera_name: str, payload: str) -> None: + """Callback for detections topic.""" + review_settings = self.config.cameras[camera_name].review + + if payload == "ON": + if not self.config.cameras[camera_name].review.detections.enabled_in_config: + logger.error( + "Detections must be enabled in the config to be turned on via MQTT." + ) + return + + if not review_settings.detections.enabled: + logger.info(f"Turning on detections for {camera_name}") + review_settings.detections.enabled = True + elif payload == "OFF": + if review_settings.detections.enabled: + logger.info(f"Turning off detections for {camera_name}") + review_settings.detections.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review, camera_name), + review_settings, + ) + self.publish(f"{camera_name}/review_detections/state", payload, retain=True) + + def _on_object_description_command(self, camera_name: str, payload: str) -> None: + """Callback for object description topic.""" + genai_settings = self.config.cameras[camera_name].objects.genai + + if payload == "ON": + if not self.config.cameras[camera_name].objects.genai.enabled_in_config: + logger.error( + "GenAI must be enabled in the config to be turned on via MQTT." + ) + return + + if not genai_settings.enabled: + logger.info(f"Turning on object descriptions for {camera_name}") + genai_settings.enabled = True + elif payload == "OFF": + if genai_settings.enabled: + logger.info(f"Turning off object descriptions for {camera_name}") + genai_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.object_genai, camera_name), + genai_settings, + ) + self.publish(f"{camera_name}/object_descriptions/state", payload, retain=True) + + def _on_review_description_command(self, camera_name: str, payload: str) -> None: + """Callback for review description topic.""" + genai_settings = self.config.cameras[camera_name].review.genai + + if payload == "ON": + if not self.config.cameras[camera_name].review.genai.enabled_in_config: + logger.error( + "GenAI Alerts or Detections must be enabled in the config to be turned on via MQTT." + ) + return + + if not genai_settings.enabled: + logger.info(f"Turning on review descriptions for {camera_name}") + genai_settings.enabled = True + elif payload == "OFF": + if genai_settings.enabled: + logger.info(f"Turning off review descriptions for {camera_name}") + genai_settings.enabled = False + + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.review_genai, camera_name), + genai_settings, + ) + self.publish(f"{camera_name}/review_descriptions/state", payload, retain=True) diff --git a/sam2-cpu/frigate-dev/frigate/comms/embeddings_updater.py b/sam2-cpu/frigate-dev/frigate/comms/embeddings_updater.py new file mode 100644 index 0000000..f7fd9c2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/embeddings_updater.py @@ -0,0 +1,91 @@ +"""Facilitates communication between processes.""" + +import logging +from enum import Enum +from typing import Any, Callable + +import zmq + +logger = logging.getLogger(__name__) + + +SOCKET_REP_REQ = "ipc:///tmp/cache/embeddings" + + +class EmbeddingsRequestEnum(Enum): + # audio + transcribe_audio = "transcribe_audio" + # custom classification + reload_classification_model = "reload_classification_model" + # face + clear_face_classifier = "clear_face_classifier" + recognize_face = "recognize_face" + register_face = "register_face" + reprocess_face = "reprocess_face" + # semantic search + embed_description = "embed_description" + embed_thumbnail = "embed_thumbnail" + generate_search = "generate_search" + reindex = "reindex" + # LPR + reprocess_plate = "reprocess_plate" + # Review Descriptions + summarize_review = "summarize_review" + + +class EmbeddingsResponder: + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(SOCKET_REP_REQ) + + def check_for_request(self, process: Callable) -> None: + while True: # load all messages that are queued + has_message, _, _ = zmq.select([self.socket], [], [], 0.01) + + if not has_message: + break + + try: + raw = self.socket.recv_json(flags=zmq.NOBLOCK) + + if isinstance(raw, list): + (topic, value) = raw + response = process(topic, value) + else: + logging.warning( + f"Received unexpected data type in ZMQ recv_json: {type(raw)}" + ) + response = None + + if response is not None: + self.socket.send_json(response) + else: + self.socket.send_json([]) + except zmq.ZMQError: + break + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + +class EmbeddingsRequestor: + """Simplifies sending data to EmbeddingsResponder and getting a reply.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect(SOCKET_REP_REQ) + + def send_data(self, topic: str, data: Any) -> Any: + """Sends data and then waits for reply.""" + try: + self.socket.send_json((topic, data)) + return self.socket.recv_json() + except zmq.ZMQError: + return "" + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/sam2-cpu/frigate-dev/frigate/comms/event_metadata_updater.py b/sam2-cpu/frigate-dev/frigate/comms/event_metadata_updater.py new file mode 100644 index 0000000..8977788 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/event_metadata_updater.py @@ -0,0 +1,49 @@ +"""Facilitates communication between processes.""" + +import logging +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class EventMetadataTypeEnum(str, Enum): + all = "" + manual_event_create = "manual_event_create" + manual_event_end = "manual_event_end" + regenerate_description = "regenerate_description" + sub_label = "sub_label" + attribute = "attribute" + lpr_event_create = "lpr_event_create" + save_lpr_snapshot = "save_lpr_snapshot" + + +class EventMetadataPublisher(Publisher): + """Simplifies receiving event metadata.""" + + topic_base = "event_metadata/" + + def __init__(self) -> None: + super().__init__() + + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class EventMetadataSubscriber(Subscriber): + """Simplifies receiving event metadata.""" + + topic_base = "event_metadata/" + + def __init__(self, topic: EventMetadataTypeEnum) -> None: + super().__init__(topic.value) + + def _return_object( + self, topic: str, payload: tuple | None + ) -> tuple[str, Any] | tuple[None, None]: + if payload is None: + return (None, None) + + return (topic, payload) diff --git a/sam2-cpu/frigate-dev/frigate/comms/events_updater.py b/sam2-cpu/frigate-dev/frigate/comms/events_updater.py new file mode 100644 index 0000000..cfd958d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/events_updater.py @@ -0,0 +1,61 @@ +"""Facilitates communication between processes.""" + +from typing import Any + +from frigate.events.types import EventStateEnum, EventTypeEnum + +from .zmq_proxy import Publisher, Subscriber + + +class EventUpdatePublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]] +): + """Publishes events (objects, audio, manual).""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("update") + + def publish( + self, + payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]], + sub_topic: str = "", + ) -> None: + super().publish(payload, sub_topic) + + +class EventUpdateSubscriber(Subscriber): + """Receives event updates.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("update") + + +class EventEndPublisher( + Publisher[tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]]] +): + """Publishes events that have ended.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("finalized") + + def publish( + self, + payload: tuple[EventTypeEnum, EventStateEnum, str, dict[str, Any]], + sub_topic: str = "", + ) -> None: + super().publish(payload, sub_topic) + + +class EventEndSubscriber(Subscriber): + """Receives events that have ended.""" + + topic_base = "event/" + + def __init__(self) -> None: + super().__init__("finalized") diff --git a/sam2-cpu/frigate-dev/frigate/comms/inter_process.py b/sam2-cpu/frigate-dev/frigate/comms/inter_process.py new file mode 100644 index 0000000..e4aad91 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/inter_process.py @@ -0,0 +1,86 @@ +"""Facilitates communication between processes.""" + +import logging +import multiprocessing as mp +import threading +from multiprocessing.synchronize import Event as MpEvent +from typing import Any, Callable + +import zmq + +from frigate.comms.base_communicator import Communicator + +logger = logging.getLogger(__name__) + +SOCKET_REP_REQ = "ipc:///tmp/cache/comms" + + +class InterProcessCommunicator(Communicator): + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REP) + self.socket.bind(SOCKET_REP_REQ) + self.stop_event: MpEvent = mp.Event() + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """There is no communication back to the processes.""" + pass + + def subscribe(self, receiver: Callable) -> None: + self._dispatcher = receiver + self.reader_thread = threading.Thread(target=self.read) + self.reader_thread.start() + + def read(self) -> None: + while not self.stop_event.is_set(): + while True: # load all messages that are queued + has_message, _, _ = zmq.select([self.socket], [], [], 1) + + if not has_message: + break + + try: + raw = self.socket.recv_json(flags=zmq.NOBLOCK) + + if isinstance(raw, list): + (topic, value) = raw + response = self._dispatcher(topic, value) + else: + logging.warning( + f"Received unexpected data type in ZMQ recv_json: {type(raw)}" + ) + response = None + + if response is not None: + self.socket.send_json(response) + else: + self.socket.send_json([]) + except zmq.ZMQError: + break + + def stop(self) -> None: + self.stop_event.set() + self.reader_thread.join() + self.socket.close() + self.context.destroy() + + +class InterProcessRequestor: + """Simplifies sending data to InterProcessCommunicator and getting a reply.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + self.socket.connect(SOCKET_REP_REQ) + + def send_data(self, topic: str, data: Any) -> Any: + """Sends data and then waits for reply.""" + try: + self.socket.send_json((topic, data)) + return self.socket.recv_json() + except zmq.ZMQError: + return "" + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/sam2-cpu/frigate-dev/frigate/comms/mqtt.py b/sam2-cpu/frigate-dev/frigate/comms/mqtt.py new file mode 100644 index 0000000..0af56e2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/mqtt.py @@ -0,0 +1,283 @@ +import logging +import threading +from typing import Any, Callable + +import paho.mqtt.client as mqtt +from paho.mqtt.enums import CallbackAPIVersion + +from frigate.comms.base_communicator import Communicator +from frigate.config import FrigateConfig + +logger = logging.getLogger(__name__) + + +class MqttClient(Communicator): + """Frigate wrapper for mqtt client.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.mqtt_config = config.mqtt + self.connected = False + + def subscribe(self, receiver: Callable) -> None: + """Wrapper for allowing dispatcher to subscribe.""" + self._dispatcher = receiver + self._start() + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Wrapper for publishing when client is in valid state.""" + if not self.connected: + logger.debug(f"Unable to publish to {topic}: client is not connected") + return + + self.client.publish( + f"{self.mqtt_config.topic_prefix}/{topic}", + payload, + qos=self.config.mqtt.qos, + retain=retain, + ) + + def stop(self) -> None: + self.client.disconnect() + + def _set_initial_topics(self) -> None: + """Set initial state topics.""" + for camera_name, camera in self.config.cameras.items(): + self.publish( + f"{camera_name}/enabled/state", + "ON" if camera.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/recordings/state", + "ON" if camera.record.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/snapshots/state", + "ON" if camera.snapshots.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/audio/state", + "ON" if camera.audio.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/detect/state", + "ON" if camera.detect.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/motion/state", + "ON", + retain=True, + ) + self.publish( + f"{camera_name}/improve_contrast/state", + "ON" if camera.motion.improve_contrast else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/ptz_autotracker/state", + "ON" if camera.onvif.autotracking.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/motion_threshold/state", + camera.motion.threshold, + retain=True, + ) + self.publish( + f"{camera_name}/motion_contour_area/state", + camera.motion.contour_area, + retain=True, + ) + self.publish( + f"{camera_name}/motion", + "OFF", + retain=False, + ) + self.publish( + f"{camera_name}/birdseye/state", + "ON" if camera.birdseye.enabled else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/birdseye_mode/state", + ( + camera.birdseye.mode.value.upper() + if camera.birdseye.enabled + else "OFF" + ), + retain=True, + ) + self.publish( + f"{camera_name}/review_alerts/state", + "ON" if camera.review.alerts.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/review_detections/state", + "ON" if camera.review.detections.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/object_descriptions/state", + "ON" if camera.objects.genai.enabled_in_config else "OFF", + retain=True, + ) + self.publish( + f"{camera_name}/review_descriptions/state", + "ON" if camera.review.genai.enabled_in_config else "OFF", + retain=True, + ) + + if self.config.notifications.enabled_in_config: + self.publish( + "notifications/state", + "ON" if self.config.notifications.enabled else "OFF", + retain=True, + ) + + self.publish("available", "online", retain=True) + + def on_mqtt_command( + self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage + ) -> None: + self._dispatcher( + message.topic.replace(f"{self.mqtt_config.topic_prefix}/", "", 1), + message.payload.decode(), + ) + + def _on_connect( + self, + client: mqtt.Client, + userdata: Any, + flags: Any, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] + properties: Any, + ) -> None: + """Mqtt connection callback.""" + threading.current_thread().name = "mqtt" + if reason_code != 0: + if reason_code == "Server unavailable": + logger.error( + "Unable to connect to MQTT server: MQTT Server unavailable" + ) + elif reason_code == "Bad user name or password": + logger.error( + "Unable to connect to MQTT server: MQTT Bad username or password" + ) + elif reason_code == "Not authorized": + logger.error("Unable to connect to MQTT server: MQTT Not authorized") + else: + logger.error( + "Unable to connect to MQTT server: Connection refused. Error code: " + + reason_code.getName() + ) + + self.connected = True + logger.debug("MQTT connected") + client.subscribe(f"{self.mqtt_config.topic_prefix}/#", qos=self.config.mqtt.qos) + self._set_initial_topics() + + def _on_disconnect( + self, + client: mqtt.Client, + userdata: Any, + flags: Any, + reason_code: mqtt.ReasonCode, # type: ignore[name-defined] + properties: Any, + ) -> None: + """Mqtt disconnection callback.""" + self.connected = False + logger.error("MQTT disconnected") + + def _start(self) -> None: + """Start mqtt client.""" + self.client = mqtt.Client( + callback_api_version=CallbackAPIVersion.VERSION2, + client_id=self.mqtt_config.client_id, + ) + self.client.on_connect = self._on_connect + self.client.on_disconnect = self._on_disconnect + self.client.will_set( + self.mqtt_config.topic_prefix + "/available", + payload="offline", + qos=1, + retain=True, + ) + + # register callbacks + callback_types = [ + "enabled", + "recordings", + "snapshots", + "detect", + "audio", + "motion", + "improve_contrast", + "ptz_autotracker", + "motion_threshold", + "motion_contour_area", + "birdseye", + "birdseye_mode", + "review_alerts", + "review_detections", + "genai", + ] + + for name in self.config.cameras.keys(): + for callback in callback_types: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/{callback}/set", + self.on_mqtt_command, + ) + + if self.config.cameras[name].onvif.host: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/ptz", + self.on_mqtt_command, + ) + + if self.config.notifications.enabled_in_config: + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/notifications/set", + self.on_mqtt_command, + ) + + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/onConnect", self.on_mqtt_command + ) + + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/restart", self.on_mqtt_command + ) + + if self.mqtt_config.tls_ca_certs is not None: + if ( + self.mqtt_config.tls_client_cert is not None + and self.mqtt_config.tls_client_key is not None + ): + self.client.tls_set( + self.mqtt_config.tls_ca_certs, + self.mqtt_config.tls_client_cert, + self.mqtt_config.tls_client_key, + ) + else: + self.client.tls_set(self.mqtt_config.tls_ca_certs) + if self.mqtt_config.tls_insecure is not None: + self.client.tls_insecure_set(self.mqtt_config.tls_insecure) + if self.mqtt_config.user is not None: + self.client.username_pw_set( + self.mqtt_config.user, password=self.mqtt_config.password + ) + try: + # https://stackoverflow.com/a/55390477 + # with connect_async, retries are handled automatically + self.client.connect_async(self.mqtt_config.host, self.mqtt_config.port, 60) + self.client.loop_start() + except Exception as e: + logger.error(f"Unable to connect to MQTT server: {e}") + return diff --git a/sam2-cpu/frigate-dev/frigate/comms/object_detector_signaler.py b/sam2-cpu/frigate-dev/frigate/comms/object_detector_signaler.py new file mode 100644 index 0000000..e8871db --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/object_detector_signaler.py @@ -0,0 +1,92 @@ +"""Facilitates communication between processes for object detection signals.""" + +import threading + +import zmq + +SOCKET_PUB = "ipc:///tmp/cache/detector_pub" +SOCKET_SUB = "ipc:///tmp/cache/detector_sub" + + +class ZmqProxyRunner(threading.Thread): + def __init__(self, context: zmq.Context[zmq.Socket]) -> None: + super().__init__(name="detector_proxy") + self.context = context + + def run(self) -> None: + """Run the proxy.""" + incoming = self.context.socket(zmq.XSUB) + incoming.bind(SOCKET_PUB) + outgoing = self.context.socket(zmq.XPUB) + outgoing.bind(SOCKET_SUB) + + # Blocking: This will unblock (via exception) when we destroy the context + # The incoming and outgoing sockets will be closed automatically + # when the context is destroyed as well. + try: + zmq.proxy(incoming, outgoing) + except zmq.ZMQError: + pass + + +class DetectorProxy: + """Proxies object detection signals.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.runner = ZmqProxyRunner(self.context) + self.runner.start() + + def stop(self) -> None: + # destroying the context will tell the proxy to stop + self.context.destroy() + self.runner.join() + + +class ObjectDetectorPublisher: + """Publishes signal for object detection to different processes.""" + + topic_base = "object_detector/" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.connect(SOCKET_PUB) + + def publish(self, sub_topic: str = "") -> None: + """Publish message.""" + self.socket.send_string(f"{self.topic}{sub_topic}/") + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + +class ObjectDetectorSubscriber: + """Simplifies receiving a signal for object detection.""" + + topic_base = "object_detector/" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}/" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) + self.socket.connect(SOCKET_SUB) + + def check_for_update(self, timeout: float = 5) -> str | None: + """Returns message or None if no update.""" + try: + has_update, _, _ = zmq.select([self.socket], [], [], timeout) + + if has_update: + return self.socket.recv_string(flags=zmq.NOBLOCK) + except zmq.ZMQError: + pass + + return None + + def stop(self) -> None: + self.socket.close() + self.context.destroy() diff --git a/sam2-cpu/frigate-dev/frigate/comms/recordings_updater.py b/sam2-cpu/frigate-dev/frigate/comms/recordings_updater.py new file mode 100644 index 0000000..249c2f6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/recordings_updater.py @@ -0,0 +1,46 @@ +"""Facilitates communication between processes.""" + +import logging +from enum import Enum +from typing import Any + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class RecordingsDataTypeEnum(str, Enum): + all = "" + saved = "saved" # segment has been saved to db + latest = "latest" # segment is in cache + valid = "valid" # segment is valid + invalid = "invalid" # segment is invalid + + +class RecordingsDataPublisher(Publisher[Any]): + """Publishes latest recording data.""" + + topic_base = "recordings/" + + def __init__(self) -> None: + super().__init__() + + def publish(self, payload: Any, sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class RecordingsDataSubscriber(Subscriber): + """Receives latest recording data.""" + + topic_base = "recordings/" + + def __init__(self, topic: RecordingsDataTypeEnum) -> None: + super().__init__(topic.value) + + def _return_object( + self, topic: str, payload: tuple | None + ) -> tuple[str, Any] | tuple[None, None]: + if payload is None: + return (None, None) + + return (topic, payload) diff --git a/sam2-cpu/frigate-dev/frigate/comms/review_updater.py b/sam2-cpu/frigate-dev/frigate/comms/review_updater.py new file mode 100644 index 0000000..2b3a5b3 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/review_updater.py @@ -0,0 +1,30 @@ +"""Facilitates communication between processes.""" + +import logging + +from .zmq_proxy import Publisher, Subscriber + +logger = logging.getLogger(__name__) + + +class ReviewDataPublisher( + Publisher +): # update when typing improvement is added Publisher[tuple[str, float]] + """Publishes review item data.""" + + topic_base = "review/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) + + def publish(self, payload: tuple[str, float], sub_topic: str = "") -> None: + super().publish(payload, sub_topic) + + +class ReviewDataSubscriber(Subscriber): + """Receives review item data.""" + + topic_base = "review/" + + def __init__(self, topic: str) -> None: + super().__init__(topic) diff --git a/sam2-cpu/frigate-dev/frigate/comms/webpush.py b/sam2-cpu/frigate-dev/frigate/comms/webpush.py new file mode 100644 index 0000000..32eeb40 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/webpush.py @@ -0,0 +1,482 @@ +"""Handle sending notifications for Frigate via Firebase.""" + +import datetime +import json +import logging +import os +import queue +import threading +from dataclasses import dataclass +from multiprocessing.synchronize import Event as MpEvent +from typing import Any, Callable + +from py_vapid import Vapid01 +from pywebpush import WebPusher +from titlecase import titlecase + +from frigate.comms.base_communicator import Communicator +from frigate.comms.config_updater import ConfigSubscriber +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import BASE_DIR, CONFIG_DIR +from frigate.models import User + +logger = logging.getLogger(__name__) + + +@dataclass +class PushNotification: + user: str + payload: dict[str, Any] + title: str + message: str + direct_url: str = "" + image: str = "" + notification_type: str = "alert" + ttl: int = 0 + + +class WebPushClient(Communicator): + """Frigate wrapper for webpush client.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + self.config = config + self.stop_event = stop_event + self.claim_headers: dict[str, dict[str, str]] = {} + self.refresh: int = 0 + self.web_pushers: dict[str, list[WebPusher]] = {} + self.expired_subs: dict[str, list[str]] = {} + self.suspended_cameras: dict[str, int] = { + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() + } + self.last_camera_notification_time: dict[str, float] = { + c.name: 0 # type: ignore[misc] + for c in self.config.cameras.values() + } + self.last_notification_time: float = 0 + self.notification_queue: queue.Queue[PushNotification] = queue.Queue() + self.notification_thread = threading.Thread( + target=self._process_notifications, daemon=True + ) + self.notification_thread.start() + + if not self.config.notifications.email: + logger.warning("Email must be provided for push notifications to be sent.") + + # Pull keys from PEM or generate if they do not exist + self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem")) + + users: list[dict[str, Any]] = ( + User.select(User.username, User.notification_tokens).dicts().iterator() + ) + for user in users: + self.web_pushers[user["username"]] = [] + for sub in user["notification_tokens"]: + self.web_pushers[user["username"]].append(WebPusher(sub)) + + # notification config updater + self.global_config_subscriber = ConfigSubscriber( + "config/notifications", exact=True + ) + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, self.config.cameras, [CameraConfigUpdateEnum.notifications] + ) + + def subscribe(self, receiver: Callable) -> None: + """Wrapper for allowing dispatcher to subscribe.""" + pass + + def check_registrations(self) -> None: + # check for valid claim or create new one + now = datetime.datetime.now().timestamp() + if len(self.claim_headers) == 0 or self.refresh < now: + self.refresh = int( + (datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp() + ) + endpoints: set[str] = set() + + # get a unique set of push endpoints + for pushers in self.web_pushers.values(): + for push in pushers: + endpoint: str = push.subscription_info["endpoint"] + endpoints.add(endpoint[0 : endpoint.index("/", 10)]) + + # create new claim + for endpoint in endpoints: + claim = { + "sub": f"mailto:{self.config.notifications.email}", + "aud": endpoint, + "exp": self.refresh, + } + self.claim_headers[endpoint] = self.vapid.sign(claim) + + def cleanup_registrations(self) -> None: + # delete any expired subs + if len(self.expired_subs) > 0: + for user, expired in self.expired_subs.items(): + user_subs = [] + + # get all subscriptions, removing ones that are expired + stored_user: User = User.get_by_id(user) + for token in stored_user.notification_tokens: + if token["endpoint"] in expired: + continue + + user_subs.append(token) + + # overwrite the database and reset web pushers + User.update(notification_tokens=user_subs).where( + User.username == user + ).execute() + + self.web_pushers[user] = [] + + for sub in user_subs: + self.web_pushers[user].append(WebPusher(sub)) + + logger.info( + f"Cleaned up {len(expired)} notification subscriptions for {user}" + ) + + self.expired_subs = {} + + def suspend_notifications(self, camera: str, minutes: int) -> None: + """Suspend notifications for a specific camera.""" + suspend_until = int( + (datetime.datetime.now() + datetime.timedelta(minutes=minutes)).timestamp() + ) + self.suspended_cameras[camera] = suspend_until + logger.info( + f"Notifications for {camera} suspended until {datetime.datetime.fromtimestamp(suspend_until).strftime('%Y-%m-%d %H:%M:%S')}" + ) + + def unsuspend_notifications(self, camera: str) -> None: + """Unsuspend notifications for a specific camera.""" + self.suspended_cameras[camera] = 0 + logger.info(f"Notifications for {camera} unsuspended") + + def is_camera_suspended(self, camera: str) -> bool: + return datetime.datetime.now().timestamp() <= self.suspended_cameras[camera] + + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: + """Wrapper for publishing when client is in valid state.""" + # check for updated notification config + _, updated_notification_config = ( + self.global_config_subscriber.check_for_update() + ) + + if updated_notification_config: + self.config.notifications = updated_notification_config + + updates = self.config_subscriber.check_for_updates() + + if "add" in updates: + for camera in updates["add"]: + self.suspended_cameras[camera] = 0 + self.last_camera_notification_time[camera] = 0 + + if topic == "reviews": + decoded = json.loads(payload) + camera = decoded["before"]["camera"] + if not self.config.cameras[camera].notifications.enabled: + return + if self.is_camera_suspended(camera): + logger.debug(f"Notifications for {camera} are currently suspended.") + return + self.send_alert(decoded) + if topic == "triggers": + decoded = json.loads(payload) + + camera = decoded["camera"] + name = decoded["name"] + + # ensure notifications are enabled and the specific trigger has + # notification action enabled + if ( + not self.config.cameras[camera].notifications.enabled + or name not in self.config.cameras[camera].semantic_search.triggers + or "notification" + not in self.config.cameras[camera] + .semantic_search.triggers[name] + .actions + ): + return + + if self.is_camera_suspended(camera): + logger.debug(f"Notifications for {camera} are currently suspended.") + return + self.send_trigger(decoded) + elif topic == "notification_test": + if not self.config.notifications.enabled and not any( + cam.notifications.enabled for cam in self.config.cameras.values() + ): + logger.debug( + "No cameras have notifications enabled, test notification not sent" + ) + return + self.send_notification_test() + + def send_push_notification( + self, + user: str, + payload: dict[str, Any], + title: str, + message: str, + direct_url: str = "", + image: str = "", + notification_type: str = "alert", + ttl: int = 0, + ) -> None: + notification = PushNotification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + notification_type=notification_type, + ttl=ttl, + ) + self.notification_queue.put(notification) + + def _process_notifications(self) -> None: + while not self.stop_event.is_set(): + try: + notification = self.notification_queue.get(timeout=1.0) + self.check_registrations() + + for pusher in self.web_pushers[notification.user]: + endpoint = pusher.subscription_info["endpoint"] + headers = self.claim_headers[ + endpoint[: endpoint.index("/", 10)] + ].copy() + headers["urgency"] = "high" + + resp = pusher.send( + headers=headers, + ttl=notification.ttl, + data=json.dumps( + { + "title": notification.title, + "message": notification.message, + "direct_url": notification.direct_url, + "image": notification.image, + "id": notification.payload.get("after", {}).get( + "id", "" + ), + "type": notification.notification_type, + } + ), + timeout=10, + ) + + if resp.status_code in (404, 410): + self.expired_subs.setdefault(notification.user, []).append( + endpoint + ) + logger.debug( + f"Notification endpoint expired for {notification.user}, received {resp.status_code}" + ) + elif resp.status_code != 201: + logger.warning( + f"Failed to send notification to {notification.user} :: {resp.status_code}" + ) + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error processing notification: {str(e)}") + + def _within_cooldown(self, camera: str) -> bool: + now = datetime.datetime.now().timestamp() + if now - self.last_notification_time < self.config.notifications.cooldown: + logger.debug( + f"Skipping notification for {camera} - in global cooldown period" + ) + return True + if ( + now - self.last_camera_notification_time[camera] + < self.config.cameras[camera].notifications.cooldown + ): + logger.debug( + f"Skipping notification for {camera} - in camera-specific cooldown period" + ) + return True + return False + + def send_notification_test(self) -> None: + if not self.config.notifications.email: + return + + self.check_registrations() + + logger.debug("Sending test notification") + + for user in self.web_pushers: + self.send_push_notification( + user=user, + payload={}, + title="Test Notification", + message="This is a test notification from Frigate.", + direct_url="/", + notification_type="test", + ) + + def send_alert(self, payload: dict[str, Any]) -> None: + if ( + not self.config.notifications.email + or payload["after"]["severity"] != "alert" + ): + return + + camera: str = payload["after"]["camera"] + camera_name: str = getattr( + self.config.cameras[camera], "friendly_name", None + ) or titlecase(camera.replace("_", " ")) + current_time = datetime.datetime.now().timestamp() + + if self._within_cooldown(camera): + return + + self.check_registrations() + + state = payload["type"] + + # Don't notify if message is an update and important fields don't have an update + if ( + state == "update" + and len(payload["before"]["data"]["objects"]) + == len(payload["after"]["data"]["objects"]) + and len(payload["before"]["data"]["zones"]) + == len(payload["after"]["data"]["zones"]) + ): + logger.debug( + f"Skipping notification for {camera} - message is an update and important fields don't have an update" + ) + return + + self.last_camera_notification_time[camera] = current_time + self.last_notification_time = current_time + + reviewId = payload["after"]["id"] + sorted_objects: set[str] = set() + + for obj in payload["after"]["data"]["objects"]: + if "-verified" not in obj: + sorted_objects.add(obj) + + sorted_objects.update(payload["after"]["data"]["sub_labels"]) + + image = f"{payload['after']['thumb_path'].replace(BASE_DIR, '')}" + ended = state == "end" or state == "genai" + + if state == "genai" and payload["after"]["data"]["metadata"]: + base_title = payload["after"]["data"]["metadata"]["title"] + threat_level = payload["after"]["data"]["metadata"].get( + "potential_threat_level", 0 + ) + + # Add prefix for threat levels 1 and 2 + if threat_level == 1: + title = f"Needs Review: {base_title}" + elif threat_level == 2: + title = f"Security Concern: {base_title}" + else: + title = base_title + + message = payload["after"]["data"]["metadata"]["scene"] + else: + zone_names = payload["after"]["data"]["zones"] + formatted_zone_names = [] + + for zone_name in zone_names: + if zone_name in self.config.cameras[camera].zones: + formatted_zone_names.append( + self.config.cameras[camera] + .zones[zone_name] + .get_formatted_name(zone_name) + ) + else: + formatted_zone_names.append(titlecase(zone_name.replace("_", " "))) + + title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {', '.join(formatted_zone_names)}" + message = f"Detected on {camera_name}" + + if ended: + logger.debug( + f"Sending a notification with state {state} and message {message}" + ) + + # if event is ongoing open to live view otherwise open to recordings view + direct_url = f"/review?id={reviewId}" if ended else f"/#{camera}" + ttl = 3600 if ended else 0 + + logger.debug(f"Sending push notification for {camera}, review ID {reviewId}") + + for user in self.web_pushers: + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + ttl=ttl, + ) + + self.cleanup_registrations() + + def send_trigger(self, payload: dict[str, Any]) -> None: + if not self.config.notifications.email: + return + + camera: str = payload["camera"] + camera_name: str = getattr( + self.config.cameras[camera], "friendly_name", None + ) or titlecase(camera.replace("_", " ")) + current_time = datetime.datetime.now().timestamp() + + if self._within_cooldown(camera): + return + + self.check_registrations() + + self.last_camera_notification_time[camera] = current_time + self.last_notification_time = current_time + + trigger_type = payload["type"] + event_id = payload["event_id"] + name = payload["name"] + score = payload["score"] + + title = f"{name.replace('_', ' ')} triggered on {camera_name}" + message = f"{titlecase(trigger_type)} trigger fired for {camera_name} with score {score:.2f}" + image = f"clips/triggers/{camera}/{event_id}.webp" + + direct_url = f"/explore?event_id={event_id}" + ttl = 0 + + logger.debug( + f"Sending push notification for {camera_name}, trigger name {name}" + ) + + for user in self.web_pushers: + self.send_push_notification( + user=user, + payload=payload, + title=title, + message=message, + direct_url=direct_url, + image=image, + ttl=ttl, + ) + + self.cleanup_registrations() + + def stop(self) -> None: + logger.info("Closing notification queue") + self.notification_thread.join() diff --git a/sam2-cpu/frigate-dev/frigate/comms/ws.py b/sam2-cpu/frigate-dev/frigate/comms/ws.py new file mode 100644 index 0000000..6cfe4ec --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/ws.py @@ -0,0 +1,119 @@ +"""Websocket communicator.""" + +import errno +import json +import logging +import threading +from typing import Any, Callable +from wsgiref.simple_server import make_server + +from ws4py.server.wsgirefserver import ( + WebSocketWSGIHandler, + WebSocketWSGIRequestHandler, + WSGIServer, +) +from ws4py.server.wsgiutils import WebSocketWSGIApplication +from ws4py.websocket import WebSocket as WebSocket_ + +from frigate.comms.base_communicator import Communicator +from frigate.config import FrigateConfig + +logger = logging.getLogger(__name__) + + +class WebSocket(WebSocket_): # type: ignore[misc] + def unhandled_error(self, error: Any) -> None: + """ + Handles the unfriendly socket closures on the server side + without showing a confusing error message + """ + if hasattr(error, "errno") and error.errno == errno.ECONNRESET: + pass + else: + logging.getLogger("ws4py").exception("Failed to receive data") + + +class WebSocketClient(Communicator): + """Frigate wrapper for ws client.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.websocket_server: WSGIServer | None = None + + def subscribe(self, receiver: Callable) -> None: + self._dispatcher = receiver + self.start() + + def start(self) -> None: + """Start the websocket client.""" + + class _WebSocketHandler(WebSocket): + receiver = self._dispatcher + + def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined] + try: + json_message = json.loads(message.data.decode("utf-8")) + json_message = { + "topic": json_message.get("topic"), + "payload": json_message.get("payload"), + } + except Exception: + logger.warning( + f"Unable to parse websocket message as valid json: {message.data.decode('utf-8')}" + ) + return + + logger.debug( + f"Publishing mqtt message from websockets at {json_message['topic']}." + ) + self.receiver( + json_message["topic"], + json_message["payload"], + ) + + # start a websocket server on 5002 + WebSocketWSGIHandler.http_version = "1.1" + self.websocket_server = make_server( + "127.0.0.1", + 5002, + server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=_WebSocketHandler), + ) + self.websocket_server.initialize_websockets_manager() + self.websocket_thread = threading.Thread( + target=self.websocket_server.serve_forever + ) + self.websocket_thread.start() + + def publish(self, topic: str, payload: Any, _: bool = False) -> None: + try: + ws_message = json.dumps( + { + "topic": topic, + "payload": payload, + } + ) + except Exception: + # if the payload can't be decoded don't relay to clients + logger.debug(f"payload for {topic} wasn't text. Skipping...") + return + + if self.websocket_server is None: + logger.debug("Skipping message, websocket not connected yet") + return + + try: + self.websocket_server.manager.broadcast(ws_message) + except ConnectionResetError: + pass + + def stop(self) -> None: + if self.websocket_server is not None: + self.websocket_server.manager.close_all() + self.websocket_server.manager.stop() + self.websocket_server.manager.join() + self.websocket_server.shutdown() + + self.websocket_thread.join() + logger.info("Exiting websocket client...") diff --git a/sam2-cpu/frigate-dev/frigate/comms/zmq_proxy.py b/sam2-cpu/frigate-dev/frigate/comms/zmq_proxy.py new file mode 100644 index 0000000..29329ec --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/comms/zmq_proxy.py @@ -0,0 +1,103 @@ +"""Facilitates communication over zmq proxy.""" + +import json +import threading +from typing import Generic, TypeVar + +import zmq + +from frigate.const import FAST_QUEUE_TIMEOUT + +SOCKET_PUB = "ipc:///tmp/cache/proxy_pub" +SOCKET_SUB = "ipc:///tmp/cache/proxy_sub" + + +class ZmqProxyRunner(threading.Thread): + def __init__(self, context: zmq.Context[zmq.Socket]) -> None: + super().__init__(name="detection_proxy") + self.context = context + + def run(self) -> None: + """Run the proxy.""" + incoming = self.context.socket(zmq.XSUB) + incoming.bind(SOCKET_PUB) + outgoing = self.context.socket(zmq.XPUB) + outgoing.bind(SOCKET_SUB) + + # Blocking: This will unblock (via exception) when we destroy the context + # The incoming and outgoing sockets will be closed automatically + # when the context is destroyed as well. + try: + zmq.proxy(incoming, outgoing) + except zmq.ZMQError: + pass + + +class ZmqProxy: + """Proxies video and audio detections.""" + + def __init__(self) -> None: + self.context = zmq.Context() + self.runner = ZmqProxyRunner(self.context) + self.runner.start() + + def stop(self) -> None: + # destroying the context will tell the proxy to stop + self.context.destroy() + self.runner.join() + + +T = TypeVar("T") + + +class Publisher(Generic[T]): + """Publishes messages.""" + + topic_base: str = "" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.PUB) + self.socket.connect(SOCKET_PUB) + + def publish(self, payload: T, sub_topic: str = "") -> None: + """Publish message.""" + self.socket.send_string(f"{self.topic}{sub_topic} {json.dumps(payload)}") + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + +class Subscriber(Generic[T]): + """Receives messages.""" + + topic_base: str = "" + + def __init__(self, topic: str = "") -> None: + self.topic = f"{self.topic_base}{topic}" + self.context = zmq.Context() + self.socket = self.context.socket(zmq.SUB) + self.socket.setsockopt_string(zmq.SUBSCRIBE, self.topic) + self.socket.connect(SOCKET_SUB) + + def check_for_update(self, timeout: float | None = FAST_QUEUE_TIMEOUT) -> T | None: + """Returns message or None if no update.""" + try: + has_update, _, _ = zmq.select([self.socket], [], [], timeout) + + if has_update: + parts = self.socket.recv_string(flags=zmq.NOBLOCK).split(maxsplit=1) + return self._return_object(parts[0], json.loads(parts[1])) + except zmq.ZMQError: + pass + + return self._return_object("", None) + + def stop(self) -> None: + self.socket.close() + self.context.destroy() + + def _return_object(self, topic: str, payload: T | None) -> T | None: + return payload diff --git a/sam2-cpu/frigate-dev/frigate/config/__init__.py b/sam2-cpu/frigate-dev/frigate/config/__init__.py new file mode 100644 index 0000000..c6ff535 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/__init__.py @@ -0,0 +1,14 @@ +from frigate.detectors import DetectorConfig, ModelConfig # noqa: F401 + +from .auth import * # noqa: F403 +from .camera import * # noqa: F403 +from .camera_group import * # noqa: F403 +from .classification import * # noqa: F403 +from .config import * # noqa: F403 +from .database import * # noqa: F403 +from .logger import * # noqa: F403 +from .mqtt import * # noqa: F403 +from .proxy import * # noqa: F403 +from .telemetry import * # noqa: F403 +from .tls import * # noqa: F403 +from .ui import * # noqa: F403 diff --git a/sam2-cpu/frigate-dev/frigate/config/auth.py b/sam2-cpu/frigate-dev/frigate/config/auth.py new file mode 100644 index 0000000..6935350 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/auth.py @@ -0,0 +1,81 @@ +from typing import Dict, List, Optional + +from pydantic import Field, field_validator, model_validator + +from .base import FrigateBaseModel + +__all__ = ["AuthConfig"] + + +class AuthConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable authentication") + reset_admin_password: bool = Field( + default=False, title="Reset the admin password on startup" + ) + cookie_name: str = Field( + default="frigate_token", title="Name for jwt token cookie", pattern=r"^[a-z_]+$" + ) + cookie_secure: bool = Field(default=False, title="Set secure flag on cookie") + session_length: int = Field( + default=86400, title="Session length for jwt session tokens", ge=60 + ) + refresh_time: int = Field( + default=1800, + title="Refresh the session if it is going to expire in this many seconds", + ge=30, + ) + failed_login_rate_limit: Optional[str] = Field( + default=None, + title="Rate limits for failed login attempts.", + ) + trusted_proxies: list[str] = Field( + default=[], + title="Trusted proxies for determining IP address to rate limit", + ) + # As of Feb 2023, OWASP recommends 600000 iterations for PBKDF2-SHA256 + hash_iterations: int = Field(default=600000, title="Password hash iterations") + roles: Dict[str, List[str]] = Field( + default_factory=dict, + title="Role to camera mappings. Empty list grants access to all cameras.", + ) + admin_first_time_login: Optional[bool] = Field( + default=False, + title="Internal field to expose first-time admin login flag to the UI", + description=( + "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. " + ), + ) + + @field_validator("roles") + @classmethod + def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]: + # Ensure role names are valid (alphanumeric with underscores) + for role in v.keys(): + if not role.replace("_", "").isalnum(): + raise ValueError( + f"Invalid role name '{role}'. Must be alphanumeric with underscores." + ) + + # Ensure 'admin' and 'viewer' are not used as custom role names + reserved_roles = {"admin", "viewer"} + if v.keys() & reserved_roles: + raise ValueError( + f"Reserved roles {reserved_roles} cannot be used as custom roles." + ) + + # Ensure no role has an empty camera list + for role, allowed_cameras in v.items(): + if not allowed_cameras: + raise ValueError( + f"Role '{role}' has no cameras assigned. Custom roles must have at least one camera." + ) + + return v + + @model_validator(mode="after") + def ensure_default_roles(self): + # Ensure admin and viewer are never overridden + self.roles["admin"] = [] + self.roles["viewer"] = [] + + return self diff --git a/sam2-cpu/frigate-dev/frigate/config/base.py b/sam2-cpu/frigate-dev/frigate/config/base.py new file mode 100644 index 0000000..1e369e2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/base.py @@ -0,0 +1,29 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict + + +class FrigateBaseModel(BaseModel): + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + def get_nested_object(self, path: str) -> Any: + parts = path.split("/") + obj = self + for part in parts: + if part == "config": + continue + + if isinstance(obj, BaseModel): + try: + obj = getattr(obj, part) + except AttributeError: + return None + elif isinstance(obj, dict): + try: + obj = obj[part] + except KeyError: + return None + else: + return None + + return obj diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/__init__.py b/sam2-cpu/frigate-dev/frigate/config/camera/__init__.py new file mode 100644 index 0000000..573fd2e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/__init__.py @@ -0,0 +1,17 @@ +from .audio import * # noqa: F403 +from .birdseye import * # noqa: F403 +from .camera import * # noqa: F403 +from .detect import * # noqa: F403 +from .ffmpeg import * # noqa: F403 +from .genai import * # noqa: F403 +from .live import * # noqa: F403 +from .motion import * # noqa: F403 +from .mqtt import * # noqa: F403 +from .objects import * # noqa: F403 +from .onvif import * # noqa: F403 +from .record import * # noqa: F403 +from .review import * # noqa: F403 +from .snapshots import * # noqa: F403 +from .timestamp import * # noqa: F403 +from .ui import * # noqa: F403 +from .zone import * # noqa: F403 diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/audio.py b/sam2-cpu/frigate-dev/frigate/config/camera/audio.py new file mode 100644 index 0000000..3734455 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/audio.py @@ -0,0 +1,41 @@ +from typing import Optional + +from pydantic import Field + +from frigate.const import AUDIO_MIN_CONFIDENCE + +from ..base import FrigateBaseModel + +__all__ = ["AudioConfig", "AudioFilterConfig"] + + +DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"] + + +class AudioFilterConfig(FrigateBaseModel): + threshold: float = Field( + default=0.8, + ge=AUDIO_MIN_CONFIDENCE, + lt=1.0, + title="Minimum detection confidence threshold for audio to be counted.", + ) + + +class AudioConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable audio events.") + max_not_heard: int = Field( + default=30, title="Seconds of not hearing the type of audio to end the event." + ) + min_volume: int = Field( + default=500, title="Min volume required to run audio detection." + ) + listen: list[str] = Field( + default=DEFAULT_LISTEN_AUDIO, title="Audio to listen for." + ) + filters: Optional[dict[str, AudioFilterConfig]] = Field( + None, title="Audio filters." + ) + enabled_in_config: Optional[bool] = Field( + None, title="Keep track of original state of audio detection." + ) + num_threads: int = Field(default=2, title="Number of detection threads", ge=1) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/birdseye.py b/sam2-cpu/frigate-dev/frigate/config/camera/birdseye.py new file mode 100644 index 0000000..1e6f0f3 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/birdseye.py @@ -0,0 +1,73 @@ +from enum import Enum +from typing import Optional + +from pydantic import BaseModel, Field + +from ..base import FrigateBaseModel + +__all__ = [ + "BirdseyeCameraConfig", + "BirdseyeConfig", + "BirdseyeLayoutConfig", + "BirdseyeModeEnum", +] + + +class BirdseyeModeEnum(str, Enum): + objects = "objects" + motion = "motion" + continuous = "continuous" + + @classmethod + def get_index(cls, type): + return list(cls).index(type) + + @classmethod + def get(cls, index): + return list(cls)[index] + + +class BirdseyeLayoutConfig(FrigateBaseModel): + scaling_factor: float = Field( + default=2.0, title="Birdseye Scaling Factor", ge=1.0, le=5.0 + ) + max_cameras: Optional[int] = Field(default=None, title="Max cameras") + + +class BirdseyeConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable birdseye view.") + mode: BirdseyeModeEnum = Field( + default=BirdseyeModeEnum.objects, title="Tracking mode." + ) + + restream: bool = Field(default=False, title="Restream birdseye via RTSP.") + width: int = Field(default=1280, title="Birdseye width.") + height: int = Field(default=720, title="Birdseye height.") + quality: int = Field( + default=8, + title="Encoding quality.", + ge=1, + le=31, + ) + inactivity_threshold: int = Field( + default=30, title="Birdseye Inactivity Threshold", gt=0 + ) + layout: BirdseyeLayoutConfig = Field( + default_factory=BirdseyeLayoutConfig, title="Birdseye Layout Config" + ) + idle_heartbeat_fps: float = Field( + default=0.0, + ge=0.0, + le=10.0, + title="Idle heartbeat FPS (0 disables, max 10)", + ) + + +# uses BaseModel because some global attributes are not available at the camera level +class BirdseyeCameraConfig(BaseModel): + enabled: bool = Field(default=True, title="Enable birdseye view for camera.") + mode: BirdseyeModeEnum = Field( + default=BirdseyeModeEnum.objects, title="Tracking mode for camera." + ) + + order: int = Field(default=0, title="Position of the camera in the birdseye view.") diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/camera.py b/sam2-cpu/frigate-dev/frigate/config/camera/camera.py new file mode 100644 index 0000000..0f2b1c8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/camera.py @@ -0,0 +1,273 @@ +import os +from enum import Enum +from typing import Optional + +from pydantic import Field, PrivateAttr, model_validator + +from frigate.const import CACHE_DIR, CACHE_SEGMENT_FORMAT, REGEX_CAMERA_NAME +from frigate.ffmpeg_presets import ( + parse_preset_hardware_acceleration_decode, + parse_preset_hardware_acceleration_scale, + parse_preset_input, + parse_preset_output_record, +) +from frigate.util.builtin import ( + escape_special_characters, + generate_color_palette, + get_ffmpeg_arg_list, +) + +from ..base import FrigateBaseModel +from ..classification import ( + CameraAudioTranscriptionConfig, + CameraFaceRecognitionConfig, + CameraLicensePlateRecognitionConfig, + CameraSemanticSearchConfig, +) +from .audio import AudioConfig +from .birdseye import BirdseyeCameraConfig +from .detect import DetectConfig +from .ffmpeg import CameraFfmpegConfig, CameraInput +from .live import CameraLiveConfig +from .motion import MotionConfig +from .mqtt import CameraMqttConfig +from .notification import NotificationConfig +from .objects import ObjectConfig +from .onvif import OnvifConfig +from .record import RecordConfig +from .review import ReviewConfig +from .snapshots import SnapshotsConfig +from .timestamp import TimestampStyleConfig +from .ui import CameraUiConfig +from .zone import ZoneConfig + +__all__ = ["CameraConfig"] + + +class CameraTypeEnum(str, Enum): + generic = "generic" + lpr = "lpr" + + +class CameraConfig(FrigateBaseModel): + name: Optional[str] = Field(None, title="Camera name.", pattern=REGEX_CAMERA_NAME) + + friendly_name: Optional[str] = Field( + None, title="Camera friendly name used in the Frigate UI." + ) + + @model_validator(mode="before") + @classmethod + def handle_friendly_name(cls, values): + if isinstance(values, dict) and "friendly_name" in values: + pass + return values + + enabled: bool = Field(default=True, title="Enable camera.") + + # Options with global fallback + audio: AudioConfig = Field( + default_factory=AudioConfig, title="Audio events configuration." + ) + audio_transcription: CameraAudioTranscriptionConfig = Field( + default_factory=CameraAudioTranscriptionConfig, + title="Audio transcription config.", + ) + birdseye: BirdseyeCameraConfig = Field( + default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration." + ) + detect: DetectConfig = Field( + default_factory=DetectConfig, title="Object detection configuration." + ) + face_recognition: CameraFaceRecognitionConfig = Field( + default_factory=CameraFaceRecognitionConfig, title="Face recognition config." + ) + ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, title="Live playback settings." + ) + lpr: CameraLicensePlateRecognitionConfig = Field( + default_factory=CameraLicensePlateRecognitionConfig, title="LPR config." + ) + motion: MotionConfig = Field(None, title="Motion detection configuration.") + objects: ObjectConfig = Field( + default_factory=ObjectConfig, title="Object configuration." + ) + record: RecordConfig = Field( + default_factory=RecordConfig, title="Record configuration." + ) + review: ReviewConfig = Field( + default_factory=ReviewConfig, title="Review configuration." + ) + semantic_search: CameraSemanticSearchConfig = Field( + default_factory=CameraSemanticSearchConfig, + title="Semantic search configuration.", + ) + snapshots: SnapshotsConfig = Field( + default_factory=SnapshotsConfig, title="Snapshot configuration." + ) + timestamp_style: TimestampStyleConfig = Field( + default_factory=TimestampStyleConfig, title="Timestamp style configuration." + ) + + # Options without global fallback + best_image_timeout: int = Field( + default=60, + title="How long to wait for the image with the highest confidence score.", + ) + mqtt: CameraMqttConfig = Field( + default_factory=CameraMqttConfig, title="MQTT configuration." + ) + notifications: NotificationConfig = Field( + default_factory=NotificationConfig, title="Notifications configuration." + ) + onvif: OnvifConfig = Field( + default_factory=OnvifConfig, title="Camera Onvif Configuration." + ) + type: CameraTypeEnum = Field(default=CameraTypeEnum.generic, title="Camera Type") + ui: CameraUiConfig = Field( + default_factory=CameraUiConfig, title="Camera UI Modifications." + ) + webui_url: Optional[str] = Field( + None, + title="URL to visit the camera directly from system page", + ) + zones: dict[str, ZoneConfig] = Field( + default_factory=dict, title="Zone configuration." + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of camera." + ) + + _ffmpeg_cmds: list[dict[str, list[str]]] = PrivateAttr() + + def __init__(self, **config): + # Set zone colors + if "zones" in config: + colors = generate_color_palette(len(config["zones"])) + + config["zones"] = { + name: {**z, "color": color} + for (name, z), color in zip(config["zones"].items(), colors) + } + + # add roles to the input if there is only one + if len(config["ffmpeg"]["inputs"]) == 1: + has_audio = "audio" in config["ffmpeg"]["inputs"][0].get("roles", []) + + config["ffmpeg"]["inputs"][0]["roles"] = [ + "record", + "detect", + ] + + if has_audio: + config["ffmpeg"]["inputs"][0]["roles"].append("audio") + + super().__init__(**config) + + @property + def frame_shape(self) -> tuple[int, int]: + return self.detect.height, self.detect.width + + @property + def frame_shape_yuv(self) -> tuple[int, int]: + return self.detect.height * 3 // 2, self.detect.width + + @property + def ffmpeg_cmds(self) -> list[dict[str, list[str]]]: + return self._ffmpeg_cmds + + def get_formatted_name(self) -> str: + """Return the friendly name if set, otherwise return a formatted version of the camera name.""" + if self.friendly_name: + return self.friendly_name + return self.name.replace("_", " ").title() if self.name else "" + + def create_ffmpeg_cmds(self): + if "_ffmpeg_cmds" in self: + return + ffmpeg_cmds = [] + for ffmpeg_input in self.ffmpeg.inputs: + ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input) + if ffmpeg_cmd is None: + continue + + ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd}) + self._ffmpeg_cmds = ffmpeg_cmds + + def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): + ffmpeg_output_args = [] + if "detect" in ffmpeg_input.roles: + detect_args = get_ffmpeg_arg_list(self.ffmpeg.output_args.detect) + scale_detect_args = parse_preset_hardware_acceleration_scale( + ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args, + detect_args, + self.detect.fps, + self.detect.width, + self.detect.height, + ) + + ffmpeg_output_args = scale_detect_args + ffmpeg_output_args + ["pipe:"] + + if "record" in ffmpeg_input.roles and self.record.enabled: + record_args = get_ffmpeg_arg_list( + parse_preset_output_record( + self.ffmpeg.output_args.record, + self.ffmpeg.apple_compatibility, + ) + or self.ffmpeg.output_args.record + ) + + ffmpeg_output_args = ( + record_args + + [f"{os.path.join(CACHE_DIR, self.name)}@{CACHE_SEGMENT_FORMAT}.mp4"] + + ffmpeg_output_args + ) + + # if there aren't any outputs enabled for this input + if len(ffmpeg_output_args) == 0: + return None + + global_args = get_ffmpeg_arg_list( + ffmpeg_input.global_args or self.ffmpeg.global_args + ) + + camera_arg = ( + self.ffmpeg.hwaccel_args if self.ffmpeg.hwaccel_args != "auto" else None + ) + hwaccel_args = get_ffmpeg_arg_list( + parse_preset_hardware_acceleration_decode( + ffmpeg_input.hwaccel_args, + self.detect.fps, + self.detect.width, + self.detect.height, + self.ffmpeg.gpu, + ) + or ffmpeg_input.hwaccel_args + or parse_preset_hardware_acceleration_decode( + camera_arg, + self.detect.fps, + self.detect.width, + self.detect.height, + self.ffmpeg.gpu, + ) + or camera_arg + or [] + ) + input_args = get_ffmpeg_arg_list( + parse_preset_input(ffmpeg_input.input_args, self.detect.fps) + or ffmpeg_input.input_args + or parse_preset_input(self.ffmpeg.input_args, self.detect.fps) + or self.ffmpeg.input_args + ) + + cmd = ( + [self.ffmpeg.ffmpeg_path] + + global_args + + (hwaccel_args if "detect" in ffmpeg_input.roles else []) + + input_args + + ["-i", escape_special_characters(ffmpeg_input.path)] + + ffmpeg_output_args + ) + + return [part for part in cmd if part != ""] diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/detect.py b/sam2-cpu/frigate-dev/frigate/config/camera/detect.py new file mode 100644 index 0000000..1926f32 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/detect.py @@ -0,0 +1,63 @@ +from typing import Optional + +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"] + + +class StationaryMaxFramesConfig(FrigateBaseModel): + default: Optional[int] = Field(default=None, title="Default max frames.", ge=1) + objects: dict[str, int] = Field( + default_factory=dict, title="Object specific max frames." + ) + + +class StationaryConfig(FrigateBaseModel): + interval: Optional[int] = Field( + default=None, + title="Frame interval for checking stationary objects.", + gt=0, + ) + threshold: Optional[int] = Field( + default=None, + title="Number of frames without a position change for an object to be considered stationary", + ge=1, + ) + max_frames: StationaryMaxFramesConfig = Field( + default_factory=StationaryMaxFramesConfig, + title="Max frames for stationary objects.", + ) + classifier: bool = Field( + default=True, + title="Enable visual classifier for determing if objects with jittery bounding boxes are stationary.", + ) + + +class DetectConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Detection Enabled.") + height: Optional[int] = Field( + default=None, title="Height of the stream for the detect role." + ) + width: Optional[int] = Field( + default=None, title="Width of the stream for the detect role." + ) + fps: int = Field( + default=5, title="Number of frames per second to process through detection." + ) + min_initialized: Optional[int] = Field( + default=None, + title="Minimum number of consecutive hits for an object to be initialized by the tracker.", + ) + max_disappeared: Optional[int] = Field( + default=None, + title="Maximum number of frames the object can disappear before detection ends.", + ) + stationary: StationaryConfig = Field( + default_factory=StationaryConfig, + title="Stationary objects config.", + ) + annotation_offset: int = Field( + default=0, title="Milliseconds to offset detect annotations by." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/ffmpeg.py b/sam2-cpu/frigate-dev/frigate/config/camera/ffmpeg.py new file mode 100644 index 0000000..2c1e4cd --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/ffmpeg.py @@ -0,0 +1,125 @@ +from enum import Enum +from typing import Union + +from pydantic import Field, field_validator + +from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS + +from ..base import FrigateBaseModel +from ..env import EnvString + +__all__ = [ + "CameraFfmpegConfig", + "CameraInput", + "CameraRoleEnum", + "FfmpegConfig", + "FfmpegOutputArgsConfig", +] + +# Note: Setting threads to less than 2 caused several issues with recording segments +# https://github.com/blakeblackshear/frigate/issues/5659 +FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning", "-threads", "2"] +FFMPEG_INPUT_ARGS_DEFAULT = "preset-rtsp-generic" + +RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = "preset-record-generic-audio-aac" +DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ + "-threads", + "2", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", +] + + +class FfmpegOutputArgsConfig(FrigateBaseModel): + detect: Union[str, list[str]] = Field( + default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT, + title="Detect role FFmpeg output arguments.", + ) + record: Union[str, list[str]] = Field( + default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, + title="Record role FFmpeg output arguments.", + ) + + +class FfmpegConfig(FrigateBaseModel): + path: str = Field(default="default", title="FFmpeg path") + global_args: Union[str, list[str]] = Field( + default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments." + ) + hwaccel_args: Union[str, list[str]] = Field( + default="auto", title="FFmpeg hardware acceleration arguments." + ) + input_args: Union[str, list[str]] = Field( + default=FFMPEG_INPUT_ARGS_DEFAULT, title="FFmpeg input arguments." + ) + output_args: FfmpegOutputArgsConfig = Field( + default_factory=FfmpegOutputArgsConfig, + title="FFmpeg output arguments per role.", + ) + retry_interval: float = Field( + default=10.0, + title="Time in seconds to wait before FFmpeg retries connecting to the camera.", + gt=0.0, + ) + apple_compatibility: bool = Field( + default=False, + title="Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players.", + ) + gpu: int = Field(default=0, title="GPU index to use for hardware acceleration.") + + @property + def ffmpeg_path(self) -> str: + if self.path == "default": + return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" + elif self.path in INCLUDED_FFMPEG_VERSIONS: + return f"/usr/lib/ffmpeg/{self.path}/bin/ffmpeg" + else: + return f"{self.path}/bin/ffmpeg" + + @property + def ffprobe_path(self) -> str: + if self.path == "default": + return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe" + elif self.path in INCLUDED_FFMPEG_VERSIONS: + return f"/usr/lib/ffmpeg/{self.path}/bin/ffprobe" + else: + return f"{self.path}/bin/ffprobe" + + +class CameraRoleEnum(str, Enum): + audio = "audio" + record = "record" + detect = "detect" + + +class CameraInput(FrigateBaseModel): + path: EnvString = Field(title="Camera input path.") + roles: list[CameraRoleEnum] = Field(title="Roles assigned to this input.") + global_args: Union[str, list[str]] = Field( + default_factory=list, title="FFmpeg global arguments." + ) + hwaccel_args: Union[str, list[str]] = Field( + default_factory=list, title="FFmpeg hardware acceleration arguments." + ) + input_args: Union[str, list[str]] = Field( + default_factory=list, title="FFmpeg input arguments." + ) + + +class CameraFfmpegConfig(FfmpegConfig): + inputs: list[CameraInput] = Field(title="Camera inputs.") + + @field_validator("inputs") + @classmethod + def validate_roles(cls, v): + roles = [role for input in v for role in input.roles] + + if len(roles) != len(set(roles)): + raise ValueError("Each input role may only be used once.") + + if "detect" not in roles: + raise ValueError("The detect role is required.") + + return v diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/genai.py b/sam2-cpu/frigate-dev/frigate/config/camera/genai.py new file mode 100644 index 0000000..3c6baeb --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/genai.py @@ -0,0 +1,28 @@ +from enum import Enum +from typing import Any, Optional + +from pydantic import Field + +from ..base import FrigateBaseModel +from ..env import EnvString + +__all__ = ["GenAIConfig", "GenAIProviderEnum"] + + +class GenAIProviderEnum(str, Enum): + openai = "openai" + azure_openai = "azure_openai" + gemini = "gemini" + ollama = "ollama" + + +class GenAIConfig(FrigateBaseModel): + """Primary GenAI Config to define GenAI Provider.""" + + api_key: Optional[EnvString] = Field(default=None, title="Provider API key.") + base_url: Optional[str] = Field(default=None, title="Provider base url.") + model: str = Field(default="gpt-4o", title="GenAI model.") + provider: GenAIProviderEnum | None = Field(default=None, title="GenAI provider.") + provider_options: dict[str, Any] = Field( + default={}, title="GenAI Provider extra options." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/live.py b/sam2-cpu/frigate-dev/frigate/config/camera/live.py new file mode 100644 index 0000000..13ae2d0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/live.py @@ -0,0 +1,16 @@ +from typing import Dict + +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraLiveConfig"] + + +class CameraLiveConfig(FrigateBaseModel): + streams: Dict[str, str] = Field( + default_factory=list, + title="Friendly names and restream names to use for live view.", + ) + height: int = Field(default=720, title="Live camera view height") + quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality") diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/motion.py b/sam2-cpu/frigate-dev/frigate/config/camera/motion.py new file mode 100644 index 0000000..65c03f7 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/motion.py @@ -0,0 +1,44 @@ +from typing import Any, Optional, Union + +from pydantic import Field, field_serializer + +from ..base import FrigateBaseModel + +__all__ = ["MotionConfig"] + + +class MotionConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable motion on all cameras.") + threshold: int = Field( + default=30, + title="Motion detection threshold (1-255).", + ge=1, + le=255, + ) + lightning_threshold: float = Field( + default=0.8, title="Lightning detection threshold (0.3-1.0).", ge=0.3, le=1.0 + ) + improve_contrast: bool = Field(default=True, title="Improve Contrast") + contour_area: Optional[int] = Field(default=10, title="Contour Area") + delta_alpha: float = Field(default=0.2, title="Delta Alpha") + frame_alpha: float = Field(default=0.01, title="Frame Alpha") + frame_height: Optional[int] = Field(default=100, title="Frame Height") + mask: Union[str, list[str]] = Field( + default="", title="Coordinates polygon for the motion mask." + ) + mqtt_off_delay: int = Field( + default=30, + title="Delay for updating MQTT with no motion detected.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of motion detection." + ) + raw_mask: Union[str, list[str]] = "" + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + return self.raw_mask + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/mqtt.py b/sam2-cpu/frigate-dev/frigate/config/camera/mqtt.py new file mode 100644 index 0000000..132fee0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/mqtt.py @@ -0,0 +1,23 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraMqttConfig"] + + +class CameraMqttConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Send image over MQTT.") + timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.") + bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.") + crop: bool = Field(default=True, title="Crop MQTT image to detected object.") + height: int = Field(default=270, title="MQTT image height.") + required_zones: list[str] = Field( + default_factory=list, + title="List of required zones to be entered in order to send the image.", + ) + quality: int = Field( + default=70, + title="Quality of the encoded jpeg (0-100).", + ge=0, + le=100, + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/notification.py b/sam2-cpu/frigate-dev/frigate/config/camera/notification.py new file mode 100644 index 0000000..ce1ac82 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/notification.py @@ -0,0 +1,18 @@ +from typing import Optional + +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["NotificationConfig"] + + +class NotificationConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable notifications") + email: Optional[str] = Field(default=None, title="Email required for push.") + cooldown: int = Field( + default=0, ge=0, title="Cooldown period for notifications (time in seconds)." + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of notifications." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/objects.py b/sam2-cpu/frigate-dev/frigate/config/camera/objects.py new file mode 100644 index 0000000..7b6317d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/objects.py @@ -0,0 +1,131 @@ +from typing import Any, Optional, Union + +from pydantic import Field, PrivateAttr, field_serializer, field_validator + +from ..base import FrigateBaseModel + +__all__ = ["ObjectConfig", "GenAIObjectConfig", "FilterConfig"] + + +DEFAULT_TRACKED_OBJECTS = ["person"] + + +class FilterConfig(FrigateBaseModel): + min_area: Union[int, float] = Field( + default=0, + title="Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", + ) + max_area: Union[int, float] = Field( + default=24000000, + title="Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", + ) + min_ratio: float = Field( + default=0, + title="Minimum ratio of bounding box's width/height for object to be counted.", + ) + max_ratio: float = Field( + default=24000000, + title="Maximum ratio of bounding box's width/height for object to be counted.", + ) + threshold: float = Field( + default=0.7, + title="Average detection confidence threshold for object to be counted.", + ) + min_score: float = Field( + default=0.5, title="Minimum detection confidence for object to be counted." + ) + mask: Optional[Union[str, list[str]]] = Field( + default=None, + title="Detection area polygon mask for this filter configuration.", + ) + raw_mask: Union[str, list[str]] = "" + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + return self.raw_mask + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None + + +class GenAIObjectTriggerConfig(FrigateBaseModel): + tracked_object_end: bool = Field( + default=True, title="Send once the object is no longer tracked." + ) + after_significant_updates: Optional[int] = Field( + default=None, + title="Send an early request to generative AI when X frames accumulated.", + ge=1, + ) + + +class GenAIObjectConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable GenAI for camera.") + use_snapshot: bool = Field( + default=False, title="Use snapshots for generating descriptions." + ) + prompt: str = Field( + default="Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", + title="Default caption prompt.", + ) + object_prompts: dict[str, str] = Field( + default_factory=dict, title="Object specific prompts." + ) + + objects: Union[str, list[str]] = Field( + default_factory=list, + title="List of objects to run generative AI for.", + ) + required_zones: Union[str, list[str]] = Field( + default_factory=list, + title="List of required zones to be entered in order to run generative AI.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails sent to generative AI for debugging purposes.", + ) + send_triggers: GenAIObjectTriggerConfig = Field( + default_factory=GenAIObjectTriggerConfig, + title="What triggers to use to send frames to generative AI for a tracked object.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of generative AI." + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class ObjectConfig(FrigateBaseModel): + track: list[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.") + filters: dict[str, FilterConfig] = Field( + default_factory=dict, title="Object filters." + ) + mask: Union[str, list[str]] = Field(default="", title="Object mask.") + genai: GenAIObjectConfig = Field( + default_factory=GenAIObjectConfig, + title="Config for using genai to analyze objects.", + ) + _all_objects: list[str] = PrivateAttr() + + @property + def all_objects(self) -> list[str]: + return self._all_objects + + def parse_all_objects(self, cameras): + if "_all_objects" in self: + return + + # get list of unique enabled labels for tracking + enabled_labels = set(self.track) + + for camera in cameras.values(): + enabled_labels.update(camera.objects.track) + + self._all_objects = list(enabled_labels) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/onvif.py b/sam2-cpu/frigate-dev/frigate/config/camera/onvif.py new file mode 100644 index 0000000..d495579 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/onvif.py @@ -0,0 +1,87 @@ +from enum import Enum +from typing import Optional, Union + +from pydantic import Field, field_validator + +from ..base import FrigateBaseModel +from ..env import EnvString +from .objects import DEFAULT_TRACKED_OBJECTS + +__all__ = ["OnvifConfig", "PtzAutotrackConfig", "ZoomingModeEnum"] + + +class ZoomingModeEnum(str, Enum): + disabled = "disabled" + absolute = "absolute" + relative = "relative" + + +class PtzAutotrackConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable PTZ object autotracking.") + calibrate_on_startup: bool = Field( + default=False, title="Perform a camera calibration when Frigate starts." + ) + zooming: ZoomingModeEnum = Field( + default=ZoomingModeEnum.disabled, title="Autotracker zooming mode." + ) + zoom_factor: float = Field( + default=0.3, + title="Zooming factor (0.1-0.75).", + ge=0.1, + le=0.75, + ) + track: list[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.") + required_zones: list[str] = Field( + default_factory=list, + title="List of required zones to be entered in order to begin autotracking.", + ) + return_preset: str = Field( + default="home", + title="Name of camera preset to return to when object tracking is over.", + ) + timeout: int = Field( + default=10, title="Seconds to delay before returning to preset." + ) + movement_weights: Optional[Union[str, list[str]]] = Field( + default_factory=list, + title="Internal value used for PTZ movements based on the speed of your camera's motor.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of autotracking." + ) + + @field_validator("movement_weights", mode="before") + @classmethod + def validate_weights(cls, v): + if v is None: + return None + + if isinstance(v, str): + weights = list(map(str, map(float, v.split(",")))) + elif isinstance(v, list): + weights = [str(float(val)) for val in v] + else: + raise ValueError("Invalid type for movement_weights") + + if len(weights) != 6: + raise ValueError( + "movement_weights must have exactly 6 floats, remove this line from your config and run autotracking calibration" + ) + + return weights + + +class OnvifConfig(FrigateBaseModel): + host: str = Field(default="", title="Onvif Host") + port: int = Field(default=8000, title="Onvif Port") + user: Optional[EnvString] = Field(default=None, title="Onvif Username") + password: Optional[EnvString] = Field(default=None, title="Onvif Password") + tls_insecure: bool = Field(default=False, title="Onvif Disable TLS verification") + autotracking: PtzAutotrackConfig = Field( + default_factory=PtzAutotrackConfig, + title="PTZ auto tracking config.", + ) + ignore_time_mismatch: bool = Field( + default=False, + title="Onvif Ignore Time Synchronization Mismatch Between Camera and Server", + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/record.py b/sam2-cpu/frigate-dev/frigate/config/camera/record.py new file mode 100644 index 0000000..09a7a84 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/record.py @@ -0,0 +1,124 @@ +from enum import Enum +from typing import Optional + +from pydantic import Field + +from frigate.const import MAX_PRE_CAPTURE +from frigate.review.types import SeverityEnum + +from ..base import FrigateBaseModel + +__all__ = [ + "RecordConfig", + "RecordExportConfig", + "RecordPreviewConfig", + "RecordQualityEnum", + "EventsConfig", + "ReviewRetainConfig", + "RecordRetainConfig", + "RetainModeEnum", +] + +DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" + + +class RecordRetainConfig(FrigateBaseModel): + days: float = Field(default=0, ge=0, title="Default retention period.") + + +class RetainModeEnum(str, Enum): + all = "all" + motion = "motion" + active_objects = "active_objects" + + +class ReviewRetainConfig(FrigateBaseModel): + days: float = Field(default=10, ge=0, title="Default retention period.") + mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.") + + +class EventsConfig(FrigateBaseModel): + pre_capture: int = Field( + default=5, + title="Seconds to retain before event starts.", + le=MAX_PRE_CAPTURE, + ge=0, + ) + post_capture: int = Field( + default=5, ge=0, title="Seconds to retain after event ends." + ) + retain: ReviewRetainConfig = Field( + default_factory=ReviewRetainConfig, title="Event retention settings." + ) + + +class RecordQualityEnum(str, Enum): + very_low = "very_low" + low = "low" + medium = "medium" + high = "high" + very_high = "very_high" + + +class RecordPreviewConfig(FrigateBaseModel): + quality: RecordQualityEnum = Field( + default=RecordQualityEnum.medium, title="Quality of recording preview." + ) + + +class RecordExportConfig(FrigateBaseModel): + timelapse_args: str = Field( + default=DEFAULT_TIME_LAPSE_FFMPEG_ARGS, title="Timelapse Args" + ) + + +class RecordConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable record on all cameras.") + sync_recordings: bool = Field( + default=False, title="Sync recordings with disk on startup and once a day." + ) + expire_interval: int = Field( + default=60, + title="Number of minutes to wait between cleanup runs.", + ) + continuous: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, + title="Continuous recording retention settings.", + ) + motion: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, title="Motion recording retention settings." + ) + detections: EventsConfig = Field( + default_factory=EventsConfig, title="Detection specific retention settings." + ) + alerts: EventsConfig = Field( + default_factory=EventsConfig, title="Alert specific retention settings." + ) + export: RecordExportConfig = Field( + default_factory=RecordExportConfig, title="Recording Export Config" + ) + preview: RecordPreviewConfig = Field( + default_factory=RecordPreviewConfig, title="Recording Preview Config" + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of recording." + ) + + @property + def event_pre_capture(self) -> int: + return max( + self.alerts.pre_capture, + self.detections.pre_capture, + ) + + def get_review_pre_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.pre_capture + else: + return self.detections.pre_capture + + def get_review_post_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.post_capture + else: + return self.detections.post_capture diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/review.py b/sam2-cpu/frigate-dev/frigate/config/camera/review.py new file mode 100644 index 0000000..67ba3b6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/review.py @@ -0,0 +1,156 @@ +from enum import Enum +from typing import Optional, Union + +from pydantic import Field, field_validator + +from ..base import FrigateBaseModel + +__all__ = ["ReviewConfig", "DetectionsConfig", "AlertsConfig", "ImageSourceEnum"] + + +class ImageSourceEnum(str, Enum): + """Image source options for GenAI Review.""" + + preview = "preview" + recordings = "recordings" + + +DEFAULT_ALERT_OBJECTS = ["person", "car"] + + +class AlertsConfig(FrigateBaseModel): + """Configure alerts""" + + enabled: bool = Field(default=True, title="Enable alerts.") + + labels: list[str] = Field( + default=DEFAULT_ALERT_OBJECTS, title="Labels to create alerts for." + ) + required_zones: Union[str, list[str]] = Field( + default_factory=list, + title="List of required zones to be entered in order to save the event as an alert.", + ) + + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of alerts." + ) + cutoff_time: int = Field( + default=40, + title="Time to cutoff alerts after no alert-causing activity has occurred.", + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class DetectionsConfig(FrigateBaseModel): + """Configure detections""" + + enabled: bool = Field(default=True, title="Enable detections.") + + labels: Optional[list[str]] = Field( + default=None, title="Labels to create detections for." + ) + required_zones: Union[str, list[str]] = Field( + default_factory=list, + title="List of required zones to be entered in order to save the event as a detection.", + ) + cutoff_time: int = Field( + default=30, + title="Time to cutoff detection after no detection-causing activity has occurred.", + ) + + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of detections." + ) + + @field_validator("required_zones", mode="before") + @classmethod + def validate_required_zones(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + +class GenAIReviewConfig(FrigateBaseModel): + enabled: bool = Field( + default=False, + title="Enable GenAI descriptions for review items.", + ) + alerts: bool = Field(default=True, title="Enable GenAI for alerts.") + detections: bool = Field(default=False, title="Enable GenAI for detections.") + image_source: ImageSourceEnum = Field( + default=ImageSourceEnum.preview, + title="Image source for review descriptions.", + ) + additional_concerns: list[str] = Field( + default=[], + title="Additional concerns that GenAI should make note of on this camera.", + ) + debug_save_thumbnails: bool = Field( + default=False, + title="Save thumbnails sent to generative AI for debugging purposes.", + ) + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of generative AI." + ) + preferred_language: str | None = Field( + title="Preferred language for GenAI Response", + default=None, + ) + activity_context_prompt: str = Field( + default="""### Normal Activity Indicators (Level 0) +- Known/verified people in any zone at any time +- People with pets in residential areas +- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving +- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime +- Activity confined to public areas only (sidewalks, streets) without entering property at any time + +### Suspicious Activity Indicators (Level 1) +- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration +- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration +- Taking items that don't belong to them (packages, objects from porches/driveways) +- Climbing or jumping fences/barriers to access property +- Attempting to conceal actions or items from view +- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence + +### Critical Threat Indicators (Level 2) +- Holding break-in tools (crowbars, pry bars, bolt cutters) +- Weapons visible (guns, knives, bats used aggressively) +- Forced entry in progress +- Physical aggression or violence +- Active property damage or theft in progress + +### Assessment Guidance +Evaluate in this order: + +1. **If person is verified/known** → Level 0 regardless of time or activity +2. **If person is unidentified:** + - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 + - Check actions: If testing doors/handles, taking items, climbing → Level 1 + - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0 +3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) + +The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.""", + title="Custom activity context prompt defining normal and suspicious activity patterns for this property.", + ) + + +class ReviewConfig(FrigateBaseModel): + """Configure reviews""" + + alerts: AlertsConfig = Field( + default_factory=AlertsConfig, title="Review alerts config." + ) + detections: DetectionsConfig = Field( + default_factory=DetectionsConfig, title="Review detections config." + ) + genai: GenAIReviewConfig = Field( + default_factory=GenAIReviewConfig, title="Review description genai config." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/snapshots.py b/sam2-cpu/frigate-dev/frigate/config/camera/snapshots.py new file mode 100644 index 0000000..156b56a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/snapshots.py @@ -0,0 +1,44 @@ +from typing import Optional + +from pydantic import Field + +from ..base import FrigateBaseModel +from .record import RetainModeEnum + +__all__ = ["SnapshotsConfig", "RetainConfig"] + + +class RetainConfig(FrigateBaseModel): + default: float = Field(default=10, title="Default retention period.") + mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.") + objects: dict[str, float] = Field( + default_factory=dict, title="Object retention period." + ) + + +class SnapshotsConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Snapshots enabled.") + clean_copy: bool = Field( + default=True, title="Create a clean copy of the snapshot image." + ) + timestamp: bool = Field( + default=False, title="Add a timestamp overlay on the snapshot." + ) + bounding_box: bool = Field( + default=True, title="Add a bounding box overlay on the snapshot." + ) + crop: bool = Field(default=False, title="Crop the snapshot to the detected object.") + required_zones: list[str] = Field( + default_factory=list, + title="List of required zones to be entered in order to save a snapshot.", + ) + height: Optional[int] = Field(default=None, title="Snapshot image height.") + retain: RetainConfig = Field( + default_factory=RetainConfig, title="Snapshot retention." + ) + quality: int = Field( + default=70, + title="Quality of the encoded jpeg (0-100).", + ge=0, + le=100, + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/timestamp.py b/sam2-cpu/frigate-dev/frigate/config/camera/timestamp.py new file mode 100644 index 0000000..fcf352a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/timestamp.py @@ -0,0 +1,49 @@ +from enum import Enum +from typing import Optional + +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = [ + "TimestampStyleConfig", + "TimestampEffectEnum", + "ColorConfig", + "TimestampPositionEnum", +] + + +# TODO: Identify what the default format to display timestamps is +DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" +# German Style: +# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S" + + +class TimestampPositionEnum(str, Enum): + tl = "tl" + tr = "tr" + bl = "bl" + br = "br" + + +class ColorConfig(FrigateBaseModel): + red: int = Field(default=255, ge=0, le=255, title="Red") + green: int = Field(default=255, ge=0, le=255, title="Green") + blue: int = Field(default=255, ge=0, le=255, title="Blue") + + +class TimestampEffectEnum(str, Enum): + solid = "solid" + shadow = "shadow" + + +class TimestampStyleConfig(FrigateBaseModel): + position: TimestampPositionEnum = Field( + default=TimestampPositionEnum.tl, title="Timestamp position." + ) + format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.") + color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.") + thickness: int = Field(default=2, title="Timestamp thickness.") + effect: Optional[TimestampEffectEnum] = Field( + default=None, title="Timestamp effect." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/ui.py b/sam2-cpu/frigate-dev/frigate/config/camera/ui.py new file mode 100644 index 0000000..b6b9c58 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/ui.py @@ -0,0 +1,12 @@ +from pydantic import Field + +from ..base import FrigateBaseModel + +__all__ = ["CameraUiConfig"] + + +class CameraUiConfig(FrigateBaseModel): + order: int = Field(default=0, title="Order of camera in UI.") + dashboard: bool = Field( + default=True, title="Show this camera in Frigate dashboard UI." + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/updater.py b/sam2-cpu/frigate-dev/frigate/config/camera/updater.py new file mode 100644 index 0000000..125094f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/updater.py @@ -0,0 +1,147 @@ +"""Convenience classes for updating configurations dynamically.""" + +from dataclasses import dataclass +from enum import Enum +from typing import Any + +from frigate.comms.config_updater import ConfigPublisher, ConfigSubscriber +from frigate.config import CameraConfig, FrigateConfig + + +class CameraConfigUpdateEnum(str, Enum): + """Supported camera config update types.""" + + add = "add" # for adding a camera + audio = "audio" + audio_transcription = "audio_transcription" + birdseye = "birdseye" + detect = "detect" + enabled = "enabled" + motion = "motion" # includes motion and motion masks + notifications = "notifications" + objects = "objects" + object_genai = "object_genai" + record = "record" + remove = "remove" # for removing a camera + review = "review" + review_genai = "review_genai" + semantic_search = "semantic_search" # for semantic search triggers + snapshots = "snapshots" + zones = "zones" + + +@dataclass +class CameraConfigUpdateTopic: + update_type: CameraConfigUpdateEnum + camera: str + + @property + def topic(self) -> str: + return f"config/cameras/{self.camera}/{self.update_type.name}" + + +class CameraConfigUpdatePublisher: + def __init__(self): + self.publisher = ConfigPublisher() + + def publish_update(self, topic: CameraConfigUpdateTopic, config: Any) -> None: + self.publisher.publish(topic.topic, config) + + def stop(self) -> None: + self.publisher.stop() + + +class CameraConfigUpdateSubscriber: + def __init__( + self, + config: FrigateConfig | None, + camera_configs: dict[str, CameraConfig], + topics: list[CameraConfigUpdateEnum], + ): + self.config = config + self.camera_configs = camera_configs + self.topics = topics + + base_topic = "config/cameras" + + if len(self.camera_configs) == 1: + base_topic += f"/{list(self.camera_configs.keys())[0]}" + + self.subscriber = ConfigSubscriber( + base_topic, + exact=False, + ) + + def __update_config( + self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any + ) -> None: + if update_type == CameraConfigUpdateEnum.add: + self.config.cameras[camera] = updated_config + self.camera_configs[camera] = updated_config + return + elif update_type == CameraConfigUpdateEnum.remove: + self.config.cameras.pop(camera) + self.camera_configs.pop(camera) + return + + config = self.camera_configs.get(camera) + + if not config: + return + + if update_type == CameraConfigUpdateEnum.audio: + config.audio = updated_config + elif update_type == CameraConfigUpdateEnum.audio_transcription: + config.audio_transcription = updated_config + elif update_type == CameraConfigUpdateEnum.birdseye: + config.birdseye = updated_config + elif update_type == CameraConfigUpdateEnum.detect: + config.detect = updated_config + elif update_type == CameraConfigUpdateEnum.enabled: + config.enabled = updated_config + elif update_type == CameraConfigUpdateEnum.object_genai: + config.objects.genai = updated_config + elif update_type == CameraConfigUpdateEnum.motion: + config.motion = updated_config + elif update_type == CameraConfigUpdateEnum.notifications: + config.notifications = updated_config + elif update_type == CameraConfigUpdateEnum.objects: + config.objects = updated_config + elif update_type == CameraConfigUpdateEnum.record: + config.record = updated_config + elif update_type == CameraConfigUpdateEnum.review: + config.review = updated_config + elif update_type == CameraConfigUpdateEnum.review_genai: + config.review.genai = updated_config + elif update_type == CameraConfigUpdateEnum.semantic_search: + config.semantic_search = updated_config + elif update_type == CameraConfigUpdateEnum.snapshots: + config.snapshots = updated_config + elif update_type == CameraConfigUpdateEnum.zones: + config.zones = updated_config + + def check_for_updates(self) -> dict[str, list[str]]: + updated_topics: dict[str, list[str]] = {} + + # get all updates available + while True: + update_topic, update_config = self.subscriber.check_for_update() + + if update_topic is None or update_config is None: + break + + _, _, camera, raw_type = update_topic.split("/") + update_type = CameraConfigUpdateEnum[raw_type] + + if update_type in self.topics: + if update_type.name in updated_topics: + updated_topics[update_type.name].append(camera) + else: + updated_topics[update_type.name] = [camera] + + self.__update_config(camera, update_type, update_config) + + return updated_topics + + def stop(self) -> None: + self.subscriber.stop() diff --git a/sam2-cpu/frigate-dev/frigate/config/camera/zone.py b/sam2-cpu/frigate-dev/frigate/config/camera/zone.py new file mode 100644 index 0000000..7df1a1f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera/zone.py @@ -0,0 +1,171 @@ +# this uses the base model because the color is an extra attribute +import logging +from typing import Optional, Union + +import numpy as np +from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator + +from .objects import FilterConfig + +__all__ = ["ZoneConfig"] + +logger = logging.getLogger(__name__) + + +class ZoneConfig(BaseModel): + friendly_name: Optional[str] = Field( + None, title="Zone friendly name used in the Frigate UI." + ) + filters: dict[str, FilterConfig] = Field( + default_factory=dict, title="Zone filters." + ) + coordinates: Union[str, list[str]] = Field( + title="Coordinates polygon for the defined zone." + ) + distances: Optional[Union[str, list[str]]] = Field( + default_factory=list, + title="Real-world distances for the sides of quadrilateral for the defined zone.", + ) + inertia: int = Field( + default=3, + title="Number of consecutive frames required for object to be considered present in the zone.", + gt=0, + ) + loitering_time: int = Field( + default=0, + ge=0, + title="Number of seconds that an object must loiter to be considered in the zone.", + ) + speed_threshold: Optional[float] = Field( + default=None, + ge=0.1, + title="Minimum speed value for an object to be considered in the zone.", + ) + objects: Union[str, list[str]] = Field( + default_factory=list, + title="List of objects that can trigger the zone.", + ) + _color: Optional[tuple[int, int, int]] = PrivateAttr() + _contour: np.ndarray = PrivateAttr() + + @property + def color(self) -> tuple[int, int, int]: + return self._color + + @property + def contour(self) -> np.ndarray: + return self._contour + + def get_formatted_name(self, zone_name: str) -> str: + """Return the friendly name if set, otherwise return a formatted version of the zone name.""" + if self.friendly_name: + return self.friendly_name + return zone_name.replace("_", " ").title() + + @field_validator("objects", mode="before") + @classmethod + def validate_objects(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v + + @field_validator("distances", mode="before") + @classmethod + def validate_distances(cls, v): + if v is None: + return None + + if isinstance(v, str): + distances = list(map(str, map(float, v.split(",")))) + elif isinstance(v, list): + distances = [str(float(val)) for val in v] + else: + raise ValueError("Invalid type for distances") + + if len(distances) != 4: + raise ValueError("distances must have exactly 4 values") + + return distances + + @model_validator(mode="after") + def check_loitering_time_constraints(self): + if self.loitering_time > 0 and ( + self.speed_threshold is not None or len(self.distances) > 0 + ): + logger.warning( + "loitering_time should not be set on a zone if speed_threshold or distances is set." + ) + return self + + def __init__(self, **config): + super().__init__(**config) + + self._color = config.get("color", (0, 0, 0)) + self._contour = config.get("contour", np.array([])) + + def generate_contour(self, frame_shape: tuple[int, int]): + coordinates = self.coordinates + + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + if isinstance(coordinates, list): + explicit = any(p.split(",")[0] > "1.0" for p in coordinates) + try: + self._contour = np.array( + [ + ( + [int(p.split(",")[0]), int(p.split(",")[1])] + if explicit + else [ + int(float(p.split(",")[0]) * frame_shape[1]), + int(float(p.split(",")[1]) * frame_shape[0]), + ] + ) + for p in coordinates + ] + ) + except ValueError: + raise ValueError( + f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" + ) + + if explicit: + self.coordinates = ",".join( + [ + f"{round(int(p.split(',')[0]) / frame_shape[1], 3)},{round(int(p.split(',')[1]) / frame_shape[0], 3)}" + for p in coordinates + ] + ) + elif isinstance(coordinates, str): + points = coordinates.split(",") + explicit = any(p > "1.0" for p in points) + try: + self._contour = np.array( + [ + ( + [int(points[i]), int(points[i + 1])] + if explicit + else [ + int(float(points[i]) * frame_shape[1]), + int(float(points[i + 1]) * frame_shape[0]), + ] + ) + for i in range(0, len(points), 2) + ] + ) + except ValueError: + raise ValueError( + f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" + ) + + if explicit: + self.coordinates = ",".join( + [ + f"{round(int(points[i]) / frame_shape[1], 3)},{round(int(points[i + 1]) / frame_shape[0], 3)}" + for i in range(0, len(points), 2) + ] + ) + else: + self._contour = np.array([]) diff --git a/sam2-cpu/frigate-dev/frigate/config/camera_group.py b/sam2-cpu/frigate-dev/frigate/config/camera_group.py new file mode 100644 index 0000000..7449e86 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/camera_group.py @@ -0,0 +1,25 @@ +from typing import Union + +from pydantic import Field, field_validator + +from .base import FrigateBaseModel + +__all__ = ["CameraGroupConfig"] + + +class CameraGroupConfig(FrigateBaseModel): + """Represents a group of cameras.""" + + cameras: Union[str, list[str]] = Field( + default_factory=list, title="List of cameras in this group." + ) + icon: str = Field(default="generic", title="Icon that represents camera group.") + order: int = Field(default=0, title="Sort order for group.") + + @field_validator("cameras", mode="before") + @classmethod + def validate_cameras(cls, v): + if isinstance(v, str) and "," not in v: + return [v] + + return v diff --git a/sam2-cpu/frigate-dev/frigate/config/classification.py b/sam2-cpu/frigate-dev/frigate/config/classification.py new file mode 100644 index 0000000..fb8e3de --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/classification.py @@ -0,0 +1,325 @@ +from enum import Enum +from typing import Dict, List, Optional + +from pydantic import ConfigDict, Field + +from .base import FrigateBaseModel + +__all__ = [ + "CameraFaceRecognitionConfig", + "CameraLicensePlateRecognitionConfig", + "CameraAudioTranscriptionConfig", + "FaceRecognitionConfig", + "SemanticSearchConfig", + "CameraSemanticSearchConfig", + "LicensePlateRecognitionConfig", +] + + +class SemanticSearchModelEnum(str, Enum): + jinav1 = "jinav1" + jinav2 = "jinav2" + + +class EnrichmentsDeviceEnum(str, Enum): + GPU = "GPU" + CPU = "CPU" + + +class TriggerType(str, Enum): + THUMBNAIL = "thumbnail" + DESCRIPTION = "description" + + +class TriggerAction(str, Enum): + NOTIFICATION = "notification" + SUB_LABEL = "sub_label" + ATTRIBUTE = "attribute" + + +class ObjectClassificationType(str, Enum): + sub_label = "sub_label" + attribute = "attribute" + + +class AudioTranscriptionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable audio transcription.") + language: str = Field( + default="en", + title="Language abbreviation to use for audio event transcription/translation.", + ) + device: Optional[EnrichmentsDeviceEnum] = Field( + default=EnrichmentsDeviceEnum.CPU, + title="The device used for audio transcription.", + ) + model_size: str = Field( + default="small", title="The size of the embeddings model used." + ) + live_enabled: Optional[bool] = Field( + default=False, title="Enable live transcriptions." + ) + + +class BirdClassificationConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable bird classification.") + threshold: float = Field( + default=0.9, + title="Minimum classification score required to be considered a match.", + gt=0.0, + le=1.0, + ) + + +class CustomClassificationStateCameraConfig(FrigateBaseModel): + crop: list[float, float, float, float] = Field( + title="Crop of image frame on this camera to run classification on." + ) + + +class CustomClassificationStateConfig(FrigateBaseModel): + cameras: Dict[str, CustomClassificationStateCameraConfig] = Field( + title="Cameras to run classification on." + ) + motion: bool = Field( + default=False, + title="If classification should be run when motion is detected in the crop.", + ) + interval: int | None = Field( + default=None, + title="Interval to run classification on in seconds.", + gt=0, + ) + + +class CustomClassificationObjectConfig(FrigateBaseModel): + objects: list[str] = Field(title="Object types to classify.") + classification_type: ObjectClassificationType = Field( + default=ObjectClassificationType.sub_label, + title="Type of classification that is applied.", + ) + + +class CustomClassificationConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable running the model.") + name: str | None = Field(default=None, title="Name of classification model.") + threshold: float = Field( + default=0.8, title="Classification score threshold to change the state." + ) + save_attempts: int | None = Field( + default=None, + title="Number of classification attempts to save in the recent classifications tab. If not specified, defaults to 200 for object classification and 100 for state classification.", + ge=0, + ) + object_config: CustomClassificationObjectConfig | None = Field(default=None) + state_config: CustomClassificationStateConfig | None = Field(default=None) + + +class ClassificationConfig(FrigateBaseModel): + bird: BirdClassificationConfig = Field( + default_factory=BirdClassificationConfig, title="Bird classification config." + ) + custom: Dict[str, CustomClassificationConfig] = Field( + default={}, title="Custom Classification Model Configs." + ) + + +class SemanticSearchConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable semantic search.") + reindex: Optional[bool] = Field( + default=False, title="Reindex all tracked objects on startup." + ) + model: Optional[SemanticSearchModelEnum] = Field( + default=SemanticSearchModelEnum.jinav1, + title="The CLIP model to use for semantic search.", + ) + model_size: str = Field( + default="small", title="The size of the embeddings model used." + ) + device: Optional[str] = Field( + default=None, + title="The device key to use for semantic search.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + + +class TriggerConfig(FrigateBaseModel): + friendly_name: Optional[str] = Field( + None, title="Trigger friendly name used in the Frigate UI." + ) + enabled: bool = Field(default=True, title="Enable this trigger") + type: TriggerType = Field(default=TriggerType.DESCRIPTION, title="Type of trigger") + data: str = Field(title="Trigger content (text phrase or image ID)") + threshold: float = Field( + title="Confidence score required to run the trigger", + default=0.8, + gt=0.0, + le=1.0, + ) + actions: List[TriggerAction] = Field( + default=[], title="Actions to perform when trigger is matched" + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraSemanticSearchConfig(FrigateBaseModel): + triggers: Dict[str, TriggerConfig] = Field( + default={}, + title="Trigger actions on tracked objects that match existing thumbnails or descriptions", + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class FaceRecognitionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable face recognition.") + model_size: str = Field( + default="small", title="The size of the embeddings model used." + ) + unknown_score: float = Field( + title="Minimum face distance score required to be marked as a potential match.", + default=0.8, + gt=0.0, + le=1.0, + ) + detection_threshold: float = Field( + default=0.7, + title="Minimum face detection score required to be considered a face.", + gt=0.0, + le=1.0, + ) + recognition_threshold: float = Field( + default=0.9, + title="Minimum face distance score required to be considered a match.", + gt=0.0, + le=1.0, + ) + min_area: int = Field( + default=750, title="Min area of face box to consider running face recognition." + ) + min_faces: int = Field( + default=1, + gt=0, + le=6, + title="Min face recognitions for the sub label to be applied to the person object.", + ) + save_attempts: int = Field( + default=200, + ge=0, + title="Number of face attempts to save in the recent recognitions tab.", + ) + blur_confidence_filter: bool = Field( + default=True, title="Apply blur quality filter to face confidence." + ) + device: Optional[str] = Field( + default=None, + title="The device key to use for face recognition.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + + +class CameraFaceRecognitionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable face recognition.") + min_area: int = Field( + default=750, title="Min area of face box to consider running face recognition." + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class ReplaceRule(FrigateBaseModel): + pattern: str = Field(..., title="Regex pattern to match.") + replacement: str = Field( + ..., title="Replacement string (supports backrefs like '\\1')." + ) + + +class LicensePlateRecognitionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable license plate recognition.") + model_size: str = Field( + default="small", title="The size of the embeddings model used." + ) + detection_threshold: float = Field( + default=0.7, + title="License plate object confidence score required to begin running recognition.", + gt=0.0, + le=1.0, + ) + min_area: int = Field( + default=1000, + title="Minimum area of license plate to begin running recognition.", + ) + recognition_threshold: float = Field( + default=0.9, + title="Recognition confidence score required to add the plate to the object as a sub label.", + gt=0.0, + le=1.0, + ) + min_plate_length: int = Field( + default=4, + title="Minimum number of characters a license plate must have to be added to the object as a sub label.", + ) + format: Optional[str] = Field( + default=None, + title="Regular expression for the expected format of license plate.", + ) + match_distance: int = Field( + default=1, + title="Allow this number of missing/incorrect characters to still cause a detected plate to match a known plate.", + ge=0, + ) + known_plates: Optional[Dict[str, List[str]]] = Field( + default={}, title="Known plates to track (strings or regular expressions)." + ) + enhancement: int = Field( + default=0, + title="Amount of contrast adjustment and denoising to apply to license plate images before recognition.", + ge=0, + le=10, + ) + debug_save_plates: bool = Field( + default=False, + title="Save plates captured for LPR for debugging purposes.", + ) + device: Optional[str] = Field( + default=None, + title="The device key to use for LPR.", + description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", + ) + replace_rules: List[ReplaceRule] = Field( + default_factory=list, + title="List of regex replacement rules for normalizing detected plates. Each rule has 'pattern' and 'replacement'.", + ) + + +class CameraLicensePlateRecognitionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable license plate recognition.") + expire_time: int = Field( + default=3, + title="Expire plates not seen after number of seconds (for dedicated LPR cameras only).", + gt=0, + ) + min_area: int = Field( + default=1000, + title="Minimum area of license plate to begin running recognition.", + ) + enhancement: int = Field( + default=0, + title="Amount of contrast adjustment and denoising to apply to license plate images before recognition.", + ge=0, + le=10, + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class CameraAudioTranscriptionConfig(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable audio transcription.") + enabled_in_config: Optional[bool] = Field( + default=None, title="Keep track of original state of audio transcription." + ) + live_enabled: Optional[bool] = Field( + default=False, title="Enable live transcriptions." + ) + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) diff --git a/sam2-cpu/frigate-dev/frigate/config/config.py b/sam2-cpu/frigate-dev/frigate/config/config.py new file mode 100644 index 0000000..6342c13 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/config.py @@ -0,0 +1,814 @@ +from __future__ import annotations + +import json +import logging +import os +from typing import Any, Dict, List, Optional, Union + +import numpy as np +from pydantic import ( + BaseModel, + ConfigDict, + Field, + TypeAdapter, + ValidationInfo, + field_serializer, + field_validator, + model_validator, +) +from ruamel.yaml import YAML +from typing_extensions import Self + +from frigate.const import REGEX_JSON +from frigate.detectors import DetectorConfig, ModelConfig +from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.plus import PlusApi +from frigate.util.builtin import ( + deep_merge, + get_ffmpeg_arg_list, +) +from frigate.util.config import ( + StreamInfoRetriever, + convert_area_to_pixels, + find_config_file, + get_relative_coordinates, + migrate_frigate_config, +) +from frigate.util.image import create_mask +from frigate.util.services import auto_detect_hwaccel + +from .auth import AuthConfig +from .base import FrigateBaseModel +from .camera import CameraConfig, CameraLiveConfig +from .camera.audio import AudioConfig +from .camera.birdseye import BirdseyeConfig +from .camera.detect import DetectConfig +from .camera.ffmpeg import FfmpegConfig +from .camera.genai import GenAIConfig +from .camera.motion import MotionConfig +from .camera.notification import NotificationConfig +from .camera.objects import FilterConfig, ObjectConfig +from .camera.record import RecordConfig +from .camera.review import ReviewConfig +from .camera.snapshots import SnapshotsConfig +from .camera.timestamp import TimestampStyleConfig +from .camera_group import CameraGroupConfig +from .classification import ( + AudioTranscriptionConfig, + ClassificationConfig, + FaceRecognitionConfig, + LicensePlateRecognitionConfig, + SemanticSearchConfig, +) +from .database import DatabaseConfig +from .env import EnvVars +from .logger import LoggerConfig +from .mqtt import MqttConfig +from .network import NetworkingConfig +from .proxy import ProxyConfig +from .telemetry import TelemetryConfig +from .tls import TlsConfig +from .ui import UIConfig + +__all__ = ["FrigateConfig"] + +logger = logging.getLogger(__name__) + +yaml = YAML() + +DEFAULT_CONFIG = """ +mqtt: + enabled: False + +cameras: {} # No cameras defined, UI wizard should be used +""" + +DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} +DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720} + +# stream info handler +stream_info_retriever = StreamInfoRetriever() + + +class RuntimeMotionConfig(MotionConfig): + raw_mask: Union[str, List[str]] = "" + mask: np.ndarray = None + + def __init__(self, **config): + frame_shape = config.get("frame_shape", (1, 1)) + + mask = get_relative_coordinates(config.get("mask", ""), frame_shape) + config["raw_mask"] = mask + + if mask: + config["mask"] = create_mask(frame_shape, mask) + else: + empty_mask = np.zeros(frame_shape, np.uint8) + empty_mask[:] = 255 + config["mask"] = empty_mask + + super().__init__(**config) + + def dict(self, **kwargs): + ret = super().model_dump(**kwargs) + if "mask" in ret: + ret["mask"] = ret["raw_mask"] + ret.pop("raw_mask") + return ret + + @field_serializer("mask", when_used="json") + def serialize_mask(self, value: Any, info): + return self.raw_mask + + @field_serializer("raw_mask", when_used="json") + def serialize_raw_mask(self, value: Any, info): + return None + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore") + + +class RuntimeFilterConfig(FilterConfig): + mask: Optional[np.ndarray] = None + raw_mask: Optional[Union[str, List[str]]] = None + + def __init__(self, **config): + frame_shape = config.get("frame_shape", (1, 1)) + mask = get_relative_coordinates(config.get("mask"), frame_shape) + + config["raw_mask"] = mask + + if mask is not None: + config["mask"] = create_mask(frame_shape, mask) + + # Convert min_area and max_area to pixels if they're percentages + if "min_area" in config: + config["min_area"] = convert_area_to_pixels(config["min_area"], frame_shape) + + if "max_area" in config: + config["max_area"] = convert_area_to_pixels(config["max_area"], frame_shape) + + super().__init__(**config) + + def dict(self, **kwargs): + ret = super().model_dump(**kwargs) + if "mask" in ret: + ret["mask"] = ret["raw_mask"] + ret.pop("raw_mask") + return ret + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="ignore") + + +class RestreamConfig(BaseModel): + model_config = ConfigDict(extra="allow") + + +def verify_config_roles(camera_config: CameraConfig) -> None: + """Verify that roles are setup in the config correctly.""" + assigned_roles = list( + set([r for i in camera_config.ffmpeg.inputs for r in i.roles]) + ) + + if camera_config.record.enabled and "record" not in assigned_roles: + raise ValueError( + f"Camera {camera_config.name} has record enabled, but record is not assigned to an input." + ) + + if camera_config.audio.enabled and "audio" not in assigned_roles: + raise ValueError( + f"Camera {camera_config.name} has audio events enabled, but audio is not assigned to an input." + ) + + +def verify_valid_live_stream_names( + frigate_config: FrigateConfig, camera_config: CameraConfig +) -> ValueError | None: + """Verify that a restream exists to use for live view.""" + for _, stream_name in camera_config.live.streams.items(): + if ( + stream_name + not in frigate_config.go2rtc.model_dump().get("streams", {}).keys() + ): + return ValueError( + f"No restream with name {stream_name} exists for camera {camera_config.name}." + ) + + +def verify_recording_segments_setup_with_reasonable_time( + camera_config: CameraConfig, +) -> None: + """Verify that recording segments are setup and segment time is not greater than 60.""" + record_args: list[str] = get_ffmpeg_arg_list( + camera_config.ffmpeg.output_args.record + ) + + if record_args[0].startswith("preset"): + return + + try: + seg_arg_index = record_args.index("-segment_time") + except ValueError: + raise ValueError( + f"Camera {camera_config.name} has no segment_time in \ + recording output args, segment args are required for record." + ) + + if int(record_args[seg_arg_index + 1]) > 60: + raise ValueError( + f"Camera {camera_config.name} has invalid segment_time output arg, \ + segment_time must be 60 or less." + ) + + +def verify_zone_objects_are_tracked(camera_config: CameraConfig) -> None: + """Verify that user has not entered zone objects that are not in the tracking config.""" + for zone_name, zone in camera_config.zones.items(): + for obj in zone.objects: + if obj not in camera_config.objects.track: + raise ValueError( + f"Zone {zone_name} is configured to track {obj} but that object type is not added to objects -> track." + ) + + +def verify_required_zones_exist(camera_config: CameraConfig) -> None: + for det_zone in camera_config.review.detections.required_zones: + if det_zone not in camera_config.zones.keys(): + raise ValueError( + f"Camera {camera_config.name} has a required zone for detections {det_zone} that is not defined." + ) + + for det_zone in camera_config.review.alerts.required_zones: + if det_zone not in camera_config.zones.keys(): + raise ValueError( + f"Camera {camera_config.name} has a required zone for alerts {det_zone} that is not defined." + ) + + +def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None: + """Verify that required_zones are specified when autotracking is enabled.""" + if ( + camera_config.onvif.autotracking.enabled + and not camera_config.onvif.autotracking.required_zones + ): + raise ValueError( + f"Camera {camera_config.name} has autotracking enabled, required_zones must be set to at least one of the camera's zones." + ) + + +def verify_motion_and_detect(camera_config: CameraConfig) -> ValueError | None: + """Verify that motion detection is not disabled and object detection is enabled.""" + if camera_config.detect.enabled and not camera_config.motion.enabled: + raise ValueError( + f"Camera {camera_config.name} has motion detection disabled and object detection enabled but object detection requires motion detection." + ) + + +def verify_objects_track( + camera_config: CameraConfig, enabled_objects: list[str] +) -> None: + """Verify that a user has not specified an object to track that is not in the labelmap.""" + valid_objects = [ + obj for obj in camera_config.objects.track if obj in enabled_objects + ] + + if len(valid_objects) != len(camera_config.objects.track): + invalid_objects = set(camera_config.objects.track) - set(valid_objects) + logger.warning( + f"{camera_config.name} is configured to track {list(invalid_objects)} objects, which are not supported by the current model." + ) + camera_config.objects.track = valid_objects + + +def verify_lpr_and_face( + frigate_config: FrigateConfig, camera_config: CameraConfig +) -> ValueError | None: + """Verify that lpr and face are enabled at the global level if enabled at the camera level.""" + if camera_config.lpr.enabled and not frigate_config.lpr.enabled: + raise ValueError( + f"Camera {camera_config.name} has lpr enabled but lpr is disabled at the global level of the config. You must enable lpr at the global level." + ) + if ( + camera_config.face_recognition.enabled + and not frigate_config.face_recognition.enabled + ): + raise ValueError( + f"Camera {camera_config.name} has face_recognition enabled but face_recognition is disabled at the global level of the config. You must enable face_recognition at the global level." + ) + + +class FrigateConfig(FrigateBaseModel): + version: Optional[str] = Field(default=None, title="Current config version.") + safe_mode: bool = Field( + default=False, title="If Frigate should be started in safe mode." + ) + + # Fields that install global state should be defined first, so that their validators run first. + environment_vars: EnvVars = Field( + default_factory=dict, title="Frigate environment variables." + ) + logger: LoggerConfig = Field( + default_factory=LoggerConfig, + title="Logging configuration.", + validate_default=True, + ) + + # Global config + auth: AuthConfig = Field(default_factory=AuthConfig, title="Auth configuration.") + database: DatabaseConfig = Field( + default_factory=DatabaseConfig, title="Database configuration." + ) + go2rtc: RestreamConfig = Field( + default_factory=RestreamConfig, title="Global restream configuration." + ) + mqtt: MqttConfig = Field(title="MQTT configuration.") + notifications: NotificationConfig = Field( + default_factory=NotificationConfig, title="Global notification configuration." + ) + networking: NetworkingConfig = Field( + default_factory=NetworkingConfig, title="Networking configuration" + ) + proxy: ProxyConfig = Field( + default_factory=ProxyConfig, title="Proxy configuration." + ) + telemetry: TelemetryConfig = Field( + default_factory=TelemetryConfig, title="Telemetry configuration." + ) + tls: TlsConfig = Field(default_factory=TlsConfig, title="TLS configuration.") + ui: UIConfig = Field(default_factory=UIConfig, title="UI configuration.") + + # Detector config + detectors: Dict[str, BaseDetectorConfig] = Field( + default=DEFAULT_DETECTORS, + title="Detector hardware configuration.", + ) + model: ModelConfig = Field( + default_factory=ModelConfig, title="Detection model configuration." + ) + + # GenAI config + genai: GenAIConfig = Field( + default_factory=GenAIConfig, title="Generative AI configuration." + ) + + # Camera config + cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.") + audio: AudioConfig = Field( + default_factory=AudioConfig, title="Global Audio events configuration." + ) + birdseye: BirdseyeConfig = Field( + default_factory=BirdseyeConfig, title="Birdseye configuration." + ) + detect: DetectConfig = Field( + default_factory=DetectConfig, title="Global object tracking configuration." + ) + ffmpeg: FfmpegConfig = Field( + default_factory=FfmpegConfig, title="Global FFmpeg configuration." + ) + live: CameraLiveConfig = Field( + default_factory=CameraLiveConfig, title="Live playback settings." + ) + motion: Optional[MotionConfig] = Field( + default=None, title="Global motion detection configuration." + ) + objects: ObjectConfig = Field( + default_factory=ObjectConfig, title="Global object configuration." + ) + record: RecordConfig = Field( + default_factory=RecordConfig, title="Global record configuration." + ) + review: ReviewConfig = Field( + default_factory=ReviewConfig, title="Review configuration." + ) + snapshots: SnapshotsConfig = Field( + default_factory=SnapshotsConfig, title="Global snapshots configuration." + ) + timestamp_style: TimestampStyleConfig = Field( + default_factory=TimestampStyleConfig, + title="Global timestamp style configuration.", + ) + + # Classification Config + audio_transcription: AudioTranscriptionConfig = Field( + default_factory=AudioTranscriptionConfig, title="Audio transcription config." + ) + classification: ClassificationConfig = Field( + default_factory=ClassificationConfig, title="Object classification config." + ) + semantic_search: SemanticSearchConfig = Field( + default_factory=SemanticSearchConfig, title="Semantic search configuration." + ) + face_recognition: FaceRecognitionConfig = Field( + default_factory=FaceRecognitionConfig, title="Face recognition config." + ) + lpr: LicensePlateRecognitionConfig = Field( + default_factory=LicensePlateRecognitionConfig, + title="License Plate recognition config.", + ) + + camera_groups: Dict[str, CameraGroupConfig] = Field( + default_factory=dict, title="Camera group configuration" + ) + + _plus_api: PlusApi + + @property + def plus_api(self) -> PlusApi: + return self._plus_api + + @model_validator(mode="after") + def post_validation(self, info: ValidationInfo) -> Self: + # Load plus api from context, if possible. + self._plus_api = None + if isinstance(info.context, dict): + self._plus_api = info.context.get("plus_api") + + # Ensure self._plus_api is set, if no explicit value is provided. + if self._plus_api is None: + self._plus_api = PlusApi() + + # set notifications state + self.notifications.enabled_in_config = self.notifications.enabled + + # set default min_score for object attributes + for attribute in self.model.all_attributes: + if not self.objects.filters.get(attribute): + self.objects.filters[attribute] = FilterConfig(min_score=0.7) + elif self.objects.filters[attribute].min_score == 0.5: + self.objects.filters[attribute].min_score = 0.7 + + # auto detect hwaccel args + if self.ffmpeg.hwaccel_args == "auto": + self.ffmpeg.hwaccel_args = auto_detect_hwaccel() + + # Global config to propagate down to camera level + global_config = self.model_dump( + include={ + "audio": ..., + "audio_transcription": ..., + "birdseye": ..., + "face_recognition": ..., + "lpr": ..., + "record": ..., + "snapshots": ..., + "live": ..., + "objects": ..., + "review": ..., + "motion": ..., + "notifications": ..., + "detect": ..., + "ffmpeg": ..., + "timestamp_style": ..., + }, + exclude_unset=True, + ) + + for key, detector in self.detectors.items(): + adapter = TypeAdapter(DetectorConfig) + model_dict = ( + detector + if isinstance(detector, dict) + else detector.model_dump(warnings="none") + ) + detector_config: BaseDetectorConfig = adapter.validate_python(model_dict) + + # users should not set model themselves + if detector_config.model: + detector_config.model = None + + model_config = self.model.model_dump(exclude_unset=True, warnings="none") + + if detector_config.model_path: + model_config["path"] = detector_config.model_path + + if "path" not in model_config: + if detector_config.type == "cpu" or detector_config.type.endswith( + "_tfl" + ): + model_config["path"] = "/cpu_model.tflite" + elif detector_config.type == "edgetpu": + model_config["path"] = "/edgetpu_model.tflite" + + model = ModelConfig.model_validate(model_config) + model.check_and_load_plus_model(self.plus_api, detector_config.type) + model.compute_model_hash() + labelmap_objects = model.merged_labelmap.values() + detector_config.model = model + self.detectors[key] = detector_config + + for name, camera in self.cameras.items(): + modified_global_config = global_config.copy() + + # only populate some fields down to the camera level for specific keys + allowed_fields_map = { + "face_recognition": ["enabled", "min_area"], + "lpr": ["enabled", "expire_time", "min_area", "enhancement"], + "audio_transcription": ["enabled", "live_enabled"], + } + + for section in allowed_fields_map: + if section in modified_global_config: + modified_global_config[section] = { + k: v + for k, v in modified_global_config[section].items() + if k in allowed_fields_map[section] + } + + merged_config = deep_merge( + camera.model_dump(exclude_unset=True), modified_global_config + ) + camera_config: CameraConfig = CameraConfig.model_validate( + {"name": name, **merged_config} + ) + + if camera_config.ffmpeg.hwaccel_args == "auto": + camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args + + for input in camera_config.ffmpeg.inputs: + need_detect_dimensions = "detect" in input.roles and ( + camera_config.detect.height is None + or camera_config.detect.width is None + ) + + if need_detect_dimensions: + stream_info = {"width": 0, "height": 0, "fourcc": None} + try: + stream_info = stream_info_retriever.get_stream_info( + self.ffmpeg, input.path + ) + except Exception: + logger.warning( + f"Error detecting stream parameters automatically for {input.path} Applying default values." + ) + stream_info = {"width": 0, "height": 0, "fourcc": None} + + if need_detect_dimensions: + camera_config.detect.width = ( + stream_info["width"] + if stream_info.get("width") + else DEFAULT_DETECT_DIMENSIONS["width"] + ) + camera_config.detect.height = ( + stream_info["height"] + if stream_info.get("height") + else DEFAULT_DETECT_DIMENSIONS["height"] + ) + + # Warn if detect fps > 10 + if camera_config.detect.fps > 10 and camera_config.type != "lpr": + logger.warning( + f"{camera_config.name} detect fps is set to {camera_config.detect.fps}. This does NOT need to match your camera's frame rate. High values could lead to reduced performance. Recommended value is 5." + ) + if camera_config.detect.fps > 15 and camera_config.type == "lpr": + logger.warning( + f"{camera_config.name} detect fps is set to {camera_config.detect.fps}. This does NOT need to match your camera's frame rate. High values could lead to reduced performance. Recommended value for LPR cameras are between 5-15." + ) + + # Default min_initialized configuration + min_initialized = int(camera_config.detect.fps / 2) + if camera_config.detect.min_initialized is None: + camera_config.detect.min_initialized = min_initialized + + # Default max_disappeared configuration + max_disappeared = camera_config.detect.fps * 5 + if camera_config.detect.max_disappeared is None: + camera_config.detect.max_disappeared = max_disappeared + + # Default stationary_threshold configuration + stationary_threshold = camera_config.detect.fps * 10 + if camera_config.detect.stationary.threshold is None: + camera_config.detect.stationary.threshold = stationary_threshold + # default to the stationary_threshold if not defined + if camera_config.detect.stationary.interval is None: + camera_config.detect.stationary.interval = stationary_threshold + + # set config pre-value + camera_config.enabled_in_config = camera_config.enabled + camera_config.audio.enabled_in_config = camera_config.audio.enabled + camera_config.audio_transcription.enabled_in_config = ( + camera_config.audio_transcription.enabled + ) + camera_config.record.enabled_in_config = camera_config.record.enabled + camera_config.notifications.enabled_in_config = ( + camera_config.notifications.enabled + ) + camera_config.onvif.autotracking.enabled_in_config = ( + camera_config.onvif.autotracking.enabled + ) + camera_config.review.alerts.enabled_in_config = ( + camera_config.review.alerts.enabled + ) + camera_config.review.detections.enabled_in_config = ( + camera_config.review.detections.enabled + ) + camera_config.objects.genai.enabled_in_config = ( + camera_config.objects.genai.enabled + ) + camera_config.review.genai.enabled_in_config = ( + camera_config.review.genai.enabled + ) + + # Add default filters + object_keys = camera_config.objects.track + if camera_config.objects.filters is None: + camera_config.objects.filters = {} + object_keys = object_keys - camera_config.objects.filters.keys() + for key in object_keys: + camera_config.objects.filters[key] = FilterConfig() + + # Apply global object masks and convert masks to numpy array + for object, filter in camera_config.objects.filters.items(): + if camera_config.objects.mask: + filter_mask = [] + if filter.mask is not None: + filter_mask = ( + filter.mask + if isinstance(filter.mask, list) + else [filter.mask] + ) + object_mask = ( + get_relative_coordinates( + ( + camera_config.objects.mask + if isinstance(camera_config.objects.mask, list) + else [camera_config.objects.mask] + ), + camera_config.frame_shape, + ) + or [] + ) + filter.mask = filter_mask + object_mask + + # Set runtime filter to create masks + camera_config.objects.filters[object] = RuntimeFilterConfig( + frame_shape=camera_config.frame_shape, + **filter.model_dump(exclude_unset=True), + ) + + # Convert motion configuration + if camera_config.motion is None: + camera_config.motion = RuntimeMotionConfig( + frame_shape=camera_config.frame_shape + ) + else: + camera_config.motion = RuntimeMotionConfig( + frame_shape=camera_config.frame_shape, + raw_mask=camera_config.motion.mask, + **camera_config.motion.model_dump(exclude_unset=True), + ) + camera_config.motion.enabled_in_config = camera_config.motion.enabled + + # generate zone contours + if len(camera_config.zones) > 0: + for zone in camera_config.zones.values(): + zone.generate_contour(camera_config.frame_shape) + + # Set live view stream if none is set + if not camera_config.live.streams: + camera_config.live.streams = {name: name} + + # generate the ffmpeg commands + camera_config.create_ffmpeg_cmds() + self.cameras[name] = camera_config + + verify_config_roles(camera_config) + verify_valid_live_stream_names(self, camera_config) + verify_recording_segments_setup_with_reasonable_time(camera_config) + verify_zone_objects_are_tracked(camera_config) + verify_required_zones_exist(camera_config) + verify_autotrack_zones(camera_config) + verify_motion_and_detect(camera_config) + verify_objects_track(camera_config, labelmap_objects) + verify_lpr_and_face(self, camera_config) + + # set names on classification configs + for name, config in self.classification.custom.items(): + config.name = name + + self.objects.parse_all_objects(self.cameras) + self.model.create_colormap(sorted(self.objects.all_objects)) + self.model.check_and_load_plus_model(self.plus_api) + + # Check audio transcription and audio detection requirements + if self.audio_transcription.enabled: + # If audio transcription is enabled globally, at least one camera must have audio detection enabled + if not any(camera.audio.enabled for camera in self.cameras.values()): + raise ValueError( + "Audio transcription is enabled globally, but no cameras have audio detection enabled. At least one camera must have audio detection enabled." + ) + else: + # If audio transcription is disabled globally, check each camera with audio_transcription enabled + for camera in self.cameras.values(): + if camera.audio_transcription.enabled and not camera.audio.enabled: + raise ValueError( + f"Camera {camera.name} has audio transcription enabled, but audio detection is not enabled for this camera. Audio detection must be enabled for cameras with audio transcription when it is disabled globally." + ) + + if self.plus_api and not self.snapshots.clean_copy: + logger.warning( + "Frigate+ is configured but clean snapshots are not enabled, submissions to Frigate+ will not be possible./" + ) + + # Validate auth roles against cameras + camera_names = set(self.cameras.keys()) + + for role, allowed_cameras in self.auth.roles.items(): + invalid_cameras = [ + cam for cam in allowed_cameras if cam not in camera_names + ] + if invalid_cameras: + logger.warning( + f"Role '{role}' references non-existent cameras: {invalid_cameras}. " + ) + + return self + + @field_validator("cameras") + @classmethod + def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]): + zones = [zone for camera in v.values() for zone in camera.zones.keys()] + for zone in zones: + if zone in v.keys(): + raise ValueError("Zones cannot share names with cameras") + return v + + @classmethod + def load(cls, **kwargs): + """Loads the Frigate config file, runs migrations, and creates the config object.""" + config_path = find_config_file() + + # No configuration file found, create one. + new_config = False + if not os.path.isfile(config_path): + logger.info("No config file found, saving default config") + config_path = config_path + new_config = True + else: + # Check if the config file needs to be migrated. + migrate_frigate_config(config_path) + + # Finally, load the resulting configuration file. + with open(config_path, "a+" if new_config else "r") as f: + # Only write the default config if the opened file is non-empty. This can happen as + # a race condition. It's extremely unlikely, but eh. Might as well check it. + if new_config and f.tell() == 0: + f.write(DEFAULT_CONFIG) + logger.info( + "Created default config file, see the getting started docs \ + for configuration https://docs.frigate.video/guides/getting_started" + ) + + f.seek(0) + return FrigateConfig.parse(f, **kwargs) + + @classmethod + def parse(cls, config, *, is_json=None, safe_load=False, **context): + # If config is a file, read its contents. + if hasattr(config, "read"): + fname = getattr(config, "name", None) + config = config.read() + + # Try to guess the value of is_json from the file extension. + if is_json is None and fname: + _, ext = os.path.splitext(fname) + if ext in (".yaml", ".yml"): + is_json = False + elif ext == ".json": + is_json = True + + # At this point, try to sniff the config string, to guess if it is json or not. + if is_json is None: + is_json = REGEX_JSON.match(config) is not None + + # Parse the config into a dictionary. + if is_json: + config = json.load(config) + else: + config = yaml.load(config) + + # load minimal Frigate config after the full config did not validate + if safe_load: + safe_config = {"safe_mode": True, "cameras": {}, "mqtt": {"enabled": False}} + + # copy over auth and proxy config in case auth needs to be enforced + safe_config["auth"] = config.get("auth", {}) + safe_config["proxy"] = config.get("proxy", {}) + + # copy over database config for auth and so a new db is not created + safe_config["database"] = config.get("database", {}) + + return cls.parse_object(safe_config, **context) + + # Validate and return the config dict. + return cls.parse_object(config, **context) + + @classmethod + def parse_yaml(cls, config_yaml, **context): + return cls.parse(config_yaml, is_json=False, **context) + + @classmethod + def parse_object( + cls, obj: Any, *, plus_api: Optional[PlusApi] = None, install: bool = False + ): + return cls.model_validate( + obj, context={"plus_api": plus_api, "install": install} + ) diff --git a/sam2-cpu/frigate-dev/frigate/config/database.py b/sam2-cpu/frigate-dev/frigate/config/database.py new file mode 100644 index 0000000..8daca0d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/database.py @@ -0,0 +1,11 @@ +from pydantic import Field + +from frigate.const import DEFAULT_DB_PATH + +from .base import FrigateBaseModel + +__all__ = ["DatabaseConfig"] + + +class DatabaseConfig(FrigateBaseModel): + path: str = Field(default=DEFAULT_DB_PATH, title="Database path.") # noqa: F821 diff --git a/sam2-cpu/frigate-dev/frigate/config/env.py b/sam2-cpu/frigate-dev/frigate/config/env.py new file mode 100644 index 0000000..6534ff4 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/env.py @@ -0,0 +1,33 @@ +import os +from pathlib import Path +from typing import Annotated + +from pydantic import AfterValidator, ValidationInfo + +FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} +secrets_dir = os.environ.get("CREDENTIALS_DIRECTORY", "/run/secrets") +# read secret files as env vars too +if os.path.isdir(secrets_dir) and os.access(secrets_dir, os.R_OK): + for secret_file in os.listdir(secrets_dir): + if secret_file.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[secret_file] = ( + Path(os.path.join(secrets_dir, secret_file)).read_text().strip() + ) + + +def validate_env_string(v: str) -> str: + return v.format(**FRIGATE_ENV_VARS) + + +EnvString = Annotated[str, AfterValidator(validate_env_string)] + + +def validate_env_vars(v: dict[str, str], info: ValidationInfo) -> dict[str, str]: + if isinstance(info.context, dict) and info.context.get("install", False): + for k, v in v.items(): + os.environ[k] = v + + return v + + +EnvVars = Annotated[dict[str, str], AfterValidator(validate_env_vars)] diff --git a/sam2-cpu/frigate-dev/frigate/config/logger.py b/sam2-cpu/frigate-dev/frigate/config/logger.py new file mode 100644 index 0000000..0ba3e69 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/logger.py @@ -0,0 +1,22 @@ +from pydantic import Field, ValidationInfo, model_validator +from typing_extensions import Self + +from frigate.log import LogLevel, apply_log_levels + +from .base import FrigateBaseModel + +__all__ = ["LoggerConfig"] + + +class LoggerConfig(FrigateBaseModel): + default: LogLevel = Field(default=LogLevel.info, title="Default logging level.") + logs: dict[str, LogLevel] = Field( + default_factory=dict, title="Log level for specified processes." + ) + + @model_validator(mode="after") + def post_validation(self, info: ValidationInfo) -> Self: + if isinstance(info.context, dict) and info.context.get("install", False): + apply_log_levels(self.default.value.upper(), self.logs) + + return self diff --git a/sam2-cpu/frigate-dev/frigate/config/mqtt.py b/sam2-cpu/frigate-dev/frigate/config/mqtt.py new file mode 100644 index 0000000..a760d0a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/mqtt.py @@ -0,0 +1,39 @@ +from typing import Optional + +from pydantic import Field, ValidationInfo, model_validator +from typing_extensions import Self + +from frigate.const import FREQUENCY_STATS_POINTS + +from .base import FrigateBaseModel +from .env import EnvString + +__all__ = ["MqttConfig"] + + +class MqttConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable MQTT Communication.") + host: str = Field(default="", title="MQTT Host") + port: int = Field(default=1883, title="MQTT Port") + topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix") + client_id: str = Field(default="frigate", title="MQTT Client ID") + stats_interval: int = Field( + default=60, ge=FREQUENCY_STATS_POINTS, title="MQTT Camera Stats Interval" + ) + user: Optional[EnvString] = Field(default=None, title="MQTT Username") + password: Optional[EnvString] = Field( + default=None, title="MQTT Password", validate_default=True + ) + tls_ca_certs: Optional[str] = Field(default=None, title="MQTT TLS CA Certificates") + tls_client_cert: Optional[str] = Field( + default=None, title="MQTT TLS Client Certificate" + ) + tls_client_key: Optional[str] = Field(default=None, title="MQTT TLS Client Key") + tls_insecure: Optional[bool] = Field(default=None, title="MQTT TLS Insecure") + qos: int = Field(default=0, title="MQTT QoS") + + @model_validator(mode="after") + def user_requires_pass(self, info: ValidationInfo) -> Self: + if (self.user is None) != (self.password is None): + raise ValueError("Password must be provided with username.") + return self diff --git a/sam2-cpu/frigate-dev/frigate/config/network.py b/sam2-cpu/frigate-dev/frigate/config/network.py new file mode 100644 index 0000000..c8b3cfd --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/network.py @@ -0,0 +1,13 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["IPv6Config", "NetworkingConfig"] + + +class IPv6Config(FrigateBaseModel): + enabled: bool = Field(default=False, title="Enable IPv6 for port 5000 and/or 8971") + + +class NetworkingConfig(FrigateBaseModel): + ipv6: IPv6Config = Field(default_factory=IPv6Config, title="Network configuration") diff --git a/sam2-cpu/frigate-dev/frigate/config/proxy.py b/sam2-cpu/frigate-dev/frigate/config/proxy.py new file mode 100644 index 0000000..a46b7b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/proxy.py @@ -0,0 +1,50 @@ +from typing import Optional + +from pydantic import Field, field_validator + +from .base import FrigateBaseModel +from .env import EnvString + +__all__ = ["ProxyConfig", "HeaderMappingConfig"] + + +class HeaderMappingConfig(FrigateBaseModel): + user: str = Field( + default=None, title="Header name from upstream proxy to identify user." + ) + role: str = Field( + default=None, + title="Header name from upstream proxy to identify user role.", + ) + role_map: Optional[dict[str, list[str]]] = Field( + default_factory=dict, + title=("Mapping of Frigate roles to upstream group values. "), + ) + + +class ProxyConfig(FrigateBaseModel): + header_map: HeaderMappingConfig = Field( + default_factory=HeaderMappingConfig, + title="Header mapping definitions for proxy user passing.", + ) + logout_url: Optional[str] = Field( + default=None, title="Redirect url for logging out with proxy." + ) + auth_secret: Optional[EnvString] = Field( + default=None, + title="Secret value for proxy authentication.", + ) + default_role: Optional[str] = Field( + default="viewer", title="Default role for proxy users." + ) + separator: Optional[str] = Field( + default=",", + title="The character used to separate values in a mapped header.", + ) + + @field_validator("separator", mode="before") + @classmethod + def validate_separator_length(cls, v): + if v is not None and len(v) != 1: + raise ValueError("Separator must be exactly one character") + return v diff --git a/sam2-cpu/frigate-dev/frigate/config/telemetry.py b/sam2-cpu/frigate-dev/frigate/config/telemetry.py new file mode 100644 index 0000000..ab18831 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/telemetry.py @@ -0,0 +1,29 @@ +from typing import Optional + +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TelemetryConfig", "StatsConfig"] + + +class StatsConfig(FrigateBaseModel): + amd_gpu_stats: bool = Field(default=True, title="Enable AMD GPU stats.") + intel_gpu_stats: bool = Field(default=True, title="Enable Intel GPU stats.") + network_bandwidth: bool = Field( + default=False, title="Enable network bandwidth for ffmpeg processes." + ) + intel_gpu_device: Optional[str] = Field( + default=None, title="Define the device to use when gathering SR-IOV stats." + ) + + +class TelemetryConfig(FrigateBaseModel): + network_interfaces: list[str] = Field( + default=[], + title="Enabled network interfaces for bandwidth calculation.", + ) + stats: StatsConfig = Field( + default_factory=StatsConfig, title="System Stats Configuration" + ) + version_check: bool = Field(default=True, title="Enable latest version check.") diff --git a/sam2-cpu/frigate-dev/frigate/config/tls.py b/sam2-cpu/frigate-dev/frigate/config/tls.py new file mode 100644 index 0000000..673e105 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/tls.py @@ -0,0 +1,9 @@ +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TlsConfig"] + + +class TlsConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable TLS for port 8971") diff --git a/sam2-cpu/frigate-dev/frigate/config/ui.py b/sam2-cpu/frigate-dev/frigate/config/ui.py new file mode 100644 index 0000000..8e0d4d7 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/config/ui.py @@ -0,0 +1,42 @@ +from enum import Enum +from typing import Optional + +from pydantic import Field + +from .base import FrigateBaseModel + +__all__ = ["TimeFormatEnum", "DateTimeStyleEnum", "UnitSystemEnum", "UIConfig"] + + +class TimeFormatEnum(str, Enum): + browser = "browser" + hours12 = "12hour" + hours24 = "24hour" + + +class DateTimeStyleEnum(str, Enum): + full = "full" + long = "long" + medium = "medium" + short = "short" + + +class UnitSystemEnum(str, Enum): + imperial = "imperial" + metric = "metric" + + +class UIConfig(FrigateBaseModel): + timezone: Optional[str] = Field(default=None, title="Override UI timezone.") + time_format: TimeFormatEnum = Field( + default=TimeFormatEnum.browser, title="Override UI time format." + ) + date_style: DateTimeStyleEnum = Field( + default=DateTimeStyleEnum.short, title="Override UI dateStyle." + ) + time_style: DateTimeStyleEnum = Field( + default=DateTimeStyleEnum.medium, title="Override UI timeStyle." + ) + unit_system: UnitSystemEnum = Field( + default=UnitSystemEnum.metric, title="The unit system to use for measurements." + ) diff --git a/sam2-cpu/frigate-dev/frigate/const.py b/sam2-cpu/frigate-dev/frigate/const.py new file mode 100644 index 0000000..11e8988 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/const.py @@ -0,0 +1,151 @@ +import os +import re + +INSTALL_DIR = "/opt/frigate" +CONFIG_DIR = "/config" +DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db" +MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache" +BASE_DIR = "/media/frigate" +CLIPS_DIR = f"{BASE_DIR}/clips" +EXPORT_DIR = f"{BASE_DIR}/exports" +FACE_DIR = f"{CLIPS_DIR}/faces" +THUMB_DIR = f"{CLIPS_DIR}/thumbs" +RECORD_DIR = f"{BASE_DIR}/recordings" +TRIGGER_DIR = f"{CLIPS_DIR}/triggers" +BIRDSEYE_PIPE = "/tmp/cache/birdseye" +CACHE_DIR = "/tmp/cache" +FRIGATE_LOCALHOST = "http://127.0.0.1:5000" +PLUS_ENV_VAR = "PLUS_API_KEY" +PLUS_API_HOST = "https://api.frigate.video" + +SHM_FRAMES_VAR = "SHM_MAX_FRAMES" + +# Attribute & Object constants + +DEFAULT_ATTRIBUTE_LABEL_MAP = { + "person": ["amazon", "face"], + "car": [ + "amazon", + "an_post", + "canada_post", + "dhl", + "dpd", + "fedex", + "gls", + "license_plate", + "nzpost", + "postnl", + "postnord", + "purolator", + "royal_mail", + "ups", + "usps", + ], + "motorcycle": ["license_plate"], +} +LABEL_CONSOLIDATION_MAP = { + "car": 0.8, + "face": 0.5, +} +LABEL_CONSOLIDATION_DEFAULT = 0.9 +LABEL_NMS_MAP = { + "car": 0.6, +} +LABEL_NMS_DEFAULT = 0.4 + +# Audio constants + +AUDIO_DURATION = 0.975 +AUDIO_FORMAT = "s16le" +AUDIO_MAX_BIT_RANGE = 32768.0 +AUDIO_SAMPLE_RATE = 16000 +AUDIO_MIN_CONFIDENCE = 0.5 + +# DB constants + +MAX_WAL_SIZE = 10 # MB + +# Ffmpeg constants + +DEFAULT_FFMPEG_VERSION = os.environ.get("DEFAULT_FFMPEG_VERSION", "") +INCLUDED_FFMPEG_VERSIONS = os.environ.get("INCLUDED_FFMPEG_VERSIONS", "").split(":") +LIBAVFORMAT_VERSION_MAJOR = int(os.environ.get("LIBAVFORMAT_VERSION_MAJOR", "59")) +FFMPEG_HWACCEL_NVIDIA = "preset-nvidia" +FFMPEG_HWACCEL_VAAPI = "preset-vaapi" +FFMPEG_HWACCEL_VULKAN = "preset-vulkan" +FFMPEG_HWACCEL_RKMPP = "preset-rkmpp" +FFMPEG_HWACCEL_AMF = "preset-amd-amf" +FFMPEG_HVC1_ARGS = ["-tag:v", "hvc1"] + +# Regex constants + +REGEX_CAMERA_NAME = r"^[a-zA-Z0-9_-]+$" +REGEX_RTSP_CAMERA_USER_PASS = r":\/\/[a-zA-Z0-9_-]+:[\S]+@" +REGEX_HTTP_CAMERA_USER_PASS = r"user=[a-zA-Z0-9_-]+&password=[\S]+" +REGEX_JSON = re.compile(r"^\s*\{") + +# Known Driver Names + +DRIVER_ENV_VAR = "LIBVA_DRIVER_NAME" +DRIVER_AMD = "radeonsi" +DRIVER_INTEL_i965 = "i965" +DRIVER_INTEL_iHD = "iHD" + +# Preview Values + +PREVIEW_FRAME_TYPE = "webp" + +# Record Values + +CACHE_SEGMENT_FORMAT = "%Y%m%d%H%M%S%z" +MAX_PRE_CAPTURE = 60 +MAX_SEGMENT_DURATION = 600 +MAX_SEGMENTS_IN_CACHE = 6 +MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times + +# Internal Comms Topics + +INSERT_MANY_RECORDINGS = "insert_many_recordings" +INSERT_PREVIEW = "insert_preview" +REQUEST_REGION_GRID = "request_region_grid" +UPSERT_REVIEW_SEGMENT = "upsert_review_segment" +CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments" +UPDATE_CAMERA_ACTIVITY = "update_camera_activity" +UPDATE_AUDIO_ACTIVITY = "update_audio_activity" +EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity" +UPDATE_AUDIO_TRANSCRIPTION_STATE = "update_audio_transcription_state" +UPDATE_EVENT_DESCRIPTION = "update_event_description" +UPDATE_REVIEW_DESCRIPTION = "update_review_description" +UPDATE_MODEL_STATE = "update_model_state" +UPDATE_EMBEDDINGS_REINDEX_PROGRESS = "handle_embeddings_reindex_progress" +UPDATE_BIRDSEYE_LAYOUT = "update_birdseye_layout" +NOTIFICATION_TEST = "notification_test" + +# IO Nice Values + +PROCESS_PRIORITY_HIGH = 0 +PROCESS_PRIORITY_MED = 10 +PROCESS_PRIORITY_LOW = 19 + +# Stats Values + +FREQUENCY_STATS_POINTS = 15 + +# Autotracking + +AUTOTRACKING_MAX_AREA_RATIO = 0.6 +AUTOTRACKING_MOTION_MIN_DISTANCE = 20 +AUTOTRACKING_MOTION_MAX_POINTS = 500 +AUTOTRACKING_MAX_MOVE_METRICS = 500 +AUTOTRACKING_ZOOM_OUT_HYSTERESIS = 1.1 +AUTOTRACKING_ZOOM_IN_HYSTERESIS = 0.95 +AUTOTRACKING_ZOOM_EDGE_THRESHOLD = 0.05 + +# Auth + +JWT_SECRET_ENV_VAR = "FRIGATE_JWT_SECRET" +PASSWORD_HASH_ALGORITHM = "pbkdf2_sha256" + +# Queues + +FAST_QUEUE_TIMEOUT = 0.00001 # seconds diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/common/audio_transcription/model.py b/sam2-cpu/frigate-dev/frigate/data_processing/common/audio_transcription/model.py new file mode 100644 index 0000000..82472ad --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/common/audio_transcription/model.py @@ -0,0 +1,83 @@ +"""Set up audio transcription models based on model size.""" + +import logging +import os + +import sherpa_onnx + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR +from frigate.data_processing.types import AudioTranscriptionModel +from frigate.util.downloader import ModelDownloader + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionModelRunner: + def __init__( + self, + device: str = "CPU", + model_size: str = "small", + ): + self.model: AudioTranscriptionModel = None + self.requestor = InterProcessRequestor() + + if model_size == "large": + # use the Whisper download function instead of our own + # Import dynamically to avoid crashes on systems without AVX support + from faster_whisper.utils import download_model + + logger.debug("Downloading Whisper audio transcription model") + download_model( + size_or_id="small" if device == "cuda" else "tiny", + local_files_only=False, + cache_dir=os.path.join(MODEL_CACHE_DIR, "whisper"), + ) + logger.debug("Whisper audio transcription model downloaded") + + else: + # small model as default + download_path = os.path.join(MODEL_CACHE_DIR, "sherpa-onnx") + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + self.model_files = { + "encoder.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/encoder-epoch-99-avg-1-chunk-16-left-128.onnx", + "decoder.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/decoder-epoch-99-avg-1-chunk-16-left-128.onnx", + "joiner.onnx": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/joiner-epoch-99-avg-1-chunk-16-left-128.onnx", + "tokens.txt": f"{HF_ENDPOINT}/csukuangfj/sherpa-onnx-streaming-zipformer-en-2023-06-26/resolve/main/tokens.txt", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + self.downloader = ModelDownloader( + model_name="sherpa-onnx", + download_path=download_path, + file_names=self.model_files.keys(), + download_func=self.__download_models, + ) + self.downloader.ensure_model_files() + self.downloader.wait_for_download() + + self.model = sherpa_onnx.OnlineRecognizer.from_transducer( + tokens=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/tokens.txt"), + encoder=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/encoder.onnx"), + decoder=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/decoder.onnx"), + joiner=os.path.join(MODEL_CACHE_DIR, "sherpa-onnx/joiner.onnx"), + num_threads=2, + sample_rate=16000, + feature_dim=80, + enable_endpoint_detection=True, + rule1_min_trailing_silence=2.4, + rule2_min_trailing_silence=1.2, + rule3_min_utterance_length=300, + decoding_method="greedy_search", + provider="cpu", + ) + + def __download_models(self, path: str) -> None: + try: + file_name = os.path.basename(path) + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/common/face/model.py b/sam2-cpu/frigate-dev/frigate/data_processing/common/face/model.py new file mode 100644 index 0000000..51ee649 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/common/face/model.py @@ -0,0 +1,373 @@ +import logging +import os +import queue +import threading +from abc import ABC, abstractmethod + +import cv2 +import numpy as np +from scipy import stats + +from frigate.config import FrigateConfig +from frigate.const import FACE_DIR, MODEL_CACHE_DIR +from frigate.embeddings.onnx.face_embedding import ArcfaceEmbedding, FaceNetEmbedding +from frigate.log import redirect_output_to_logger + +logger = logging.getLogger(__name__) + + +class FaceRecognizer(ABC): + """Face recognition runner.""" + + def __init__(self, config: FrigateConfig) -> None: + self.config = config + self.landmark_detector: cv2.face.FacemarkLBF = None + self.init_landmark_detector() + + @abstractmethod + def build(self) -> None: + """Build face recognition model.""" + pass + + @abstractmethod + def clear(self) -> None: + """Clear current built model.""" + pass + + @abstractmethod + def classify(self, face_image: np.ndarray) -> tuple[str, float] | None: + pass + + @redirect_output_to_logger(logger, logging.DEBUG) + def init_landmark_detector(self) -> None: + landmark_model = os.path.join(MODEL_CACHE_DIR, "facedet/landmarkdet.yaml") + + if os.path.exists(landmark_model): + self.landmark_detector = cv2.face.createFacemarkLBF() + self.landmark_detector.loadModel(landmark_model) + + def align_face( + self, + image: np.ndarray, + output_width: int, + output_height: int, + ) -> np.ndarray: + # landmark is run on grayscale images + + if image.ndim == 3: + land_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + else: + land_image = image + + _, lands = self.landmark_detector.fit( + land_image, np.array([(0, 0, land_image.shape[1], land_image.shape[0])]) + ) + landmarks: np.ndarray = lands[0][0] + + # get landmarks for eyes + leftEyePts = landmarks[42:48] + rightEyePts = landmarks[36:42] + + # compute the center of mass for each eye + leftEyeCenter = leftEyePts.mean(axis=0).astype("int") + rightEyeCenter = rightEyePts.mean(axis=0).astype("int") + + # compute the angle between the eye centroids + dY = rightEyeCenter[1] - leftEyeCenter[1] + dX = rightEyeCenter[0] - leftEyeCenter[0] + angle = np.degrees(np.arctan2(dY, dX)) - 180 + + # compute the desired right eye x-coordinate based on the + # desired x-coordinate of the left eye + desiredRightEyeX = 1.0 - 0.35 + + # determine the scale of the new resulting image by taking + # the ratio of the distance between eyes in the *current* + # image to the ratio of distance between eyes in the + # *desired* image + dist = np.sqrt((dX**2) + (dY**2)) + desiredDist = desiredRightEyeX - 0.35 + desiredDist *= output_width + scale = desiredDist / dist + + # compute center (x, y)-coordinates (i.e., the median point) + # between the two eyes in the input image + # grab the rotation matrix for rotating and scaling the face + eyesCenter = ( + int((leftEyeCenter[0] + rightEyeCenter[0]) // 2), + int((leftEyeCenter[1] + rightEyeCenter[1]) // 2), + ) + M = cv2.getRotationMatrix2D(eyesCenter, angle, scale) + + # update the translation component of the matrix + tX = output_width * 0.5 + tY = output_height * 0.35 + M[0, 2] += tX - eyesCenter[0] + M[1, 2] += tY - eyesCenter[1] + + # apply the affine transformation + return cv2.warpAffine( + image, M, (output_width, output_height), flags=cv2.INTER_CUBIC + ) + + def get_blur_confidence_reduction(self, input: np.ndarray) -> float: + """Calculates the reduction in confidence based on the blur of the image.""" + if not self.config.face_recognition.blur_confidence_filter: + return 0.0 + + variance = cv2.Laplacian(input, cv2.CV_64F).var() + logger.debug(f"face detected with blurriness {variance}") + + if variance < 120: # image is very blurry + return 0.06 + elif variance < 160: # image moderately blurry + return 0.04 + elif variance < 200: # image is slightly blurry + return 0.02 + elif variance < 250: # image is mostly clear + return 0.01 + else: + return 0.0 + + +def similarity_to_confidence( + cosine_similarity: float, median=0.3, range_width=0.6, slope_factor=12 +): + """ + Default sigmoid function to map cosine similarity to confidence. + + Args: + cosine_similarity (float): The input cosine similarity. + median (float): Assumed median of cosine similarity distribution. + range_width (float): Assumed range of cosine similarity distribution (90th percentile - 10th percentile). + slope_factor (float): Adjusts the steepness of the curve. + + Returns: + float: The confidence score. + """ + + # Calculate slope and bias + slope = slope_factor / range_width + bias = median + + # Calculate confidence + confidence = 1 / (1 + np.exp(-slope * (cosine_similarity - bias))) + return confidence + + +class FaceNetRecognizer(FaceRecognizer): + def __init__(self, config: FrigateConfig): + super().__init__(config) + self.mean_embs: dict[int, np.ndarray] = {} + self.face_embedder: FaceNetEmbedding = FaceNetEmbedding() + self.model_builder_queue: queue.Queue | None = None + + def clear(self) -> None: + self.mean_embs = {} + + def run_build_task(self) -> None: + self.model_builder_queue = queue.Queue() + + def build_model(): + face_embeddings_map: dict[str, list[np.ndarray]] = {} + idx = 0 + + dir = FACE_DIR + for name in os.listdir(dir): + if name == "train": + continue + + face_folder = os.path.join(dir, name) + + if not os.path.isdir(face_folder): + continue + + face_embeddings_map[name] = [] + for image in os.listdir(face_folder): + img = cv2.imread(os.path.join(face_folder, image)) + + if img is None: + continue + + img = self.align_face(img, img.shape[1], img.shape[0]) + emb = self.face_embedder([img])[0].squeeze() + face_embeddings_map[name].append(emb) + + idx += 1 + + self.model_builder_queue.put(face_embeddings_map) + + thread = threading.Thread(target=build_model, daemon=True) + thread.start() + + def build(self): + if not self.landmark_detector: + self.init_landmark_detector() + return None + + if self.model_builder_queue is not None: + try: + face_embeddings_map: dict[str, list[np.ndarray]] = ( + self.model_builder_queue.get(timeout=0.1) + ) + self.model_builder_queue = None + except queue.Empty: + return + else: + self.run_build_task() + return + + if not face_embeddings_map: + return + + for name, embs in face_embeddings_map.items(): + if embs: + self.mean_embs[name] = stats.trim_mean(embs, 0.15) + + logger.debug("Finished building ArcFace model") + + def classify(self, face_image): + if not self.landmark_detector: + return None + + if not self.mean_embs: + self.build() + + if not self.mean_embs: + return None + + # face recognition is best run on grayscale images + + # get blur factor before aligning face + blur_reduction = self.get_blur_confidence_reduction(face_image) + + # align face and run recognition + img = self.align_face(face_image, face_image.shape[1], face_image.shape[0]) + embedding = self.face_embedder([img])[0].squeeze() + + score = 0 + label = "" + + for name, mean_emb in self.mean_embs.items(): + dot_product = np.dot(embedding, mean_emb) + magnitude_A = np.linalg.norm(embedding) + magnitude_B = np.linalg.norm(mean_emb) + + cosine_similarity = dot_product / (magnitude_A * magnitude_B) + confidence = similarity_to_confidence( + cosine_similarity, median=0.5, range_width=0.6 + ) + + if confidence > score: + score = confidence + label = name + + return label, max(0, round(score - blur_reduction, 2)) + + +class ArcFaceRecognizer(FaceRecognizer): + def __init__(self, config: FrigateConfig): + super().__init__(config) + self.mean_embs: dict[int, np.ndarray] = {} + self.face_embedder: ArcfaceEmbedding = ArcfaceEmbedding(config.face_recognition) + self.model_builder_queue: queue.Queue | None = None + + def clear(self) -> None: + self.mean_embs = {} + + def run_build_task(self) -> None: + self.model_builder_queue = queue.Queue() + + def build_model(): + face_embeddings_map: dict[str, list[np.ndarray]] = {} + idx = 0 + + dir = FACE_DIR + for name in os.listdir(dir): + if name == "train": + continue + + face_folder = os.path.join(dir, name) + + if not os.path.isdir(face_folder): + continue + + face_embeddings_map[name] = [] + for image in os.listdir(face_folder): + img = cv2.imread(os.path.join(face_folder, image)) + + if img is None: + continue + + img = self.align_face(img, img.shape[1], img.shape[0]) + emb = self.face_embedder([img])[0].squeeze() + face_embeddings_map[name].append(emb) + + idx += 1 + + self.model_builder_queue.put(face_embeddings_map) + + thread = threading.Thread(target=build_model, daemon=True) + thread.start() + + def build(self): + if not self.landmark_detector: + self.init_landmark_detector() + return None + + if self.model_builder_queue is not None: + try: + face_embeddings_map: dict[str, list[np.ndarray]] = ( + self.model_builder_queue.get(timeout=0.1) + ) + self.model_builder_queue = None + except queue.Empty: + return + else: + self.run_build_task() + return + + if not face_embeddings_map: + return + + for name, embs in face_embeddings_map.items(): + if embs: + self.mean_embs[name] = stats.trim_mean(embs, 0.15) + + logger.debug("Finished building ArcFace model") + + def classify(self, face_image): + if not self.landmark_detector: + return None + + if not self.mean_embs: + self.build() + + if not self.mean_embs: + return None + + # face recognition is best run on grayscale images + + # get blur reduction before aligning face + blur_reduction = self.get_blur_confidence_reduction(face_image) + + # align face and run recognition + img = self.align_face(face_image, face_image.shape[1], face_image.shape[0]) + embedding = self.face_embedder([img])[0].squeeze() + + score = 0 + label = "" + + for name, mean_emb in self.mean_embs.items(): + dot_product = np.dot(embedding, mean_emb) + magnitude_A = np.linalg.norm(embedding) + magnitude_B = np.linalg.norm(mean_emb) + + cosine_similarity = dot_product / (magnitude_A * magnitude_B) + confidence = similarity_to_confidence(cosine_similarity) + + if confidence > score: + score = confidence + label = name + + return label, max(0, round(score - blur_reduction, 2)) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/mixin.py b/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/mixin.py new file mode 100644 index 0000000..a2509d4 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/mixin.py @@ -0,0 +1,1812 @@ +"""Handle processing images for face detection and recognition.""" + +import base64 +import datetime +import json +import logging +import math +import os +import random +import re +import string +from pathlib import Path +from typing import Any, List, Optional, Tuple + +import cv2 +import numpy as np +from pyclipper import ET_CLOSEDPOLYGON, JT_ROUND, PyclipperOffset +from rapidfuzz.distance import JaroWinkler, Levenshtein +from shapely.geometry import Polygon + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR +from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import area + +logger = logging.getLogger(__name__) + +WRITE_DEBUG_IMAGES = False + + +class LicensePlateProcessingMixin: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.plate_rec_speed = InferenceSpeed(self.metrics.alpr_speed) + self.plates_rec_second = EventsPerSecond() + self.plates_rec_second.start() + self.plate_det_speed = InferenceSpeed(self.metrics.yolov9_lpr_speed) + self.plates_det_second = EventsPerSecond() + self.plates_det_second.start() + self.event_metadata_publisher = EventMetadataPublisher() + self.ctc_decoder = CTCDecoder( + character_dict_path=os.path.join( + MODEL_CACHE_DIR, "paddleocr-onnx", "ppocr_keys_v1.txt" + ) + ) + # process plates that are stationary and have no position changes for 5 seconds + self.stationary_scan_duration = 5 + + self.batch_size = 6 + + # Object config + self.lp_objects: list[str] = [] + + for obj, attributes in self.config.model.attributes_map.items(): + if "license_plate" in attributes: + self.lp_objects.append(obj) + + # Detection specific parameters + self.min_size = 8 + self.max_size = 960 + self.box_thresh = 0.6 + self.mask_thresh = 0.6 + + # matching + self.similarity_threshold = 0.8 + self.cluster_threshold = 0.85 + + def _detect(self, image: np.ndarray) -> List[np.ndarray]: + """ + Detect possible areas of text in the input image by first resizing and normalizing it, + running a detection model, and filtering out low-probability regions. + + Args: + image (np.ndarray): The input image in which license plates will be detected. + + Returns: + List[np.ndarray]: A list of bounding box coordinates representing detected license plates. + """ + h, w = image.shape[:2] + + if sum([h, w]) < 64: + image = self._zero_pad(image) + + resized_image = self._resize_image(image) + normalized_image = self._normalize_image(resized_image) + + if WRITE_DEBUG_IMAGES: + current_time = int(datetime.datetime.now().timestamp()) + cv2.imwrite( + f"debug/frames/license_plate_resized_{current_time}.jpg", + resized_image, + ) + + try: + outputs = self.model_runner.detection_model([normalized_image])[0] + except Exception as e: + logger.warning(f"Error running LPR box detection model: {e}") + return [] + + outputs = outputs[0, :, :] + + if False: + current_time = int(datetime.datetime.now().timestamp()) + cv2.imwrite( + f"debug/frames/probability_map_{current_time}.jpg", + (outputs * 255).astype(np.uint8), + ) + + boxes, _ = self._boxes_from_bitmap(outputs, outputs > self.mask_thresh, w, h) + return self._filter_polygon(boxes, (h, w)) + + def _classify( + self, images: List[np.ndarray] + ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]: + """ + Classify the orientation or category of each detected license plate. + + Args: + images (List[np.ndarray]): A list of images of detected license plates. + + Returns: + Tuple[List[np.ndarray], List[Tuple[str, float]]]: A tuple of rotated/normalized plate images + and classification results with confidence scores. + """ + num_images = len(images) + indices = np.argsort([x.shape[1] / x.shape[0] for x in images]) + + for i in range(0, num_images, self.batch_size): + norm_images = [] + for j in range(i, min(num_images, i + self.batch_size)): + norm_img = self._preprocess_classification_image(images[indices[j]]) + norm_img = norm_img[np.newaxis, :] + norm_images.append(norm_img) + + try: + outputs = self.model_runner.classification_model(norm_images) + except Exception as e: + logger.warning(f"Error running LPR classification model: {e}") + return + + return self._process_classification_output(images, outputs) + + def _recognize( + self, camera: string, images: List[np.ndarray] + ) -> Tuple[List[str], List[List[float]]]: + """ + Recognize the characters on the detected license plates using the recognition model. + + Args: + images (List[np.ndarray]): A list of images of license plates to recognize. + + Returns: + Tuple[List[str], List[List[float]]]: A tuple of recognized license plate texts and confidence scores. + """ + input_shape = [3, 48, 320] + num_images = len(images) + + for index in range(0, num_images, self.batch_size): + input_h, input_w = input_shape[1], input_shape[2] + max_wh_ratio = input_w / input_h + norm_images = [] + + # calculate the maximum aspect ratio in the current batch + for i in range(index, min(num_images, index + self.batch_size)): + h, w = images[i].shape[0:2] + max_wh_ratio = max(max_wh_ratio, w * 1.0 / h) + + # preprocess the images based on the max aspect ratio + for i in range(index, min(num_images, index + self.batch_size)): + norm_image = self._preprocess_recognition_image( + camera, images[i], max_wh_ratio + ) + norm_image = norm_image[np.newaxis, :] + norm_images.append(norm_image) + + try: + outputs = self.model_runner.recognition_model(norm_images) + except Exception as e: + logger.warning(f"Error running LPR recognition model: {e}") + return [], [] + + return self.ctc_decoder(outputs) + + def _process_license_plate( + self, camera: str, id: str, image: np.ndarray + ) -> Tuple[List[str], List[List[float]], List[int]]: + """ + Complete pipeline for detecting, classifying, and recognizing license plates in the input image. + Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom, + but only combines boxes if their average confidence scores meet the threshold and their heights are similar. + + Args: + camera (str): Camera identifier. + id (str): Event identifier. + image (np.ndarray): The input image in which to detect, classify, and recognize license plates. + + Returns: + Tuple[List[str], List[List[float]], List[int]]: Detected license plate texts, character-level confidence scores for each plate (flattened into a single list per plate), and areas of the plates. + """ + if ( + self.model_runner.detection_model.runner is None + or self.model_runner.classification_model.runner is None + or self.model_runner.recognition_model.runner is None + ): + # we might still be downloading the models + logger.debug("Model runners not loaded") + return [], [], [] + + boxes = self._detect(image) + if len(boxes) == 0: + logger.debug(f"{camera}: No boxes found by OCR detector model") + return [], [], [] + + if len(boxes) > 0: + plate_left = np.min([np.min(box[:, 0]) for box in boxes]) + plate_right = np.max([np.max(box[:, 0]) for box in boxes]) + plate_width = plate_right - plate_left + else: + plate_width = 0 + + boxes = self._merge_nearby_boxes( + boxes, plate_width=plate_width, gap_fraction=0.1 + ) + + current_time = int(datetime.datetime.now().timestamp()) + if WRITE_DEBUG_IMAGES: + debug_image = image.copy() + for box in boxes: + box = box.astype(int) + x_min, y_min = np.min(box[:, 0]), np.min(box[:, 1]) + x_max, y_max = np.max(box[:, 0]), np.max(box[:, 1]) + cv2.rectangle( + debug_image, + (x_min, y_min), + (x_max, y_max), + color=(0, 255, 0), + thickness=2, + ) + + cv2.imwrite( + f"debug/frames/license_plate_boxes_{current_time}.jpg", debug_image + ) + + boxes = self._sort_boxes(list(boxes)) + + # Step 1: Compute box heights and group boxes by vertical alignment and height similarity + box_info = [] + for i, box in enumerate(boxes): + y_coords = box[:, 1] + y_min, y_max = np.min(y_coords), np.max(y_coords) + height = y_max - y_min + box_info.append((y_min, y_max, height, i)) + + # Initial grouping based on y-coordinate overlap and height similarity + initial_groups = [] + current_group = [box_info[0]] + height_tolerance = 0.25 # Allow 25% difference in height for grouping + + for i in range(1, len(box_info)): + prev_y_min, prev_y_max, prev_height, _ = current_group[-1] + curr_y_min, _, curr_height, _ = box_info[i] + + # Check y-coordinate overlap + overlap_threshold = 0.1 * (prev_y_max - prev_y_min) + overlaps = curr_y_min <= prev_y_max + overlap_threshold + + # Check height similarity + height_ratio = min(prev_height, curr_height) / max(prev_height, curr_height) + height_similar = height_ratio >= (1 - height_tolerance) + + if overlaps and height_similar: + current_group.append(box_info[i]) + else: + initial_groups.append(current_group) + current_group = [box_info[i]] + initial_groups.append(current_group) + + # Step 2: Process each initial group, filter by confidence + all_license_plates = [] + all_confidences = [] + all_areas = [] + processed_indices = set() + + recognition_threshold = self.lpr_config.recognition_threshold + + for group in initial_groups: + # Sort group by y-coordinate (top to bottom) + group.sort(key=lambda x: x[0]) + group_indices = [item[3] for item in group] + + # Skip if all indices in this group have already been processed + if all(idx in processed_indices for idx in group_indices): + continue + + # Crop images for the group + group_boxes = [boxes[i] for i in group_indices] + group_plate_images = [ + self._crop_license_plate(image, box) for box in group_boxes + ] + + if WRITE_DEBUG_IMAGES: + for i, img in enumerate(group_plate_images): + cv2.imwrite( + f"debug/frames/license_plate_cropped_{current_time}_{group_indices[i] + 1}.jpg", + img, + ) + + if self.config.lpr.debug_save_plates: + logger.debug(f"{camera}: Saving plates for event {id}") + Path(os.path.join(CLIPS_DIR, f"lpr/{camera}/{id}")).mkdir( + parents=True, exist_ok=True + ) + for i, img in enumerate(group_plate_images): + cv2.imwrite( + os.path.join( + CLIPS_DIR, + f"lpr/{camera}/{id}/{current_time}_{group_indices[i] + 1}.jpg", + ), + img, + ) + + # Recognize text in each cropped image + results, confidences = self._recognize(camera, group_plate_images) + + if not results: + continue + + if not confidences: + confidences = [[0.0] for _ in results] + + # Compute average confidence for each box's recognized text + avg_confidences = [] + for conf_list in confidences: + avg_conf = sum(conf_list) / len(conf_list) if conf_list else 0.0 + avg_confidences.append(avg_conf) + + # Filter boxes based on the recognition threshold + qualifying_indices = [] + qualifying_results = [] + qualifying_confidences = [] + for i, (avg_conf, result, conf_list) in enumerate( + zip(avg_confidences, results, confidences) + ): + if avg_conf >= recognition_threshold: + qualifying_indices.append(group_indices[i]) + qualifying_results.append(result) + qualifying_confidences.append(conf_list) + + if not qualifying_results: + continue + + processed_indices.update(qualifying_indices) + + # Combine the qualifying results into a single plate string + combined_plate = " ".join(qualifying_results) + + flat_confidences = [ + conf for conf_list in qualifying_confidences for conf in conf_list + ] + + # Apply replace rules to combined_plate if configured + original_combined = combined_plate + if self.lpr_config.replace_rules: + for rule in self.lpr_config.replace_rules: + try: + pattern = getattr(rule, "pattern", "") + replacement = getattr(rule, "replacement", "") + if pattern: + combined_plate = re.sub( + pattern, replacement, combined_plate + ) + except re.error as e: + logger.warning( + f"{camera}: Invalid regex in replace_rules '{pattern}': {e}" + ) + + if combined_plate != original_combined: + logger.debug( + f"{camera}: Rules applied: '{original_combined}' -> '{combined_plate}'" + ) + + # Compute the combined area for qualifying boxes + qualifying_boxes = [boxes[i] for i in qualifying_indices] + qualifying_plate_images = [ + self._crop_license_plate(image, box) for box in qualifying_boxes + ] + group_areas = [ + img.shape[0] * img.shape[1] for img in qualifying_plate_images + ] + combined_area = sum(group_areas) + + all_license_plates.append(combined_plate) + all_confidences.append(flat_confidences) + all_areas.append(combined_area) + + # Step 3: Filter and sort the combined plates + if all_license_plates: + filtered_data = [] + for plate, conf_list, area in zip( + all_license_plates, all_confidences, all_areas + ): + if len(plate) < self.lpr_config.min_plate_length: + logger.debug( + f"{camera}: Filtered out '{plate}' due to length ({len(plate)} < {self.lpr_config.min_plate_length})" + ) + continue + + if self.lpr_config.format: + try: + if not re.fullmatch(self.lpr_config.format, plate): + logger.debug( + f"{camera}: Filtered out '{plate}' due to format mismatch" + ) + continue + except re.error: + # Skip format filtering if regex is invalid + logger.error( + f"{camera}: Invalid regex in LPR format configuration: {self.lpr_config.format}" + ) + + filtered_data.append((plate, conf_list, area)) + + sorted_data = sorted( + filtered_data, + key=lambda x: (x[2], len(x[0]), sum(x[1]) / len(x[1]) if x[1] else 0), + reverse=True, + ) + + if sorted_data: + return map(list, zip(*sorted_data)) + + return [], [], [] + + def _resize_image(self, image: np.ndarray) -> np.ndarray: + """ + Resize the input image while maintaining the aspect ratio, ensuring dimensions are multiples of 32. + + Args: + image (np.ndarray): The input image to resize. + + Returns: + np.ndarray: The resized image. + """ + h, w = image.shape[:2] + ratio = min(self.max_size / max(h, w), 1.0) + resize_h = max(int(round(int(h * ratio) / 32) * 32), 32) + resize_w = max(int(round(int(w * ratio) / 32) * 32), 32) + return cv2.resize(image, (resize_w, resize_h)) + + def _normalize_image(self, image: np.ndarray) -> np.ndarray: + """ + Normalize the input image by subtracting the mean and multiplying by the standard deviation. + + Args: + image (np.ndarray): The input image to normalize. + + Returns: + np.ndarray: The normalized image, transposed to match the model's expected input format. + """ + mean = np.array([123.675, 116.28, 103.53]).reshape(1, -1).astype("float64") + std = 1 / np.array([58.395, 57.12, 57.375]).reshape(1, -1).astype("float64") + + image = image.astype("float32") + cv2.subtract(image, mean, image) + cv2.multiply(image, std, image) + return image.transpose((2, 0, 1))[np.newaxis, ...] + + def _merge_nearby_boxes( + self, + boxes: List[np.ndarray], + plate_width: float, + gap_fraction: float = 0.1, + min_overlap_fraction: float = -0.2, + ) -> List[np.ndarray]: + """ + Merge bounding boxes that are likely part of the same license plate based on proximity, + with a dynamic max_gap based on the provided width of the entire license plate. + + Args: + boxes (List[np.ndarray]): List of bounding boxes with shape (n, 4, 2), where n is the number of boxes, + each box has 4 corners, and each corner has (x, y) coordinates. + plate_width (float): The width of the entire license plate in pixels, used to calculate max_gap. + gap_fraction (float): Fraction of the plate width to use as the maximum gap. + Default is 0.1 (10% of the plate width). + + Returns: + List[np.ndarray]: List of merged bounding boxes. + """ + if len(boxes) == 0: + return [] + + max_gap = plate_width * gap_fraction + min_overlap = plate_width * min_overlap_fraction + + # Sort boxes by top left x + sorted_boxes = sorted(boxes, key=lambda x: x[0][0]) + + merged_boxes = [] + current_box = sorted_boxes[0] + + for i in range(1, len(sorted_boxes)): + next_box = sorted_boxes[i] + + # Calculate the horizontal gap between the current box and the next box + current_right = np.max( + current_box[:, 0] + ) # Rightmost x-coordinate of current box + next_left = np.min(next_box[:, 0]) # Leftmost x-coordinate of next box + horizontal_gap = next_left - current_right + + # Check if the boxes are vertically aligned (similar y-coordinates) + current_top = np.min(current_box[:, 1]) + current_bottom = np.max(current_box[:, 1]) + next_top = np.min(next_box[:, 1]) + next_bottom = np.max(next_box[:, 1]) + + # Consider boxes part of the same plate if they are close horizontally or overlap + # within the allowed limit and their vertical positions overlap significantly + if min_overlap <= horizontal_gap <= max_gap and max( + current_top, next_top + ) <= min(current_bottom, next_bottom): + merged_points = np.vstack((current_box, next_box)) + new_box = np.array( + [ + [ + np.min(merged_points[:, 0]), + np.min(merged_points[:, 1]), + ], + [ + np.max(merged_points[:, 0]), + np.min(merged_points[:, 1]), + ], + [ + np.max(merged_points[:, 0]), + np.max(merged_points[:, 1]), + ], + [ + np.min(merged_points[:, 0]), + np.max(merged_points[:, 1]), + ], + ] + ) + current_box = new_box + else: + # If the boxes are not close enough or overlap too much, add the current box to the result + merged_boxes.append(current_box) + current_box = next_box + + # Add the last box + merged_boxes.append(current_box) + + return np.array(merged_boxes, dtype=np.int32) + + def _boxes_from_bitmap( + self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int + ) -> Tuple[np.ndarray, List[float]]: + """ + Process the binary mask to extract bounding boxes and associated confidence scores. + + Args: + output (np.ndarray): Output confidence map from the model. + mask (np.ndarray): Binary mask of detected regions. + dest_width (int): Target width for scaling the box coordinates. + dest_height (int): Target height for scaling the box coordinates. + + Returns: + Tuple[np.ndarray, List[float]]: Array of bounding boxes and list of corresponding scores. + """ + + mask = (mask * 255).astype(np.uint8) + height, width = mask.shape + outs = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + + # handle different return values of findContours between OpenCV versions + contours = outs[0] if len(outs) == 2 else outs[1] + + boxes = [] + scores = [] + + for index in range(len(contours)): + contour = contours[index] + + # get minimum bounding box (rotated rectangle) around the contour and the smallest side length. + points, sside = self._get_min_boxes(contour) + if sside < self.min_size: + continue + + points = np.array(points, dtype=np.float32) + + score = self._box_score(output, contour) + if self.box_thresh > score: + continue + + points = self._expand_box(points) + + # Get the minimum area rectangle again after expansion + points, sside = self._get_min_boxes(points.reshape(-1, 1, 2)) + if sside < self.min_size + 2: + continue + + points = np.array(points, dtype=np.float32) + + # normalize and clip box coordinates to fit within the destination image size. + points[:, 0] = np.clip( + np.round(points[:, 0] / width * dest_width), 0, dest_width + ) + points[:, 1] = np.clip( + np.round(points[:, 1] / height * dest_height), 0, dest_height + ) + + boxes.append(points.astype("int32")) + scores.append(score) + + return np.array(boxes, dtype="int32"), scores + + @staticmethod + def _get_min_boxes(contour: np.ndarray) -> Tuple[List[Tuple[float, float]], float]: + """ + Calculate the minimum bounding box (rotated rectangle) for a given contour. + + Args: + contour (np.ndarray): The contour points of the detected shape. + + Returns: + Tuple[List[Tuple[float, float]], float]: A list of four points representing the + corners of the bounding box, and the length of the shortest side. + """ + bounding_box = cv2.minAreaRect(contour) + points = sorted(cv2.boxPoints(bounding_box), key=lambda x: x[0]) + index_1, index_4 = (0, 1) if points[1][1] > points[0][1] else (1, 0) + index_2, index_3 = (2, 3) if points[3][1] > points[2][1] else (3, 2) + box = [points[index_1], points[index_2], points[index_3], points[index_4]] + return box, min(bounding_box[1]) + + @staticmethod + def _box_score(bitmap: np.ndarray, contour: np.ndarray) -> float: + """ + Calculate the average score within the bounding box of a contour. + + Args: + bitmap (np.ndarray): The output confidence map from the model. + contour (np.ndarray): The contour of the detected shape. + + Returns: + float: The average score of the pixels inside the contour region. + """ + h, w = bitmap.shape[:2] + contour = contour.reshape(-1, 2) + x1, y1 = np.clip(contour.min(axis=0), 0, [w - 1, h - 1]) + x2, y2 = np.clip(contour.max(axis=0), 0, [w - 1, h - 1]) + mask = np.zeros((y2 - y1 + 1, x2 - x1 + 1), dtype=np.uint8) + cv2.fillPoly(mask, [contour - [x1, y1]], 1) + return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0] + + @staticmethod + def _expand_box(points: List[Tuple[float, float]]) -> np.ndarray: + """ + Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio. + + Args: + points (List[Tuple[float, float]]): Points of the polygon to expand. + + Returns: + np.ndarray: Expanded polygon points. + """ + polygon = Polygon(points) + distance = polygon.area / polygon.length + offset = PyclipperOffset() + offset.AddPath(points, JT_ROUND, ET_CLOSEDPOLYGON) + expanded = np.array(offset.Execute(distance * 1.5)).reshape((-1, 2)) + return expanded + + def _filter_polygon( + self, points: List[np.ndarray], shape: Tuple[int, int] + ) -> np.ndarray: + """ + Filter a set of polygons to include only valid ones that fit within an image shape + and meet size constraints. + + Args: + points (List[np.ndarray]): List of polygons to filter. + shape (Tuple[int, int]): Shape of the image (height, width). + + Returns: + np.ndarray: List of filtered polygons. + """ + height, width = shape + return np.array( + [ + self._clockwise_order(point) + for point in points + if self._is_valid_polygon(point, width, height) + ] + ) + + @staticmethod + def _is_valid_polygon(point: np.ndarray, width: int, height: int) -> bool: + """ + Check if a polygon is valid, meaning it fits within the image bounds + and has sides of a minimum length. + + Args: + point (np.ndarray): The polygon to validate. + width (int): Image width. + height (int): Image height. + + Returns: + bool: Whether the polygon is valid or not. + """ + return ( + point[:, 0].min() >= 0 + and point[:, 0].max() < width + and point[:, 1].min() >= 0 + and point[:, 1].max() < height + and np.linalg.norm(point[0] - point[1]) > 3 + and np.linalg.norm(point[0] - point[3]) > 3 + ) + + @staticmethod + def _clockwise_order(pts: np.ndarray) -> np.ndarray: + """ + Arrange the points of a polygon in order: top-left, top-right, bottom-right, bottom-left. + taken from https://github.com/PyImageSearch/imutils/blob/master/imutils/perspective.py + + Args: + pts (np.ndarray): Array of points of the polygon. + + Returns: + np.ndarray: Points ordered clockwise starting from top-left. + """ + # Sort the points based on their x-coordinates + x_sorted = pts[np.argsort(pts[:, 0]), :] + + # Separate the left-most and right-most points + left_most = x_sorted[:2, :] + right_most = x_sorted[2:, :] + + # Sort the left-most coordinates by y-coordinates + left_most = left_most[np.argsort(left_most[:, 1]), :] + (tl, bl) = left_most # Top-left and bottom-left + + # Use the top-left as an anchor to calculate distances to right points + # The further point will be the bottom-right + distances = np.sqrt( + ((tl[0] - right_most[:, 0]) ** 2) + ((tl[1] - right_most[:, 1]) ** 2) + ) + + # Sort right points by distance (descending) + right_idx = np.argsort(distances)[::-1] + (br, tr) = right_most[right_idx, :] # Bottom-right and top-right + + return np.array([tl, tr, br, bl]) + + @staticmethod + def _sort_boxes(boxes): + """ + Sort polygons based on their position in the image. If boxes are close in vertical + position (within 5 pixels), sort them by horizontal position. + + Args: + points: detected text boxes with shape [4, 2] + + Returns: + List: sorted boxes(array) with shape [4, 2] + """ + boxes.sort(key=lambda x: (x[0][1], x[0][0])) + for i in range(len(boxes) - 1): + for j in range(i, -1, -1): + if abs(boxes[j + 1][0][1] - boxes[j][0][1]) < 5 and ( + boxes[j + 1][0][0] < boxes[j][0][0] + ): + temp = boxes[j] + boxes[j] = boxes[j + 1] + boxes[j + 1] = temp + else: + break + return boxes + + @staticmethod + def _zero_pad(image: np.ndarray) -> np.ndarray: + """ + Apply zero-padding to an image, ensuring its dimensions are at least 32x32. + The padding is added only if needed. + + Args: + image (np.ndarray): Input image. + + Returns: + np.ndarray: Zero-padded image. + """ + h, w, c = image.shape + pad = np.zeros((max(32, h), max(32, w), c), np.uint8) + pad[:h, :w, :] = image + return pad + + @staticmethod + def _preprocess_classification_image(image: np.ndarray) -> np.ndarray: + """ + Preprocess a single image for classification by resizing, normalizing, and padding. + + This method resizes the input image to a fixed height of 48 pixels while adjusting + the width dynamically up to a maximum of 192 pixels. The image is then normalized and + padded to fit the required input dimensions for classification. + + Args: + image (np.ndarray): Input image to preprocess. + + Returns: + np.ndarray: Preprocessed and padded image. + """ + # fixed height of 48, dynamic width up to 192 + input_shape = (3, 48, 192) + input_c, input_h, input_w = input_shape + + h, w = image.shape[:2] + ratio = w / h + resized_w = min(input_w, math.ceil(input_h * ratio)) + + resized_image = cv2.resize(image, (resized_w, input_h)) + + # handle single-channel images (grayscale) if needed + if input_c == 1 and resized_image.ndim == 2: + resized_image = resized_image[np.newaxis, :, :] + else: + resized_image = resized_image.transpose((2, 0, 1)) + + # normalize + resized_image = (resized_image.astype("float32") / 255.0 - 0.5) / 0.5 + + padded_image = np.zeros((input_c, input_h, input_w), dtype=np.float32) + padded_image[:, :, :resized_w] = resized_image + + return padded_image + + def _process_classification_output( + self, images: List[np.ndarray], outputs: List[np.ndarray] + ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]: + """ + Process the classification model output by matching labels with confidence scores. + + This method processes the outputs from the classification model and rotates images + with high confidence of being labeled "180". It ensures that results are mapped to + the original image order. + + Args: + images (List[np.ndarray]): List of input images. + outputs (List[np.ndarray]): Corresponding model outputs. + + Returns: + Tuple[List[np.ndarray], List[Tuple[str, float]]]: A tuple of processed images and + classification results (label and confidence score). + """ + labels = ["0", "180"] + results = [["", 0.0]] * len(images) + indices = np.argsort(np.array([x.shape[1] / x.shape[0] for x in images])) + + outputs = np.stack(outputs) + + outputs = [ + (labels[idx], outputs[i, idx]) + for i, idx in enumerate(outputs.argmax(axis=1)) + ] + + for i in range(0, len(images), self.batch_size): + for j in range(len(outputs)): + label, score = outputs[j] + results[indices[i + j]] = [label, score] + # make sure we have high confidence if we need to flip a box + if "180" in label and score >= 0.7: + images[indices[i + j]] = cv2.rotate( + images[indices[i + j]], cv2.ROTATE_180 + ) + + return images, results + + def _preprocess_recognition_image( + self, camera: string, image: np.ndarray, max_wh_ratio: float + ) -> np.ndarray: + """ + Preprocess an image for recognition by dynamically adjusting its width. + + This method adjusts the width of the image based on the maximum width-to-height ratio + while keeping the height fixed at 48 pixels. The image is then normalized and padded + to fit the required input dimensions for recognition. + + Args: + image (np.ndarray): Input image to preprocess. + max_wh_ratio (float): Maximum width-to-height ratio for resizing. + + Returns: + np.ndarray: Preprocessed and padded image. + """ + # fixed height of 48, dynamic width based on ratio + input_shape = [3, 48, 320] + input_h, input_w = input_shape[1], input_shape[2] + + assert image.shape[2] == input_shape[0], "Unexpected number of image channels." + + # convert to grayscale + if image.shape[2] == 3: + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + else: + gray = image + + if self.config.cameras[camera].lpr.enhancement > 3: + # denoise using a configurable pixel neighborhood value + logger.debug( + f"{camera}: Denoising recognition image (level: {self.config.cameras[camera].lpr.enhancement})" + ) + smoothed = cv2.bilateralFilter( + gray, + d=5 + self.config.cameras[camera].lpr.enhancement, + sigmaColor=10 * self.config.cameras[camera].lpr.enhancement, + sigmaSpace=10 * self.config.cameras[camera].lpr.enhancement, + ) + sharpening_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]) + processed = cv2.filter2D(smoothed, -1, sharpening_kernel) + else: + processed = gray + + if self.config.cameras[camera].lpr.enhancement > 0: + # always apply the same CLAHE for contrast enhancement when enhancement level is above 3 + logger.debug( + f"{camera}: Enhancing contrast for recognition image (level: {self.config.cameras[camera].lpr.enhancement})" + ) + grid_size = ( + max(4, input_w // 40), + max(4, input_h // 40), + ) + clahe = cv2.createCLAHE( + clipLimit=2 if self.config.cameras[camera].lpr.enhancement > 5 else 1.5, + tileGridSize=grid_size, + ) + enhanced = clahe.apply(processed) + else: + enhanced = processed + + # Convert back to 3-channel for model compatibility + image = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB) + + # dynamically adjust input width based on max_wh_ratio + input_w = int(input_h * max_wh_ratio) + + # check for model-specific input width + model_input_w = self.model_runner.recognition_model.runner.get_input_width() + if isinstance(model_input_w, int) and model_input_w > 0: + input_w = model_input_w + + h, w = image.shape[:2] + aspect_ratio = w / h + resized_w = min(input_w, math.ceil(input_h * aspect_ratio)) + + resized_image = cv2.resize(image, (resized_w, input_h)) + resized_image = resized_image.transpose((2, 0, 1)) + resized_image = (resized_image.astype("float32") / 255.0 - 0.5) / 0.5 + + # Compute mean pixel value of the resized image (per channel) + mean_pixel = np.mean(resized_image, axis=(1, 2), keepdims=True) + padded_image = np.full( + (input_shape[0], input_h, input_w), mean_pixel, dtype=np.float32 + ) + padded_image[:, :, :resized_w] = resized_image + + if False: + current_time = int(datetime.datetime.now().timestamp() * 1000) + cv2.imwrite( + f"debug/frames/preprocessed_recognition_{current_time}.jpg", + image, + ) + + return padded_image + + @staticmethod + def _crop_license_plate(image: np.ndarray, points: np.ndarray) -> np.ndarray: + """ + Crop the license plate from the image using four corner points. + + This method crops the region containing the license plate by using the perspective + transformation based on four corner points. If the resulting image is significantly + taller than wide, the image is rotated to the correct orientation. + + Args: + image (np.ndarray): Input image containing the license plate. + points (np.ndarray): Four corner points defining the plate's position. + + Returns: + np.ndarray: Cropped and potentially rotated license plate image. + """ + assert len(points) == 4, "shape of points must be 4*2" + points = points.astype(np.float32) + crop_width = int( + max( + np.linalg.norm(points[0] - points[1]), + np.linalg.norm(points[2] - points[3]), + ) + ) + crop_height = int( + max( + np.linalg.norm(points[0] - points[3]), + np.linalg.norm(points[1] - points[2]), + ) + ) + pts_std = np.float32( + [[0, 0], [crop_width, 0], [crop_width, crop_height], [0, crop_height]] + ) + matrix = cv2.getPerspectiveTransform(points, pts_std) + image = cv2.warpPerspective( + image, + matrix, + (crop_width, crop_height), + borderMode=cv2.BORDER_REPLICATE, + flags=cv2.INTER_CUBIC, + ) + height, width = image.shape[0:2] + if height * 1.0 / width >= 1.5: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + return image + + def _detect_license_plate( + self, camera: string, input: np.ndarray + ) -> tuple[int, int, int, int]: + """ + Use a lightweight YOLOv9 model to detect license plates for users without Frigate+ + + Return the dimensions of the detected plate as [x1, y1, x2, y2]. + """ + try: + predictions = self.model_runner.yolov9_detection_model(input) + except Exception as e: + logger.warning(f"Error running YOLOv9 license plate detection model: {e}") + return None + + confidence_threshold = self.lpr_config.detection_threshold + + top_score = -1 + top_box = None + + img_h, img_w = input.shape[0], input.shape[1] + + # Calculate resized dimensions and padding based on _preprocess_inputs + if img_w > img_h: + resized_h = int(((img_h / img_w) * LPR_EMBEDDING_SIZE) // 4 * 4) + resized_w = LPR_EMBEDDING_SIZE + x_offset = (LPR_EMBEDDING_SIZE - resized_w) // 2 + y_offset = (LPR_EMBEDDING_SIZE - resized_h) // 2 + scale_x = img_w / resized_w + scale_y = img_h / resized_h + else: + resized_w = int(((img_w / img_h) * LPR_EMBEDDING_SIZE) // 4 * 4) + resized_h = LPR_EMBEDDING_SIZE + x_offset = (LPR_EMBEDDING_SIZE - resized_w) // 2 + y_offset = (LPR_EMBEDDING_SIZE - resized_h) // 2 + scale_x = img_w / resized_w + scale_y = img_h / resized_h + + # Loop over predictions + for prediction in predictions: + score = prediction[6] + if score >= confidence_threshold: + bbox = prediction[1:5] + # Adjust for padding and scale to original image + bbox[0] = (bbox[0] - x_offset) * scale_x + bbox[1] = (bbox[1] - y_offset) * scale_y + bbox[2] = (bbox[2] - x_offset) * scale_x + bbox[3] = (bbox[3] - y_offset) * scale_y + + if score > top_score: + top_score = score + top_box = bbox + + if score > top_score: + top_score = score + top_box = bbox + + # Return the top scoring bounding box if found + if top_box is not None: + # expand box by 5% to help with OCR + expansion = (top_box[2:] - top_box[:2]) * 0.05 + + # Expand box + expanded_box = np.array( + [ + top_box[0] - expansion[0], # x1 + top_box[1] - expansion[1], # y1 + top_box[2] + expansion[0], # x2 + top_box[3] + expansion[1], # y2 + ] + ).clip(0, [input.shape[1], input.shape[0]] * 2) + + logger.debug( + f"{camera}: Found license plate. Bounding box: {expanded_box.astype(int)}" + ) + return tuple(expanded_box.astype(int)) + else: + return None # No detection above the threshold + + def _get_cluster_rep( + self, plates: List[dict] + ) -> Tuple[str, float, List[float], int]: + """ + Cluster plate variants and select the representative from the best cluster. + """ + if len(plates) == 0: + return "", 0.0, [], 0 + + if len(plates) == 1: + p = plates[0] + return p["plate"], p["conf"], p["char_confidences"], p["area"] + + # Log initial variants + logger.debug(f"Clustering {len(plates)} plate variants:") + for i, p in enumerate(plates): + logger.debug( + f" Variant {i + 1}: '{p['plate']}' (conf: {p['conf']:.3f}, area: {p['area']})" + ) + + clusters = [] + for i, plate in enumerate(plates): + merged = False + for j, cluster in enumerate(clusters): + sims = [ + JaroWinkler.similarity(plate["plate"], v["plate"]) for v in cluster + ] + if len(sims) > 0: + avg_sim = sum(sims) / len(sims) + if avg_sim >= self.cluster_threshold: + cluster.append(plate) + logger.debug( + f" Merged variant {i + 1} '{plate['plate']}' (conf: {plate['conf']:.3f}) into cluster {j + 1} (avg_sim: {avg_sim:.3f})" + ) + merged = True + break + if not merged: + clusters.append([plate]) + logger.debug( + f" Started new cluster {len(clusters)} with variant {i + 1} '{plate['plate']}' (conf: {plate['conf']:.3f})" + ) + + if not clusters: + return "", 0.0, [], 0 + + # Log cluster summaries + for j, cluster in enumerate(clusters): + cluster_size = len(cluster) + max_conf = max(v["conf"] for v in cluster) + sample_variants = [v["plate"] for v in cluster[:3]] # First 3 for brevity + logger.debug( + f" Cluster {j + 1}: size {cluster_size}, max conf {max_conf:.3f}, variants: {sample_variants}{'...' if cluster_size > 3 else ''}" + ) + + # Best cluster: largest size, tiebroken by max conf + def cluster_score(c): + return (len(c), max(v["conf"] for v in c)) + + best_cluster_idx = max( + range(len(clusters)), key=lambda j: cluster_score(clusters[j]) + ) + best_cluster = clusters[best_cluster_idx] + best_size, best_max_conf = cluster_score(best_cluster) + logger.debug( + f" Selected best cluster {best_cluster_idx + 1}: size {best_size}, max conf {best_max_conf:.3f}" + ) + + # Rep: highest conf in best cluster + rep = max(best_cluster, key=lambda v: v["conf"]) + logger.debug( + f" Selected rep from best cluster: '{rep['plate']}' (conf: {rep['conf']:.3f})" + ) + logger.debug( + f" Final clustered plate: '{rep['plate']}' (conf: {rep['conf']:.3f})" + ) + + return rep["plate"], rep["conf"], rep["char_confidences"], rep["area"] + + def _generate_plate_event(self, camera: str, plate: str, plate_score: float) -> str: + """Generate a unique ID for a plate event based on camera and text.""" + now = datetime.datetime.now().timestamp() + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + event_id = f"{now}-{rand_id}" + + self.event_metadata_publisher.publish( + ( + now, + camera, + "license_plate", + event_id, + True, + plate_score, + None, + plate, + ), + EventMetadataTypeEnum.lpr_event_create.value, + ) + return event_id + + def lpr_process( + self, obj_data: dict[str, Any], frame: np.ndarray, dedicated_lpr: bool = False + ): + """Look for license plates in image.""" + self.metrics.alpr_pps.value = self.plates_rec_second.eps() + self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps() + camera = obj_data if dedicated_lpr else obj_data["camera"] + current_time = int(datetime.datetime.now().timestamp()) + + if not self.config.cameras[camera].lpr.enabled: + return + + # dedicated LPR cam without frigate+ + if dedicated_lpr: + id = "dedicated-lpr" + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # apply motion mask + rgb[self.config.cameras[obj_data].motion.mask == 0] = [0, 0, 0] + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/dedicated_lpr_masked_{current_time}.jpg", + rgb, + ) + + yolov9_start = datetime.datetime.now().timestamp() + license_plate = self._detect_license_plate(camera, rgb) + + logger.debug( + f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" + ) + self.plates_det_second.update() + self.plate_det_speed.update( + datetime.datetime.now().timestamp() - yolov9_start + ) + + if not license_plate: + logger.debug(f"{camera}: Detected no license plates in full frame.") + return + + license_plate_area = (license_plate[2] - license_plate[0]) * ( + license_plate[3] - license_plate[1] + ) + if license_plate_area < self.config.cameras[camera].lpr.min_area: + logger.debug(f"{camera}: License plate area below minimum threshold.") + return + + license_plate_frame = rgb[ + license_plate[1] : license_plate[3], + license_plate[0] : license_plate[2], + ] + + # Double the size for better OCR + license_plate_frame = cv2.resize( + license_plate_frame, + ( + int(2 * license_plate_frame.shape[1]), + int(2 * license_plate_frame.shape[0]), + ), + ) + + else: + id = obj_data["id"] + + # don't run for non car/motorcycle or non license plate (dedicated lpr with frigate+) objects + if ( + obj_data.get("label") not in self.lp_objects + and obj_data.get("label") != "license_plate" + ): + logger.debug( + f"{camera}: Not a processing license plate for non car/motorcycle object." + ) + return + + # don't run for non-stationary objects with no position changes to avoid processing uncertain moving objects + # zero position_changes is the initial state after registering a new tracked object + # LPR will run 2 frames after detect.min_initialized is reached + if obj_data.get("position_changes", 0) == 0 and not obj_data.get( + "stationary", False + ): + logger.debug( + f"{camera}: Skipping LPR for non-stationary {obj_data['label']} object {id} with no position changes. (Detected in {self.config.cameras[camera].detect.min_initialized + 1} concurrent frames, threshold to run is {self.config.cameras[camera].detect.min_initialized + 2} frames)" + ) + return + + # run for stationary objects for a limited time after they become stationary + if obj_data.get("stationary") == True: + threshold = self.config.cameras[camera].detect.stationary.threshold + if obj_data.get("motionless_count", 0) >= threshold: + frames_since_stationary = ( + obj_data.get("motionless_count", 0) - threshold + ) + fps = self.config.cameras[camera].detect.fps + time_since_stationary = frames_since_stationary / fps + + # only print this log for a short time to avoid log spam + if ( + self.stationary_scan_duration + < time_since_stationary + <= self.stationary_scan_duration + 1 + ): + logger.debug( + f"{camera}: {obj_data.get('label', 'An')} object {id} has been stationary for > {self.stationary_scan_duration} seconds, skipping LPR." + ) + + if time_since_stationary > self.stationary_scan_duration: + return + + license_plate: Optional[dict[str, Any]] = None + + if "license_plate" not in self.config.cameras[camera].objects.track: + logger.debug(f"{camera}: Running manual license_plate detection.") + + car_box = obj_data.get("box") + + if not car_box: + return + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # apply motion mask + rgb[self.config.cameras[camera].motion.mask == 0] = [0, 0, 0] + + left, top, right, bottom = car_box + car = rgb[top:bottom, left:right] + + # double the size of the car for better box detection + car = cv2.resize(car, (int(2 * car.shape[1]), int(2 * car.shape[0]))) + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/car_frame_{current_time}.jpg", + car, + ) + + yolov9_start = datetime.datetime.now().timestamp() + license_plate = self._detect_license_plate(camera, car) + logger.debug( + f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" + ) + self.plates_det_second.update() + self.plate_det_speed.update( + datetime.datetime.now().timestamp() - yolov9_start + ) + + if not license_plate: + logger.debug( + f"{camera}: Detected no license plates for car/motorcycle object." + ) + return + + license_plate_area = max( + 0, + (license_plate[2] - license_plate[0]) + * (license_plate[3] - license_plate[1]), + ) + + # check that license plate is valid + # double the value because we've doubled the size of the car + if license_plate_area < self.config.cameras[camera].lpr.min_area * 2: + logger.debug(f"{camera}: License plate is less than min_area") + return + + license_plate_frame = car[ + license_plate[1] : license_plate[3], + license_plate[0] : license_plate[2], + ] + else: + # don't run for object without attributes if this isn't dedicated lpr with frigate+ + if ( + not obj_data.get("current_attributes") + and obj_data.get("label") != "license_plate" + ): + logger.debug(f"{camera}: No attributes to parse.") + return + + if obj_data.get("label") in self.lp_objects: + attributes: list[dict[str, Any]] = obj_data.get( + "current_attributes", [] + ) + for attr in attributes: + if attr.get("label") != "license_plate": + continue + + if license_plate is None or attr.get( + "score", 0.0 + ) > license_plate.get("score", 0.0): + license_plate = attr + + # no license plates detected in this frame + if not license_plate: + return + + # we are using dedicated lpr with frigate+ + if obj_data.get("label") == "license_plate": + license_plate = obj_data + + license_plate_box = license_plate.get("box") + + # check that license plate is valid + if ( + not license_plate_box + or area(license_plate_box) + < self.config.cameras[camera].lpr.min_area + ): + logger.debug( + f"{camera}: Area for license plate box {area(license_plate_box)} is less than min_area {self.config.cameras[camera].lpr.min_area}" + ) + return + + license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + # Expand the license_plate_box by 10% + box_array = np.array(license_plate_box) + expansion = (box_array[2:] - box_array[:2]) * 0.10 + expanded_box = np.array( + [ + license_plate_box[0] - expansion[0], + license_plate_box[1] - expansion[1], + license_plate_box[2] + expansion[0], + license_plate_box[3] + expansion[1], + ] + ).clip( + 0, [license_plate_frame.shape[1], license_plate_frame.shape[0]] * 2 + ) + + # Crop using the expanded box + license_plate_frame = license_plate_frame[ + int(expanded_box[1]) : int(expanded_box[3]), + int(expanded_box[0]) : int(expanded_box[2]), + ] + + # double the size of the license plate frame for better OCR + license_plate_frame = cv2.resize( + license_plate_frame, + ( + int(2 * license_plate_frame.shape[1]), + int(2 * license_plate_frame.shape[0]), + ), + ) + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/license_plate_frame_{current_time}.jpg", + license_plate_frame, + ) + + logger.debug(f"{camera}: Running plate recognition for id: {id}.") + + # run detection, returns results sorted by confidence, best first + start = datetime.datetime.now().timestamp() + license_plates, confidences, areas = self._process_license_plate( + camera, id, license_plate_frame + ) + self.plates_rec_second.update() + self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start) + + if license_plates: + for plate, confidence, text_area in zip(license_plates, confidences, areas): + avg_confidence = ( + (sum(confidence) / len(confidence)) if confidence else 0 + ) + + logger.debug( + f"{camera}: Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)" + ) + else: + logger.debug(f"{camera}: No text detected") + return + + top_plate, top_char_confidences, top_area = ( + license_plates[0], + confidences[0], + areas[0], + ) + avg_confidence = ( + (sum(top_char_confidences) / len(top_char_confidences)) + if top_char_confidences + else 0 + ) + + # Check against minimum confidence threshold + if avg_confidence < self.lpr_config.recognition_threshold: + logger.debug( + f"{camera}: Average character confidence {avg_confidence} is less than recognition_threshold ({self.lpr_config.recognition_threshold})" + ) + return + + # For dedicated LPR cameras, match or assign plate ID using Jaro-Winkler distance + if ( + dedicated_lpr + and "license_plate" not in self.config.cameras[camera].objects.track + ): + plate_id = None + + for existing_id, data in self.detected_license_plates.items(): + if ( + data["camera"] == camera + and data["last_seen"] is not None + and current_time - data["last_seen"] + <= self.config.cameras[camera].lpr.expire_time + ): + similarity = JaroWinkler.similarity(data["plate"], top_plate) + if similarity >= self.similarity_threshold: + plate_id = existing_id + logger.debug( + f"{camera}: Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})" + ) + break + if plate_id is None: + plate_id = self._generate_plate_event(camera, top_plate, avg_confidence) + logger.debug( + f"{camera}: New plate event for dedicated LPR camera {plate_id}: {top_plate}" + ) + else: + logger.debug( + f"{camera}: Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}" + ) + self.detected_license_plates[plate_id]["last_seen"] = current_time + + id = plate_id + + is_new = id not in self.detected_license_plates + + # Collect variant + variant = { + "plate": top_plate, + "conf": avg_confidence, + "char_confidences": top_char_confidences, + "area": top_area, + "timestamp": current_time, + } + + # Initialize or append to plates + self.detected_license_plates.setdefault(id, {"plates": [], "camera": camera}) + self.detected_license_plates[id]["plates"].append(variant) + + # Prune old variants - this is probably higher than it needs to be + # since we don't detect a plate every frame + num_variants = self.config.cameras[camera].detect.fps * 5 + if len(self.detected_license_plates[id]["plates"]) > num_variants: + self.detected_license_plates[id]["plates"] = self.detected_license_plates[ + id + ]["plates"][-num_variants:] + + # Cluster and select rep + plates = self.detected_license_plates[id]["plates"] + rep_plate, rep_conf, rep_char_confs, rep_area = self._get_cluster_rep(plates) + + if rep_plate != top_plate: + logger.debug( + f"{camera}: Clustering changed top plate '{top_plate}' (conf: {avg_confidence:.3f}) to rep '{rep_plate}' (conf: {rep_conf:.3f})" + ) + + # Update stored rep + self.detected_license_plates[id].update( + { + "plate": rep_plate, + "char_confidences": rep_char_confs, + "area": rep_area, + "last_seen": current_time if dedicated_lpr else None, + } + ) + + if not dedicated_lpr: + self.detected_license_plates[id]["obj_data"] = obj_data + + if is_new: + if camera not in self.camera_current_cars: + self.camera_current_cars[camera] = [] + self.camera_current_cars[camera].append(id) + + # Determine subLabel based on known plates, use regex matching + # Default to the detected plate, use label name if there's a match + sub_label = None + try: + sub_label = next( + ( + label + for label, plates_list in self.lpr_config.known_plates.items() + if any( + re.match(f"^{plate}$", rep_plate) + or Levenshtein.distance(plate, rep_plate) + <= self.lpr_config.match_distance + for plate in plates_list + ) + ), + None, + ) + except re.error: + logger.error( + f"{camera}: Invalid regex in known plates configuration: {self.lpr_config.known_plates}" + ) + + # If it's a known plate, publish to sub_label + if sub_label is not None: + self.sub_label_publisher.publish( + (id, sub_label, rep_conf), EventMetadataTypeEnum.sub_label.value + ) + + # always publish to recognized_license_plate field + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.lpr, + "name": sub_label, + "plate": rep_plate, + "score": rep_conf, + "id": id, + "camera": camera, + "timestamp": start, + } + ), + ) + self.sub_label_publisher.publish( + (id, "recognized_license_plate", rep_plate, rep_conf), + EventMetadataTypeEnum.attribute.value, + ) + + # save the best snapshot for dedicated lpr cams not using frigate+ + if ( + dedicated_lpr + and "license_plate" not in self.config.cameras[camera].objects.track + ): + logger.debug( + f"{camera}: Writing snapshot for {id}, {rep_plate}, {current_time}" + ) + frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + _, encoded_img = cv2.imencode(".jpg", frame_bgr) + self.sub_label_publisher.publish( + (base64.b64encode(encoded_img).decode("ASCII"), id, camera), + EventMetadataTypeEnum.save_lpr_snapshot.value, + ) + + def handle_request(self, topic, request_data) -> dict[str, Any] | None: + return + + def lpr_expire(self, object_id: str, camera: str): + if object_id in self.detected_license_plates: + self.detected_license_plates.pop(object_id) + + if object_id in self.camera_current_cars.get(camera, []): + self.camera_current_cars[camera].remove(object_id) + + +class CTCDecoder: + """ + A decoder for interpreting the output of a CTC (Connectionist Temporal Classification) model. + + This decoder converts the model's output probabilities into readable sequences of characters + while removing duplicates and handling blank tokens. It also calculates the confidence scores + for each decoded character sequence. + """ + + def __init__(self, character_dict_path=None): + """ + Initializes the CTCDecoder. + :param character_dict_path: Path to the character dictionary file. + If None, a default (English-focused) list is used. + For Chinese models, this should point to the correct + character dictionary file provided with the model. + """ + self.characters = [] + if character_dict_path and os.path.exists(character_dict_path): + with open(character_dict_path, "r", encoding="utf-8") as f: + self.characters = ( + ["blank"] + [line.strip() for line in f if line.strip()] + [" "] + ) + else: + self.characters = [ + "blank", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "[", + "\\", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + " ", + " ", + ] + + self.char_map = {i: char for i, char in enumerate(self.characters)} + + def __call__( + self, outputs: List[np.ndarray] + ) -> Tuple[List[str], List[List[float]]]: + """ + Decode a batch of model outputs into character sequences and their confidence scores. + + The method takes the output probability distributions for each time step and uses + the best path decoding strategy. It then merges repeating characters and ignores + blank tokens. Confidence scores for each decoded character are also calculated. + + Args: + outputs (List[np.ndarray]): A list of model outputs, where each element is + a probability distribution for each time step. + + Returns: + Tuple[List[str], List[List[float]]]: A tuple of decoded character sequences + and confidence scores for each sequence. + """ + results = [] + confidences = [] + for output in outputs: + seq_log_probs = np.log(output + 1e-8) + best_path = np.argmax(seq_log_probs, axis=1) + + merged_path = [] + merged_probs = [] + for t, char_index in enumerate(best_path): + if char_index != 0 and (t == 0 or char_index != best_path[t - 1]): + merged_path.append(char_index) + merged_probs.append(seq_log_probs[t, char_index]) + + result = "".join(self.char_map.get(idx, "") for idx in merged_path) + results.append(result) + + confidence = np.exp(merged_probs).tolist() + confidences.append(confidence) + + return results, confidences diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/model.py b/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/model.py new file mode 100644 index 0000000..f53ed7d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/common/license_plate/model.py @@ -0,0 +1,31 @@ +from frigate.embeddings.onnx.lpr_embedding import ( + LicensePlateDetector, + PaddleOCRClassification, + PaddleOCRDetection, + PaddleOCRRecognition, +) + +from ...types import DataProcessorModelRunner + + +class LicensePlateModelRunner(DataProcessorModelRunner): + def __init__(self, requestor, device: str = "CPU", model_size: str = "small"): + super().__init__(requestor, device, model_size) + self.detection_model = PaddleOCRDetection( + model_size=model_size, requestor=requestor, device=device + ) + self.classification_model = PaddleOCRClassification( + model_size=model_size, requestor=requestor, device=device + ) + self.recognition_model = PaddleOCRRecognition( + model_size=model_size, requestor=requestor, device=device + ) + self.yolov9_detection_model = LicensePlateDetector( + model_size=model_size, requestor=requestor, device=device + ) + + # Load all models once + self.detection_model._load_model_and_utils() + self.classification_model._load_model_and_utils() + self.recognition_model._load_model_and_utils() + self.yolov9_detection_model._load_model_and_utils() diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/api.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/api.py new file mode 100644 index 0000000..c341bd8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/api.py @@ -0,0 +1,52 @@ +"""Local or remote processors to handle post processing.""" + +import logging +from abc import ABC, abstractmethod +from typing import Any + +from frigate.config import FrigateConfig + +from ..types import DataProcessorMetrics, DataProcessorModelRunner, PostProcessDataEnum + +logger = logging.getLogger(__name__) + + +class PostProcessorApi(ABC): + @abstractmethod + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics, + model_runner: DataProcessorModelRunner, + ) -> None: + self.config = config + self.metrics = metrics + self.model_runner = model_runner + pass + + @abstractmethod + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + """Processes the data of data type. + Args: + data (dict): containing data about the input. + data_type (enum): Describing the data that is being processed. + + Returns: + None. + """ + pass + + @abstractmethod + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + """Handle metadata requests. + Args: + request_data (dict): containing data about requested change to process. + + Returns: + None if request was not handled, otherwise return response. + """ + pass diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/audio_transcription.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/audio_transcription.py new file mode 100644 index 0000000..b7b6cb0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/audio_transcription.py @@ -0,0 +1,217 @@ +"""Handle post-processing for audio transcription.""" + +import logging +import os +import threading +import time +from typing import Optional + +from peewee import DoesNotExist + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import ( + CACHE_DIR, + MODEL_CACHE_DIR, + UPDATE_AUDIO_TRANSCRIPTION_STATE, + UPDATE_EVENT_DESCRIPTION, +) +from frigate.data_processing.types import PostProcessDataEnum +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.audio import get_audio_from_recording + +from ..types import DataProcessorMetrics +from .api import PostProcessorApi + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionPostProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + embeddings, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics, None) + self.config = config + self.requestor = requestor + self.embeddings = embeddings + self.recognizer = None + self.transcription_lock = threading.Lock() + self.transcription_thread = None + self.transcription_running = False + + # faster-whisper handles model downloading automatically + self.model_path = os.path.join(MODEL_CACHE_DIR, "whisper") + os.makedirs(self.model_path, exist_ok=True) + + self.__build_recognizer() + + def __build_recognizer(self) -> None: + try: + # Import dynamically to avoid crashes on systems without AVX support + from faster_whisper import WhisperModel + + self.recognizer = WhisperModel( + model_size_or_path="small", + device="cuda" + if self.config.audio_transcription.device == "GPU" + else "cpu", + download_root=self.model_path, + local_files_only=False, # Allow downloading if not cached + compute_type="int8", + ) + logger.debug("Audio transcription (recordings) initialized") + except Exception as e: + logger.error(f"Failed to initialize recordings audio transcription: {e}") + self.recognizer = None + + def process_data( + self, data: dict[str, any], data_type: PostProcessDataEnum + ) -> None: + """Transcribe audio from a recording. + + Args: + data (dict): Contains data about the input (event_id, camera, etc.). + data_type (enum): Describes the data being processed (recording or tracked_object). + + Returns: + None + """ + event_id = data["event_id"] + camera_name = data["camera"] + + if data_type == PostProcessDataEnum.recording: + start_ts = data["frame_time"] + recordings_available_through = data["recordings_available"] + end_ts = min(recordings_available_through, start_ts + 60) # Default 60s + + elif data_type == PostProcessDataEnum.tracked_object: + obj_data = data["event"]["data"] + obj_data["id"] = data["event"]["id"] + obj_data["camera"] = data["event"]["camera"] + start_ts = data["event"]["start_time"] + end_ts = data["event"].get( + "end_time", start_ts + 60 + ) # Use end_time if available + + else: + logger.error("No data type passed to audio transcription post-processing") + return + + try: + audio_data = get_audio_from_recording( + self.config.cameras[camera_name].ffmpeg, + camera_name, + start_ts, + end_ts, + sample_rate=16000, + ) + + if not audio_data: + logger.debug(f"No audio data extracted for {event_id}") + return + + transcription = self.__transcribe_audio(audio_data) + if not transcription: + logger.debug("No transcription generated from audio") + return + + logger.debug(f"Transcribed audio for {event_id}: '{transcription}'") + + self.requestor.send_data( + UPDATE_EVENT_DESCRIPTION, + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event_id, + "description": transcription, + "camera": camera_name, + }, + ) + + # Embed the description + self.embeddings.embed_description(event_id, transcription) + + except DoesNotExist: + logger.debug("No recording found for audio transcription post-processing") + return + except Exception as e: + logger.error(f"Error in audio transcription post-processing: {e}") + + def __transcribe_audio(self, audio_data: bytes) -> Optional[tuple[str, float]]: + """Transcribe WAV audio data using faster-whisper.""" + if not self.recognizer: + logger.debug("Recognizer not initialized") + return None + + try: + # Save audio data to a temporary wav (faster-whisper expects a file) + temp_wav = os.path.join(CACHE_DIR, f"temp_audio_{int(time.time())}.wav") + with open(temp_wav, "wb") as f: + f.write(audio_data) + + segments, info = self.recognizer.transcribe( + temp_wav, + language=self.config.audio_transcription.language, + beam_size=5, + ) + + os.remove(temp_wav) + + # Combine all segment texts + text = " ".join(segment.text.strip() for segment in segments) + if not text: + return None + + logger.debug( + "Detected language '%s' with probability %f" + % (info.language, info.language_probability) + ) + + return text + except Exception as e: + logger.error(f"Error transcribing audio: {e}") + return None + + def _transcription_wrapper(self, event: dict[str, any]) -> None: + """Wrapper to run transcription and reset running flag when done.""" + try: + self.process_data( + { + "event_id": event["id"], + "camera": event["camera"], + "event": event, + }, + PostProcessDataEnum.tracked_object, + ) + finally: + with self.transcription_lock: + self.transcription_running = False + self.transcription_thread = None + + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "idle") + + def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None: + if topic == "transcribe_audio": + event = request_data["event"] + + with self.transcription_lock: + if self.transcription_running: + logger.warning( + "Audio transcription for a speech event is already running." + ) + return "in_progress" + + # Mark as running and start the thread + self.transcription_running = True + self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "processing") + + self.transcription_thread = threading.Thread( + target=self._transcription_wrapper, args=(event,), daemon=True + ) + self.transcription_thread.start() + return "started" + + return None diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/license_plate.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/license_plate.py new file mode 100644 index 0000000..e95cf23 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/license_plate.py @@ -0,0 +1,234 @@ +"""Handle post processing for license plate recognition.""" + +import datetime +import logging +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.data_processing.common.license_plate.mixin import ( + WRITE_DEBUG_IMAGES, + LicensePlateProcessingMixin, +) +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) +from frigate.data_processing.types import PostProcessDataEnum +from frigate.models import Recordings +from frigate.util.image import get_image_from_recording + +from ..types import DataProcessorMetrics +from .api import PostProcessorApi + +logger = logging.getLogger(__name__) + + +class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + model_runner: LicensePlateModelRunner, + detected_license_plates: dict[str, dict[str, Any]], + ): + self.requestor = requestor + self.detected_license_plates = detected_license_plates + self.model_runner = model_runner + self.lpr_config = config.lpr + self.config = config + self.sub_label_publisher = sub_label_publisher + super().__init__(config, metrics, model_runner) + + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + """Look for license plates in recording stream image + Args: + data (dict): containing data about the input. + data_type (enum): Describing the data that is being processed. + + Returns: + None. + """ + # don't run LPR post processing for now + return + + event_id = data["event_id"] + camera_name = data["camera"] + + if data_type == PostProcessDataEnum.recording: + obj_data = data["obj_data"] + frame_time = obj_data["frame_time"] + recordings_available_through = data["recordings_available"] + + if frame_time > recordings_available_through: + logger.debug( + f"LPR post processing: No recordings available for this frame time {frame_time}, available through {recordings_available_through}" + ) + + elif data_type == PostProcessDataEnum.tracked_object: + # non-functional, need to think about snapshot time + obj_data = data["event"]["data"] + obj_data["id"] = data["event"]["id"] + obj_data["camera"] = data["event"]["camera"] + # TODO: snapshot time? + frame_time = data["event"]["start_time"] + + else: + logger.error("No data type passed to LPR postprocessing") + return + + recording_query = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where( + ( + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) + ) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.desc()) + .limit(1) + ) + + try: + recording: Recordings = recording_query.get() + time_in_segment = frame_time - recording.start_time + codec = "mjpeg" + + image_data = get_image_from_recording( + self.config.ffmpeg, recording.path, time_in_segment, codec, None + ) + + if not image_data: + logger.debug( + "LPR post processing: Unable to fetch license plate from recording" + ) + + # Convert bytes to numpy array + image_array = np.frombuffer(image_data, dtype=np.uint8) + + if len(image_array) == 0: + logger.debug("LPR post processing: No image") + return + + image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) + + except DoesNotExist: + logger.debug("Error fetching license plate for postprocessing") + return + + if WRITE_DEBUG_IMAGES: + cv2.imwrite( + f"debug/frames/lpr_post_{datetime.datetime.now().timestamp()}.jpg", + image, + ) + + # convert to yuv for processing + frame = cv2.cvtColor(image, cv2.COLOR_BGR2YUV_I420) + + detect_width = self.config.cameras[camera_name].detect.width + detect_height = self.config.cameras[camera_name].detect.height + + # Scale the boxes based on detect dimensions + scale_x = image.shape[1] / detect_width + scale_y = image.shape[0] / detect_height + + # Determine which box to enlarge based on detection mode + if "license_plate" not in self.config.cameras[camera_name].objects.track: + # Scale and enlarge the car box + box = obj_data.get("box") + if not box: + return + + # Scale original car box to detection dimensions + left = int(box[0] * scale_x) + top = int(box[1] * scale_y) + right = int(box[2] * scale_x) + bottom = int(box[3] * scale_y) + box = [left, top, right, bottom] + else: + # Get the license plate box from attributes + if not obj_data.get("current_attributes"): + return + + license_plate = None + for attr in obj_data["current_attributes"]: + if attr.get("label") != "license_plate": + continue + if license_plate is None or attr.get("score", 0.0) > license_plate.get( + "score", 0.0 + ): + license_plate = attr + + if not license_plate or not license_plate.get("box"): + return + + # Scale license plate box to detection dimensions + orig_box = license_plate["box"] + left = int(orig_box[0] * scale_x) + top = int(orig_box[1] * scale_y) + right = int(orig_box[2] * scale_x) + bottom = int(orig_box[3] * scale_y) + box = [left, top, right, bottom] + + width_box = right - left + height_box = bottom - top + + # Enlarge box slightly to account for drift in detect vs recording stream + enlarge_factor = 0.3 + new_left = max(0, int(left - (width_box * enlarge_factor / 2))) + new_top = max(0, int(top - (height_box * enlarge_factor / 2))) + new_right = min(image.shape[1], int(right + (width_box * enlarge_factor / 2))) + new_bottom = min( + image.shape[0], int(bottom + (height_box * enlarge_factor / 2)) + ) + + keyframe_obj_data = obj_data.copy() + if "license_plate" not in self.config.cameras[camera_name].objects.track: + # car box + keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom] + else: + # Update the license plate box in the attributes + new_attributes = [] + for attr in obj_data["current_attributes"]: + if attr.get("label") == "license_plate": + new_attr = attr.copy() + new_attr["box"] = [new_left, new_top, new_right, new_bottom] + new_attributes.append(new_attr) + else: + new_attributes.append(attr) + keyframe_obj_data["current_attributes"] = new_attributes + + # run the frame through lpr processing + logger.debug(f"Post processing plate: {event_id}, {frame_time}") + self.lpr_process(keyframe_obj_data, frame) + + def handle_request(self, topic, request_data) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.reprocess_plate.value: + event = request_data["event"] + + self.process_data( + { + "event_id": event["id"], + "camera": event["camera"], + "event": event, + }, + PostProcessDataEnum.tracked_object, + ) + + return { + "message": "Successfully requested reprocessing of license plate.", + "success": True, + } diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/object_descriptions.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/object_descriptions.py new file mode 100644 index 0000000..1f4608b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/object_descriptions.py @@ -0,0 +1,349 @@ +"""Post processor for object descriptions using GenAI.""" + +import datetime +import logging +import os +import threading +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, FrigateConfig +from frigate.const import CLIPS_DIR, UPDATE_EVENT_DESCRIPTION +from frigate.data_processing.post.semantic_trigger import SemanticTriggerProcessor +from frigate.data_processing.types import PostProcessDataEnum +from frigate.genai import GenAIClient +from frigate.models import Event +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import create_thumbnail, ensure_jpeg_bytes + +if TYPE_CHECKING: + from frigate.embeddings import Embeddings + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +MAX_THUMBNAILS = 10 + + +class ObjectDescriptionProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + embeddings: "Embeddings", + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + client: GenAIClient, + semantic_trigger_processor: SemanticTriggerProcessor | None, + ): + super().__init__(config, metrics, None) + self.config = config + self.embeddings = embeddings + self.requestor = requestor + self.metrics = metrics + self.genai_client = client + self.semantic_trigger_processor = semantic_trigger_processor + self.tracked_events: dict[str, list[Any]] = {} + self.early_request_sent: dict[str, bool] = {} + self.object_desc_speed = InferenceSpeed(self.metrics.object_desc_speed) + self.object_desc_dps = EventsPerSecond() + self.object_desc_dps.start() + + def __handle_frame_update( + self, camera: str, data: dict, yuv_frame: np.ndarray + ) -> None: + """Handle an update to a frame for an object.""" + camera_config = self.config.cameras[camera] + + # no need to save our own thumbnails if genai is not enabled + # or if the object has become stationary + if not data["stationary"]: + if data["id"] not in self.tracked_events: + self.tracked_events[data["id"]] = [] + + data["thumbnail"] = create_thumbnail(yuv_frame, data["box"]) + + # Limit the number of thumbnails saved + if len(self.tracked_events[data["id"]]) >= MAX_THUMBNAILS: + # Always keep the first thumbnail for the event + self.tracked_events[data["id"]].pop(1) + + self.tracked_events[data["id"]].append(data) + + # check if we're configured to send an early request after a minimum number of updates received + if camera_config.objects.genai.send_triggers.after_significant_updates: + if ( + len(self.tracked_events.get(data["id"], [])) + >= camera_config.objects.genai.send_triggers.after_significant_updates + and data["id"] not in self.early_request_sent + ): + if data["has_clip"] and data["has_snapshot"]: + event: Event = Event.get(Event.id == data["id"]) + + if ( + not camera_config.objects.genai.objects + or event.label in camera_config.objects.genai.objects + ) and ( + not camera_config.objects.genai.required_zones + or set(data["entered_zones"]) + & set(camera_config.objects.genai.required_zones) + ): + logger.debug(f"{camera} sending early request to GenAI") + + self.early_request_sent[data["id"]] = True + threading.Thread( + target=self._genai_embed_description, + name=f"_genai_embed_description_{event.id}", + daemon=True, + args=( + event, + [ + data["thumbnail"] + for data in self.tracked_events[data["id"]] + ], + ), + ).start() + + def __handle_frame_finalize( + self, camera: str, event: Event, thumbnail: bytes + ) -> None: + """Handle the finalization of a frame.""" + camera_config = self.config.cameras[camera] + + if ( + camera_config.objects.genai.enabled + and camera_config.objects.genai.send_triggers.tracked_object_end + and ( + not camera_config.objects.genai.objects + or event.label in camera_config.objects.genai.objects + ) + and ( + not camera_config.objects.genai.required_zones + or set(event.zones) & set(camera_config.objects.genai.required_zones) + ) + ): + self._process_genai_description(event, camera_config, thumbnail) + + def __regenerate_description(self, event_id: str, source: str, force: bool) -> None: + """Regenerate the description for an event.""" + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + logger.error(f"Event {event_id} not found for description regeneration") + return + + if self.genai_client is None: + logger.error("GenAI not enabled") + return + + camera_config = self.config.cameras[event.camera] + if not camera_config.objects.genai.enabled and not force: + logger.error(f"GenAI not enabled for camera {event.camera}") + return + + thumbnail = get_event_thumbnail_bytes(event) + + # ensure we have a jpeg to pass to the model + thumbnail = ensure_jpeg_bytes(thumbnail) + + logger.debug( + f"Trying {source} regeneration for {event}, has_snapshot: {event.has_snapshot}" + ) + + if event.has_snapshot and source == "snapshot": + snapshot_image = self._read_and_crop_snapshot(event) + if not snapshot_image: + return + + embed_image = ( + [snapshot_image] + if event.has_snapshot and source == "snapshot" + else ( + [data["thumbnail"] for data in self.tracked_events[event_id]] + if len(self.tracked_events.get(event_id, [])) > 0 + else [thumbnail] + ) + ) + + self._genai_embed_description(event, embed_image) + + def process_data(self, frame_data: dict, data_type: PostProcessDataEnum) -> None: + """Process a frame update.""" + self.metrics.object_desc_dps.value = self.object_desc_dps.eps() + + if data_type != PostProcessDataEnum.tracked_object: + return + + state: str | None = frame_data.get("state", None) + + if state is not None: + logger.debug(f"Processing {state} for {frame_data['camera']}") + + if state == "update": + self.__handle_frame_update( + frame_data["camera"], frame_data["data"], frame_data["yuv_frame"] + ) + elif state == "finalize": + self.__handle_frame_finalize( + frame_data["camera"], frame_data["event"], frame_data["thumbnail"] + ) + + def handle_request(self, topic: str, data: dict[str, Any]) -> str | None: + """Handle a request.""" + if topic == "regenerate_description": + self.__regenerate_description( + data["event_id"], data["source"], data["force"] + ) + return None + + def _read_and_crop_snapshot(self, event: Event) -> bytes | None: + """Read, decode, and crop the snapshot image.""" + + snapshot_file = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg") + + if not os.path.isfile(snapshot_file): + logger.error( + f"Cannot load snapshot for {event.id}, file not found: {snapshot_file}" + ) + return None + + try: + with open(snapshot_file, "rb") as image_file: + snapshot_image = image_file.read() + + img = cv2.imdecode( + np.frombuffer(snapshot_image, dtype=np.int8), + cv2.IMREAD_COLOR, + ) + + # Crop snapshot based on region + # provide full image if region doesn't exist (manual events) + height, width = img.shape[:2] + x1_rel, y1_rel, width_rel, height_rel = event.data.get( + "region", [0, 0, 1, 1] + ) + x1, y1 = int(x1_rel * width), int(y1_rel * height) + + cropped_image = img[ + y1 : y1 + int(height_rel * height), + x1 : x1 + int(width_rel * width), + ] + + _, buffer = cv2.imencode(".jpg", cropped_image) + + return buffer.tobytes() + except Exception: + return None + + def _process_genai_description( + self, event: Event, camera_config: CameraConfig, thumbnail + ) -> None: + if event.has_snapshot and camera_config.objects.genai.use_snapshot: + snapshot_image = self._read_and_crop_snapshot(event) + if not snapshot_image: + return + + num_thumbnails = len(self.tracked_events.get(event.id, [])) + + # ensure we have a jpeg to pass to the model + thumbnail = ensure_jpeg_bytes(thumbnail) + + embed_image = ( + [snapshot_image] + if event.has_snapshot and camera_config.objects.genai.use_snapshot + else ( + [data["thumbnail"] for data in self.tracked_events[event.id]] + if num_thumbnails > 0 + else [thumbnail] + ) + ) + + if camera_config.objects.genai.debug_save_thumbnails and num_thumbnails > 0: + logger.debug(f"Saving {num_thumbnails} thumbnails for event {event.id}") + + Path(os.path.join(CLIPS_DIR, f"genai-requests/{event.id}")).mkdir( + parents=True, exist_ok=True + ) + + for idx, data in enumerate(self.tracked_events[event.id], 1): + jpg_bytes: bytes | None = data["thumbnail"] + + if jpg_bytes is None: + logger.warning(f"Unable to save thumbnail {idx} for {event.id}.") + else: + with open( + os.path.join( + CLIPS_DIR, + f"genai-requests/{event.id}/{idx}.jpg", + ), + "wb", + ) as j: + j.write(jpg_bytes) + + # Generate the description. Call happens in a thread since it is network bound. + threading.Thread( + target=self._genai_embed_description, + name=f"_genai_embed_description_{event.id}", + daemon=True, + args=( + event, + embed_image, + ), + ).start() + + # Delete tracked events based on the event_id + if event.id in self.tracked_events: + del self.tracked_events[event.id] + + def _genai_embed_description(self, event: Event, thumbnails: list[bytes]) -> None: + """Embed the description for an event.""" + start = datetime.datetime.now().timestamp() + camera_config = self.config.cameras[event.camera] + description = self.genai_client.generate_object_description( + camera_config, thumbnails, event + ) + + if not description: + logger.debug("Failed to generate description for %s", event.id) + return + + # fire and forget description update + self.requestor.send_data( + UPDATE_EVENT_DESCRIPTION, + { + "type": TrackedObjectUpdateTypesEnum.description, + "id": event.id, + "description": description, + "camera": event.camera, + }, + ) + + # Embed the description + if self.config.semantic_search.enabled: + self.embeddings.embed_description(event.id, description) + + # Check semantic trigger for this description + if self.semantic_trigger_processor is not None: + self.semantic_trigger_processor.process_data( + {"event_id": event.id, "camera": event.camera, "type": "text"}, + PostProcessDataEnum.tracked_object, + ) + + # Update inference timing metrics + self.object_desc_speed.update(datetime.datetime.now().timestamp() - start) + self.object_desc_dps.update() + + logger.debug( + "Generated description for %s (%d images): %s", + event.id, + len(thumbnails), + description, + ) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/review_descriptions.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/review_descriptions.py new file mode 100644 index 0000000..7932d56 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/review_descriptions.py @@ -0,0 +1,560 @@ +"""Post processor for review items to get descriptions.""" + +import copy +import datetime +import logging +import math +import os +import shutil +import threading +from pathlib import Path +from typing import Any + +import cv2 +from peewee import DoesNotExist +from titlecase import titlecase + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.camera import CameraConfig +from frigate.config.camera.review import GenAIReviewConfig, ImageSourceEnum +from frigate.const import CACHE_DIR, CLIPS_DIR, UPDATE_REVIEW_DESCRIPTION +from frigate.data_processing.types import PostProcessDataEnum +from frigate.genai import GenAIClient +from frigate.models import Recordings, ReviewSegment +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import get_image_from_recording + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +RECORDING_BUFFER_EXTENSION_PERCENT = 0.10 +MIN_RECORDING_DURATION = 10 + + +class ReviewDescriptionProcessor(PostProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + client: GenAIClient, + ): + super().__init__(config, metrics, None) + self.requestor = requestor + self.metrics = metrics + self.genai_client = client + self.review_desc_speed = InferenceSpeed(self.metrics.review_desc_speed) + self.review_descs_dps = EventsPerSecond() + self.review_descs_dps.start() + + def calculate_frame_count( + self, + camera: str, + image_source: ImageSourceEnum = ImageSourceEnum.preview, + height: int = 480, + ) -> int: + """Calculate optimal number of frames based on context size, image source, and resolution. + + Token usage varies by resolution: larger images (ultrawide aspect ratios) use more tokens. + Estimates ~1 token per 1250 pixels. Targets 98% context utilization with safety margin. + Capped at 20 frames. + """ + context_size = self.genai_client.get_context_size() + camera_config = self.config.cameras[camera] + + detect_width = camera_config.detect.width + detect_height = camera_config.detect.height + aspect_ratio = detect_width / detect_height + + if image_source == ImageSourceEnum.recordings: + if aspect_ratio >= 1: + # Landscape or square: constrain height + width = int(height * aspect_ratio) + else: + # Portrait: constrain width + width = height + height = int(width / aspect_ratio) + else: + if aspect_ratio >= 1: + # Landscape or square: constrain height + target_height = 180 + width = int(target_height * aspect_ratio) + height = target_height + else: + # Portrait: constrain width + target_width = 180 + width = target_width + height = int(target_width / aspect_ratio) + + pixels_per_image = width * height + tokens_per_image = pixels_per_image / 1250 + prompt_tokens = 3500 + response_tokens = 300 + available_tokens = context_size - prompt_tokens - response_tokens + max_frames = int(available_tokens / tokens_per_image) + + return min(max(max_frames, 3), 20) + + def process_data(self, data, data_type): + self.metrics.review_desc_dps.value = self.review_descs_dps.eps() + + if data_type != PostProcessDataEnum.review: + return + + camera = data["after"]["camera"] + camera_config = self.config.cameras[camera] + + if not camera_config.review.genai.enabled: + return + + id = data["after"]["id"] + + if data["type"] == "new" or data["type"] == "update": + return + else: + final_data = data["after"] + + if ( + final_data["severity"] == "alert" + and not camera_config.review.genai.alerts + ): + return + elif ( + final_data["severity"] == "detection" + and not camera_config.review.genai.detections + ): + return + + image_source = camera_config.review.genai.image_source + + if image_source == ImageSourceEnum.recordings: + duration = final_data["end_time"] - final_data["start_time"] + buffer_extension = min(5, duration * RECORDING_BUFFER_EXTENSION_PERCENT) + + # Ensure minimum total duration for short review items + # This provides better context for brief events + total_duration = duration + (2 * buffer_extension) + if total_duration < MIN_RECORDING_DURATION: + # Expand buffer to reach minimum duration, still respecting max of 5s per side + additional_buffer_per_side = (MIN_RECORDING_DURATION - duration) / 2 + buffer_extension = min(5, additional_buffer_per_side) + + thumbs = self.get_recording_frames( + camera, + final_data["start_time"] - buffer_extension, + final_data["end_time"] + buffer_extension, + height=480, # Use 480p for good balance between quality and token usage + ) + + if not thumbs: + # Fallback to preview frames if no recordings available + logger.warning( + f"No recording frames found for {camera}, falling back to preview frames" + ) + thumbs = self.get_preview_frames_as_bytes( + camera, + final_data["start_time"], + final_data["end_time"], + final_data["thumb_path"], + id, + camera_config.review.genai.debug_save_thumbnails, + ) + elif camera_config.review.genai.debug_save_thumbnails: + # Save debug thumbnails for recordings + Path(os.path.join(CLIPS_DIR, "genai-requests", id)).mkdir( + parents=True, exist_ok=True + ) + for idx, frame_bytes in enumerate(thumbs): + with open( + os.path.join(CLIPS_DIR, f"genai-requests/{id}/{idx}.jpg"), + "wb", + ) as f: + f.write(frame_bytes) + else: + # Use preview frames + thumbs = self.get_preview_frames_as_bytes( + camera, + final_data["start_time"], + final_data["end_time"], + final_data["thumb_path"], + id, + camera_config.review.genai.debug_save_thumbnails, + ) + + # kickoff analysis + self.review_descs_dps.update() + threading.Thread( + target=run_analysis, + args=( + self.requestor, + self.genai_client, + self.review_desc_speed, + camera_config, + final_data, + thumbs, + camera_config.review.genai, + list(self.config.model.merged_labelmap.values()), + self.config.model.all_attributes, + ), + ).start() + + def handle_request(self, topic, request_data): + if topic == EmbeddingsRequestEnum.summarize_review.value: + start_ts = request_data["start_ts"] + end_ts = request_data["end_ts"] + logger.debug( + f"Found GenAI Review Summary request for {start_ts} to {end_ts}" + ) + + # Query all review segments with camera and time information + segments: list[dict[str, Any]] = [ + { + "camera": r["camera"].replace("_", " ").title(), + "start_time": r["start_time"], + "end_time": r["end_time"], + "metadata": r["data"]["metadata"], + } + for r in ( + ReviewSegment.select( + ReviewSegment.camera, + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.data, + ) + .where( + (ReviewSegment.data["metadata"].is_null(False)) + & (ReviewSegment.start_time < end_ts) + & (ReviewSegment.end_time > start_ts) + ) + .order_by(ReviewSegment.start_time.asc()) + .dicts() + .iterator() + ) + ] + + if len(segments) == 0: + logger.debug("No review items with metadata found during time period") + return "No activity was found during this time period." + + # Identify primary items (important items that need review) + primary_segments = [ + seg + for seg in segments + if seg["metadata"].get("potential_threat_level", 0) > 0 + or seg["metadata"].get("other_concerns") + ] + + if not primary_segments: + return "No concerns were found during this time period." + + # Build hierarchical structure: each primary event with its contextual items + events_with_context = [] + + for primary_seg in primary_segments: + # Start building the primary event structure + primary_item = copy.deepcopy(primary_seg["metadata"]) + primary_item["camera"] = primary_seg["camera"] + primary_item["start_time"] = primary_seg["start_time"] + primary_item["end_time"] = primary_seg["end_time"] + + # Find overlapping contextual items from other cameras + primary_start = primary_seg["start_time"] + primary_end = primary_seg["end_time"] + primary_camera = primary_seg["camera"] + contextual_items = [] + seen_contextual_cameras = set() + + for seg in segments: + seg_camera = seg["camera"] + + if seg_camera == primary_camera: + continue + + if seg in primary_segments: + continue + + seg_start = seg["start_time"] + seg_end = seg["end_time"] + + if seg_start < primary_end and primary_start < seg_end: + # Avoid duplicates if same camera has multiple overlapping segments + if seg_camera not in seen_contextual_cameras: + contextual_item = copy.deepcopy(seg["metadata"]) + contextual_item["camera"] = seg_camera + contextual_item["start_time"] = seg_start + contextual_item["end_time"] = seg_end + contextual_items.append(contextual_item) + seen_contextual_cameras.add(seg_camera) + + # Add context array to primary item + primary_item["context"] = contextual_items + events_with_context.append(primary_item) + + total_context_items = sum( + len(event.get("context", [])) for event in events_with_context + ) + logger.debug( + f"Summary includes {len(events_with_context)} primary events with " + f"{total_context_items} total contextual items" + ) + + if self.config.review.genai.debug_save_thumbnails: + Path( + os.path.join(CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}") + ).mkdir(parents=True, exist_ok=True) + + return self.genai_client.generate_review_summary( + start_ts, + end_ts, + events_with_context, + self.config.review.genai.debug_save_thumbnails, + ) + else: + return None + + def get_cache_frames( + self, + camera: str, + start_time: float, + end_time: float, + ) -> list[str]: + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{camera}" + start_file = f"{file_start}-{start_time}.webp" + end_file = f"{file_start}-{end_time}.webp" + all_frames = [] + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + if len(all_frames): + all_frames[0] = os.path.join(preview_dir, file) + else: + all_frames.append(os.path.join(preview_dir, file)) + + continue + + if file > end_file: + all_frames.append(os.path.join(preview_dir, file)) + break + + all_frames.append(os.path.join(preview_dir, file)) + + frame_count = len(all_frames) + desired_frame_count = self.calculate_frame_count(camera) + + if frame_count <= desired_frame_count: + return all_frames + + selected_frames = [] + step_size = (frame_count - 1) / (desired_frame_count - 1) + + for i in range(desired_frame_count): + index = round(i * step_size) + selected_frames.append(all_frames[index]) + + return selected_frames + + def get_recording_frames( + self, + camera: str, + start_time: float, + end_time: float, + height: int = 480, + ) -> list[bytes]: + """Get frames from recordings at specified timestamps.""" + duration = end_time - start_time + desired_frame_count = self.calculate_frame_count( + camera, ImageSourceEnum.recordings, height + ) + + # Calculate evenly spaced timestamps throughout the duration + if desired_frame_count == 1: + timestamps = [start_time + duration / 2] + else: + step = duration / (desired_frame_count - 1) + timestamps = [start_time + (i * step) for i in range(desired_frame_count)] + + def extract_frame_from_recording(ts: float) -> bytes | None: + """Extract a single frame from recording at given timestamp.""" + try: + recording = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + ) + .where((ts >= Recordings.start_time) & (ts <= Recordings.end_time)) + .where(Recordings.camera == camera) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + + time_in_segment = ts - recording.start_time + return get_image_from_recording( + self.config.ffmpeg, + recording.path, + time_in_segment, + "mjpeg", + height=height, + ) + except DoesNotExist: + return None + + frames = [] + + for timestamp in timestamps: + try: + # Try to extract frame at exact timestamp + image_data = extract_frame_from_recording(timestamp) + + if not image_data: + # Try with rounded timestamp as fallback + rounded_timestamp = math.ceil(timestamp) + image_data = extract_frame_from_recording(rounded_timestamp) + + if image_data: + frames.append(image_data) + else: + logger.warning( + f"No recording found for {camera} at timestamp {timestamp}" + ) + except Exception as e: + logger.error( + f"Error extracting frame from recording for {camera} at {timestamp}: {e}" + ) + continue + + return frames + + def get_preview_frames_as_bytes( + self, + camera: str, + start_time: float, + end_time: float, + thumb_path_fallback: str, + review_id: str, + save_debug: bool, + ) -> list[bytes]: + """Get preview frames and convert them to JPEG bytes. + + Args: + camera: Camera name + start_time: Start timestamp + end_time: End timestamp + thumb_path_fallback: Fallback thumbnail path if no preview frames found + review_id: Review item ID for debug saving + save_debug: Whether to save debug thumbnails + + Returns: + List of JPEG image bytes + """ + frame_paths = self.get_cache_frames(camera, start_time, end_time) + if not frame_paths: + frame_paths = [thumb_path_fallback] + + thumbs = [] + for idx, thumb_path in enumerate(frame_paths): + thumb_data = cv2.imread(thumb_path) + ret, jpg = cv2.imencode( + ".jpg", thumb_data, [int(cv2.IMWRITE_JPEG_QUALITY), 100] + ) + if ret: + thumbs.append(jpg.tobytes()) + + if save_debug: + Path(os.path.join(CLIPS_DIR, "genai-requests", review_id)).mkdir( + parents=True, exist_ok=True + ) + shutil.copy( + thumb_path, + os.path.join(CLIPS_DIR, f"genai-requests/{review_id}/{idx}.webp"), + ) + + return thumbs + + +@staticmethod +def run_analysis( + requestor: InterProcessRequestor, + genai_client: GenAIClient, + review_inference_speed: InferenceSpeed, + camera_config: CameraConfig, + final_data: dict[str, str], + thumbs: list[bytes], + genai_config: GenAIReviewConfig, + labelmap_objects: list[str], + attribute_labels: list[str], +) -> None: + start = datetime.datetime.now().timestamp() + + # Format zone names using zone config friendly names if available + formatted_zones = [] + for zone_name in final_data["data"]["zones"]: + if zone_name in camera_config.zones: + formatted_zones.append( + camera_config.zones[zone_name].get_formatted_name(zone_name) + ) + + analytics_data = { + "id": final_data["id"], + "camera": camera_config.get_formatted_name(), + "zones": formatted_zones, + "start": datetime.datetime.fromtimestamp(final_data["start_time"]).strftime( + "%A, %I:%M %p" + ), + "duration": round(final_data["end_time"] - final_data["start_time"]), + } + + unified_objects = [] + + objects_list = final_data["data"]["objects"] + sub_labels_list = final_data["data"]["sub_labels"] + + for i, verified_label in enumerate(final_data["data"]["verified_objects"]): + object_type = verified_label.replace("-verified", "").replace("_", " ") + name = titlecase(sub_labels_list[i].replace("_", " ")) + unified_objects.append(f"{name} ({object_type})") + + for label in objects_list: + if "-verified" in label: + continue + elif label in labelmap_objects: + object_type = titlecase(label.replace("_", " ")) + + if label in attribute_labels: + unified_objects.append(f"{object_type} (delivery/service)") + else: + unified_objects.append(object_type) + + analytics_data["unified_objects"] = unified_objects + + metadata = genai_client.generate_review_description( + analytics_data, + thumbs, + genai_config.additional_concerns, + genai_config.preferred_language, + genai_config.debug_save_thumbnails, + genai_config.activity_context_prompt, + ) + review_inference_speed.update(datetime.datetime.now().timestamp() - start) + + if not metadata: + return None + + prev_data = copy.deepcopy(final_data) + final_data["data"]["metadata"] = metadata.model_dump() + requestor.send_data( + UPDATE_REVIEW_DESCRIPTION, + { + "type": "genai", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in final_data.items()}, + }, + ) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/semantic_trigger.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/semantic_trigger.py new file mode 100644 index 0000000..ec9e5d2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/semantic_trigger.py @@ -0,0 +1,269 @@ +"""Post time processor to trigger actions based on similar embeddings.""" + +import datetime +import json +import logging +import os +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import CONFIG_DIR +from frigate.data_processing.types import PostProcessDataEnum +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.embeddings.util import ZScoreNormalization +from frigate.models import Event, Trigger +from frigate.util.builtin import cosine_distance +from frigate.util.file import get_event_thumbnail_bytes + +from ..post.api import PostProcessorApi +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + +WRITE_DEBUG_IMAGES = False + + +class SemanticTriggerProcessor(PostProcessorApi): + def __init__( + self, + db: SqliteVecQueueDatabase, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + embeddings, + ): + super().__init__(config, metrics, None) + self.db = db + self.embeddings = embeddings + self.requestor = requestor + self.sub_label_publisher = sub_label_publisher + self.trigger_embeddings: list[np.ndarray] = [] + + self.thumb_stats = ZScoreNormalization() + self.desc_stats = ZScoreNormalization() + + # load stats from disk + try: + with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f: + data = json.loads(f.read()) + self.thumb_stats.from_dict(data["thumb_stats"]) + self.desc_stats.from_dict(data["desc_stats"]) + except FileNotFoundError: + pass + + def process_data( + self, data: dict[str, Any], data_type: PostProcessDataEnum + ) -> None: + event_id = data["event_id"] + camera = data["camera"] + process_type = data["type"] + + if self.config.cameras[camera].semantic_search.triggers is None: + return + + triggers = ( + Trigger.select( + Trigger.camera, + Trigger.name, + Trigger.data, + Trigger.type, + Trigger.embedding, + Trigger.threshold, + ) + .where(Trigger.camera == camera) + .dicts() + .iterator() + ) + + for trigger in triggers: + if ( + trigger["name"] + not in self.config.cameras[camera].semantic_search.triggers + or not self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .enabled + ): + logger.debug( + f"Trigger {trigger['name']} is disabled for camera {camera}" + ) + continue + + logger.debug( + f"Processing {trigger['type']} trigger for {event_id} on {trigger['camera']}: {trigger['name']}" + ) + + trigger_embedding = np.frombuffer(trigger["embedding"], dtype=np.float32) + + # Get embeddings based on type + thumbnail_embedding = None + description_embedding = None + + if process_type == "image": + cursor = self.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [event_id], + ) + row = cursor.fetchone() if cursor else None + if row: + thumbnail_embedding = np.frombuffer(row[0], dtype=np.float32) + + if process_type == "text": + cursor = self.db.execute_sql( + """ + SELECT description_embedding FROM vec_descriptions WHERE id = ? + """, + [event_id], + ) + row = cursor.fetchone() if cursor else None + if row: + description_embedding = np.frombuffer(row[0], dtype=np.float32) + + # Skip processing if we don't have any embeddings + if thumbnail_embedding is None and description_embedding is None: + logger.debug(f"No embeddings found for {event_id}") + return + + # Determine which embedding to compare based on trigger type + if ( + trigger["type"] in ["text", "thumbnail"] + and thumbnail_embedding is not None + ): + data_embedding = thumbnail_embedding + normalized_distance = self.thumb_stats.normalize( + [cosine_distance(data_embedding, trigger_embedding)], + save_stats=False, + )[0] + elif trigger["type"] == "description" and description_embedding is not None: + data_embedding = description_embedding + normalized_distance = self.desc_stats.normalize( + [cosine_distance(data_embedding, trigger_embedding)], + save_stats=False, + )[0] + + else: + continue + + similarity = 1 - normalized_distance + + logger.debug( + f"Trigger {trigger['name']} ({trigger['data'] if trigger['type'] == 'text' or trigger['type'] == 'description' else 'image'}): " + f"normalized distance: {normalized_distance:.4f}, " + f"similarity: {similarity:.4f}, threshold: {trigger['threshold']}" + ) + + # Check if similarity meets threshold + if similarity >= trigger["threshold"]: + logger.debug( + f"Trigger {trigger['name']} activated with similarity {similarity:.4f}" + ) + + # Update the trigger's last_triggered and triggering_event_id + Trigger.update( + last_triggered=datetime.datetime.now(), triggering_event_id=event_id + ).where( + Trigger.camera == camera, Trigger.name == trigger["name"] + ).execute() + + # Always publish MQTT message + self.requestor.send_data( + "triggers", + json.dumps( + { + "name": trigger["name"], + "camera": camera, + "event_id": event_id, + "type": trigger["type"], + "score": similarity, + } + ), + ) + + friendly_name = ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .friendly_name + ) + + if ( + self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + # handle actions for the trigger + # notifications already handled by webpush + if ( + "sub_label" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + (event_id, friendly_name, similarity), + EventMetadataTypeEnum.sub_label, + ) + if ( + "attribute" + in self.config.cameras[camera] + .semantic_search.triggers[trigger["name"]] + .actions + ): + self.sub_label_publisher.publish( + ( + event_id, + trigger["name"], + trigger["type"], + similarity, + ), + EventMetadataTypeEnum.attribute.value, + ) + + if WRITE_DEBUG_IMAGES: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + return + + # Skip the event if not an object + if event.data.get("type") != "object": + return + + thumbnail_bytes = get_event_thumbnail_bytes(event) + + nparr = np.frombuffer(thumbnail_bytes, np.uint8) + thumbnail = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + font_scale = 0.5 + font = cv2.FONT_HERSHEY_SIMPLEX + cv2.putText( + thumbnail, + f"{similarity:.4f}", + (10, 30), + font, + fontScale=font_scale, + color=(0, 255, 0), + thickness=2, + ) + + current_time = int(datetime.datetime.now().timestamp()) + cv2.imwrite( + f"debug/frames/trigger-{event_id}_{current_time}.jpg", + thumbnail, + ) + + def handle_request(self, topic, request_data): + return None + + def expire_object(self, object_id, camera): + pass diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/post/types.py b/sam2-cpu/frigate-dev/frigate/data_processing/post/types.py new file mode 100644 index 0000000..70fec9b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/post/types.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, ConfigDict, Field + + +class ReviewMetadata(BaseModel): + model_config = ConfigDict(extra="ignore", protected_namespaces=()) + + title: str = Field(description="A concise title for the activity.") + scene: str = Field( + description="A comprehensive description of the setting and entities, including relevant context and plausible inferences if supported by visual evidence." + ) + confidence: float = Field( + description="A float between 0 and 1 representing your overall confidence in this analysis." + ) + potential_threat_level: int = Field( + ge=0, + le=3, + description="An integer representing the potential threat level (1-3). 1: Minor anomaly. 2: Moderate concern. 3: High threat. Only include this field if a clear security concern is observable; otherwise, omit it.", + ) + other_concerns: list[str] | None = Field( + default=None, + description="Other concerns highlighted by the user that are observed.", + ) + time: str | None = Field(default=None, description="Time of activity.") diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/api.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/api.py new file mode 100644 index 0000000..0fa0f99 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/api.py @@ -0,0 +1,63 @@ +"""Local only processors for handling real time object processing.""" + +import logging +from abc import ABC, abstractmethod +from typing import Any + +import numpy as np + +from frigate.config import FrigateConfig + +from ..types import DataProcessorMetrics + +logger = logging.getLogger(__name__) + + +class RealTimeProcessorApi(ABC): + @abstractmethod + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics, + ) -> None: + self.config = config + self.metrics = metrics + pass + + @abstractmethod + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray) -> None: + """Processes the frame with object data. + Args: + obj_data (dict): containing data about focused object in frame. + frame (ndarray): full yuv frame. + + Returns: + None. + """ + pass + + @abstractmethod + def handle_request( + self, topic: str, request_data: dict[str, Any] + ) -> dict[str, Any] | None: + """Handle metadata requests. + Args: + topic (str): topic that dictates what work is requested. + request_data (dict): containing data about requested change to process. + + Returns: + None if request was not handled, otherwise return response. + """ + pass + + @abstractmethod + def expire_object(self, object_id: str, camera: str) -> None: + """Handle objects that are no longer detected. + Args: + object_id (str): id of object that is no longer detected. + camera (str): name of camera that object was detected on. + + Returns: + None. + """ + pass diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/audio_transcription.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/audio_transcription.py new file mode 100644 index 0000000..2e6d599 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/audio_transcription.py @@ -0,0 +1,281 @@ +"""Handle processing audio for speech transcription using sherpa-onnx with FFmpeg pipe.""" + +import logging +import os +import queue +import threading +from typing import Optional + +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.data_processing.common.audio_transcription.model import ( + AudioTranscriptionModelRunner, +) +from frigate.data_processing.real_time.whisper_online import ( + FasterWhisperASR, + OnlineASRProcessor, +) + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + camera_config: CameraConfig, + requestor: InterProcessRequestor, + model_runner: AudioTranscriptionModelRunner, + metrics: DataProcessorMetrics, + stop_event: threading.Event, + ): + super().__init__(config, metrics) + self.config = config + self.camera_config = camera_config + self.requestor = requestor + self.stream = None + self.whisper_model = None + self.model_runner = model_runner + self.transcription_segments = [] + self.audio_queue = queue.Queue() + self.stop_event = stop_event + + def __build_recognizer(self) -> None: + try: + if self.config.audio_transcription.model_size == "large": + # Whisper models need to be per-process and can only run one stream at a time + # TODO: try parallel: https://github.com/SYSTRAN/faster-whisper/issues/100 + logger.debug(f"Loading Whisper model for {self.camera_config.name}") + self.whisper_model = FasterWhisperASR( + modelsize="tiny", + device="cuda" + if self.config.audio_transcription.device == "GPU" + else "cpu", + lan=self.config.audio_transcription.language, + model_dir=os.path.join(MODEL_CACHE_DIR, "whisper"), + ) + self.whisper_model.use_vad() + self.stream = OnlineASRProcessor( + asr=self.whisper_model, + ) + else: + logger.debug(f"Loading sherpa stream for {self.camera_config.name}") + self.stream = self.model_runner.model.create_stream() + logger.debug( + f"Audio transcription (live) initialized for {self.camera_config.name}" + ) + except Exception as e: + logger.error( + f"Failed to initialize live streaming audio transcription: {e}" + ) + + def __process_audio_stream( + self, audio_data: np.ndarray + ) -> Optional[tuple[str, bool]]: + if ( + self.model_runner.model is None + and self.config.audio_transcription.model_size == "small" + ): + logger.debug("Audio transcription (live) model not initialized") + return None + + if not self.stream: + self.__build_recognizer() + + try: + if audio_data.dtype != np.float32: + audio_data = audio_data.astype(np.float32) + + if audio_data.max() > 1.0 or audio_data.min() < -1.0: + audio_data = audio_data / 32768.0 # Normalize from int16 + + rms = float(np.sqrt(np.mean(np.absolute(np.square(audio_data))))) + logger.debug(f"Audio chunk size: {audio_data.size}, RMS: {rms:.4f}") + + if self.config.audio_transcription.model_size == "large": + # large model + self.stream.insert_audio_chunk(audio_data) + output = self.stream.process_iter() + text = output[2].strip() + is_endpoint = ( + text.endswith((".", "!", "?")) + and sum(len(str(lines)) for lines in self.transcription_segments) + > 300 + ) + + if text: + self.transcription_segments.append(text) + concatenated_text = " ".join(self.transcription_segments) + logger.debug(f"Concatenated transcription: '{concatenated_text}'") + text = concatenated_text + + else: + # small model + self.stream.accept_waveform(16000, audio_data) + + while self.model_runner.model.is_ready(self.stream): + self.model_runner.model.decode_stream(self.stream) + + text = self.model_runner.model.get_result(self.stream).strip() + is_endpoint = self.model_runner.model.is_endpoint(self.stream) + + logger.debug(f"Transcription result: '{text}'") + + if not text: + logger.debug("No transcription, returning") + return None + + logger.debug(f"Endpoint detected: {is_endpoint}") + + if is_endpoint and self.config.audio_transcription.model_size == "small": + # reset sherpa if we've reached an endpoint + self.model_runner.model.reset(self.stream) + + return text, is_endpoint + except Exception as e: + logger.error(f"Error processing audio stream: {e}") + return None + + def process_frame(self, obj_data: dict[str, any], frame: np.ndarray) -> None: + pass + + def process_audio(self, obj_data: dict[str, any], audio: np.ndarray) -> bool | None: + if audio is None or audio.size == 0: + logger.debug("No audio data provided for transcription") + return None + + # enqueue audio data for processing in the thread + self.audio_queue.put((obj_data, audio)) + return None + + def run(self) -> None: + """Run method for the transcription thread to process queued audio data.""" + logger.debug( + f"Starting audio transcription thread for {self.camera_config.name}" + ) + + # start with an empty transcription + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + + while not self.stop_event.is_set(): + try: + # Get audio data from queue with a timeout to check stop_event + _, audio = self.audio_queue.get(timeout=0.1) + result = self.__process_audio_stream(audio) + + if not result: + continue + + text, is_endpoint = result + logger.debug(f"Transcribed audio: '{text}', Endpoint: {is_endpoint}") + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", text + ) + + self.audio_queue.task_done() + + if is_endpoint: + self.reset() + + except queue.Empty: + continue + except Exception as e: + logger.error(f"Error processing audio in thread: {e}") + self.audio_queue.task_done() + + logger.debug( + f"Stopping audio transcription thread for {self.camera_config.name}" + ) + + def clear_audio_queue(self) -> None: + # Clear the audio queue + while not self.audio_queue.empty(): + try: + self.audio_queue.get_nowait() + self.audio_queue.task_done() + except queue.Empty: + break + + def reset(self) -> None: + if self.config.audio_transcription.model_size == "large": + # get final output from whisper + output = self.stream.finish() + self.transcription_segments = [] + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + (output[2].strip() + " "), + ) + + # reset whisper + self.stream.init() + self.transcription_segments = [] + else: + # reset sherpa + self.model_runner.model.reset(self.stream) + + logger.debug("Stream reset") + + def check_unload_model(self) -> None: + # regularly called in the loop in audio maintainer + if ( + self.config.audio_transcription.model_size == "large" + and self.whisper_model is not None + ): + logger.debug(f"Unloading Whisper model for {self.camera_config.name}") + self.clear_audio_queue() + self.transcription_segments = [] + self.stream = None + self.whisper_model = None + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + if ( + self.config.audio_transcription.model_size == "small" + and self.stream is not None + ): + logger.debug(f"Clearing sherpa stream for {self.camera_config.name}") + self.stream = None + + self.requestor.send_data( + f"{self.camera_config.name}/audio/transcription", + "", + ) + + def stop(self) -> None: + """Stop the transcription thread and clean up.""" + self.stop_event.set() + # Clear the queue to prevent processing stale data + while not self.audio_queue.empty(): + try: + self.audio_queue.get_nowait() + self.audio_queue.task_done() + except queue.Empty: + break + logger.debug( + f"Transcription thread stop signaled for {self.camera_config.name}" + ) + + def handle_request( + self, topic: str, request_data: dict[str, any] + ) -> dict[str, any] | None: + if topic == "clear_audio_recognizer": + self.stream = None + self.__build_recognizer() + return {"message": "Audio recognizer cleared and rebuilt", "success": True} + return None + + def expire_object(self, object_id: str) -> None: + pass diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/bird.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/bird.py new file mode 100644 index 0000000..e599ab0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/bird.py @@ -0,0 +1,176 @@ +"""Handle processing images to classify birds.""" + +import logging +import os +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.config import FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.log import redirect_output_to_logger +from frigate.util.object import calculate_region + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + +logger = logging.getLogger(__name__) + + +class BirdRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.interpreter: Interpreter = None + self.sub_label_publisher = sub_label_publisher + self.tensor_input_details: dict[str, Any] = None + self.tensor_output_details: dict[str, Any] = None + self.detected_birds: dict[str, float] = {} + self.labelmap: dict[int, str] = {} + + GITHUB_RAW_ENDPOINT = os.environ.get( + "GITHUB_RAW_ENDPOINT", "https://raw.githubusercontent.com" + ) + download_path = os.path.join(MODEL_CACHE_DIR, "bird") + self.model_files = { + "bird.tflite": f"{GITHUB_RAW_ENDPOINT}/google-coral/test_data/master/mobilenet_v2_1.0_224_inat_bird_quant.tflite", + "birdmap.txt": f"{GITHUB_RAW_ENDPOINT}/google-coral/test_data/master/inat_bird_labels.txt", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + self.downloader = ModelDownloader( + model_name="bird", + download_path=download_path, + file_names=self.model_files.keys(), + download_func=self.__download_models, + complete_func=self.__build_detector, + ) + self.downloader.ensure_model_files() + else: + self.__build_detector() + + def __download_models(self, path: str) -> None: + try: + file_name = os.path.basename(path) + + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") + + @redirect_output_to_logger(logger, logging.DEBUG) + def __build_detector(self) -> None: + self.interpreter = Interpreter( + model_path=os.path.join(MODEL_CACHE_DIR, "bird/bird.tflite"), + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + i = 0 + + with open(os.path.join(MODEL_CACHE_DIR, "bird/birdmap.txt")) as f: + line = f.readline() + while line: + start = line.find("(") + end = line.find(")") + self.labelmap[i] = line[start + 1 : end] + i += 1 + line = f.readline() + + def process_frame(self, obj_data, frame): + if not self.interpreter: + return + + if obj_data["label"] != "bird": + return + + x, y, x2, y2 = calculate_region( + frame.shape, + obj_data["box"][0], + obj_data["box"][1], + obj_data["box"][2], + obj_data["box"][3], + int( + max( + obj_data["box"][1] - obj_data["box"][0], + obj_data["box"][3] - obj_data["box"][2], + ) + * 1.1 + ), + 1.0, + ) + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + input = rgb[ + y:y2, + x:x2, + ] + + if input.shape != (224, 224): + try: + input = cv2.resize(input, (224, 224)) + except Exception: + logger.warning("Failed to resize image for bird classification") + return + + input = np.expand_dims(input, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + best_id = np.argmax(probs) + + if best_id == 964: + logger.debug("No bird classification was detected.") + return + + score = round(probs[best_id], 2) + + if score < self.config.classification.bird.threshold: + logger.debug(f"Score {score} is not above required threshold") + return + + previous_score = self.detected_birds.get(obj_data["id"], 0.0) + + if score <= previous_score: + logger.debug(f"Score {score} is worse than previous score {previous_score}") + return + + self.sub_label_publisher.publish( + (obj_data["id"], self.labelmap[best_id], score), + EventMetadataTypeEnum.sub_label.value, + ) + self.detected_birds[obj_data["id"]] = score + + def handle_request(self, topic, request_data): + return None + + def expire_object(self, object_id, camera): + if object_id in self.detected_birds: + self.detected_birds.pop(object_id) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/custom_classification.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/custom_classification.py new file mode 100644 index 0000000..a2f88ee --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/custom_classification.py @@ -0,0 +1,658 @@ +"""Real time processor that works with classification tflite models.""" + +import datetime +import json +import logging +import os +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import ( + CustomClassificationConfig, + ObjectClassificationType, +) +from frigate.const import CLIPS_DIR, MODEL_CACHE_DIR +from frigate.log import redirect_output_to_logger +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed, load_labels +from frigate.util.object import box_overlaps, calculate_region + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + +logger = logging.getLogger(__name__) + +MAX_OBJECT_CLASSIFICATIONS = 16 + + +class CustomStateClassificationProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.model_config = model_config + self.requestor = requestor + self.model_dir = os.path.join(MODEL_CACHE_DIR, self.model_config.name) + self.train_dir = os.path.join(CLIPS_DIR, self.model_config.name, "train") + self.interpreter: Interpreter = None + self.tensor_input_details: dict[str, Any] | None = None + self.tensor_output_details: dict[str, Any] | None = None + self.labelmap: dict[int, str] = {} + self.classifications_per_second = EventsPerSecond() + self.state_history: dict[str, dict[str, Any]] = {} + + if ( + self.metrics + and self.model_config.name in self.metrics.classification_speeds + ): + self.inference_speed = InferenceSpeed( + self.metrics.classification_speeds[self.model_config.name] + ) + else: + self.inference_speed = None + + self.last_run = datetime.datetime.now().timestamp() + self.__build_detector() + + @redirect_output_to_logger(logger, logging.DEBUG) + def __build_detector(self) -> None: + try: + from tflite_runtime.interpreter import Interpreter + except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + + model_path = os.path.join(self.model_dir, "model.tflite") + labelmap_path = os.path.join(self.model_dir, "labelmap.txt") + + if not os.path.exists(model_path) or not os.path.exists(labelmap_path): + self.interpreter = None + self.tensor_input_details = None + self.tensor_output_details = None + self.labelmap = {} + return + + self.interpreter = Interpreter( + model_path=model_path, + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.labelmap = load_labels(labelmap_path, prefill=0) + self.classifications_per_second.start() + + def __update_metrics(self, duration: float) -> None: + self.classifications_per_second.update() + if self.inference_speed: + self.inference_speed.update(duration) + + def _should_save_image( + self, camera: str, detected_state: str, score: float = 1.0 + ) -> bool: + """ + Determine if we should save the image for training. + Save when: + - State is changing or being verified (regardless of score) + - Score is less than 100% (even if state matches, useful for training) + Don't save when: + - State is stable (matches current_state) AND score is 100% + """ + if camera not in self.state_history: + # First detection for this camera, save it + return True + + verification = self.state_history[camera] + current_state = verification.get("current_state") + pending_state = verification.get("pending_state") + + # Save if there's a pending state change being verified + if pending_state is not None: + return True + + # Save if the detected state differs from the current verified state + # (state is changing) + if current_state is not None and detected_state != current_state: + return True + + # If score is less than 100%, save even if state matches + # (useful for training to improve confidence) + if score < 1.0: + return True + + # Don't save if state is stable (detected_state == current_state) AND score is 100% + return False + + def verify_state_change(self, camera: str, detected_state: str) -> str | None: + """ + Verify state change requires 3 consecutive identical states before publishing. + Returns state to publish or None if verification not complete. + """ + if camera not in self.state_history: + self.state_history[camera] = { + "current_state": None, + "pending_state": None, + "consecutive_count": 0, + } + + verification = self.state_history[camera] + + if detected_state == verification["current_state"]: + verification["pending_state"] = None + verification["consecutive_count"] = 0 + return None + + if detected_state == verification["pending_state"]: + verification["consecutive_count"] += 1 + + if verification["consecutive_count"] >= 3: + verification["current_state"] = detected_state + verification["pending_state"] = None + verification["consecutive_count"] = 0 + return detected_state + else: + verification["pending_state"] = detected_state + verification["consecutive_count"] = 1 + logger.debug( + f"New state '{detected_state}' detected for {camera}, need {3 - verification['consecutive_count']} more consecutive detections" + ) + + return None + + def process_frame(self, frame_data: dict[str, Any], frame: np.ndarray): + if self.metrics and self.model_config.name in self.metrics.classification_cps: + self.metrics.classification_cps[ + self.model_config.name + ].value = self.classifications_per_second.eps() + camera = frame_data.get("camera") + + if camera not in self.model_config.state_config.cameras: + return + + camera_config = self.model_config.state_config.cameras[camera] + crop = [ + camera_config.crop[0] * self.config.cameras[camera].detect.width, + camera_config.crop[1] * self.config.cameras[camera].detect.height, + camera_config.crop[2] * self.config.cameras[camera].detect.width, + camera_config.crop[3] * self.config.cameras[camera].detect.height, + ] + should_run = False + + now = datetime.datetime.now().timestamp() + if ( + self.model_config.state_config.interval + and now > self.last_run + self.model_config.state_config.interval + ): + self.last_run = now + should_run = True + + if ( + not should_run + and self.model_config.state_config.motion + and any([box_overlaps(crop, mb) for mb in frame_data.get("motion", [])]) + ): + # classification should run at most once per second + if now > self.last_run + 1: + self.last_run = now + should_run = True + + # Shortcut: always run if we have a pending state verification to complete + if ( + not should_run + and camera in self.state_history + and self.state_history[camera]["pending_state"] is not None + and now > self.last_run + 0.5 + ): + self.last_run = now + should_run = True + logger.debug( + f"Running verification check for pending state: {self.state_history[camera]['pending_state']} ({self.state_history[camera]['consecutive_count']}/3)" + ) + + if not should_run: + return + + x, y, x2, y2 = calculate_region( + frame.shape, + crop[0], + crop[1], + crop[2], + crop[3], + 224, + 1.0, + ) + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + frame = rgb[ + y:y2, + x:x2, + ] + + if frame.shape != (224, 224): + try: + resized_frame = cv2.resize(frame, (224, 224)) + except Exception: + logger.warning("Failed to resize image for state classification") + return + + if self.interpreter is None: + # When interpreter is None, always save (score is 0.0, which is < 1.0) + if self._should_save_image(camera, "unknown", 0.0): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) + write_classification_attempt( + self.train_dir, + cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), + "none-none", + now, + "unknown", + 0.0, + max_files=save_attempts, + ) + return + + input = np.expand_dims(resized_frame, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + logger.debug( + f"{self.model_config.name} Ran state classification with probabilities: {probs}" + ) + best_id = np.argmax(probs) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - now) + + detected_state = self.labelmap[best_id] + + if self._should_save_image(camera, detected_state, score): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) + write_classification_attempt( + self.train_dir, + cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), + "none-none", + now, + detected_state, + score, + max_files=save_attempts, + ) + + if score < self.model_config.threshold: + logger.debug( + f"Score {score} below threshold {self.model_config.threshold}, skipping verification" + ) + return + + verified_state = self.verify_state_change(camera, detected_state) + + if verified_state is not None: + self.requestor.send_data( + f"{camera}/classification/{self.model_config.name}", + verified_state, + ) + + def handle_request(self, topic, request_data): + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + self.__build_detector() + logger.info( + f"Successfully loaded updated model for {self.model_config.name}" + ) + return { + "success": True, + "message": f"Loaded {self.model_config.name} model.", + } + else: + return None + else: + return None + + def expire_object(self, object_id, camera): + pass + + +class CustomObjectClassificationProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + model_config: CustomClassificationConfig, + sub_label_publisher: EventMetadataPublisher, + requestor: InterProcessRequestor, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.model_config = model_config + self.model_dir = os.path.join(MODEL_CACHE_DIR, self.model_config.name) + self.train_dir = os.path.join(CLIPS_DIR, self.model_config.name, "train") + self.interpreter: Interpreter = None + self.sub_label_publisher = sub_label_publisher + self.requestor = requestor + self.tensor_input_details: dict[str, Any] | None = None + self.tensor_output_details: dict[str, Any] | None = None + self.classification_history: dict[str, list[tuple[str, float, float]]] = {} + self.labelmap: dict[int, str] = {} + self.classifications_per_second = EventsPerSecond() + + if ( + self.metrics + and self.model_config.name in self.metrics.classification_speeds + ): + self.inference_speed = InferenceSpeed( + self.metrics.classification_speeds[self.model_config.name] + ) + else: + self.inference_speed = None + + self.__build_detector() + + @redirect_output_to_logger(logger, logging.DEBUG) + def __build_detector(self) -> None: + model_path = os.path.join(self.model_dir, "model.tflite") + labelmap_path = os.path.join(self.model_dir, "labelmap.txt") + + if not os.path.exists(model_path) or not os.path.exists(labelmap_path): + self.interpreter = None + self.tensor_input_details = None + self.tensor_output_details = None + self.labelmap = {} + return + + self.interpreter = Interpreter( + model_path=model_path, + num_threads=2, + ) + self.interpreter.allocate_tensors() + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.labelmap = load_labels(labelmap_path, prefill=0) + + def __update_metrics(self, duration: float) -> None: + self.classifications_per_second.update() + if self.inference_speed: + self.inference_speed.update(duration) + + def get_weighted_score( + self, + object_id: str, + current_label: str, + current_score: float, + current_time: float, + ) -> tuple[str | None, float]: + """ + Determine weighted score based on history to prevent false positives/negatives. + Requires 60% of attempts to agree on a label before publishing. + Returns (weighted_label, weighted_score) or (None, 0.0) if no weighted score. + """ + if object_id not in self.classification_history: + self.classification_history[object_id] = [] + + self.classification_history[object_id].append( + (current_label, current_score, current_time) + ) + + history = self.classification_history[object_id] + + if len(history) < 3: + return None, 0.0 + + label_counts = {} + label_scores = {} + total_attempts = len(history) + + for label, score, timestamp in history: + if label not in label_counts: + label_counts[label] = 0 + label_scores[label] = [] + + label_counts[label] += 1 + label_scores[label].append(score) + + best_label = max(label_counts, key=label_counts.get) + best_count = label_counts[best_label] + + consensus_threshold = total_attempts * 0.6 + if best_count < consensus_threshold: + return None, 0.0 + + avg_score = sum(label_scores[best_label]) / len(label_scores[best_label]) + + if best_label == "none": + return None, 0.0 + + return best_label, avg_score + + def process_frame(self, obj_data, frame): + if self.metrics and self.model_config.name in self.metrics.classification_cps: + self.metrics.classification_cps[ + self.model_config.name + ].value = self.classifications_per_second.eps() + + if obj_data["false_positive"]: + return + + if obj_data["label"] not in self.model_config.object_config.objects: + return + + if obj_data.get("end_time") is not None: + return + + object_id = obj_data["id"] + + if ( + object_id in self.classification_history + and len(self.classification_history[object_id]) + >= MAX_OBJECT_CLASSIFICATIONS + ): + return + + now = datetime.datetime.now().timestamp() + x, y, x2, y2 = calculate_region( + frame.shape, + obj_data["box"][0], + obj_data["box"][1], + obj_data["box"][2], + obj_data["box"][3], + max( + obj_data["box"][2] - obj_data["box"][0], + obj_data["box"][3] - obj_data["box"][1], + ), + 1.0, + ) + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + crop = rgb[ + y:y2, + x:x2, + ] + + if crop.shape != (224, 224): + try: + resized_crop = cv2.resize(crop, (224, 224)) + except Exception: + logger.warning("Failed to resize image for state classification") + return + + if self.interpreter is None: + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) + write_classification_attempt( + self.train_dir, + cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), + object_id, + now, + "unknown", + 0.0, + max_files=save_attempts, + ) + return + + input = np.expand_dims(resized_crop, axis=0) + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], input) + self.interpreter.invoke() + res: np.ndarray = self.interpreter.get_tensor( + self.tensor_output_details[0]["index"] + )[0] + probs = res / res.sum(axis=0) + logger.debug( + f"{self.model_config.name} Ran object classification with probabilities: {probs}" + ) + best_id = np.argmax(probs) + score = round(probs[best_id], 2) + self.__update_metrics(datetime.datetime.now().timestamp() - now) + + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) + write_classification_attempt( + self.train_dir, + cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), + object_id, + now, + self.labelmap[best_id], + score, + max_files=save_attempts, + ) + + if score < self.model_config.threshold: + logger.debug(f"Score {score} is less than threshold.") + return + + sub_label = self.labelmap[best_id] + + consensus_label, consensus_score = self.get_weighted_score( + object_id, sub_label, score, now + ) + + if consensus_label is not None: + camera = obj_data["camera"] + + if ( + self.model_config.object_config.classification_type + == ObjectClassificationType.sub_label + ): + self.sub_label_publisher.publish( + (object_id, consensus_label, consensus_score), + EventMetadataTypeEnum.sub_label, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": now, + "model": self.model_config.name, + "sub_label": consensus_label, + "score": consensus_score, + } + ), + ) + elif ( + self.model_config.object_config.classification_type + == ObjectClassificationType.attribute + ): + self.sub_label_publisher.publish( + ( + object_id, + self.model_config.name, + consensus_label, + consensus_score, + ), + EventMetadataTypeEnum.attribute.value, + ) + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.classification, + "id": object_id, + "camera": camera, + "timestamp": now, + "model": self.model_config.name, + "attribute": consensus_label, + "score": consensus_score, + } + ), + ) + + def handle_request(self, topic, request_data): + if topic == EmbeddingsRequestEnum.reload_classification_model.value: + if request_data.get("model_name") == self.model_config.name: + logger.info( + f"Successfully loaded updated model for {self.model_config.name}" + ) + return { + "success": True, + "message": f"Loaded {self.model_config.name} model.", + } + else: + return None + else: + return None + + def expire_object(self, object_id, camera): + if object_id in self.classification_history: + self.classification_history.pop(object_id) + + +@staticmethod +def write_classification_attempt( + folder: str, + frame: np.ndarray, + event_id: str, + timestamp: float, + label: str, + score: float, + max_files: int = 100, +) -> None: + if "-" in label: + label = label.replace("-", "_") + + file = os.path.join(folder, f"{event_id}-{timestamp}-{label}-{score}.webp") + os.makedirs(folder, exist_ok=True) + cv2.imwrite(file, frame) + + # delete oldest face image if maximum is reached + try: + files = sorted( + filter(lambda f: (f.endswith(".webp")), os.listdir(folder)), + key=lambda f: os.path.getctime(os.path.join(folder, f)), + reverse=True, + ) + + if len(files) > max_files: + os.unlink(os.path.join(folder, files[-1])) + except FileNotFoundError: + pass diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/face.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/face.py new file mode 100644 index 0000000..1901a81 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/face.py @@ -0,0 +1,549 @@ +"""Handle processing images for face detection and recognition.""" + +import base64 +import datetime +import json +import logging +import os +import shutil +from pathlib import Path +from typing import Any, Optional + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataTypeEnum, +) +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import FACE_DIR, MODEL_CACHE_DIR +from frigate.data_processing.common.face.model import ( + ArcFaceRecognizer, + FaceNetRecognizer, + FaceRecognizer, +) +from frigate.types import TrackedObjectUpdateTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed +from frigate.util.image import area + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +MAX_DETECTION_HEIGHT = 1080 +MAX_FACES_ATTEMPTS_AFTER_REC = 6 +MAX_FACE_ATTEMPTS = 12 + + +class FaceRealTimeProcessor(RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + ): + super().__init__(config, metrics) + self.face_config = config.face_recognition + self.requestor = requestor + self.sub_label_publisher = sub_label_publisher + self.face_detector: cv2.FaceDetectorYN = None + self.requires_face_detection = "face" not in self.config.objects.all_objects + self.person_face_history: dict[str, list[tuple[str, float, int]]] = {} + self.camera_current_people: dict[str, list[str]] = {} + self.recognizer: FaceRecognizer | None = None + self.faces_per_second = EventsPerSecond() + self.inference_speed = InferenceSpeed(self.metrics.face_rec_speed) + + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + + download_path = os.path.join(MODEL_CACHE_DIR, "facedet") + self.model_files = { + "facedet.onnx": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/facedet.onnx", + "landmarkdet.yaml": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/landmarkdet.yaml", + } + + if not all( + os.path.exists(os.path.join(download_path, n)) + for n in self.model_files.keys() + ): + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + self.downloader = ModelDownloader( + model_name="facedet", + download_path=download_path, + file_names=self.model_files.keys(), + download_func=self.__download_models, + complete_func=self.__build_detector, + ) + self.downloader.ensure_model_files() + else: + self.__build_detector() + + self.label_map: dict[int, str] = {} + + if self.face_config.model_size == "small": + self.recognizer = FaceNetRecognizer(self.config) + else: + self.recognizer = ArcFaceRecognizer(self.config) + + self.recognizer.build() + + def __download_models(self, path: str) -> None: + try: + file_name = os.path.basename(path) + # conditionally import ModelDownloader + from frigate.util.downloader import ModelDownloader + + ModelDownloader.download_from_url(self.model_files[file_name], path) + except Exception as e: + logger.error(f"Failed to download {path}: {e}") + + def __build_detector(self) -> None: + self.face_detector = cv2.FaceDetectorYN.create( + os.path.join(MODEL_CACHE_DIR, "facedet/facedet.onnx"), + config="", + input_size=(320, 320), + score_threshold=0.5, + nms_threshold=0.3, + ) + self.faces_per_second.start() + + def __detect_face( + self, input: np.ndarray, threshold: float + ) -> tuple[int, int, int, int]: + """Detect faces in input image.""" + if not self.face_detector: + return None + + # YN face detector fails at extreme definitions + # this rescales to a size that can properly detect faces + # still retaining plenty of detail + if input.shape[0] > MAX_DETECTION_HEIGHT: + scale_factor = MAX_DETECTION_HEIGHT / input.shape[0] + new_width = int(scale_factor * input.shape[1]) + input = cv2.resize(input, (new_width, MAX_DETECTION_HEIGHT)) + else: + scale_factor = 1 + + self.face_detector.setInputSize((input.shape[1], input.shape[0])) + faces = self.face_detector.detect(input) + + if faces is None or faces[1] is None: + return None + + face = None + + for _, potential_face in enumerate(faces[1]): + if potential_face[-1] < threshold: + continue + + raw_bbox = potential_face[0:4].astype(np.uint16) + x: int = int(max(raw_bbox[0], 0) / scale_factor) + y: int = int(max(raw_bbox[1], 0) / scale_factor) + w: int = int(raw_bbox[2] / scale_factor) + h: int = int(raw_bbox[3] / scale_factor) + bbox = (x, y, x + w, y + h) + + if face is None or area(bbox) > area(face): + face = bbox + + return face + + def __update_metrics(self, duration: float) -> None: + self.faces_per_second.update() + self.inference_speed.update(duration) + + def process_frame(self, obj_data: dict[str, Any], frame: np.ndarray): + """Look for faces in image.""" + self.metrics.face_rec_fps.value = self.faces_per_second.eps() + camera = obj_data["camera"] + + if not self.config.cameras[camera].face_recognition.enabled: + logger.debug(f"Face recognition disabled for camera {camera}, skipping") + return + + start = datetime.datetime.now().timestamp() + id = obj_data["id"] + + # don't run for non person objects + if obj_data.get("label") != "person": + logger.debug("Not processing face for a non person object.") + return + + # don't overwrite sub label for objects that have a sub label + # that is not a face + if obj_data.get("sub_label") and id not in self.person_face_history: + logger.debug( + f"Not processing face due to existing sub label: {obj_data.get('sub_label')}." + ) + return + + # check if we have hit limits + if ( + id in self.person_face_history + and len(self.person_face_history[id]) >= MAX_FACES_ATTEMPTS_AFTER_REC + ): + # if we are at max attempts after rec and we have a rec + if obj_data.get("sub_label"): + logger.debug( + "Not processing due to hitting max attempts after true recognition." + ) + return + + # if we don't have a rec and are at max attempts + if len(self.person_face_history[id]) >= MAX_FACE_ATTEMPTS: + logger.debug("Not processing due to hitting max rec attempts.") + return + + face: Optional[dict[str, Any]] = None + + if self.requires_face_detection: + logger.debug("Running manual face detection.") + person_box = obj_data.get("box") + + if not person_box: + logger.debug(f"No person box available for {id}") + return + + rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2RGB_I420) + left, top, right, bottom = person_box + person = rgb[top:bottom, left:right] + face_box = self.__detect_face(person, self.face_config.detection_threshold) + + if not face_box: + logger.debug("Detected no faces for person object.") + return + + face_frame = person[ + max(0, face_box[1]) : min(frame.shape[0], face_box[3]), + max(0, face_box[0]) : min(frame.shape[1], face_box[2]), + ] + + # check that face is correct size + if area(face_box) < self.config.cameras[camera].face_recognition.min_area: + logger.debug( + f"Detected face that is smaller than the min_area {face} < {self.config.cameras[camera].face_recognition.min_area}" + ) + return + + try: + face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR) + except Exception as e: + logger.debug(f"Failed to convert face frame color for {id}: {e}") + return + else: + # don't run for object without attributes + if not obj_data.get("current_attributes"): + logger.debug("No attributes to parse.") + return + + attributes: list[dict[str, Any]] = obj_data.get("current_attributes", []) + for attr in attributes: + if attr.get("label") != "face": + continue + + if face is None or attr.get("score", 0.0) > face.get("score", 0.0): + face = attr + + # no faces detected in this frame + if not face: + logger.debug(f"No face attributes found for {id}") + return + + face_box = face.get("box") + + # check that face is valid + if ( + not face_box + or area(face_box) + < self.config.cameras[camera].face_recognition.min_area + ): + logger.debug(f"Invalid face box {face}") + return + + face_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + + face_frame = face_frame[ + max(0, face_box[1]) : min(frame.shape[0], face_box[3]), + max(0, face_box[0]) : min(frame.shape[1], face_box[2]), + ] + + res = self.recognizer.classify(face_frame) + + if not res: + logger.debug(f"Face recognizer returned no result for {id}") + self.__update_metrics(datetime.datetime.now().timestamp() - start) + return + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + logger.debug( + f"Detected best face for person as: {sub_label} with probability {score}" + ) + + self.write_face_attempt( + face_frame, id, datetime.datetime.now().timestamp(), sub_label, score + ) + + if id not in self.person_face_history: + self.person_face_history[id] = [] + + if camera not in self.camera_current_people: + self.camera_current_people[camera] = [] + + self.camera_current_people[camera].append(id) + + self.person_face_history[id].append( + (sub_label, score, face_frame.shape[0] * face_frame.shape[1]) + ) + (weighted_sub_label, weighted_score) = self.weighted_average( + self.person_face_history[id] + ) + + self.requestor.send_data( + "tracked_object_update", + json.dumps( + { + "type": TrackedObjectUpdateTypesEnum.face, + "name": weighted_sub_label, + "score": weighted_score, + "id": id, + "camera": camera, + "timestamp": start, + } + ), + ) + + if weighted_score >= self.face_config.recognition_threshold: + self.sub_label_publisher.publish( + (id, weighted_sub_label, weighted_score), + EventMetadataTypeEnum.sub_label.value, + ) + + self.__update_metrics(datetime.datetime.now().timestamp() - start) + + def handle_request(self, topic, request_data) -> dict[str, Any] | None: + if topic == EmbeddingsRequestEnum.clear_face_classifier.value: + self.recognizer.clear() + return {"success": True, "message": "Face classifier cleared."} + elif topic == EmbeddingsRequestEnum.recognize_face.value: + img = cv2.imdecode( + np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + + # detect faces with lower confidence since we expect the face + # to be visible in uploaded images + face_box = self.__detect_face(img, 0.5) + + if not face_box: + return {"message": "No face was detected.", "success": False} + + face = img[face_box[1] : face_box[3], face_box[0] : face_box[2]] + res = self.recognizer.classify(face) + + if not res: + return {"success": False, "message": "No face was recognized."} + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + return {"success": True, "score": score, "face_name": sub_label} + elif topic == EmbeddingsRequestEnum.register_face.value: + label = request_data["face_name"] + + if request_data.get("cropped"): + thumbnail = request_data["image"] + else: + img = cv2.imdecode( + np.frombuffer( + base64.b64decode(request_data["image"]), dtype=np.uint8 + ), + cv2.IMREAD_COLOR, + ) + + # detect faces with lower confidence since we expect the face + # to be visible in uploaded images + face_box = self.__detect_face(img, 0.5) + + if not face_box: + return { + "message": "No face was detected.", + "success": False, + } + + face = img[face_box[1] : face_box[3], face_box[0] : face_box[2]] + _, thumbnail = cv2.imencode( + ".webp", face, [int(cv2.IMWRITE_WEBP_QUALITY), 100] + ) + + # write face to library + folder = os.path.join(FACE_DIR, label) + file = os.path.join( + folder, f"{label}_{datetime.datetime.now().timestamp()}.webp" + ) + os.makedirs(folder, exist_ok=True) + + # save face image + with open(file, "wb") as output: + output.write(thumbnail.tobytes()) + + self.recognizer.clear() + return { + "message": "Successfully registered face.", + "success": True, + } + elif topic == EmbeddingsRequestEnum.reprocess_face.value: + current_file: str = request_data["image_file"] + (id_time, id_rand, timestamp, _, _) = current_file.split("-") + img = None + id = f"{id_time}-{id_rand}" + + if current_file: + img = cv2.imread(current_file) + + if img is None: + return { + "message": "Invalid image file.", + "success": False, + } + + res = self.recognizer.classify(img) + + if not res: + return { + "message": "Model is still training, please try again in a few moments.", + "success": False, + } + + sub_label, score = res + + if score <= self.face_config.unknown_score: + sub_label = "unknown" + + if "-" in sub_label: + sub_label = sub_label.replace("-", "_") + + if self.config.face_recognition.save_attempts: + # write face to library + folder = os.path.join(FACE_DIR, "train") + os.makedirs(folder, exist_ok=True) + new_file = os.path.join( + folder, f"{id}-{timestamp}-{sub_label}-{score}.webp" + ) + shutil.move(current_file, new_file) + + return { + "message": f"Successfully reprocessed face. Result: {sub_label} (score: {score:.2f})", + "success": True, + "face_name": sub_label, + "score": score, + } + + def expire_object(self, object_id: str, camera: str): + if object_id in self.person_face_history: + self.person_face_history.pop(object_id) + + if object_id in self.camera_current_people.get(camera, []): + self.camera_current_people[camera].remove(object_id) + + def weighted_average( + self, results_list: list[tuple[str, float, int]], max_weight: int = 4000 + ): + """ + Calculates a robust weighted average, capping the area weight and giving more weight to higher scores. + + Args: + results_list: A list of tuples, where each tuple contains (name, score, face_area). + max_weight: The maximum weight to apply based on face area. + + Returns: + A tuple containing the prominent name and its weighted average score, or (None, 0.0) if the list is empty. + """ + if not results_list: + return None, 0.0 + + counts: dict[str, int] = {} + weighted_scores: dict[str, int] = {} + total_weights: dict[str, int] = {} + + for name, score, face_area in results_list: + if name == "unknown": + continue + + if name not in weighted_scores: + counts[name] = 0 + weighted_scores[name] = 0.0 + total_weights[name] = 0.0 + + # increase count + counts[name] += 1 + + # Capped weight based on face area + weight = min(face_area, max_weight) + + # Score-based weighting (higher scores get more weight) + weight *= (score - self.face_config.unknown_score) * 10 + weighted_scores[name] += score * weight + total_weights[name] += weight + + if not weighted_scores: + return None, 0.0 + + best_name = max(weighted_scores, key=weighted_scores.get) + + # If the number of faces for this person < min_faces, we are not confident it is a correct result + if counts[best_name] < self.face_config.min_faces: + return None, 0.0 + + # If the best name has the same number of results as another name, we are not confident it is a correct result + for name, count in counts.items(): + if name != best_name and counts[best_name] == count: + return None, 0.0 + + weighted_average = weighted_scores[best_name] / total_weights[best_name] + + return best_name, weighted_average + + def write_face_attempt( + self, + frame: np.ndarray, + event_id: str, + timestamp: float, + sub_label: str, + score: float, + ) -> None: + if self.config.face_recognition.save_attempts: + # write face to library + folder = os.path.join(FACE_DIR, "train") + + if "-" in sub_label: + sub_label = sub_label.replace("-", "_") + + file = os.path.join( + folder, f"{event_id}-{timestamp}-{sub_label}-{score}.webp" + ) + os.makedirs(folder, exist_ok=True) + cv2.imwrite(file, frame) + + files = sorted( + filter(lambda f: (f.endswith(".webp")), os.listdir(folder)), + key=lambda f: os.path.getctime(os.path.join(folder, f)), + reverse=True, + ) + + # delete oldest face image if maximum is reached + if len(files) > self.config.face_recognition.save_attempts: + Path(os.path.join(folder, files[-1])).unlink(missing_ok=True) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/license_plate.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/license_plate.py new file mode 100644 index 0000000..59c625d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/license_plate.py @@ -0,0 +1,57 @@ +"""Handle processing images for face detection and recognition.""" + +import logging +from typing import Any + +import numpy as np + +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.data_processing.common.license_plate.mixin import ( + LicensePlateProcessingMixin, +) +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) + +from ..types import DataProcessorMetrics +from .api import RealTimeProcessorApi + +logger = logging.getLogger(__name__) + + +class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcessorApi): + def __init__( + self, + config: FrigateConfig, + requestor: InterProcessRequestor, + sub_label_publisher: EventMetadataPublisher, + metrics: DataProcessorMetrics, + model_runner: LicensePlateModelRunner, + detected_license_plates: dict[str, dict[str, Any]], + ): + self.requestor = requestor + self.detected_license_plates = detected_license_plates + self.model_runner = model_runner + self.lpr_config = config.lpr + self.config = config + self.sub_label_publisher = sub_label_publisher + self.camera_current_cars: dict[str, list[str]] = {} + super().__init__(config, metrics) + + def process_frame( + self, + obj_data: dict[str, Any], + frame: np.ndarray, + dedicated_lpr: bool | None = False, + ): + """Look for license plates in image.""" + self.lpr_process(obj_data, frame, dedicated_lpr) + + def handle_request(self, topic, request_data) -> dict[str, Any] | None: + return + + def expire_object(self, object_id: str, camera: str): + """Expire lpr objects.""" + self.lpr_expire(object_id, camera) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/real_time/whisper_online.py b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/whisper_online.py new file mode 100644 index 0000000..024b19f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/real_time/whisper_online.py @@ -0,0 +1,1160 @@ +# imported to Frigate from https://github.com/ufal/whisper_streaming +# with only minor modifications +import io +import logging +import math +import sys +import time +from functools import lru_cache + +import librosa +import numpy as np +import soundfile as sf + +logger = logging.getLogger(__name__) + + +@lru_cache(10**6) +def load_audio(fname): + a, _ = librosa.load(fname, sr=16000, dtype=np.float32) + return a + + +def load_audio_chunk(fname, beg, end): + audio = load_audio(fname) + beg_s = int(beg * 16000) + end_s = int(end * 16000) + return audio[beg_s:end_s] + + +# Whisper backend + + +class ASRBase: + sep = "" # join transcribe words with this character (" " for whisper_timestamped, + # "" for faster-whisper because it emits the spaces when neeeded) + + def __init__( + self, + lan, + modelsize=None, + cache_dir=None, + model_dir=None, + logfile=sys.stderr, + device="cpu", + ): + self.logfile = logfile + + self.transcribe_kargs = {} + if lan == "auto": + self.original_language = None + else: + self.original_language = lan + + self.model = self.load_model(modelsize, cache_dir, model_dir, device) + + def load_model(self, modelsize, cache_dir): + raise NotImplementedError("must be implemented in the child class") + + def transcribe(self, audio, init_prompt=""): + raise NotImplementedError("must be implemented in the child class") + + def use_vad(self): + raise NotImplementedError("must be implemented in the child class") + + +class WhisperTimestampedASR(ASRBase): + """Uses whisper_timestamped library as the backend. Initially, we tested the code on this backend. It worked, but slower than faster-whisper. + On the other hand, the installation for GPU could be easier. + """ + + sep = " " + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None): + import whisper + from whisper_timestamped import transcribe_timestamped + + self.transcribe_timestamped = transcribe_timestamped + if model_dir is not None: + logger.debug("ignoring model_dir, not implemented") + return whisper.load_model(modelsize, download_root=cache_dir) + + def transcribe(self, audio, init_prompt=""): + result = self.transcribe_timestamped( + self.model, + audio, + language=self.original_language, + initial_prompt=init_prompt, + verbose=None, + condition_on_previous_text=True, + **self.transcribe_kargs, + ) + return result + + def ts_words(self, r): + # return: transcribe result object to [(beg,end,"word1"), ...] + o = [] + for s in r["segments"]: + for w in s["words"]: + t = (w["start"], w["end"], w["text"]) + o.append(t) + return o + + def segments_end_ts(self, res): + return [s["end"] for s in res["segments"]] + + def use_vad(self): + self.transcribe_kargs["vad"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class FasterWhisperASR(ASRBase): + """Uses faster-whisper library as the backend. Works much faster, appx 4-times (in offline mode). For GPU, it requires installation with a specific CUDNN version.""" + + sep = "" + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None, device="cpu"): + from faster_whisper import WhisperModel + + logging.getLogger("faster_whisper").setLevel(logging.WARNING) + + # this worked fast and reliably on NVIDIA L40 + model = WhisperModel( + model_size_or_path="small" if device == "cuda" else "tiny", + device=device, + compute_type="float16" if device == "cuda" else "int8", + local_files_only=False, + download_root=model_dir, + ) + + # or run on GPU with INT8 + # tested: the transcripts were different, probably worse than with FP16, and it was slightly (appx 20%) slower + # model = WhisperModel(model_size, device="cuda", compute_type="int8_float16") + + # or run on CPU with INT8 + # tested: works, but slow, appx 10-times than cuda FP16 + # model = WhisperModel(modelsize, device="cpu", compute_type="int8") #, download_root="faster-disk-cache-dir/") + return model + + def transcribe(self, audio, init_prompt=""): + from faster_whisper import BatchedInferencePipeline + + logging.getLogger("faster_whisper").setLevel(logging.WARNING) + + # tested: beam_size=5 is faster and better than 1 (on one 200 second document from En ESIC, min chunk 0.01) + batched_model = BatchedInferencePipeline(model=self.model) + segments, info = batched_model.transcribe( + audio, + language=self.original_language, + initial_prompt=init_prompt, + beam_size=5, + word_timestamps=True, + condition_on_previous_text=True, + **self.transcribe_kargs, + ) + # print(info) # info contains language detection result + + return list(segments) + + def ts_words(self, segments): + o = [] + for segment in segments: + for word in segment.words: + if segment.no_speech_prob > 0.9: + continue + # not stripping the spaces -- should not be merged with them! + w = word.word + t = (word.start, word.end, w) + o.append(t) + return o + + def segments_end_ts(self, res): + return [s.end for s in res] + + def use_vad(self): + self.transcribe_kargs["vad_filter"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class MLXWhisper(ASRBase): + """ + Uses MLX Whisper library as the backend, optimized for Apple Silicon. + Models available: https://huggingface.co/collections/mlx-community/whisper-663256f9964fbb1177db93dc + Significantly faster than faster-whisper (without CUDA) on Apple M1. + """ + + sep = " " + + def load_model(self, modelsize=None, cache_dir=None, model_dir=None): + """ + Loads the MLX-compatible Whisper model. + + Args: + modelsize (str, optional): The size or name of the Whisper model to load. + If provided, it will be translated to an MLX-compatible model path using the `translate_model_name` method. + Example: "large-v3-turbo" -> "mlx-community/whisper-large-v3-turbo". + cache_dir (str, optional): Path to the directory for caching models. + **Note**: This is not supported by MLX Whisper and will be ignored. + model_dir (str, optional): Direct path to a custom model directory. + If specified, it overrides the `modelsize` parameter. + """ + import mlx.core as mx # Is installed with mlx-whisper + from mlx_whisper.transcribe import ModelHolder, transcribe + + if model_dir is not None: + logger.debug( + f"Loading whisper model from model_dir {model_dir}. modelsize parameter is not used." + ) + model_size_or_path = model_dir + elif modelsize is not None: + model_size_or_path = self.translate_model_name(modelsize) + logger.debug( + f"Loading whisper model {modelsize}. You use mlx whisper, so {model_size_or_path} will be used." + ) + + self.model_size_or_path = model_size_or_path + + # Note: ModelHolder.get_model loads the model into a static class variable, + # making it a global resource. This means: + # - Only one model can be loaded at a time; switching models requires reloading. + # - This approach may not be suitable for scenarios requiring multiple models simultaneously, + # such as using whisper-streaming as a module with varying model sizes. + dtype = mx.float16 # Default to mx.float16. In mlx_whisper.transcribe: dtype = mx.float16 if decode_options.get("fp16", True) else mx.float32 + ModelHolder.get_model( + model_size_or_path, dtype + ) # Model is preloaded to avoid reloading during transcription + + return transcribe + + def translate_model_name(self, model_name): + """ + Translates a given model name to its corresponding MLX-compatible model path. + + Args: + model_name (str): The name of the model to translate. + + Returns: + str: The MLX-compatible model path. + """ + # Dictionary mapping model names to MLX-compatible paths + model_mapping = { + "tiny.en": "mlx-community/whisper-tiny.en-mlx", + "tiny": "mlx-community/whisper-tiny-mlx", + "base.en": "mlx-community/whisper-base.en-mlx", + "base": "mlx-community/whisper-base-mlx", + "small.en": "mlx-community/whisper-small.en-mlx", + "small": "mlx-community/whisper-small-mlx", + "medium.en": "mlx-community/whisper-medium.en-mlx", + "medium": "mlx-community/whisper-medium-mlx", + "large-v1": "mlx-community/whisper-large-v1-mlx", + "large-v2": "mlx-community/whisper-large-v2-mlx", + "large-v3": "mlx-community/whisper-large-v3-mlx", + "large-v3-turbo": "mlx-community/whisper-large-v3-turbo", + "large": "mlx-community/whisper-large-mlx", + } + + # Retrieve the corresponding MLX model path + mlx_model_path = model_mapping.get(model_name) + + if mlx_model_path: + return mlx_model_path + else: + raise ValueError( + f"Model name '{model_name}' is not recognized or not supported." + ) + + def transcribe(self, audio, init_prompt=""): + segments = self.model( + audio, + language=self.original_language, + initial_prompt=init_prompt, + word_timestamps=True, + condition_on_previous_text=True, + path_or_hf_repo=self.model_size_or_path, + **self.transcribe_kargs, + ) + return segments.get("segments", []) + + def ts_words(self, segments): + """ + Extract timestamped words from transcription segments and skips words with high no-speech probability. + """ + return [ + (word["start"], word["end"], word["word"]) + for segment in segments + for word in segment.get("words", []) + if segment.get("no_speech_prob", 0) <= 0.9 + ] + + def segments_end_ts(self, res): + return [s["end"] for s in res] + + def use_vad(self): + self.transcribe_kargs["vad_filter"] = True + + def set_translate_task(self): + self.transcribe_kargs["task"] = "translate" + + +class OpenaiApiASR(ASRBase): + """Uses OpenAI's Whisper API for audio transcription.""" + + def __init__(self, lan=None, temperature=0, logfile=sys.stderr): + self.logfile = logfile + + self.modelname = "whisper-1" + self.original_language = ( + None if lan == "auto" else lan + ) # ISO-639-1 language code + self.response_format = "verbose_json" + self.temperature = temperature + + self.load_model() + + self.use_vad_opt = False + + # reset the task in set_translate_task + self.task = "transcribe" + + def load_model(self, *args, **kwargs): + from openai import OpenAI + + self.client = OpenAI() + + self.transcribed_seconds = ( + 0 # for logging how many seconds were processed by API, to know the cost + ) + + def ts_words(self, segments): + no_speech_segments = [] + if self.use_vad_opt: + for segment in segments.segments: + # TODO: threshold can be set from outside + if segment["no_speech_prob"] > 0.8: + no_speech_segments.append( + (segment.get("start"), segment.get("end")) + ) + + o = [] + for word in segments.words: + start = word.start + end = word.end + if any(s[0] <= start <= s[1] for s in no_speech_segments): + # print("Skipping word", word.get("word"), "because it's in a no-speech segment") + continue + o.append((start, end, word.word)) + return o + + def segments_end_ts(self, res): + return [s.end for s in res.words] + + def transcribe(self, audio_data, prompt=None, *args, **kwargs): + # Write the audio data to a buffer + buffer = io.BytesIO() + buffer.name = "temp.wav" + sf.write(buffer, audio_data, samplerate=16000, format="WAV", subtype="PCM_16") + buffer.seek(0) # Reset buffer's position to the beginning + + self.transcribed_seconds += math.ceil( + len(audio_data) / 16000 + ) # it rounds up to the whole seconds + + params = { + "model": self.modelname, + "file": buffer, + "response_format": self.response_format, + "temperature": self.temperature, + "timestamp_granularities": ["word", "segment"], + } + if self.task != "translate" and self.original_language: + params["language"] = self.original_language + if prompt: + params["prompt"] = prompt + + if self.task == "translate": + proc = self.client.audio.translations + else: + proc = self.client.audio.transcriptions + + # Process transcription/translation + transcript = proc.create(**params) + logger.debug( + f"OpenAI API processed accumulated {self.transcribed_seconds} seconds" + ) + + return transcript + + def use_vad(self): + self.use_vad_opt = True + + def set_translate_task(self): + self.task = "translate" + + +class HypothesisBuffer: + def __init__(self, logfile=sys.stderr): + self.commited_in_buffer = [] + self.buffer = [] + self.new = [] + + self.last_commited_time = 0 + self.last_commited_word = None + + self.logfile = logfile + + def insert(self, new, offset): + # compare self.commited_in_buffer and new. It inserts only the words in new that extend the commited_in_buffer, it means they are roughly behind last_commited_time and new in content + # the new tail is added to self.new + + new = [(a + offset, b + offset, t) for a, b, t in new] + self.new = [(a, b, t) for a, b, t in new if a > self.last_commited_time - 0.1] + + if len(self.new) >= 1: + a, b, t = self.new[0] + if abs(a - self.last_commited_time) < 1: + if self.commited_in_buffer: + # it's going to search for 1, 2, ..., 5 consecutive words (n-grams) that are identical in commited and new. If they are, they're dropped. + cn = len(self.commited_in_buffer) + nn = len(self.new) + for i in range(1, min(min(cn, nn), 5) + 1): # 5 is the maximum + c = " ".join( + [self.commited_in_buffer[-j][2] for j in range(1, i + 1)][ + ::-1 + ] + ) + tail = " ".join(self.new[j - 1][2] for j in range(1, i + 1)) + if c == tail: + words = [] + for j in range(i): + words.append(repr(self.new.pop(0))) + words_msg = " ".join(words) + logger.debug(f"removing last {i} words: {words_msg}") + break + + def flush(self): + # returns commited chunk = the longest common prefix of 2 last inserts. + + commit = [] + while self.new: + na, nb, nt = self.new[0] + + if len(self.buffer) == 0: + break + + if nt == self.buffer[0][2]: + commit.append((na, nb, nt)) + self.last_commited_word = nt + self.last_commited_time = nb + self.buffer.pop(0) + self.new.pop(0) + else: + break + self.buffer = self.new + self.new = [] + self.commited_in_buffer.extend(commit) + return commit + + def pop_commited(self, time): + while self.commited_in_buffer and self.commited_in_buffer[0][1] <= time: + self.commited_in_buffer.pop(0) + + def complete(self): + return self.buffer + + +class OnlineASRProcessor: + SAMPLING_RATE = 16000 + + def __init__( + self, asr, tokenizer=None, buffer_trimming=("segment", 15), logfile=sys.stderr + ): + """asr: WhisperASR object + tokenizer: sentence tokenizer object for the target language. Must have a method *split* that behaves like the one of MosesTokenizer. It can be None, if "segment" buffer trimming option is used, then tokenizer is not used at all. + ("segment", 15) + buffer_trimming: a pair of (option, seconds), where option is either "sentence" or "segment", and seconds is a number. Buffer is trimmed if it is longer than "seconds" threshold. Default is the most recommended option. + logfile: where to store the log. + """ + self.asr = asr + self.tokenizer = tokenizer + self.logfile = logfile + + self.init() + + self.buffer_trimming_way, self.buffer_trimming_sec = buffer_trimming + + def init(self, offset=None): + """run this when starting or restarting processing""" + self.audio_buffer = np.array([], dtype=np.float32) + self.transcript_buffer = HypothesisBuffer(logfile=self.logfile) + self.buffer_time_offset = 0 + if offset is not None: + self.buffer_time_offset = offset + self.transcript_buffer.last_commited_time = self.buffer_time_offset + self.commited = [] + + def insert_audio_chunk(self, audio): + self.audio_buffer = np.append(self.audio_buffer, audio) + + def prompt(self): + """Returns a tuple: (prompt, context), where "prompt" is a 200-character suffix of commited text that is inside of the scrolled away part of audio buffer. + "context" is the commited text that is inside the audio buffer. It is transcribed again and skipped. It is returned only for debugging and logging reasons. + """ + k = max(0, len(self.commited) - 1) + while k > 0 and self.commited[k - 1][1] > self.buffer_time_offset: + k -= 1 + + p = self.commited[:k] + p = [t for _, _, t in p] + prompt = [] + y = 0 + while p and y < 200: # 200 characters prompt size + x = p.pop(-1) + y += len(x) + 1 + prompt.append(x) + non_prompt = self.commited[k:] + return self.asr.sep.join(prompt[::-1]), self.asr.sep.join( + t for _, _, t in non_prompt + ) + + def process_iter(self): + """Runs on the current audio buffer. + Returns: a tuple (beg_timestamp, end_timestamp, "text"), or (None, None, ""). + The non-emty text is confirmed (committed) partial transcript. + """ + + prompt, non_prompt = self.prompt() + logger.debug(f"PROMPT: {prompt}") + logger.debug(f"CONTEXT: {non_prompt}") + logger.debug( + f"transcribing {len(self.audio_buffer) / self.SAMPLING_RATE:2.2f} seconds from {self.buffer_time_offset:2.2f}" + ) + res = self.asr.transcribe(self.audio_buffer, init_prompt=prompt) + + # transform to [(beg,end,"word1"), ...] + tsw = self.asr.ts_words(res) + + self.transcript_buffer.insert(tsw, self.buffer_time_offset) + o = self.transcript_buffer.flush() + self.commited.extend(o) + completed = self.to_flush(o) + logger.debug(f">>>>COMPLETE NOW: {completed}") + the_rest = self.to_flush(self.transcript_buffer.complete()) + logger.debug(f"INCOMPLETE: {the_rest}") + + # there is a newly confirmed text + + if o and self.buffer_trimming_way == "sentence": # trim the completed sentences + if ( + len(self.audio_buffer) / self.SAMPLING_RATE > self.buffer_trimming_sec + ): # longer than this + self.chunk_completed_sentence() + + if self.buffer_trimming_way == "segment": + s = self.buffer_trimming_sec # trim the completed segments longer than s, + else: + s = 30 # if the audio buffer is longer than 30s, trim it + + if len(self.audio_buffer) / self.SAMPLING_RATE > s: + self.chunk_completed_segment(res) + + # alternative: on any word + # l = self.buffer_time_offset + len(self.audio_buffer)/self.SAMPLING_RATE - 10 + # let's find commited word that is less + # k = len(self.commited)-1 + # while k>0 and self.commited[k][1] > l: + # k -= 1 + # t = self.commited[k][1] + logger.debug("chunking segment") + # self.chunk_at(t) + + logger.debug( + f"len of buffer now: {len(self.audio_buffer) / self.SAMPLING_RATE:2.2f}" + ) + return self.to_flush(o) + + def chunk_completed_sentence(self): + if self.commited == []: + return + logger.debug(self.commited) + sents = self.words_to_sentences(self.commited) + for s in sents: + logger.debug(f"\t\tSENT: {s}") + if len(sents) < 2: + return + while len(sents) > 2: + sents.pop(0) + # we will continue with audio processing at this timestamp + chunk_at = sents[-2][1] + + logger.debug(f"--- sentence chunked at {chunk_at:2.2f}") + self.chunk_at(chunk_at) + + def chunk_completed_segment(self, res): + if self.commited == []: + return + + ends = self.asr.segments_end_ts(res) + + t = self.commited[-1][1] + + if len(ends) > 1: + e = ends[-2] + self.buffer_time_offset + while len(ends) > 2 and e > t: + ends.pop(-1) + e = ends[-2] + self.buffer_time_offset + if e <= t: + logger.debug(f"--- segment chunked at {e:2.2f}") + self.chunk_at(e) + else: + logger.debug("--- last segment not within commited area") + else: + logger.debug("--- not enough segments to chunk") + + def chunk_at(self, time): + """trims the hypothesis and audio buffer at "time" """ + self.transcript_buffer.pop_commited(time) + cut_seconds = time - self.buffer_time_offset + self.audio_buffer = self.audio_buffer[int(cut_seconds * self.SAMPLING_RATE) :] + self.buffer_time_offset = time + + def words_to_sentences(self, words): + """Uses self.tokenizer for sentence segmentation of words. + Returns: [(beg,end,"sentence 1"),...] + """ + + cwords = [w for w in words] + t = " ".join(o[2] for o in cwords) + s = self.tokenizer.split(t) + out = [] + while s: + beg = None + end = None + sent = s.pop(0).strip() + fsent = sent + while cwords: + b, e, w = cwords.pop(0) + w = w.strip() + if beg is None and sent.startswith(w): + beg = b + elif end is None and sent == w: + end = e + out.append((beg, end, fsent)) + break + sent = sent[len(w) :].strip() + return out + + def finish(self): + """Flush the incomplete text when the whole processing ends. + Returns: the same format as self.process_iter() + """ + o = self.transcript_buffer.complete() + f = self.to_flush(o) + logger.debug(f"last, noncommited: {f}") + self.buffer_time_offset += len(self.audio_buffer) / 16000 + return f + + def to_flush( + self, + sents, + sep=None, + offset=0, + ): + # concatenates the timestamped words or sentences into one sequence that is flushed in one line + # sents: [(beg1, end1, "sentence1"), ...] or [] if empty + # return: (beg1,end-of-last-sentence,"concatenation of sentences") or (None, None, "") if empty + if sep is None: + sep = self.asr.sep + t = sep.join(s[2] for s in sents) + if len(sents) == 0: + b = None + e = None + else: + b = offset + sents[0][0] + e = offset + sents[-1][1] + return (b, e, t) + + +class VACOnlineASRProcessor(OnlineASRProcessor): + """Wraps OnlineASRProcessor with VAC (Voice Activity Controller). + + It works the same way as OnlineASRProcessor: it receives chunks of audio (e.g. 0.04 seconds), + it runs VAD and continuously detects whether there is speech or not. + When it detects end of speech (non-voice for 500ms), it makes OnlineASRProcessor to end the utterance immediately. + """ + + def __init__(self, online_chunk_size, *a, **kw): + self.online_chunk_size = online_chunk_size + + self.online = OnlineASRProcessor(*a, **kw) + + # VAC: + import torch + + model, _ = torch.hub.load(repo_or_dir="snakers4/silero-vad", model="silero_vad") + from silero_vad_iterator import FixedVADIterator + + self.vac = FixedVADIterator( + model + ) # we use the default options there: 500ms silence, 100ms padding, etc. + + self.logfile = self.online.logfile + self.init() + + def init(self): + self.online.init() + self.vac.reset_states() + self.current_online_chunk_buffer_size = 0 + + self.is_currently_final = False + + self.status = None # or "voice" or "nonvoice" + self.audio_buffer = np.array([], dtype=np.float32) + self.buffer_offset = 0 # in frames + + def clear_buffer(self): + self.buffer_offset += len(self.audio_buffer) + self.audio_buffer = np.array([], dtype=np.float32) + + def insert_audio_chunk(self, audio): + res = self.vac(audio) + self.audio_buffer = np.append(self.audio_buffer, audio) + + if res is not None: + frame = list(res.values())[0] - self.buffer_offset + if "start" in res and "end" not in res: + self.status = "voice" + send_audio = self.audio_buffer[frame:] + self.online.init( + offset=(frame + self.buffer_offset) / self.SAMPLING_RATE + ) + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.clear_buffer() + elif "end" in res and "start" not in res: + self.status = "nonvoice" + send_audio = self.audio_buffer[:frame] + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.is_currently_final = True + self.clear_buffer() + else: + beg = res["start"] - self.buffer_offset + end = res["end"] - self.buffer_offset + self.status = "nonvoice" + send_audio = self.audio_buffer[beg:end] + self.online.init(offset=(beg + self.buffer_offset) / self.SAMPLING_RATE) + self.online.insert_audio_chunk(send_audio) + self.current_online_chunk_buffer_size += len(send_audio) + self.is_currently_final = True + self.clear_buffer() + else: + if self.status == "voice": + self.online.insert_audio_chunk(self.audio_buffer) + self.current_online_chunk_buffer_size += len(self.audio_buffer) + self.clear_buffer() + else: + # We keep 1 second because VAD may later find start of voice in it. + # But we trim it to prevent OOM. + self.buffer_offset += max( + 0, len(self.audio_buffer) - self.SAMPLING_RATE + ) + self.audio_buffer = self.audio_buffer[-self.SAMPLING_RATE :] + + def process_iter(self): + if self.is_currently_final: + return self.finish() + elif ( + self.current_online_chunk_buffer_size + > self.SAMPLING_RATE * self.online_chunk_size + ): + self.current_online_chunk_buffer_size = 0 + ret = self.online.process_iter() + return ret + else: + print("no online update, only VAD", self.status, file=self.logfile) + return (None, None, "") + + def finish(self): + ret = self.online.finish() + self.current_online_chunk_buffer_size = 0 + self.is_currently_final = False + return ret + + +WHISPER_LANG_CODES = "af,am,ar,as,az,ba,be,bg,bn,bo,br,bs,ca,cs,cy,da,de,el,en,es,et,eu,fa,fi,fo,fr,gl,gu,ha,haw,he,hi,hr,ht,hu,hy,id,is,it,ja,jw,ka,kk,km,kn,ko,la,lb,ln,lo,lt,lv,mg,mi,mk,ml,mn,mr,ms,mt,my,ne,nl,nn,no,oc,pa,pl,ps,pt,ro,ru,sa,sd,si,sk,sl,sn,so,sq,sr,su,sv,sw,ta,te,tg,th,tk,tl,tr,tt,uk,ur,uz,vi,yi,yo,zh".split( + "," +) + + +def create_tokenizer(lan): + """returns an object that has split function that works like the one of MosesTokenizer""" + + assert lan in WHISPER_LANG_CODES, ( + "language must be Whisper's supported lang code: " + + " ".join(WHISPER_LANG_CODES) + ) + + if lan == "uk": + import tokenize_uk + + class UkrainianTokenizer: + def split(self, text): + return tokenize_uk.tokenize_sents(text) + + return UkrainianTokenizer() + + # supported by fast-mosestokenizer + if ( + lan + in "as bn ca cs de el en es et fi fr ga gu hi hu is it kn lt lv ml mni mr nl or pa pl pt ro ru sk sl sv ta te yue zh".split() + ): + from mosestokenizer import MosesTokenizer + + return MosesTokenizer(lan) + + # the following languages are in Whisper, but not in wtpsplit: + if ( + lan + in "as ba bo br bs fo haw hr ht jw lb ln lo mi nn oc sa sd sn so su sw tk tl tt".split() + ): + logger.debug( + f"{lan} code is not supported by wtpsplit. Going to use None lang_code option." + ) + lan = None + + from wtpsplit import WtP + + # downloads the model from huggingface on the first use + wtp = WtP("wtp-canine-s-12l-no-adapters") + + class WtPtok: + def split(self, sent): + return wtp.split(sent, lang_code=lan) + + return WtPtok() + + +def add_shared_args(parser): + """shared args for simulation (this entry point) and server + parser: argparse.ArgumentParser object + """ + parser.add_argument( + "--min-chunk-size", + type=float, + default=1.0, + help="Minimum audio chunk size in seconds. It waits up to this time to do processing. If the processing takes shorter time, it waits, otherwise it processes the whole segment that was received by this time.", + ) + parser.add_argument( + "--model", + type=str, + default="large-v2", + choices="tiny.en,tiny,base.en,base,small.en,small,medium.en,medium,large-v1,large-v2,large-v3,large,large-v3-turbo".split( + "," + ), + help="Name size of the Whisper model to use (default: large-v2). The model is automatically downloaded from the model hub if not present in model cache dir.", + ) + parser.add_argument( + "--model_cache_dir", + type=str, + default=None, + help="Overriding the default model cache dir where models downloaded from the hub are saved", + ) + parser.add_argument( + "--model_dir", + type=str, + default=None, + help="Dir where Whisper model.bin and other files are saved. This option overrides --model and --model_cache_dir parameter.", + ) + parser.add_argument( + "--lan", + "--language", + type=str, + default="auto", + help="Source language code, e.g. en,de,cs, or 'auto' for language detection.", + ) + parser.add_argument( + "--task", + type=str, + default="transcribe", + choices=["transcribe", "translate"], + help="Transcribe or translate.", + ) + parser.add_argument( + "--backend", + type=str, + default="faster-whisper", + choices=["faster-whisper", "whisper_timestamped", "mlx-whisper", "openai-api"], + help="Load only this backend for Whisper processing.", + ) + parser.add_argument( + "--vac", + action="store_true", + default=False, + help="Use VAC = voice activity controller. Recommended. Requires torch.", + ) + parser.add_argument( + "--vac-chunk-size", type=float, default=0.04, help="VAC sample size in seconds." + ) + parser.add_argument( + "--vad", + action="store_true", + default=False, + help="Use VAD = voice activity detection, with the default parameters.", + ) + parser.add_argument( + "--buffer_trimming", + type=str, + default="segment", + choices=["sentence", "segment"], + help='Buffer trimming strategy -- trim completed sentences marked with punctuation mark and detected by sentence segmenter, or the completed segments returned by Whisper. Sentence segmenter must be installed for "sentence" option.', + ) + parser.add_argument( + "--buffer_trimming_sec", + type=float, + default=15, + help="Buffer trimming length threshold in seconds. If buffer length is longer, trimming sentence/segment is triggered.", + ) + parser.add_argument( + "-l", + "--log-level", + dest="log_level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set the log level", + default="DEBUG", + ) + + +def asr_factory(args, logfile=sys.stderr): + """ + Creates and configures an ASR and ASR Online instance based on the specified backend and arguments. + """ + backend = args.backend + if backend == "openai-api": + logger.debug("Using OpenAI API.") + asr = OpenaiApiASR(lan=args.lan) + else: + if backend == "faster-whisper": + asr_cls = FasterWhisperASR + elif backend == "mlx-whisper": + asr_cls = MLXWhisper + else: + asr_cls = WhisperTimestampedASR + + # Only for FasterWhisperASR and WhisperTimestampedASR + size = args.model + t = time.time() + logger.info(f"Loading Whisper {size} model for {args.lan}...") + asr = asr_cls( + modelsize=size, + lan=args.lan, + cache_dir=args.model_cache_dir, + model_dir=args.model_dir, + ) + e = time.time() + logger.info(f"done. It took {round(e - t, 2)} seconds.") + + # Apply common configurations + if getattr(args, "vad", False): # Checks if VAD argument is present and True + logger.info("Setting VAD filter") + asr.use_vad() + + language = args.lan + if args.task == "translate": + asr.set_translate_task() + tgt_language = "en" # Whisper translates into English + else: + tgt_language = language # Whisper transcribes in this language + + # Create the tokenizer + if args.buffer_trimming == "sentence": + tokenizer = create_tokenizer(tgt_language) + else: + tokenizer = None + + # Create the OnlineASRProcessor + if args.vac: + online = VACOnlineASRProcessor( + args.min_chunk_size, + asr, + tokenizer, + logfile=logfile, + buffer_trimming=(args.buffer_trimming, args.buffer_trimming_sec), + ) + else: + online = OnlineASRProcessor( + asr, + tokenizer, + logfile=logfile, + buffer_trimming=(args.buffer_trimming, args.buffer_trimming_sec), + ) + + return asr, online + + +def set_logging(args, logger, other="_server"): + logging.basicConfig( # format='%(name)s + format="%(levelname)s\t%(message)s" + ) + logger.setLevel(args.log_level) + logging.getLogger("whisper_online" + other).setLevel(args.log_level) + + +# logging.getLogger("whisper_online_server").setLevel(args.log_level) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "audio_path", + type=str, + help="Filename of 16kHz mono channel wav, on which live streaming is simulated.", + ) + add_shared_args(parser) + parser.add_argument( + "--start_at", + type=float, + default=0.0, + help="Start processing audio at this time.", + ) + parser.add_argument( + "--offline", action="store_true", default=False, help="Offline mode." + ) + parser.add_argument( + "--comp_unaware", + action="store_true", + default=False, + help="Computationally unaware simulation.", + ) + + args = parser.parse_args() + + # reset to store stderr to different file stream, e.g. open(os.devnull,"w") + logfile = sys.stderr + + if args.offline and args.comp_unaware: + logger.error( + "No or one option from --offline and --comp_unaware are available, not both. Exiting." + ) + sys.exit(1) + + # if args.log_level: + # logging.basicConfig(format='whisper-%(levelname)s:%(name)s: %(message)s', + # level=getattr(logging, args.log_level)) + + set_logging(args, logger) + + audio_path = args.audio_path + + SAMPLING_RATE = 16000 + duration = len(load_audio(audio_path)) / SAMPLING_RATE + logger.info("Audio duration is: %2.2f seconds" % duration) + + asr, online = asr_factory(args, logfile=logfile) + if args.vac: + min_chunk = args.vac_chunk_size + else: + min_chunk = args.min_chunk_size + + # load the audio into the LRU cache before we start the timer + a = load_audio_chunk(audio_path, 0, 1) + + # warm up the ASR because the very first transcribe takes much more time than the other + asr.transcribe(a) + + beg = args.start_at + start = time.time() - beg + + def output_transcript(o, now=None): + # output format in stdout is like: + # 4186.3606 0 1720 Takhle to je + # - the first three words are: + # - emission time from beginning of processing, in milliseconds + # - beg and end timestamp of the text segment, as estimated by Whisper model. The timestamps are not accurate, but they're useful anyway + # - the next words: segment transcript + if now is None: + now = time.time() - start + if o[0] is not None: + print( + "%1.4f %1.0f %1.0f %s" % (now * 1000, o[0] * 1000, o[1] * 1000, o[2]), + file=logfile, + flush=True, + ) + print( + "%1.4f %1.0f %1.0f %s" % (now * 1000, o[0] * 1000, o[1] * 1000, o[2]), + flush=True, + ) + else: + # No text, so no output + pass + + if args.offline: ## offline mode processing (for testing/debugging) + a = load_audio(audio_path) + online.insert_audio_chunk(a) + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {repr(e)}") + else: + output_transcript(o) + now = None + elif args.comp_unaware: # computational unaware mode + end = beg + min_chunk + while True: + a = load_audio_chunk(audio_path, beg, end) + online.insert_audio_chunk(a) + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {repr(e)}") + pass + else: + output_transcript(o, now=end) + + logger.debug(f"## last processed {end:.2f}s") + + if end >= duration: + break + + beg = end + + if end + min_chunk > duration: + end = duration + else: + end += min_chunk + now = duration + + else: # online = simultaneous mode + end = 0 + while True: + now = time.time() - start + if now < end + min_chunk: + time.sleep(min_chunk + end - now) + end = time.time() - start + a = load_audio_chunk(audio_path, beg, end) + beg = end + online.insert_audio_chunk(a) + + try: + o = online.process_iter() + except AssertionError as e: + logger.error(f"assertion error: {e}") + pass + else: + output_transcript(o) + now = time.time() - start + logger.debug( + f"## last processed {end:.2f} s, now is {now:.2f}, the latency is {now - end:.2f}" + ) + + if end >= duration: + break + now = None + + o = online.finish() + output_transcript(o, now=now) diff --git a/sam2-cpu/frigate-dev/frigate/data_processing/types.py b/sam2-cpu/frigate-dev/frigate/data_processing/types.py new file mode 100644 index 0000000..263a8b9 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/data_processing/types.py @@ -0,0 +1,67 @@ +"""Embeddings types.""" + +from enum import Enum +from multiprocessing.managers import SyncManager +from multiprocessing.sharedctypes import Synchronized + +import sherpa_onnx + +from frigate.data_processing.real_time.whisper_online import FasterWhisperASR + + +class DataProcessorMetrics: + image_embeddings_speed: Synchronized + image_embeddings_eps: Synchronized + text_embeddings_speed: Synchronized + text_embeddings_eps: Synchronized + face_rec_speed: Synchronized + face_rec_fps: Synchronized + alpr_speed: Synchronized + alpr_pps: Synchronized + yolov9_lpr_speed: Synchronized + yolov9_lpr_pps: Synchronized + review_desc_speed: Synchronized + review_desc_dps: Synchronized + object_desc_speed: Synchronized + object_desc_dps: Synchronized + classification_speeds: dict[str, Synchronized] + classification_cps: dict[str, Synchronized] + + def __init__(self, manager: SyncManager, custom_classification_models: list[str]): + self.image_embeddings_speed = manager.Value("d", 0.0) + self.image_embeddings_eps = manager.Value("d", 0.0) + self.text_embeddings_speed = manager.Value("d", 0.0) + self.text_embeddings_eps = manager.Value("d", 0.0) + self.face_rec_speed = manager.Value("d", 0.0) + self.face_rec_fps = manager.Value("d", 0.0) + self.alpr_speed = manager.Value("d", 0.0) + self.alpr_pps = manager.Value("d", 0.0) + self.yolov9_lpr_speed = manager.Value("d", 0.0) + self.yolov9_lpr_pps = manager.Value("d", 0.0) + self.review_desc_speed = manager.Value("d", 0.0) + self.review_desc_dps = manager.Value("d", 0.0) + self.object_desc_speed = manager.Value("d", 0.0) + self.object_desc_dps = manager.Value("d", 0.0) + self.classification_speeds = manager.dict() + self.classification_cps = manager.dict() + + if custom_classification_models: + for key in custom_classification_models: + self.classification_speeds[key] = manager.Value("d", 0.0) + self.classification_cps[key] = manager.Value("d", 0.0) + + +class DataProcessorModelRunner: + def __init__(self, requestor, device: str = "CPU", model_size: str = "large"): + self.requestor = requestor + self.device = device + self.model_size = model_size + + +class PostProcessDataEnum(str, Enum): + recording = "recording" + review = "review" + tracked_object = "tracked_object" + + +AudioTranscriptionModel = FasterWhisperASR | sherpa_onnx.OnlineRecognizer | None diff --git a/sam2-cpu/frigate-dev/frigate/db/sqlitevecq.py b/sam2-cpu/frigate-dev/frigate/db/sqlitevecq.py new file mode 100644 index 0000000..aa4928e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/db/sqlitevecq.py @@ -0,0 +1,69 @@ +import re +import sqlite3 + +from playhouse.sqliteq import SqliteQueueDatabase + + +class SqliteVecQueueDatabase(SqliteQueueDatabase): + def __init__(self, *args, load_vec_extension: bool = False, **kwargs) -> None: + self.load_vec_extension: bool = load_vec_extension + # no extension necessary, sqlite will load correctly for each platform + self.sqlite_vec_path = "/usr/local/lib/vec0" + super().__init__(*args, **kwargs) + + def _connect(self, *args, **kwargs) -> sqlite3.Connection: + conn: sqlite3.Connection = super()._connect(*args, **kwargs) + if self.load_vec_extension: + self._load_vec_extension(conn) + + # register REGEXP support + self._register_regexp(conn) + + return conn + + def _load_vec_extension(self, conn: sqlite3.Connection) -> None: + conn.enable_load_extension(True) + conn.load_extension(self.sqlite_vec_path) + conn.enable_load_extension(False) + + def _register_regexp(self, conn: sqlite3.Connection) -> None: + def regexp(expr: str, item: str) -> bool: + if item is None: + return False + try: + return re.search(expr, item) is not None + except re.error: + return False + + conn.create_function("REGEXP", 2, regexp) + + def delete_embeddings_thumbnail(self, event_ids: list[str]) -> None: + ids = ",".join(["?" for _ in event_ids]) + self.execute_sql(f"DELETE FROM vec_thumbnails WHERE id IN ({ids})", event_ids) + + def delete_embeddings_description(self, event_ids: list[str]) -> None: + ids = ",".join(["?" for _ in event_ids]) + self.execute_sql(f"DELETE FROM vec_descriptions WHERE id IN ({ids})", event_ids) + + def drop_embeddings_tables(self) -> None: + self.execute_sql(""" + DROP TABLE vec_descriptions; + """) + self.execute_sql(""" + DROP TABLE vec_thumbnails; + """) + + def create_embeddings_tables(self) -> None: + """Create vec0 virtual table for embeddings""" + self.execute_sql(""" + CREATE VIRTUAL TABLE IF NOT EXISTS vec_thumbnails USING vec0( + id TEXT PRIMARY KEY, + thumbnail_embedding FLOAT[768] distance_metric=cosine + ); + """) + self.execute_sql(""" + CREATE VIRTUAL TABLE IF NOT EXISTS vec_descriptions USING vec0( + id TEXT PRIMARY KEY, + description_embedding FLOAT[768] distance_metric=cosine + ); + """) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/__init__.py b/sam2-cpu/frigate-dev/frigate/detectors/__init__.py new file mode 100644 index 0000000..7465ed7 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/__init__.py @@ -0,0 +1,18 @@ +import logging + +from .detector_config import InputTensorEnum, ModelConfig, PixelFormatEnum # noqa: F401 +from .detector_types import DetectorConfig, DetectorTypeEnum, api_types # noqa: F401 + +logger = logging.getLogger(__name__) + + +def create_detector(detector_config): + if detector_config.type == DetectorTypeEnum.cpu: + logger.warning( + "CPU detectors are not recommended and should only be used for testing or for trial purposes." + ) + + api = api_types.get(detector_config.type) + if not api: + raise ValueError(detector_config.type) + return api(detector_config) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/detection_api.py b/sam2-cpu/frigate-dev/frigate/detectors/detection_api.py new file mode 100644 index 0000000..4f03f28 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/detection_api.py @@ -0,0 +1,57 @@ +import logging +from abc import ABC, abstractmethod +from typing import List + +import numpy as np + +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum + +logger = logging.getLogger(__name__) + + +class DetectionApi(ABC): + type_key: str + supported_models: List[ModelTypeEnum] + + @abstractmethod + def __init__(self, detector_config: BaseDetectorConfig): + self.detector_config = detector_config + self.thresh = 0.4 + self.height = detector_config.model.height + self.width = detector_config.model.width + + @abstractmethod + def detect_raw(self, tensor_input): + pass + + def calculate_grids_strides(self, expanded=True) -> None: + grids = [] + expanded_strides = [] + + # decode and orient predictions + strides = [8, 16, 32] + hsizes = [self.height // stride for stride in strides] + wsizes = [self.width // stride for stride in strides] + + for hsize, wsize, stride in zip(hsizes, wsizes, strides): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + + if expanded: + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + else: + xv = xv.reshape(1, 1, hsize, wsize) + yv = yv.reshape(1, 1, hsize, wsize) + grids.extend(np.concatenate((xv, yv), axis=1).tolist()) + expanded_strides.extend( + np.array([stride, stride]).reshape(1, 2, 1, 1).tolist() + ) + + if expanded: + self.grids = np.concatenate(grids, 1) + self.expanded_strides = np.concatenate(expanded_strides, 1) + else: + self.grids = grids + self.expanded_strides = expanded_strides diff --git a/sam2-cpu/frigate-dev/frigate/detectors/detection_runners.py b/sam2-cpu/frigate-dev/frigate/detectors/detection_runners.py new file mode 100644 index 0000000..56b49ec --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/detection_runners.py @@ -0,0 +1,580 @@ +"""Base runner implementation for ONNX models.""" + +import logging +import os +import platform +import threading +from abc import ABC, abstractmethod +from typing import Any + +import numpy as np +import onnxruntime as ort + +from frigate.util.model import get_ort_providers +from frigate.util.rknn_converter import auto_convert_model, is_rknn_compatible + +logger = logging.getLogger(__name__) + + +def is_arm64_platform() -> bool: + """Check if we're running on an ARM platform.""" + machine = platform.machine().lower() + return machine in ("aarch64", "arm64", "armv8", "armv7l") + + +def get_ort_session_options( + is_complex_model: bool = False, +) -> ort.SessionOptions | None: + """Get ONNX Runtime session options with appropriate settings. + + Args: + is_complex_model: Whether the model needs basic optimization to avoid graph fusion issues. + + Returns: + SessionOptions with appropriate optimization level, or None for default settings. + """ + if is_complex_model: + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ( + ort.GraphOptimizationLevel.ORT_ENABLE_BASIC + ) + return sess_options + + return None + + +# Import OpenVINO only when needed to avoid circular dependencies +try: + import openvino as ov +except ImportError: + ov = None + + +def get_openvino_available_devices() -> list[str]: + """Get available OpenVINO devices without using ONNX Runtime. + + Returns: + List of available OpenVINO device names (e.g., ['CPU', 'GPU', 'MYRIAD']) + """ + if ov is None: + logger.debug("OpenVINO is not available") + return [] + + try: + core = ov.Core() + available_devices = core.available_devices + logger.debug(f"OpenVINO available devices: {available_devices}") + return available_devices + except Exception as e: + logger.warning(f"Failed to get OpenVINO available devices: {e}") + return [] + + +def is_openvino_gpu_npu_available() -> bool: + """Check if OpenVINO GPU or NPU devices are available. + + Returns: + True if GPU or NPU devices are available, False otherwise + """ + available_devices = get_openvino_available_devices() + # Check for GPU, NPU, or other acceleration devices (excluding CPU) + acceleration_devices = ["GPU", "MYRIAD", "NPU", "GNA", "HDDL"] + return any(device in available_devices for device in acceleration_devices) + + +class BaseModelRunner(ABC): + """Abstract base class for model runners.""" + + def __init__(self, model_path: str, device: str, **kwargs): + self.model_path = model_path + self.device = device + + @abstractmethod + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + pass + + @abstractmethod + def get_input_width(self) -> int: + """Get the input width of the model.""" + pass + + @abstractmethod + def run(self, input: dict[str, Any]) -> Any | None: + """Run inference with the model.""" + pass + + +class ONNXModelRunner(BaseModelRunner): + """Run ONNX models using ONNX Runtime.""" + + @staticmethod + def is_cpu_complex_model(model_type: str) -> bool: + """Check if model needs basic optimization level to avoid graph fusion issues. + + Some models (like Jina-CLIP) have issues with aggressive optimizations like + SimplifiedLayerNormFusion that create or expect nodes that don't exist. + """ + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + ] + + @staticmethod + def is_migraphx_complex_model(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.detectors.detector_config import ModelTypeEnum + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.yolov9_license_plate.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.facenet.value, + ModelTypeEnum.rfdetr.value, + ModelTypeEnum.dfine.value, + ] + + def __init__(self, ort: ort.InferenceSession): + self.ort = ort + + def get_input_names(self) -> list[str]: + return [input.name for input in self.ort.get_inputs()] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + return self.ort.get_inputs()[0].shape[3] + + def run(self, input: dict[str, Any]) -> Any | None: + return self.ort.run(None, input) + + +class CudaGraphRunner(BaseModelRunner): + """Encapsulates CUDA Graph capture and replay using ONNX Runtime IOBinding. + + This runner assumes a single tensor input and binds all model outputs. + + NOTE: CUDA Graphs limit supported model operations, so they are not usable + for more complex models like CLIP or PaddleOCR. + """ + + @staticmethod + def is_model_supported(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.detectors.detector_config import ModelTypeEnum + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type not in [ + ModelTypeEnum.yolonas.value, + ModelTypeEnum.dfine.value, + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.yolov9_license_plate.value, + ] + + def __init__(self, session: ort.InferenceSession, cuda_device_id: int): + self._session = session + self._cuda_device_id = cuda_device_id + self._captured = False + self._io_binding: ort.IOBinding | None = None + self._input_name: str | None = None + self._output_names: list[str] | None = None + self._input_ortvalue: ort.OrtValue | None = None + self._output_ortvalues: ort.OrtValue | None = None + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + return [input.name for input in self._session.get_inputs()] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + return self._session.get_inputs()[0].shape[3] + + def run(self, input: dict[str, Any]): + # Extract the single tensor input (assuming one input) + input_name = list(input.keys())[0] + tensor_input = input[input_name] + tensor_input = np.ascontiguousarray(tensor_input) + + if not self._captured: + # Prepare IOBinding with CUDA buffers and let ORT allocate outputs on device + self._io_binding = self._session.io_binding() + self._input_name = input_name + self._output_names = [o.name for o in self._session.get_outputs()] + + self._input_ortvalue = ort.OrtValue.ortvalue_from_numpy( + tensor_input, "cuda", self._cuda_device_id + ) + self._io_binding.bind_ortvalue_input(self._input_name, self._input_ortvalue) + + for name in self._output_names: + # Bind outputs to CUDA and allow ORT to allocate appropriately + self._io_binding.bind_output(name, "cuda", self._cuda_device_id) + + # First IOBinding run to allocate, execute, and capture CUDA Graph + ro = ort.RunOptions() + self._session.run_with_iobinding(self._io_binding, ro) + self._captured = True + return self._io_binding.copy_outputs_to_cpu() + + # Replay using updated input, copy results to CPU + self._input_ortvalue.update_inplace(tensor_input) + ro = ort.RunOptions() + self._session.run_with_iobinding(self._io_binding, ro) + return self._io_binding.copy_outputs_to_cpu() + + +class OpenVINOModelRunner(BaseModelRunner): + """OpenVINO model runner that handles inference efficiently.""" + + @staticmethod + def is_complex_model(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v2.value, + ] + + @staticmethod + def is_model_npu_supported(model_type: str) -> bool: + # Import here to avoid circular imports + from frigate.embeddings.types import EnrichmentModelTypeEnum + + return model_type not in [ + EnrichmentModelTypeEnum.paddleocr.value, + EnrichmentModelTypeEnum.jina_v1.value, + EnrichmentModelTypeEnum.jina_v2.value, + EnrichmentModelTypeEnum.arcface.value, + ] + + def __init__(self, model_path: str, device: str, model_type: str, **kwargs): + self.model_path = model_path + self.device = device + self.model_type = model_type + + if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported( + model_type + ): + logger.warning( + f"OpenVINO model {model_type} is not supported on NPU, using GPU instead" + ) + device = "GPU" + + self.complex_model = OpenVINOModelRunner.is_complex_model(model_type) + + if not os.path.isfile(model_path): + raise FileNotFoundError(f"OpenVINO model file {model_path} not found.") + + if ov is None: + raise ImportError( + "OpenVINO is not available. Please install openvino package." + ) + + self.ov_core = ov.Core() + + # Apply performance optimization + self.ov_core.set_property(device, {"PERF_COUNT": "NO"}) + + if device in ["GPU", "AUTO"]: + self.ov_core.set_property(device, {"PERFORMANCE_HINT": "LATENCY"}) + + # Compile model + self.compiled_model = self.ov_core.compile_model( + model=model_path, device_name=device + ) + + # Create reusable inference request + self.infer_request = self.compiled_model.create_infer_request() + self.input_tensor: ov.Tensor | None = None + + # Thread lock to prevent concurrent inference (needed for JinaV2 which shares + # one runner between text and vision embeddings called from different threads) + self._inference_lock = threading.Lock() + + if not self.complex_model: + try: + input_shape = self.compiled_model.inputs[0].get_shape() + input_element_type = self.compiled_model.inputs[0].get_element_type() + self.input_tensor = ov.Tensor(input_element_type, input_shape) + except RuntimeError: + # model is complex and has dynamic shape + pass + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + return [input.get_any_name() for input in self.compiled_model.inputs] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + input_info = self.compiled_model.inputs + first_input = input_info[0] + + try: + partial_shape = first_input.get_partial_shape() + # width dimension + if len(partial_shape) >= 4 and partial_shape[3].is_static: + return partial_shape[3].get_length() + + # If width is dynamic or we can't determine it + return -1 + except Exception: + try: + # gemini says some ov versions might still allow this + input_shape = first_input.shape + return input_shape[3] if len(input_shape) >= 4 else -1 + except Exception: + return -1 + + def run(self, inputs: dict[str, Any]) -> list[np.ndarray]: + """Run inference with the model. + + Args: + inputs: Dictionary mapping input names to input data + + Returns: + List of output tensors + """ + # Lock prevents concurrent access to infer_request + # Needed for JinaV2: genai thread (text) + embeddings thread (vision) + with self._inference_lock: + from frigate.embeddings.types import EnrichmentModelTypeEnum + + if self.model_type in [EnrichmentModelTypeEnum.arcface.value]: + # For face recognition models, create a fresh infer_request + # for each inference to avoid state pollution that causes incorrect results. + self.infer_request = self.compiled_model.create_infer_request() + + # Handle single input case for backward compatibility + if ( + len(inputs) == 1 + and len(self.compiled_model.inputs) == 1 + and self.input_tensor is not None + ): + # Single input case - use the pre-allocated tensor for efficiency + input_data = list(inputs.values())[0] + np.copyto(self.input_tensor.data, input_data) + self.infer_request.infer(self.input_tensor) + else: + if self.complex_model: + try: + # This ensures the model starts with a clean state for each sequence + # Important for RNN models like PaddleOCR recognition + self.infer_request.reset_state() + except Exception: + # this will raise an exception for models with AUTO set as the device + pass + + # Multiple inputs case - set each input by name + for input_name, input_data in inputs.items(): + # Find the input by name and its index + input_port = None + input_index = None + for idx, port in enumerate(self.compiled_model.inputs): + if port.get_any_name() == input_name: + input_port = port + input_index = idx + break + + if input_port is None: + raise ValueError(f"Input '{input_name}' not found in model") + + # Create tensor with the correct element type + input_element_type = input_port.get_element_type() + + # Ensure input data matches the expected dtype to prevent type mismatches + # that can occur with models like Jina-CLIP v2 running on OpenVINO + expected_dtype = input_element_type.to_dtype() + if input_data.dtype != expected_dtype: + logger.debug( + f"Converting input '{input_name}' from {input_data.dtype} to {expected_dtype}" + ) + input_data = input_data.astype(expected_dtype) + + input_tensor = ov.Tensor(input_element_type, input_data.shape) + np.copyto(input_tensor.data, input_data) + + # Set the input tensor for the specific port index + self.infer_request.set_input_tensor(input_index, input_tensor) + + # Run inference + try: + self.infer_request.infer() + except Exception as e: + logger.error(f"Error during OpenVINO inference: {e}") + return [] + + # Get all output tensors + outputs = [] + for i in range(len(self.compiled_model.outputs)): + outputs.append(self.infer_request.get_output_tensor(i).data) + + return outputs + + +class RKNNModelRunner(BaseModelRunner): + """Run RKNN models for embeddings.""" + + def __init__(self, model_path: str, model_type: str = None, core_mask: int = 0): + self.model_path = model_path + self.model_type = model_type + self.core_mask = core_mask + self.rknn = None + self._load_model() + + def _load_model(self): + """Load the RKNN model.""" + try: + from rknnlite.api import RKNNLite + + self.rknn = RKNNLite(verbose=False) + + if self.rknn.load_rknn(self.model_path) != 0: + logger.error(f"Failed to load RKNN model: {self.model_path}") + raise RuntimeError("Failed to load RKNN model") + + if self.rknn.init_runtime(core_mask=self.core_mask) != 0: + logger.error("Failed to initialize RKNN runtime") + raise RuntimeError("Failed to initialize RKNN runtime") + + logger.info(f"Successfully loaded RKNN model: {self.model_path}") + + except ImportError: + logger.error("RKNN Lite not available") + raise ImportError("RKNN Lite not available") + except Exception as e: + logger.error(f"Error loading RKNN model: {e}") + raise + + def get_input_names(self) -> list[str]: + """Get input names for the model.""" + # For detection models, we typically use "input" as the default input name + # For CLIP models, we need to determine the model type from the path + model_name = os.path.basename(self.model_path).lower() + + if "vision" in model_name: + return ["pixel_values"] + elif "arcface" in model_name: + return ["data"] + else: + # Default fallback - try to infer from model type + if self.model_type and "jina-clip" in self.model_type: + if "vision" in self.model_type: + return ["pixel_values"] + + # Generic fallback + return ["input"] + + def get_input_width(self) -> int: + """Get the input width of the model.""" + # For CLIP vision models, this is typically 224 + model_name = os.path.basename(self.model_path).lower() + if "vision" in model_name: + return 224 # CLIP V1 uses 224x224 + elif "arcface" in model_name: + return 112 + # For detection models, we can't easily determine this from the RKNN model + # The calling code should provide this information + return -1 + + def run(self, inputs: dict[str, Any]) -> Any: + """Run inference with the RKNN model.""" + if not self.rknn: + raise RuntimeError("RKNN model not loaded") + + try: + input_names = self.get_input_names() + rknn_inputs = [] + + for name in input_names: + if name in inputs: + if name == "pixel_values": + # RKNN expects NHWC format, but ONNX typically provides NCHW + # Transpose from [batch, channels, height, width] to [batch, height, width, channels] + pixel_data = inputs[name] + if len(pixel_data.shape) == 4 and pixel_data.shape[1] == 3: + # Transpose from NCHW to NHWC + pixel_data = np.transpose(pixel_data, (0, 2, 3, 1)) + rknn_inputs.append(pixel_data) + else: + rknn_inputs.append(inputs[name]) + + outputs = self.rknn.inference(inputs=rknn_inputs) + return outputs + + except Exception as e: + logger.error(f"Error during RKNN inference: {e}") + raise + + def __del__(self): + """Cleanup when the runner is destroyed.""" + if self.rknn: + try: + self.rknn.release() + except Exception: + pass + + +def get_optimized_runner( + model_path: str, device: str | None, model_type: str, **kwargs +) -> BaseModelRunner: + """Get an optimized runner for the hardware.""" + device = device or "AUTO" + + if device != "CPU" and is_rknn_compatible(model_path): + rknn_path = auto_convert_model(model_path) + + if rknn_path: + return RKNNModelRunner(rknn_path) + + providers, options = get_ort_providers(device == "CPU", device, **kwargs) + + if providers[0] == "CPUExecutionProvider": + # In the default image, ONNXRuntime is used so we will only get CPUExecutionProvider + # In other images we will get CUDA / ROCm which are preferred over OpenVINO + # There is currently no way to prioritize OpenVINO over CUDA / ROCm in these images + if device != "CPU" and is_openvino_gpu_npu_available(): + return OpenVINOModelRunner(model_path, device, model_type, **kwargs) + + if ( + CudaGraphRunner.is_model_supported(model_type) + and providers[0] == "CUDAExecutionProvider" + ): + options[0] = { + **options[0], + "enable_cuda_graph": True, + } + return CudaGraphRunner( + ort.InferenceSession( + model_path, + providers=providers, + provider_options=options, + ), + options[0]["device_id"], + ) + + if ( + providers + and providers[0] == "MIGraphXExecutionProvider" + and ONNXModelRunner.is_migraphx_complex_model(model_type) + ): + # Don't use MIGraphX for models that are not supported + providers.pop(0) + options.pop(0) + + return ONNXModelRunner( + ort.InferenceSession( + model_path, + sess_options=get_ort_session_options( + ONNXModelRunner.is_cpu_complex_model(model_type) + ), + providers=providers, + provider_options=options, + ) + ) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/detector_config.py b/sam2-cpu/frigate-dev/frigate/detectors/detector_config.py new file mode 100644 index 0000000..aa92f28 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/detector_config.py @@ -0,0 +1,222 @@ +import hashlib +import json +import logging +import os +from enum import Enum +from typing import Any, Dict, Optional, Tuple + +import requests +from pydantic import BaseModel, ConfigDict, Field +from pydantic.fields import PrivateAttr + +from frigate.const import DEFAULT_ATTRIBUTE_LABEL_MAP, MODEL_CACHE_DIR +from frigate.plus import PlusApi +from frigate.util.builtin import generate_color_palette, load_labels + +logger = logging.getLogger(__name__) + + +class PixelFormatEnum(str, Enum): + rgb = "rgb" + bgr = "bgr" + yuv = "yuv" + + +class InputTensorEnum(str, Enum): + nchw = "nchw" + nhwc = "nhwc" + hwnc = "hwnc" + hwcn = "hwcn" + + +class InputDTypeEnum(str, Enum): + float = "float" + float_denorm = "float_denorm" # non-normalized float + int = "int" + + +class ModelTypeEnum(str, Enum): + dfine = "dfine" + rfdetr = "rfdetr" + ssd = "ssd" + yolox = "yolox" + yolonas = "yolonas" + yologeneric = "yolo-generic" + + +class ModelConfig(BaseModel): + path: Optional[str] = Field(None, title="Custom Object detection model path.") + labelmap_path: Optional[str] = Field( + None, title="Label map for custom object detector." + ) + width: int = Field(default=320, title="Object detection model input width.") + height: int = Field(default=320, title="Object detection model input height.") + labelmap: Dict[int, str] = Field( + default_factory=dict, title="Labelmap customization." + ) + attributes_map: Dict[str, list[str]] = Field( + default=DEFAULT_ATTRIBUTE_LABEL_MAP, + title="Map of object labels to their attribute labels.", + ) + input_tensor: InputTensorEnum = Field( + default=InputTensorEnum.nhwc, title="Model Input Tensor Shape" + ) + input_pixel_format: PixelFormatEnum = Field( + default=PixelFormatEnum.rgb, title="Model Input Pixel Color Format" + ) + input_dtype: InputDTypeEnum = Field( + default=InputDTypeEnum.int, title="Model Input D Type" + ) + model_type: ModelTypeEnum = Field( + default=ModelTypeEnum.ssd, title="Object Detection Model Type" + ) + _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr() + _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr() + _all_attributes: list[str] = PrivateAttr() + _all_attribute_logos: list[str] = PrivateAttr() + _model_hash: str = PrivateAttr() + + @property + def merged_labelmap(self) -> Dict[int, str]: + return self._merged_labelmap + + @property + def colormap(self) -> Dict[int, Tuple[int, int, int]]: + return self._colormap + + @property + def non_logo_attributes(self) -> list[str]: + return ["face", "license_plate"] + + @property + def all_attributes(self) -> list[str]: + return self._all_attributes + + @property + def all_attribute_logos(self) -> list[str]: + return self._all_attribute_logos + + @property + def model_hash(self) -> str: + return self._model_hash + + def __init__(self, **config): + super().__init__(**config) + + self._merged_labelmap = { + **load_labels(config.get("labelmap_path", "/labelmap.txt")), + **config.get("labelmap", {}), + } + self._colormap = {} + + # generate list of attribute labels + unique_attributes = set() + + for attributes in self.attributes_map.values(): + unique_attributes.update(attributes) + + self._all_attributes = list(unique_attributes) + self._all_attribute_logos = list( + unique_attributes - set(self.non_logo_attributes) + ) + + def check_and_load_plus_model( + self, plus_api: PlusApi, detector: str = None + ) -> None: + if not self.path or not self.path.startswith("plus://"): + return + + # ensure that model cache dir exists + os.makedirs(MODEL_CACHE_DIR, exist_ok=True) + + model_id = self.path[7:] + self.path = os.path.join(MODEL_CACHE_DIR, model_id) + model_info_path = f"{self.path}.json" + + # download the model if it doesn't exist + if not os.path.isfile(self.path): + download_url = plus_api.get_model_download_url(model_id) + r = requests.get(download_url) + with open(self.path, "wb") as f: + f.write(r.content) + + # download the model info if it doesn't exist + if not os.path.isfile(model_info_path): + model_info = plus_api.get_model_info(model_id) + with open(model_info_path, "w") as f: + json.dump(model_info, f) + else: + with open(model_info_path, "r") as f: + model_info: dict[str, Any] = json.load(f) + + if detector and detector not in model_info["supportedDetectors"]: + raise ValueError(f"Model does not support detector type of {detector}") + + self.width = model_info["width"] + self.height = model_info["height"] + self.input_tensor = InputTensorEnum(model_info["inputShape"]) + self.input_pixel_format = PixelFormatEnum(model_info["pixelFormat"]) + self.model_type = ModelTypeEnum(model_info["type"]) + + if model_info.get("inputDataType"): + self.input_dtype = InputDTypeEnum(model_info["inputDataType"]) + + # RKNN always uses NHWC + if detector == "rknn": + self.input_tensor = InputTensorEnum.nhwc + + # generate list of attribute labels + self.attributes_map = { + **model_info.get("attributes", DEFAULT_ATTRIBUTE_LABEL_MAP), + **self.attributes_map, + } + unique_attributes = set() + + for attributes in self.attributes_map.values(): + unique_attributes.update(attributes) + + self._all_attributes = list(unique_attributes) + self._all_attribute_logos = list( + unique_attributes - set(["face", "license_plate"]) + ) + + self._merged_labelmap = { + **{int(key): val for key, val in model_info["labelMap"].items()}, + **self.labelmap, + } + + def compute_model_hash(self) -> None: + if not self.path or not os.path.exists(self.path): + self._model_hash = hashlib.md5(b"unknown").hexdigest() + else: + with open(self.path, "rb") as f: + file_hash = hashlib.md5() + while chunk := f.read(8192): + file_hash.update(chunk) + self._model_hash = file_hash.hexdigest() + + def create_colormap(self, enabled_labels: set[str]) -> None: + """Get a list of colors for enabled labels that aren't attributes.""" + enabled_trackable_labels = list( + filter(lambda label: label not in self._all_attributes, enabled_labels) + ) + colors = generate_color_palette(len(enabled_trackable_labels)) + self._colormap = { + label: color for label, color in zip(enabled_trackable_labels, colors) + } + + model_config = ConfigDict(extra="forbid", protected_namespaces=()) + + +class BaseDetectorConfig(BaseModel): + # the type field must be defined in all subclasses + type: str = Field(default="cpu", title="Detector Type") + model: Optional[ModelConfig] = Field( + default=None, title="Detector specific model configuration." + ) + model_path: Optional[str] = Field( + default=None, title="Detector specific model path." + ) + model_config = ConfigDict( + extra="allow", arbitrary_types_allowed=True, protected_namespaces=() + ) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/detector_types.py b/sam2-cpu/frigate-dev/frigate/detectors/detector_types.py new file mode 100644 index 0000000..418fcd6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/detector_types.py @@ -0,0 +1,42 @@ +import importlib +import logging +import pkgutil +from enum import Enum +from typing import Union + +from pydantic import Field +from typing_extensions import Annotated + +from . import plugins +from .detection_api import DetectionApi +from .detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + + +_included_modules = pkgutil.iter_modules(plugins.__path__, plugins.__name__ + ".") + +plugin_modules = [] + +for _, name, _ in _included_modules: + try: + # currently openvino may fail when importing + # on an arm device with 64 KiB page size. + plugin_modules.append(importlib.import_module(name)) + except ImportError as e: + logger.error(f"Error importing detector runtime: {e}") + + +api_types = {det.type_key: det for det in DetectionApi.__subclasses__()} + + +class StrEnum(str, Enum): + pass + + +DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types}) + +DetectorConfig = Annotated[ + Union[tuple(BaseDetectorConfig.__subclasses__())], + Field(discriminator="type"), +] diff --git a/sam2-cpu/frigate-dev/frigate/detectors/detector_utils.py b/sam2-cpu/frigate-dev/frigate/detectors/detector_utils.py new file mode 100644 index 0000000..d732de8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/detector_utils.py @@ -0,0 +1,74 @@ +import logging +import os + +import numpy as np + +try: + from tflite_runtime.interpreter import Interpreter, load_delegate +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter, load_delegate + + +logger = logging.getLogger(__name__) + + +def tflite_init(self, interpreter): + self.interpreter = interpreter + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + +def tflite_detect_raw(self, tensor_input): + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + + boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0] + class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0] + scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0] + count = int(self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]) + + detections = np.zeros((20, 6), np.float32) + + for i in range(count): + if scores[i] < 0.4 or i == 20: + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + +def tflite_load_delegate_interpreter( + delegate_library: str, detector_config, device_config +): + try: + logger.info("Attempting to load NPU") + tf_delegate = load_delegate(delegate_library, device_config) + logger.info("NPU found") + interpreter = Interpreter( + model_path=detector_config.model.path, + experimental_delegates=[tf_delegate], + ) + return interpreter + except ValueError: + _, ext = os.path.splitext(detector_config.model.path) + + if ext and ext != ".tflite": + logger.error( + "Incorrect model used with NPU. Only .tflite models can be used with a TFLite delegate." + ) + else: + logger.error( + "No NPU was detected. If you do not have a TFLite device yet, you must configure CPU detectors." + ) + + raise diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/__init__.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/cpu_tfl.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/cpu_tfl.py new file mode 100644 index 0000000..37cc107 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/cpu_tfl.py @@ -0,0 +1,41 @@ +import logging + +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig +from frigate.log import redirect_output_to_logger + +from ..detector_utils import tflite_detect_raw, tflite_init + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "cpu" + + +class CpuDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + num_threads: int = Field(default=3, title="Number of detection threads") + + +class CpuTfl(DetectionApi): + type_key = DETECTOR_KEY + + @redirect_output_to_logger(logger, logging.DEBUG) + def __init__(self, detector_config: CpuDetectorConfig): + interpreter = Interpreter( + model_path=detector_config.model.path, + num_threads=detector_config.num_threads or 3, + ) + + tflite_init(self, interpreter) + + def detect_raw(self, tensor_input): + return tflite_detect_raw(self, tensor_input) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/deepstack.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/deepstack.py new file mode 100644 index 0000000..e00a4e7 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/deepstack.py @@ -0,0 +1,89 @@ +import io +import logging + +import numpy as np +import requests +from PIL import Image +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "deepstack" + + +class DeepstackDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + api_url: str = Field( + default="http://localhost:80/v1/vision/detection", title="DeepStack API URL" + ) + api_timeout: float = Field(default=0.1, title="DeepStack API timeout (in seconds)") + api_key: str = Field(default="", title="DeepStack API key (if required)") + + +class DeepStack(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DeepstackDetectorConfig): + self.api_url = detector_config.api_url + self.api_timeout = detector_config.api_timeout + self.api_key = detector_config.api_key + self.labels = detector_config.model.merged_labelmap + self.session = requests.Session() + + def get_label_index(self, label_value): + if label_value.lower() == "truck": + label_value = "car" + for index, value in self.labels.items(): + if value == label_value.lower(): + return index + return -1 + + def detect_raw(self, tensor_input): + image_data = np.squeeze(tensor_input).astype(np.uint8) + image = Image.fromarray(image_data) + self.w, self.h = image.size + with io.BytesIO() as output: + image.save(output, format="JPEG") + image_bytes = output.getvalue() + data = {"api_key": self.api_key} + + try: + response = self.session.post( + self.api_url, + data=data, + files={"image": image_bytes}, + timeout=self.api_timeout, + ) + except requests.exceptions.RequestException as ex: + logger.error("Error calling deepstack API: %s", ex) + return np.zeros((20, 6), np.float32) + + response_json = response.json() + detections = np.zeros((20, 6), np.float32) + if response_json.get("predictions") is None: + logger.debug(f"Error in parsing response json: {response_json}") + return detections + + for i, detection in enumerate(response_json.get("predictions")): + logger.debug(f"Response: {detection}") + if detection["confidence"] < 0.4: + logger.debug("Break due to confidence < 0.4") + break + label = self.get_label_index(detection["label"]) + if label < 0: + logger.debug("Break due to unknown label") + break + detections[i] = [ + label, + float(detection["confidence"]), + detection["y_min"] / self.h, + detection["x_min"] / self.w, + detection["y_max"] / self.h, + detection["x_max"] / self.w, + ] + + return detections diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/degirum.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/degirum.py new file mode 100644 index 0000000..28a1338 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/degirum.py @@ -0,0 +1,139 @@ +import logging +import queue + +import numpy as np +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) +DETECTOR_KEY = "degirum" + + +### DETECTOR CONFIG ### +class DGDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + location: str = Field(default=None, title="Inference Location") + zoo: str = Field(default=None, title="Model Zoo") + token: str = Field(default=None, title="DeGirum Cloud Token") + + +### ACTUAL DETECTOR ### +class DGDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: DGDetectorConfig): + try: + import degirum as dg + except ModuleNotFoundError: + raise ImportError("Unable to import DeGirum detector.") + + self._queue = queue.Queue() + self._zoo = dg.connect( + detector_config.location, detector_config.zoo, detector_config.token + ) + + logger.debug(f"Models in zoo: {self._zoo.list_models()}") + + self.dg_model = self._zoo.load_model( + detector_config.model.path, + ) + + # Setting input image format to raw reduces preprocessing time + self.dg_model.input_image_format = "RAW" + + # Prioritize the most powerful hardware available + self.select_best_device_type() + # Frigate handles pre processing as long as these are all set + input_shape = self.dg_model.input_shape[0] + self.model_height = input_shape[1] + self.model_width = input_shape[2] + + # Passing in dummy frame so initial connection latency happens in + # init function and not during actual prediction + frame = np.zeros( + (detector_config.model.width, detector_config.model.height, 3), + dtype=np.uint8, + ) + # Pass in frame to overcome first frame latency + self.dg_model(frame) + self.prediction = self.prediction_generator() + + def select_best_device_type(self): + """ + Helper function that selects fastest hardware available per model runtime + """ + types = self.dg_model.supported_device_types + + device_map = { + "OPENVINO": ["GPU", "NPU", "CPU"], + "HAILORT": ["HAILO8L", "HAILO8"], + "N2X": ["ORCA1", "CPU"], + "ONNX": ["VITIS_NPU", "CPU"], + "RKNN": ["RK3566", "RK3568", "RK3588"], + "TENSORRT": ["DLA", "GPU", "DLA_ONLY"], + "TFLITE": ["ARMNN", "EDGETPU", "CPU"], + } + + runtime = types[0].split("/")[0] + # Just create an array of format {runtime}/{hardware} for every hardware + # in the value for appropriate key in device_map + self.dg_model.device_type = [ + f"{runtime}/{hardware}" for hardware in device_map[runtime] + ] + + def prediction_generator(self): + """ + Generator for all incoming frames. By using this generator, we don't have to keep + reconnecting our websocket on every "predict" call. + """ + logger.debug("Prediction generator was called") + with self.dg_model as model: + while 1: + logger.info(f"q size before calling get: {self._queue.qsize()}") + data = self._queue.get(block=True) + logger.info(f"q size after calling get: {self._queue.qsize()}") + logger.debug( + f"Data we're passing into model predict: {data}, shape of data: {data.shape}" + ) + result = model.predict(data) + logger.debug(f"Prediction result: {result}") + yield result + + def detect_raw(self, tensor_input): + # Reshaping tensor to work with pysdk + truncated_input = tensor_input.reshape(tensor_input.shape[1:]) + logger.debug(f"Detect raw was called for tensor input: {tensor_input}") + + # add tensor_input to input queue + self._queue.put(truncated_input) + logger.debug(f"Queue size after adding truncated input: {self._queue.qsize()}") + + # define empty detection result + detections = np.zeros((20, 6), np.float32) + # grab prediction + res = next(self.prediction) + + # If we have an empty prediction, return immediately + if len(res.results) == 0 or len(res.results[0]) == 0: + return detections + + i = 0 + for result in res.results: + if i >= 20: + break + + detections[i] = [ + result["category_id"], + float(result["score"]), + result["bbox"][1] / self.model_height, + result["bbox"][0] / self.model_width, + result["bbox"][3] / self.model_height, + result["bbox"][2] / self.model_width, + ] + i += 1 + + logger.debug(f"Detections output: {detections}") + return detections diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/edgetpu_tfl.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/edgetpu_tfl.py new file mode 100644 index 0000000..2b94fde --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/edgetpu_tfl.py @@ -0,0 +1,361 @@ +import logging +import math +import os + +import cv2 +import numpy as np +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum + +try: + from tflite_runtime.interpreter import Interpreter, load_delegate +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter, load_delegate + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "edgetpu" + + +class EdgeTpuDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default=None, title="Device Type") + + +class EdgeTpuTfl(DetectionApi): + type_key = DETECTOR_KEY + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yologeneric, + ] + + def __init__(self, detector_config: EdgeTpuDetectorConfig): + device_config = {} + if detector_config.device is not None: + device_config = {"device": detector_config.device} + + edge_tpu_delegate = None + + try: + device_type = ( + device_config["device"] if "device" in device_config else "auto" + ) + logger.info(f"Attempting to load TPU as {device_type}") + edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config) + logger.info("TPU found") + self.interpreter = Interpreter( + model_path=detector_config.model.path, + experimental_delegates=[edge_tpu_delegate], + ) + except ValueError: + _, ext = os.path.splitext(detector_config.model.path) + + if ext and ext != ".tflite": + logger.error( + "Incorrect model used with EdgeTPU. Only .tflite models can be used with a Coral EdgeTPU." + ) + else: + logger.error( + "No EdgeTPU was detected. If you do not have a Coral device yet, you must configure CPU detectors." + ) + + raise + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + self.model_width = detector_config.model.width + self.model_height = detector_config.model.height + + self.min_score = 0.4 + self.max_detections = 20 + + self.model_type = detector_config.model.model_type + self.model_requires_int8 = self.tensor_input_details[0]["dtype"] == np.int8 + + if self.model_type == ModelTypeEnum.yologeneric: + logger.debug("Using YOLO preprocessing/postprocessing") + + if len(self.tensor_output_details) not in [2, 3]: + logger.error( + f"Invalid count of output tensors in YOLO model. Found {len(self.tensor_output_details)}, expecting 2 or 3." + ) + raise + + self.reg_max = 16 # = 64 dfl_channels // 4 # YOLO standard + self.min_logit_value = np.log( + self.min_score / (1 - self.min_score) + ) # for filtering + self._generate_anchors_and_strides() # decode bounding box DFL + self.project = np.arange( + self.reg_max, dtype=np.float32 + ) # for decoding bounding box DFL information + + # Determine YOLO tensor indices and quantization scales for + # boxes and class_scores the tensor ordering and names are + # not reliable, so use tensor shape to detect which tensor + # holds boxes or class scores. + # The tensors have shapes (B, N, C) + # where N is the number of candidates (=2100 for 320x320) + # this may guess wrong if the number of classes is exactly 64 + output_boxes_index = None + output_classes_index = None + for i, x in enumerate(self.tensor_output_details): + # the nominal index seems to start at 1 instead of 0 + if len(x["shape"]) == 3 and x["shape"][2] == 64: + output_boxes_index = i + elif len(x["shape"]) == 3 and x["shape"][2] > 1: + # require the number of classes to be more than 1 + # to differentiate from (not used) max score tensor + output_classes_index = i + if output_boxes_index is None or output_classes_index is None: + logger.warning("Unrecognized model output, unexpected tensor shapes.") + output_classes_index = ( + 0 + if (output_boxes_index is None or output_classes_index == 1) + else 1 + ) # 0 is default guess + output_boxes_index = 1 if (output_boxes_index == 0) else 0 + + scores_details = self.tensor_output_details[output_classes_index] + self.scores_tensor_index = scores_details["index"] + self.scores_scale, self.scores_zero_point = scores_details["quantization"] + # calculate the quantized version of the min_score + self.min_score_quantized = int( + (self.min_logit_value / self.scores_scale) + self.scores_zero_point + ) + self.logit_shift_to_positive_values = ( + max(0, math.ceil((128 + self.scores_zero_point) * self.scores_scale)) + + 1 + ) # round up + + boxes_details = self.tensor_output_details[output_boxes_index] + self.boxes_tensor_index = boxes_details["index"] + self.boxes_scale, self.boxes_zero_point = boxes_details["quantization"] + + elif self.model_type == ModelTypeEnum.ssd: + logger.debug("Using SSD preprocessing/postprocessing") + + # SSD model indices (4 outputs: boxes, class_ids, scores, count) + for x in self.tensor_output_details: + if len(x["shape"]) == 3: + self.output_boxes_index = x["index"] + elif len(x["shape"]) == 1: + self.output_count_index = x["index"] + + self.output_class_ids_index = None + self.output_class_scores_index = None + + else: + raise Exception( + f"{self.model_type} is currently not supported for edgetpu. See the docs for more info on supported models." + ) + + def _generate_anchors_and_strides(self): + # for decoding the bounding box DFL information into xy coordinates + all_anchors = [] + all_strides = [] + strides = (8, 16, 32) # YOLO's small, medium, large detection heads + + for stride in strides: + feat_h, feat_w = self.model_height // stride, self.model_width // stride + + grid_y, grid_x = np.meshgrid( + np.arange(feat_h, dtype=np.float32), + np.arange(feat_w, dtype=np.float32), + indexing="ij", + ) + + grid_coords = np.stack((grid_x.flatten(), grid_y.flatten()), axis=1) + anchor_points = grid_coords + 0.5 + + all_anchors.append(anchor_points) + all_strides.append(np.full((feat_h * feat_w, 1), stride, dtype=np.float32)) + + self.anchors = np.concatenate(all_anchors, axis=0) + self.anchor_strides = np.concatenate(all_strides, axis=0) + + def determine_indexes_for_non_yolo_models(self): + """Legacy method for SSD models.""" + if ( + self.output_class_ids_index is None + or self.output_class_scores_index is None + ): + for i in range(4): + index = self.tensor_output_details[i]["index"] + if ( + index != self.output_boxes_index + and index != self.output_count_index + ): + if ( + np.mod(np.float32(self.interpreter.tensor(index)()[0][0]), 1) + == 0.0 + ): + self.output_class_ids_index = index + else: + self.output_scores_index = index + + def pre_process(self, tensor_input): + if self.model_requires_int8: + tensor_input = np.bitwise_xor(tensor_input, 128).view( + np.int8 + ) # shift by -128 + return tensor_input + + def detect_raw(self, tensor_input): + tensor_input = self.pre_process(tensor_input) + + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + + if self.model_type == ModelTypeEnum.yologeneric: + # Multi-tensor YOLO model with (non-standard B(H*W)C output format). + # (the comments indicate the shape of tensors, + # using "2100" as the anchor count (for image size of 320x320), + # "NC" as number of classes, + # "N" as the count that survive after min-score filtering) + # TENSOR A) class scores (1, 2100, NC) with logit values + # TENSOR B) box coordinates (1, 2100, 64) encoded as dfl scores + # Recommend that the model clamp the logit values in tensor (A) + # to the range [-4,+4] to preserve precision from [2%,98%] + # and because NMS requires the min_score parameter to be >= 0 + + # don't dequantize scores data yet, wait until the low-confidence + # candidates are filtered out from the overall result set. + # This reduces the work and makes post-processing faster. + # this method works with raw quantized numbers when possible, + # which relies on the value of the scale factor to be >0. + # This speeds up max and argmax operations. + # Get max confidence for each detection and create the mask + detections = np.zeros( + (self.max_detections, 6), np.float32 + ) # initialize zero results + scores_output_quantized = self.interpreter.get_tensor( + self.scores_tensor_index + )[0] # (2100, NC) + max_scores_quantized = np.max(scores_output_quantized, axis=1) # (2100,) + mask = max_scores_quantized >= self.min_score_quantized # (2100,) + + if not np.any(mask): + return detections # empty results + + max_scores_filtered_shiftedpositive = ( + (max_scores_quantized[mask] - self.scores_zero_point) + * self.scores_scale + ) + self.logit_shift_to_positive_values # (N,1) shifted logit values + scores_output_quantized_filtered = scores_output_quantized[mask] + + # dequantize boxes. NMS needs them to be in float format + # remove candidates with probabilities < threshold + boxes_output_quantized_filtered = ( + self.interpreter.get_tensor(self.boxes_tensor_index)[0] + )[mask] # (N, 64) + boxes_output_filtered = ( + boxes_output_quantized_filtered.astype(np.float32) + - self.boxes_zero_point + ) * self.boxes_scale + + # 2. Decode DFL to distances (ltrb) + dfl_distributions = boxes_output_filtered.reshape( + -1, 4, self.reg_max + ) # (N, 4, 16) + + # Softmax over the 16 bins + dfl_max = np.max(dfl_distributions, axis=2, keepdims=True) + dfl_exp = np.exp(dfl_distributions - dfl_max) + dfl_probs = dfl_exp / np.sum(dfl_exp, axis=2, keepdims=True) # (N, 4, 16) + + # Weighted sum: (N, 4, 16) * (16,) -> (N, 4) + distances = np.einsum("pcr,r->pc", dfl_probs, self.project) + + # Calculate box corners in pixel coordinates + anchors_filtered = self.anchors[mask] + anchor_strides_filtered = self.anchor_strides[mask] + x1y1 = ( + anchors_filtered - distances[:, [0, 1]] + ) * anchor_strides_filtered # (N, 2) + x2y2 = ( + anchors_filtered + distances[:, [2, 3]] + ) * anchor_strides_filtered # (N, 2) + boxes_filtered_decoded = np.concatenate((x1y1, x2y2), axis=-1) # (N, 4) + + # 9. Apply NMS. Use logit scores here to defer sigmoid() + # until after filtering out redundant boxes + # Shift the logit scores to be non-negative (required by cv2) + indices = cv2.dnn.NMSBoxes( + bboxes=boxes_filtered_decoded, + scores=max_scores_filtered_shiftedpositive, + score_threshold=( + self.min_logit_value + self.logit_shift_to_positive_values + ), + nms_threshold=0.4, # should this be a model config setting? + ) + num_detections = len(indices) + if num_detections == 0: + return detections # empty results + + nms_indices = np.array(indices, dtype=np.int32).ravel() # or .flatten() + if num_detections > self.max_detections: + nms_indices = nms_indices[: self.max_detections] + num_detections = self.max_detections + kept_logits_quantized = scores_output_quantized_filtered[nms_indices] + class_ids_post_nms = np.argmax(kept_logits_quantized, axis=1) + + # Extract the final boxes and scores using fancy indexing + final_boxes = boxes_filtered_decoded[nms_indices] + final_scores_logits = ( + max_scores_filtered_shiftedpositive[nms_indices] + - self.logit_shift_to_positive_values + ) # Unshifted logits + + # Detections array format: [class_id, score, ymin, xmin, ymax, xmax] + detections[:num_detections, 0] = class_ids_post_nms + detections[:num_detections, 1] = 1.0 / ( + 1.0 + np.exp(-final_scores_logits) + ) # sigmoid + detections[:num_detections, 2] = final_boxes[:, 1] / self.model_height + detections[:num_detections, 3] = final_boxes[:, 0] / self.model_width + detections[:num_detections, 4] = final_boxes[:, 3] / self.model_height + detections[:num_detections, 5] = final_boxes[:, 2] / self.model_width + return detections + + elif self.model_type == ModelTypeEnum.ssd: + self.determine_indexes_for_non_yolo_models() + boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0] + class_ids = self.interpreter.tensor( + self.tensor_output_details[1]["index"] + )()[0] + scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[ + 0 + ] + count = int( + self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0] + ) + + detections = np.zeros((self.max_detections, 6), np.float32) + + for i in range(count): + if scores[i] < self.min_score: + break + if i == self.max_detections: + logger.debug(f"Too many detections ({count})!") + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + else: + raise Exception( + f"{self.model_type} is currently not supported for edgetpu. See the docs for more info on supported models." + ) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/hailo8l.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/hailo8l.py new file mode 100755 index 0000000..cafc809 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/hailo8l.py @@ -0,0 +1,414 @@ +import logging +import os +import subprocess +import threading +import urllib.request +from functools import partial +from typing import Dict, List, Optional, Tuple + +import cv2 +import numpy as np +from pydantic import Field +from typing_extensions import Literal + +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, +) +from frigate.object_detection.util import RequestStore, ResponseStore + +logger = logging.getLogger(__name__) + + +# ----------------- Utility Functions ----------------- # + + +def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarray: + """ + Resize an image with unchanged aspect ratio using padding. + Assumes input image shape is (H, W, 3). + """ + if image.ndim == 4 and image.shape[0] == 1: + image = image[0] + + h, w = image.shape[:2] + scale = min(model_w / w, model_h / h) + new_w, new_h = int(w * scale), int(h * scale) + resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC) + padded_image = np.full((model_h, model_w, 3), 114, dtype=image.dtype) + x_offset = (model_w - new_w) // 2 + y_offset = (model_h - new_h) // 2 + padded_image[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = ( + resized_image + ) + return padded_image + + +# ----------------- Global Constants ----------------- # +DETECTOR_KEY = "hailo8l" +ARCH = None +H8_DEFAULT_MODEL = "yolov6n.hef" +H8L_DEFAULT_MODEL = "yolov6n.hef" +H8_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef" +H8L_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/yolov6n.hef" + + +def detect_hailo_arch(): + try: + result = subprocess.run( + ["hailortcli", "fw-control", "identify"], capture_output=True, text=True + ) + if result.returncode != 0: + logger.error(f"Inference error: {result.stderr}") + return None + for line in result.stdout.split("\n"): + if "Device Architecture" in line: + if "HAILO8L" in line: + return "hailo8l" + elif "HAILO8" in line: + return "hailo8" + logger.error("Inference error: Could not determine Hailo architecture.") + return None + except Exception as e: + logger.error(f"Inference error: {e}") + return None + + +# ----------------- HailoAsyncInference Class ----------------- # +class HailoAsyncInference: + def __init__( + self, + hef_path: str, + input_store: RequestStore, + output_store: ResponseStore, + batch_size: int = 1, + input_type: Optional[str] = None, + output_type: Optional[Dict[str, str]] = None, + send_original_frame: bool = False, + ) -> None: + # when importing hailo it activates the driver + # which leaves processes running even though it may not be used. + try: + from hailo_platform import ( + HEF, + FormatType, + HailoSchedulingAlgorithm, + VDevice, + ) + except ModuleNotFoundError: + pass + + self.input_store = input_store + self.output_store = output_store + + params = VDevice.create_params() + params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN + + self.hef = HEF(hef_path) + self.target = VDevice(params) + self.infer_model = self.target.create_infer_model(hef_path) + self.infer_model.set_batch_size(batch_size) + + if input_type is not None: + self.infer_model.input().set_format_type(getattr(FormatType, input_type)) + + if output_type is not None: + for output_name, output_type in output_type.items(): + self.infer_model.output(output_name).set_format_type( + getattr(FormatType, output_type) + ) + + self.output_type = output_type + self.send_original_frame = send_original_frame + + def callback( + self, + completion_info, + bindings_list: List, + input_batch: List, + request_ids: List[int], + ): + if completion_info.exception: + logger.error(f"Inference error: {completion_info.exception}") + else: + for i, bindings in enumerate(bindings_list): + if len(bindings._output_names) == 1: + result = bindings.output().get_buffer() + else: + result = { + name: np.expand_dims(bindings.output(name).get_buffer(), axis=0) + for name in bindings._output_names + } + self.output_store.put(request_ids[i], (input_batch[i], result)) + + def _create_bindings(self, configured_infer_model) -> object: + if self.output_type is None: + output_buffers = { + output_info.name: np.empty( + self.infer_model.output(output_info.name).shape, + dtype=getattr( + np, str(output_info.format.type).split(".")[1].lower() + ), + ) + for output_info in self.hef.get_output_vstream_infos() + } + else: + output_buffers = { + name: np.empty( + self.infer_model.output(name).shape, + dtype=getattr(np, self.output_type[name].lower()), + ) + for name in self.output_type + } + return configured_infer_model.create_bindings(output_buffers=output_buffers) + + def get_input_shape(self) -> Tuple[int, ...]: + return self.hef.get_input_vstream_infos()[0].shape + + def run(self) -> None: + job = None + with self.infer_model.configure() as configured_infer_model: + while True: + batch_data = self.input_store.get() + + if batch_data is None: + break + + request_id, frame_data = batch_data + preprocessed_batch = [frame_data] + request_ids = [request_id] + input_batch = preprocessed_batch # non-send_original_frame mode + + bindings_list = [] + for frame in preprocessed_batch: + bindings = self._create_bindings(configured_infer_model) + bindings.input().set_buffer(np.array(frame)) + bindings_list.append(bindings) + configured_infer_model.wait_for_async_ready(timeout_ms=10000) + job = configured_infer_model.run_async( + bindings_list, + partial( + self.callback, + input_batch=input_batch, + request_ids=request_ids, + bindings_list=bindings_list, + ), + ) + + if job is not None: + job.wait(100) + + +# ----------------- HailoDetector Class ----------------- # +class HailoDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: "HailoDetectorConfig"): + global ARCH + ARCH = detect_hailo_arch() + self.cache_dir = MODEL_CACHE_DIR + self.device_type = detector_config.device + self.model_height = ( + detector_config.model.height + if hasattr(detector_config.model, "height") + else None + ) + self.model_width = ( + detector_config.model.width + if hasattr(detector_config.model, "width") + else None + ) + self.model_type = ( + detector_config.model.model_type + if hasattr(detector_config.model, "model_type") + else None + ) + self.tensor_format = ( + detector_config.model.input_tensor + if hasattr(detector_config.model, "input_tensor") + else None + ) + self.pixel_format = ( + detector_config.model.input_pixel_format + if hasattr(detector_config.model, "input_pixel_format") + else None + ) + self.input_dtype = ( + detector_config.model.input_dtype + if hasattr(detector_config.model, "input_dtype") + else None + ) + self.output_type = "FLOAT32" + self.set_path_and_url(detector_config.model.path) + self.working_model_path = self.check_and_prepare() + + self.batch_size = 1 + self.input_store = RequestStore() + self.response_store = ResponseStore() + + try: + logger.debug(f"[INIT] Loading HEF model from {self.working_model_path}") + self.inference_engine = HailoAsyncInference( + self.working_model_path, + self.input_store, + self.response_store, + self.batch_size, + ) + self.input_shape = self.inference_engine.get_input_shape() + logger.debug(f"[INIT] Model input shape: {self.input_shape}") + self.inference_thread = threading.Thread( + target=self.inference_engine.run, daemon=True + ) + self.inference_thread.start() + except Exception as e: + logger.error(f"[INIT] Failed to initialize HailoAsyncInference: {e}") + raise + + def set_path_and_url(self, path: str = None): + if not path: + self.model_path = None + self.url = None + return + if self.is_url(path): + self.url = path + self.model_path = None + else: + self.model_path = path + self.url = None + + def is_url(self, url: str) -> bool: + return ( + url.startswith("http://") + or url.startswith("https://") + or url.startswith("www.") + ) + + @staticmethod + def extract_model_name(path: str = None, url: str = None) -> str: + if path and path.endswith(".hef"): + return os.path.basename(path) + elif url and url.endswith(".hef"): + return os.path.basename(url) + else: + if ARCH == "hailo8": + return H8_DEFAULT_MODEL + else: + return H8L_DEFAULT_MODEL + + @staticmethod + def download_model(url: str, destination: str): + if not url.endswith(".hef"): + raise ValueError("Invalid model URL. Only .hef files are supported.") + try: + urllib.request.urlretrieve(url, destination) + logger.debug(f"Downloaded model to {destination}") + except Exception as e: + raise RuntimeError(f"Failed to download model from {url}: {str(e)}") + + def check_and_prepare(self) -> str: + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir) + model_name = self.extract_model_name(self.model_path, self.url) + cached_model_path = os.path.join(self.cache_dir, model_name) + if not self.model_path and not self.url: + if os.path.exists(cached_model_path): + logger.debug(f"Model found in cache: {cached_model_path}") + return cached_model_path + else: + logger.debug(f"Downloading default model: {model_name}") + if ARCH == "hailo8": + self.download_model(H8_DEFAULT_URL, cached_model_path) + else: + self.download_model(H8L_DEFAULT_URL, cached_model_path) + elif self.url: + logger.debug(f"Downloading model from URL: {self.url}") + self.download_model(self.url, cached_model_path) + elif self.model_path: + if os.path.exists(self.model_path): + logger.debug(f"Using existing model at: {self.model_path}") + return self.model_path + else: + raise FileNotFoundError(f"Model file not found at: {self.model_path}") + return cached_model_path + + def detect_raw(self, tensor_input): + tensor_input = self.preprocess(tensor_input) + + if isinstance(tensor_input, np.ndarray) and len(tensor_input.shape) == 3: + tensor_input = np.expand_dims(tensor_input, axis=0) + + request_id = self.input_store.put(tensor_input) + + try: + _, infer_results = self.response_store.get(request_id, timeout=1.0) + except TimeoutError: + logger.error( + f"Timeout waiting for inference results for request {request_id}" + ) + + if not self.inference_thread.is_alive(): + raise RuntimeError( + "HailoRT inference thread has stopped, restart required." + ) + + return np.zeros((20, 6), dtype=np.float32) + + if isinstance(infer_results, list) and len(infer_results) == 1: + infer_results = infer_results[0] + + threshold = 0.4 + all_detections = [] + for class_id, detection_set in enumerate(infer_results): + if not isinstance(detection_set, np.ndarray) or detection_set.size == 0: + continue + for det in detection_set: + if det.shape[0] < 5: + continue + score = float(det[4]) + if score < threshold: + continue + all_detections.append([class_id, score, det[0], det[1], det[2], det[3]]) + + if len(all_detections) == 0: + detections_array = np.zeros((20, 6), dtype=np.float32) + else: + detections_array = np.array(all_detections, dtype=np.float32) + if detections_array.shape[0] > 20: + detections_array = detections_array[:20, :] + elif detections_array.shape[0] < 20: + pad = np.zeros((20 - detections_array.shape[0], 6), dtype=np.float32) + detections_array = np.vstack((detections_array, pad)) + + return detections_array + + def preprocess(self, image): + if isinstance(image, np.ndarray): + processed = preprocess_tensor( + image, self.input_shape[1], self.input_shape[0] + ) + return np.expand_dims(processed, axis=0) + else: + raise ValueError("Unsupported image format for preprocessing") + + def close(self): + """Properly shuts down the inference engine and releases the VDevice.""" + logger.debug("[CLOSE] Closing HailoDetector") + try: + if hasattr(self, "inference_engine"): + if hasattr(self.inference_engine, "target"): + self.inference_engine.target.release() + logger.debug("Hailo VDevice released successfully") + except Exception as e: + logger.error(f"Failed to close Hailo device: {e}") + raise + + def __del__(self): + """Destructor to ensure cleanup when the object is deleted.""" + self.close() + + +# ----------------- HailoDetectorConfig Class ----------------- # +class HailoDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default="PCIe", title="Device Type") diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/memryx.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/memryx.py new file mode 100644 index 0000000..a93888f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/memryx.py @@ -0,0 +1,868 @@ +import glob +import logging +import os +import shutil +import urllib.request +import zipfile +from queue import Queue + +import cv2 +import numpy as np +from pydantic import BaseModel, Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + ModelTypeEnum, +) +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "memryx" + + +# Configuration class for model settings +class ModelConfig(BaseModel): + path: str = Field(default=None, title="Model Path") # Path to the DFP file + labelmap_path: str = Field(default=None, title="Path to Label Map") + + +class MemryXDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default="PCIe", title="Device Path") + + +class MemryXDetector(DetectionApi): + type_key = DETECTOR_KEY # Set the type key + supported_models = [ + ModelTypeEnum.ssd, + ModelTypeEnum.yolonas, + ModelTypeEnum.yologeneric, # Treated as yolov9 in MemryX implementation + ModelTypeEnum.yolox, + ] + + def __init__(self, detector_config): + """Initialize MemryX detector with the provided configuration.""" + try: + # Import MemryX SDK + from memryx import AsyncAccl + except ModuleNotFoundError: + raise ImportError( + "MemryX SDK is not installed. Install it and set up MIX environment." + ) + return + + # Initialize stop_event as None, will be set later by set_stop_event() + self.stop_event = None + + model_cfg = getattr(detector_config, "model", None) + + # Check if model_type was explicitly set by the user + if "model_type" in getattr(model_cfg, "__fields_set__", set()): + detector_config.model.model_type = model_cfg.model_type + else: + logger.info( + "model_type not set in config — defaulting to yolonas for MemryX." + ) + detector_config.model.model_type = ModelTypeEnum.yolonas + + self.capture_queue = Queue(maxsize=10) + self.output_queue = Queue(maxsize=10) + self.capture_id_queue = Queue(maxsize=10) + self.logger = logger + + self.memx_model_path = detector_config.model.path # Path to .dfp file + self.memx_post_model = None # Path to .post file + self.expected_post_model = None + + self.memx_device_path = detector_config.device # Device path + # Parse the device string to split PCIe: + device_str = self.memx_device_path + self.device_id = [] + self.device_id.append(int(device_str.split(":")[1])) + + self.memx_model_height = detector_config.model.height + self.memx_model_width = detector_config.model.width + self.memx_model_type = detector_config.model.model_type + + self.cache_dir = "/memryx_models" + + if self.memx_model_type == ModelTypeEnum.yologeneric: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_640.zip", + "yolov9_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolov9_320.zip", + "yolov9_320", + ), + ) + self.expected_dfp_model = "YOLO_v9_small_onnx.dfp" + + elif self.memx_model_type == ModelTypeEnum.yolonas: + model_mapping = { + (640, 640): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_640.zip", + "yolonas_640", + ), + (320, 320): ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + } + self.model_url, self.model_folder = model_mapping.get( + (self.memx_model_height, self.memx_model_width), + ( + "https://developer.memryx.com/example_files/2p0_frigate/yolonas_320.zip", + "yolonas_320", + ), + ) + self.expected_dfp_model = "yolo_nas_s.dfp" + self.expected_post_model = "yolo_nas_s_post.onnx" + + elif self.memx_model_type == ModelTypeEnum.yolox: + self.model_folder = "yolox" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/yolox.zip" + ) + self.expected_dfp_model = "YOLOX_640_640_3_onnx.dfp" + self.set_strides_grids() + + elif self.memx_model_type == ModelTypeEnum.ssd: + self.model_folder = "ssd" + self.model_url = ( + "https://developer.memryx.com/example_files/2p0_frigate/ssd.zip" + ) + self.expected_dfp_model = "SSDlite_MobileNet_v2_320_320_3_onnx.dfp" + self.expected_post_model = "SSDlite_MobileNet_v2_320_320_3_onnx_post.onnx" + + self.check_and_prepare_model() + logger.info( + f"Initializing MemryX with model: {self.memx_model_path} on device {self.memx_device_path}" + ) + + try: + # Load MemryX Model + logger.info(f"dfp path: {self.memx_model_path}") + + # Initialization code + # Load MemryX Model with a device target + self.accl = AsyncAccl( + self.memx_model_path, + device_ids=self.device_id, # AsyncAccl device ids + local_mode=True, + ) + + # Models that use cropped post-processing sections (YOLO-NAS and SSD) + # --> These will be moved to pure numpy in the future to improve performance on low-end CPUs + if self.memx_post_model: + self.accl.set_postprocessing_model(self.memx_post_model, model_idx=0) + + self.accl.connect_input(self.process_input) + self.accl.connect_output(self.process_output) + + logger.info( + f"Loaded MemryX model from {self.memx_model_path} and {self.memx_post_model}" + ) + + except Exception as e: + logger.error(f"Failed to initialize MemryX model: {e}") + raise + + def check_and_prepare_model(self): + if not os.path.exists(self.cache_dir): + os.makedirs(self.cache_dir, exist_ok=True) + + lock_path = os.path.join(self.cache_dir, f".{self.model_folder}.lock") + lock = FileLock(lock_path, timeout=60) + + with lock: + # ---------- CASE 1: user provided a custom model path ---------- + if self.memx_model_path: + if not self.memx_model_path.endswith(".zip"): + raise ValueError( + f"Invalid model path: {self.memx_model_path}. " + "Only .zip files are supported. Please provide a .zip model archive." + ) + if not os.path.exists(self.memx_model_path): + raise FileNotFoundError( + f"Custom model zip not found: {self.memx_model_path}" + ) + + logger.info(f"User provided zip model: {self.memx_model_path}") + + # Extract custom zip into a separate area so it never clashes with MemryX cache + custom_dir = os.path.join( + self.cache_dir, "custom_models", self.model_folder + ) + if os.path.isdir(custom_dir): + shutil.rmtree(custom_dir) + os.makedirs(custom_dir, exist_ok=True) + + with zipfile.ZipFile(self.memx_model_path, "r") as zip_ref: + zip_ref.extractall(custom_dir) + logger.info(f"Custom model extracted to {custom_dir}.") + + # Find .dfp and optional *_post.onnx recursively + dfp_candidates = glob.glob( + os.path.join(custom_dir, "**", "*.dfp"), recursive=True + ) + post_candidates = glob.glob( + os.path.join(custom_dir, "**", "*_post.onnx"), recursive=True + ) + + if not dfp_candidates: + raise FileNotFoundError( + "No .dfp file found in custom model zip after extraction." + ) + + self.memx_model_path = dfp_candidates[0] + + # Handle post model requirements by model type + if self.memx_model_type in [ + ModelTypeEnum.yolonas, + ModelTypeEnum.ssd, + ]: + if not post_candidates: + raise FileNotFoundError( + f"No *_post.onnx file found in custom model zip for {self.memx_model_type.name}." + ) + self.memx_post_model = post_candidates[0] + elif self.memx_model_type in [ + ModelTypeEnum.yolox, + ModelTypeEnum.yologeneric, + ]: + # Explicitly ignore any post model even if present + self.memx_post_model = None + else: + # Future model types can optionally use post if present + self.memx_post_model = ( + post_candidates[0] if post_candidates else None + ) + + logger.info(f"Using custom model: {self.memx_model_path}") + return + + # ---------- CASE 2: no custom model path -> use MemryX cached models ---------- + model_subdir = os.path.join(self.cache_dir, self.model_folder) + dfp_path = os.path.join(model_subdir, self.expected_dfp_model) + post_path = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + dfp_exists = os.path.exists(dfp_path) + post_exists = os.path.exists(post_path) if post_path else True + + if dfp_exists and post_exists: + logger.info("Using cached models.") + self.memx_model_path = dfp_path + self.memx_post_model = post_path + return + + # ---------- CASE 3: download MemryX model (no cache) ---------- + logger.info( + f"Model files not found locally. Downloading from {self.model_url}..." + ) + zip_path = os.path.join(self.cache_dir, f"{self.model_folder}.zip") + + try: + if not os.path.exists(zip_path): + urllib.request.urlretrieve(self.model_url, zip_path) + logger.info(f"Model ZIP downloaded to {zip_path}. Extracting...") + + if not os.path.exists(model_subdir): + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(self.cache_dir) + logger.info(f"Model extracted to {self.cache_dir}.") + + # Re-assign model paths after extraction + self.memx_model_path = os.path.join( + model_subdir, self.expected_dfp_model + ) + self.memx_post_model = ( + os.path.join(model_subdir, self.expected_post_model) + if self.expected_post_model + else None + ) + + finally: + if os.path.exists(zip_path): + try: + os.remove(zip_path) + logger.info("Cleaned up ZIP file after extraction.") + except Exception as e: + logger.warning( + f"Failed to remove downloaded zip {zip_path}: {e}" + ) + + def send_input(self, connection_id, tensor_input: np.ndarray): + """Pre-process (if needed) and send frame to MemryX input queue""" + if tensor_input is None: + raise ValueError("[send_input] No image data provided for inference") + + if self.memx_model_type == ModelTypeEnum.yolonas: + if tensor_input.ndim == 4 and tensor_input.shape[1:] == (320, 320, 3): + logger.debug("Transposing tensor from NHWC to NCHW for YOLO-NAS") + tensor_input = np.transpose( + tensor_input, (0, 3, 1, 2) + ) # (1, H, W, C) → (1, C, H, W) + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + + if self.memx_model_type == ModelTypeEnum.yolox: + # Remove batch dim → (3, 640, 640) + tensor_input = tensor_input.squeeze(0) + + # Convert CHW to HWC for OpenCV + tensor_input = np.transpose(tensor_input, (1, 2, 0)) # (640, 640, 3) + + padded_img = np.ones((640, 640, 3), dtype=np.uint8) * 114 + + scale = min( + 640 / float(tensor_input.shape[0]), 640 / float(tensor_input.shape[1]) + ) + sx, sy = ( + int(tensor_input.shape[1] * scale), + int(tensor_input.shape[0] * scale), + ) + + resized_img = cv2.resize( + tensor_input, (sx, sy), interpolation=cv2.INTER_LINEAR + ) + padded_img[:sy, :sx] = resized_img.astype(np.uint8) + + # Step 4: Slice the padded image into 4 quadrants and concatenate them into 12 channels + x0 = padded_img[0::2, 0::2, :] # Top-left + x1 = padded_img[1::2, 0::2, :] # Bottom-left + x2 = padded_img[0::2, 1::2, :] # Top-right + x3 = padded_img[1::2, 1::2, :] # Bottom-right + + # Step 5: Concatenate along the channel dimension (axis 2) + concatenated_img = np.concatenate([x0, x1, x2, x3], axis=2) + tensor_input = concatenated_img.astype(np.float32) + # Convert to CHW format (12, 320, 320) + tensor_input = np.transpose(tensor_input, (2, 0, 1)) + + # Add batch dimension → (1, 12, 320, 320) + tensor_input = np.expand_dims(tensor_input, axis=0) + + # Send frame to MemryX for processing + self.capture_queue.put(tensor_input) + self.capture_id_queue.put(connection_id) + + def process_input(self): + """Input callback function: wait for frames in the input queue, preprocess, and send to MX3 (return)""" + while True: + # Check if shutdown is requested + if self.stop_event and self.stop_event.is_set(): + logger.debug("[process_input] Stop event detected, returning None") + return None + try: + # Wait for a frame from the queue with timeout to check stop_event periodically + frame = self.capture_queue.get(block=True, timeout=0.5) + + return frame + + except Exception as e: + # Silently handle queue.Empty timeouts (expected during normal operation) + # Log any other unexpected exceptions + if "Empty" not in str(type(e).__name__): + logger.warning(f"[process_input] Unexpected error: {e}") + # Loop continues and will check stop_event at the top + + def receive_output(self): + """Retrieve processed results from MemryX output queue + a copy of the original frame""" + try: + # Get connection ID with timeout + connection_id = self.capture_id_queue.get( + block=True, timeout=1.0 + ) # Get the corresponding connection ID + detections = self.output_queue.get() # Get detections from MemryX + + return connection_id, detections + + except Exception as e: + # On timeout or stop event, return None + if self.stop_event and self.stop_event.is_set(): + logger.debug("[receive_output] Stop event detected, exiting") + # Silently handle queue.Empty timeouts, they're expected during normal operation + elif "Empty" not in str(type(e).__name__): + logger.warning(f"[receive_output] Error receiving output: {e}") + + return None, None + + def post_process_yolonas(self, output): + predictions = output[0] + + detections = np.zeros((20, 6), np.float32) + + for i, prediction in enumerate(predictions): + if i == 20: + break + + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + + if class_id < 0: + break + + detections[i] = [ + class_id, + confidence, + y_min / self.memx_model_height, + x_min / self.memx_model_width, + y_max / self.memx_model_height, + x_max / self.memx_model_width, + ] + + # Return the list of final detections + self.output_queue.put(detections) + + def process_yolo(self, class_id, conf, pos): + """ + Takes in class ID, confidence score, and array of [x, y, w, h] that describes detection position, + returns an array that's easily passable back to Frigate. + """ + return [ + class_id, # class ID + conf, # confidence score + (pos[1] - (pos[3] / 2)) / self.memx_model_height, # y_min + (pos[0] - (pos[2] / 2)) / self.memx_model_width, # x_min + (pos[1] + (pos[3] / 2)) / self.memx_model_height, # y_max + (pos[0] + (pos[2] / 2)) / self.memx_model_width, # x_max + ] + + def set_strides_grids(self): + grids = [] + expanded_strides = [] + + strides = [8, 16, 32] + + hsize_list = [self.memx_model_height // stride for stride in strides] + wsize_list = [self.memx_model_width // stride for stride in strides] + + for hsize, wsize, stride in zip(hsize_list, wsize_list, strides): + xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize)) + grid = np.stack((xv, yv), 2).reshape(1, -1, 2) + grids.append(grid) + shape = grid.shape[:2] + expanded_strides.append(np.full((*shape, 1), stride)) + self.grids = np.concatenate(grids, 1) + self.expanded_strides = np.concatenate(expanded_strides, 1) + + def sigmoid(self, x: np.ndarray) -> np.ndarray: + return 1 / (1 + np.exp(-x)) + + def onnx_concat(self, inputs: list, axis: int) -> np.ndarray: + # Ensure all inputs are numpy arrays + if not all(isinstance(x, np.ndarray) for x in inputs): + raise TypeError("All inputs must be numpy arrays.") + + # Ensure shapes match on non-concat axes + ref_shape = list(inputs[0].shape) + for i, tensor in enumerate(inputs[1:], start=1): + for ax in range(len(ref_shape)): + if ax == axis: + continue + if tensor.shape[ax] != ref_shape[ax]: + raise ValueError( + f"Shape mismatch at axis {ax} between input[0] and input[{i}]" + ) + + return np.concatenate(inputs, axis=axis) + + def onnx_reshape(self, data: np.ndarray, shape: np.ndarray) -> np.ndarray: + # Ensure shape is a 1D array of integers + target_shape = shape.astype(int).tolist() + + # Use NumPy reshape with dynamic handling of -1 + reshaped = np.reshape(data, target_shape) + + return reshaped + + def post_process_yolox(self, output): + output_785 = output[0] # 785 + output_794 = output[1] # 794 + output_795 = output[2] # 795 + output_811 = output[3] # 811 + output_820 = output[4] # 820 + output_821 = output[5] # 821 + output_837 = output[6] # 837 + output_846 = output[7] # 846 + output_847 = output[8] # 847 + + output_795 = self.sigmoid(output_795) + output_785 = self.sigmoid(output_785) + output_821 = self.sigmoid(output_821) + output_811 = self.sigmoid(output_811) + output_847 = self.sigmoid(output_847) + output_837 = self.sigmoid(output_837) + + concat_1 = self.onnx_concat([output_794, output_795, output_785], axis=1) + concat_2 = self.onnx_concat([output_820, output_821, output_811], axis=1) + concat_3 = self.onnx_concat([output_846, output_847, output_837], axis=1) + + shape = np.array([1, 85, -1], dtype=np.int64) + + reshape_1 = self.onnx_reshape(concat_1, shape) + reshape_2 = self.onnx_reshape(concat_2, shape) + reshape_3 = self.onnx_reshape(concat_3, shape) + + concat_out = self.onnx_concat([reshape_1, reshape_2, reshape_3], axis=2) + + output = concat_out.transpose(0, 2, 1) # 1, 840, 85 + + self.num_classes = output.shape[2] - 5 + + # [x, y, h, w, box_score, class_no_1, ..., class_no_80], + results = output + + results[..., :2] = (results[..., :2] + self.grids) * self.expanded_strides + results[..., 2:4] = np.exp(results[..., 2:4]) * self.expanded_strides + image_pred = results[0, ...] + + class_conf = np.max( + image_pred[:, 5 : 5 + self.num_classes], axis=1, keepdims=True + ) + class_pred = np.argmax(image_pred[:, 5 : 5 + self.num_classes], axis=1) + class_pred = np.expand_dims(class_pred, axis=1) + + conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze() + # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred) + detections = np.concatenate((image_pred[:, :5], class_conf, class_pred), axis=1) + detections = detections[conf_mask] + + # Sort by class confidence (index 5) and keep top 20 detections + ordered = detections[detections[:, 5].argsort()[::-1]][:20] + + # Prepare a final detections array of shape (20, 6) + final_detections = np.zeros((20, 6), np.float32) + for i, object_detected in enumerate(ordered): + final_detections[i] = self.process_yolo( + object_detected[6], object_detected[5], object_detected[:4] + ) + + self.output_queue.put(final_detections) + + def post_process_ssdlite(self, outputs): + dets = outputs[0].squeeze(0) # Shape: (1, num_dets, 5) + labels = outputs[1].squeeze(0) + + detections = [] + + for i in range(dets.shape[0]): + x_min, y_min, x_max, y_max, confidence = dets[i] + class_id = int(labels[i]) # Convert label to integer + + if confidence < 0.45: + continue # Skip detections below threshold + + # Convert coordinates to integers + x_min, y_min, x_max, y_max = map(int, [x_min, y_min, x_max, y_max]) + + # Append valid detections [class_id, confidence, x, y, width, height] + detections.append([class_id, confidence, x_min, y_min, x_max, y_max]) + + final_detections = np.zeros((20, 6), np.float32) + + if len(detections) == 0: + # logger.info("No detections found.") + self.output_queue.put(final_detections) + return + + # Convert to NumPy array + detections = np.array(detections, dtype=np.float32) + + # Apply Non-Maximum Suppression (NMS) + bboxes = detections[:, 2:6].tolist() # (x_min, y_min, width, height) + scores = detections[:, 1].tolist() # Confidence scores + + indices = cv2.dnn.NMSBoxes(bboxes, scores, 0.45, 0.5) + + if len(indices) > 0: + indices = indices.flatten()[:20] # Keep only the top 20 detections + selected_detections = detections[indices] + + # Normalize coordinates AFTER NMS + for i, det in enumerate(selected_detections): + class_id, confidence, x_min, y_min, x_max, y_max = det + + # Normalize coordinates + x_min /= self.memx_model_width + y_min /= self.memx_model_height + x_max /= self.memx_model_width + y_max /= self.memx_model_height + + final_detections[i] = [class_id, confidence, y_min, x_min, y_max, x_max] + + self.output_queue.put(final_detections) + + def _generate_anchors(self, sizes=[80, 40, 20]): + """Generate anchor points for YOLOv9 style processing""" + yscales = [] + xscales = [] + for s in sizes: + r = np.arange(s) + 0.5 + yscales.append(np.repeat(r, s)) + xscales.append(np.repeat(r[None, ...], s, axis=0).flatten()) + + yscales = np.concatenate(yscales) + xscales = np.concatenate(xscales) + anchors = np.stack([xscales, yscales], axis=1) + return anchors + + def _generate_scales(self, sizes=[80, 40, 20]): + """Generate scaling factors for each detection level""" + factors = [8, 16, 32] + s = np.concatenate([np.ones([int(s * s)]) * f for s, f in zip(sizes, factors)]) + return s[:, None] + + @staticmethod + def _softmax(x: np.ndarray, axis: int) -> np.ndarray: + """Efficient softmax implementation""" + x = x - np.max(x, axis=axis, keepdims=True) + np.exp(x, out=x) + x /= np.sum(x, axis=axis, keepdims=True) + return x + + def dfl(self, x: np.ndarray) -> np.ndarray: + """Distribution Focal Loss decoding - YOLOv9 style""" + x = x.reshape(-1, 4, 16) + weights = np.arange(16, dtype=np.float32) + p = self._softmax(x, axis=2) + p = p * weights[None, None, :] + out = np.sum(p, axis=2, keepdims=False) + return out + + def dist2bbox( + self, x: np.ndarray, anchors: np.ndarray, scales: np.ndarray + ) -> np.ndarray: + """Convert distances to bounding boxes - YOLOv9 style""" + lt = x[:, :2] + rb = x[:, 2:] + + x1y1 = anchors - lt + x2y2 = anchors + rb + + wh = x2y2 - x1y1 + c_xy = (x1y1 + x2y2) / 2 + + out = np.concatenate([c_xy, wh], axis=1) + out = out * scales + return out + + def post_process_yolo_optimized(self, outputs): + """ + Custom YOLOv9 post-processing optimized for MemryX ONNX outputs. + Implements DFL decoding, confidence filtering, and NMS in pure NumPy. + """ + # YOLOv9 outputs: 6 outputs (lbox, lcls, mbox, mcls, sbox, scls) + conv_out1, conv_out2, conv_out3, conv_out4, conv_out5, conv_out6 = outputs + + # Determine grid sizes based on input resolution + # YOLOv9 uses 3 detection heads with strides [8, 16, 32] + # Grid sizes = input_size / stride + sizes = [ + self.memx_model_height + // 8, # Large objects (e.g., 80 for 640x640, 40 for 320x320) + self.memx_model_height + // 16, # Medium objects (e.g., 40 for 640x640, 20 for 320x320) + self.memx_model_height + // 32, # Small objects (e.g., 20 for 640x640, 10 for 320x320) + ] + + # Generate anchors and scales if not already done + if not hasattr(self, "anchors"): + self.anchors = self._generate_anchors(sizes) + self.scales = self._generate_scales(sizes) + + # Process outputs in YOLOv9 format: reshape and moveaxis for ONNX format + lbox = np.moveaxis(conv_out1, 1, -1) # Large boxes + lcls = np.moveaxis(conv_out2, 1, -1) # Large classes + mbox = np.moveaxis(conv_out3, 1, -1) # Medium boxes + mcls = np.moveaxis(conv_out4, 1, -1) # Medium classes + sbox = np.moveaxis(conv_out5, 1, -1) # Small boxes + scls = np.moveaxis(conv_out6, 1, -1) # Small classes + + # Determine number of classes dynamically from the class output shape + # lcls shape should be (batch, height, width, num_classes) + num_classes = lcls.shape[-1] + + # Validate that all class outputs have the same number of classes + if not (mcls.shape[-1] == num_classes and scls.shape[-1] == num_classes): + raise ValueError( + f"Class output shapes mismatch: lcls={lcls.shape}, mcls={mcls.shape}, scls={scls.shape}" + ) + + # Concatenate boxes and classes + boxes = np.concatenate( + [ + lbox.reshape(-1, 64), # 64 is for 4 bbox coords * 16 DFL bins + mbox.reshape(-1, 64), + sbox.reshape(-1, 64), + ], + axis=0, + ) + + classes = np.concatenate( + [ + lcls.reshape(-1, num_classes), + mcls.reshape(-1, num_classes), + scls.reshape(-1, num_classes), + ], + axis=0, + ) + + # Apply sigmoid to classes + classes = self.sigmoid(classes) + + # Apply DFL to box predictions + boxes = self.dfl(boxes) + + # YOLOv9 postprocessing with confidence filtering and NMS + confidence_thres = 0.4 + iou_thres = 0.6 + + # Find the class with the highest score for each detection + max_scores = np.max(classes, axis=1) # Maximum class score for each detection + class_ids = np.argmax(classes, axis=1) # Index of the best class + + # Filter out detections with scores below the confidence threshold + valid_indices = np.where(max_scores >= confidence_thres)[0] + if len(valid_indices) == 0: + # Return empty detections array + final_detections = np.zeros((20, 6), np.float32) + return final_detections + + # Select only valid detections + valid_boxes = boxes[valid_indices] + valid_class_ids = class_ids[valid_indices] + valid_scores = max_scores[valid_indices] + + # Convert distances to actual bounding boxes using anchors and scales + valid_boxes = self.dist2bbox( + valid_boxes, self.anchors[valid_indices], self.scales[valid_indices] + ) + + # Convert bounding box coordinates from (x_center, y_center, w, h) to (x_min, y_min, x_max, y_max) + x_center, y_center, width, height = ( + valid_boxes[:, 0], + valid_boxes[:, 1], + valid_boxes[:, 2], + valid_boxes[:, 3], + ) + x_min = x_center - width / 2 + y_min = y_center - height / 2 + x_max = x_center + width / 2 + y_max = y_center + height / 2 + + # Convert to format expected by cv2.dnn.NMSBoxes: [x, y, width, height] + boxes_for_nms = [] + scores_for_nms = [] + + for i in range(len(valid_indices)): + # Ensure coordinates are within bounds and positive + x_min_clipped = max(0, x_min[i]) + y_min_clipped = max(0, y_min[i]) + x_max_clipped = min(self.memx_model_width, x_max[i]) + y_max_clipped = min(self.memx_model_height, y_max[i]) + + width_clipped = x_max_clipped - x_min_clipped + height_clipped = y_max_clipped - y_min_clipped + + if width_clipped > 0 and height_clipped > 0: + boxes_for_nms.append( + [x_min_clipped, y_min_clipped, width_clipped, height_clipped] + ) + scores_for_nms.append(float(valid_scores[i])) + + final_detections = np.zeros((20, 6), np.float32) + + if len(boxes_for_nms) == 0: + return final_detections + + # Apply NMS using OpenCV + indices = cv2.dnn.NMSBoxes( + boxes_for_nms, scores_for_nms, confidence_thres, iou_thres + ) + + if len(indices) > 0: + # Flatten indices if they are returned as a list of arrays + if isinstance(indices[0], list) or isinstance(indices[0], np.ndarray): + indices = [i[0] for i in indices] + + # Limit to top 20 detections + indices = indices[:20] + + # Convert to Frigate format: [class_id, confidence, y_min, x_min, y_max, x_max] (normalized) + for i, idx in enumerate(indices): + class_id = valid_class_ids[idx] + confidence = valid_scores[idx] + + # Get the box coordinates + box = boxes_for_nms[idx] + x_min_norm = box[0] / self.memx_model_width + y_min_norm = box[1] / self.memx_model_height + x_max_norm = (box[0] + box[2]) / self.memx_model_width + y_max_norm = (box[1] + box[3]) / self.memx_model_height + + final_detections[i] = [ + class_id, + confidence, + y_min_norm, # Frigate expects y_min first + x_min_norm, + y_max_norm, + x_max_norm, + ] + + return final_detections + + def process_output(self, *outputs): + """Output callback function -- receives frames from the MX3 and triggers post-processing""" + if self.memx_model_type == ModelTypeEnum.yologeneric: + # Use complete YOLOv9-style postprocessing (includes NMS) + final_detections = self.post_process_yolo_optimized(outputs) + + self.output_queue.put(final_detections) + + elif self.memx_model_type == ModelTypeEnum.yolonas: + return self.post_process_yolonas(outputs) + + elif self.memx_model_type == ModelTypeEnum.yolox: + return self.post_process_yolox(outputs) + + elif self.memx_model_type == ModelTypeEnum.ssd: + return self.post_process_ssdlite(outputs) + + else: + raise Exception( + f"{self.memx_model_type} is currently not supported for memryx. See the docs for more info on supported models." + ) + + def set_stop_event(self, stop_event): + """Set the stop event for graceful shutdown.""" + self.stop_event = stop_event + + def shutdown(self): + """Gracefully shutdown the MemryX accelerator""" + try: + if hasattr(self, "accl") and self.accl is not None: + self.accl.shutdown() + logger.info("MemryX accelerator shutdown complete") + except Exception as e: + logger.error(f"Error during MemryX shutdown: {e}") + + def detect_raw(self, tensor_input: np.ndarray): + """Removed synchronous detect_raw() function so that we only use async""" + return 0 diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/onnx.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/onnx.py new file mode 100644 index 0000000..6c9e510 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/onnx.py @@ -0,0 +1,105 @@ +import logging + +import numpy as np +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + ModelTypeEnum, +) +from frigate.util.model import ( + post_process_dfine, + post_process_rfdetr, + post_process_yolo, + post_process_yolox, +) + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "onnx" + + +class ONNXDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default="AUTO", title="Device Type") + + +class ONNXDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: ONNXDetectorConfig): + super().__init__(detector_config) + + path = detector_config.model.path + logger.info(f"ONNX: loading {detector_config.model.path}") + + self.runner = get_optimized_runner( + path, + detector_config.device, + model_type=detector_config.model.model_type, + ) + + self.onnx_model_type = detector_config.model.model_type + self.onnx_model_px = detector_config.model.input_pixel_format + self.onnx_model_shape = detector_config.model.input_tensor + + if self.onnx_model_type == ModelTypeEnum.yolox: + self.calculate_grids_strides() + + logger.info(f"ONNX: {path} loaded") + + def detect_raw(self, tensor_input: np.ndarray): + if self.onnx_model_type == ModelTypeEnum.dfine: + tensor_output = self.runner.run( + { + "images": tensor_input, + "orig_target_sizes": np.array( + [[self.height, self.width]], dtype=np.int64 + ), + } + ) + return post_process_dfine(tensor_output, self.width, self.height) + + model_input_name = self.runner.get_input_names()[0] + tensor_output = self.runner.run({model_input_name: tensor_input}) + + if self.onnx_model_type == ModelTypeEnum.rfdetr: + return post_process_rfdetr(tensor_output) + elif self.onnx_model_type == ModelTypeEnum.yolonas: + predictions = tensor_output[0] + + detections = np.zeros((20, 6), np.float32) + + for i, prediction in enumerate(predictions): + if i == 20: + break + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + # when running in GPU mode, empty predictions in the output have class_id of -1 + if class_id < 0: + break + detections[i] = [ + class_id, + confidence, + y_min / self.height, + x_min / self.width, + y_max / self.height, + x_max / self.width, + ] + return detections + elif self.onnx_model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(tensor_output, self.width, self.height) + elif self.onnx_model_type == ModelTypeEnum.yolox: + return post_process_yolox( + tensor_output[0], + self.width, + self.height, + self.grids, + self.expanded_strides, + ) + else: + raise Exception( + f"{self.onnx_model_type} is currently not supported for onnx. See the docs for more info on supported models." + ) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/openvino.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/openvino.py new file mode 100644 index 0000000..bda5c88 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/openvino.py @@ -0,0 +1,224 @@ +import logging + +import numpy as np +import openvino as ov +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import OpenVINOModelRunner +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum +from frigate.util.model import ( + post_process_dfine, + post_process_rfdetr, + post_process_yolo, +) + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "openvino" + + +class OvDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: str = Field(default=None, title="Device Type") + + +class OvDetector(DetectionApi): + type_key = DETECTOR_KEY + supported_models = [ + ModelTypeEnum.dfine, + ModelTypeEnum.rfdetr, + ModelTypeEnum.ssd, + ModelTypeEnum.yolonas, + ModelTypeEnum.yologeneric, + ModelTypeEnum.yolox, + ] + + def __init__(self, detector_config: OvDetectorConfig): + super().__init__(detector_config) + self.ov_model_type = detector_config.model.model_type + + self.h = detector_config.model.height + self.w = detector_config.model.width + + self.runner = OpenVINOModelRunner( + model_path=detector_config.model.path, + device=detector_config.device, + model_type=detector_config.model.model_type, + ) + + # For dfine models, also pre-allocate target sizes tensor + if self.ov_model_type == ModelTypeEnum.dfine: + self.target_sizes_tensor = ov.Tensor( + np.array([[self.h, self.w]], dtype=np.int64) + ) + + self.model_invalid = False + + if self.ov_model_type not in self.supported_models: + logger.error( + f"OpenVino detector does not support {self.ov_model_type} models." + ) + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.ssd: + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs + + if len(model_inputs) != 1: + logger.error( + f"SSD models must only have 1 input. Found {len(model_inputs)}." + ) + self.model_invalid = True + if len(model_outputs) != 1: + logger.error( + f"SSD models must only have 1 output. Found {len(model_outputs)}." + ) + self.model_invalid = True + + output_shape = model_outputs[0].get_shape() + if output_shape[0] != 1 or output_shape[1] != 1 or output_shape[3] != 7: + logger.error(f"SSD model output doesn't match. Found {output_shape}.") + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.yolonas: + model_inputs = self.runner.compiled_model.inputs + model_outputs = self.runner.compiled_model.outputs + + if len(model_inputs) != 1: + logger.error( + f"YoloNAS models must only have 1 input. Found {len(model_inputs)}." + ) + self.model_invalid = True + if len(model_outputs) != 1: + logger.error( + f"YoloNAS models must be exported in flat format and only have 1 output. Found {len(model_outputs)}." + ) + self.model_invalid = True + output_shape = model_outputs[0].partial_shape + if output_shape[-1] != 7: + logger.error( + f"YoloNAS models must be exported in flat format. Model output doesn't match. Found {output_shape}." + ) + self.model_invalid = True + + if self.ov_model_type == ModelTypeEnum.yolox: + self.output_indexes = 0 + while True: + try: + tensor_shape = self.runner.compiled_model.output( + self.output_indexes + ).shape + logger.info( + f"Model Output-{self.output_indexes} Shape: {tensor_shape}" + ) + self.output_indexes += 1 + except Exception: + logger.info(f"Model has {self.output_indexes} Output Tensors") + break + self.num_classes = tensor_shape[2] - 5 + logger.info(f"YOLOX model has {self.num_classes} classes") + self.calculate_grids_strides() + + ## Takes in class ID, confidence score, and array of [x, y, w, h] that describes detection position, + ## returns an array that's easily passable back to Frigate. + def process_yolo(self, class_id, conf, pos): + return [ + class_id, # class ID + conf, # confidence score + (pos[1] - (pos[3] / 2)) / self.h, # y_min + (pos[0] - (pos[2] / 2)) / self.w, # x_min + (pos[1] + (pos[3] / 2)) / self.h, # y_max + (pos[0] + (pos[2] / 2)) / self.w, # x_max + ] + + def detect_raw(self, tensor_input): + if self.model_invalid: + return np.zeros((20, 6), np.float32) + + if self.ov_model_type == ModelTypeEnum.dfine: + # Use named inputs for dfine models + inputs = { + "images": tensor_input, + "orig_target_sizes": np.array([[self.h, self.w]], dtype=np.int64), + } + outputs = self.runner.run(inputs) + tensor_output = ( + outputs[0], + outputs[1], + outputs[2], + ) + return post_process_dfine(tensor_output, self.w, self.h) + + # Run inference using the runner + input_name = self.runner.get_input_names()[0] + outputs = self.runner.run({input_name: tensor_input}) + + detections = np.zeros((20, 6), np.float32) + + if self.ov_model_type == ModelTypeEnum.rfdetr: + return post_process_rfdetr(outputs) + elif self.ov_model_type == ModelTypeEnum.ssd: + results = outputs[0][0][0] + + for i, (_, class_id, score, xmin, ymin, xmax, ymax) in enumerate(results): + if i == 20: + break + detections[i] = [ + class_id, + float(score), + ymin, + xmin, + ymax, + xmax, + ] + return detections + elif self.ov_model_type == ModelTypeEnum.yolonas: + predictions = outputs[0] + + for i, prediction in enumerate(predictions): + if i == 20: + break + (_, x_min, y_min, x_max, y_max, confidence, class_id) = prediction + # when running in GPU mode, empty predictions in the output have class_id of -1 + if class_id < 0: + break + detections[i] = [ + class_id, + confidence, + y_min / self.h, + x_min / self.w, + y_max / self.h, + x_max / self.w, + ] + return detections + elif self.ov_model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(outputs, self.w, self.h) + elif self.ov_model_type == ModelTypeEnum.yolox: + # [x, y, h, w, box_score, class_no_1, ..., class_no_80], + results = outputs[0] + results[..., :2] = (results[..., :2] + self.grids) * self.expanded_strides + results[..., 2:4] = np.exp(results[..., 2:4]) * self.expanded_strides + image_pred = results[0, ...] + + class_conf = np.max( + image_pred[:, 5 : 5 + self.num_classes], axis=1, keepdims=True + ) + class_pred = np.argmax(image_pred[:, 5 : 5 + self.num_classes], axis=1) + class_pred = np.expand_dims(class_pred, axis=1) + + conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= 0.3).squeeze() + # Detections ordered as (x1, y1, x2, y2, obj_conf, class_conf, class_pred) + detections = np.concatenate( + (image_pred[:, :5], class_conf, class_pred), axis=1 + ) + detections = detections[conf_mask] + + ordered = detections[detections[:, 5].argsort()[::-1]][:20] + + for i, object_detected in enumerate(ordered): + detections[i] = self.process_yolo( + object_detected[6], object_detected[5], object_detected[:4] + ) + return detections diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/rknn.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/rknn.py new file mode 100644 index 0000000..7018682 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/rknn.py @@ -0,0 +1,316 @@ +import logging +import os.path +import re +import urllib.request +from typing import Literal + +import cv2 +import numpy as np +from pydantic import Field + +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detection_runners import RKNNModelRunner +from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum +from frigate.util.model import post_process_yolo +from frigate.util.rknn_converter import auto_convert_model + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "rknn" + +supported_socs = ["rk3562", "rk3566", "rk3568", "rk3576", "rk3588"] + +supported_models = { + ModelTypeEnum.yologeneric: "^frigate-fp16-yolov9-[cemst]$", + ModelTypeEnum.yolonas: "^deci-fp16-yolonas_[sml]$", + ModelTypeEnum.yolox: "^rock-(fp16|i8)-yolox_(nano|tiny)$", +} + +model_cache_dir = os.path.join(MODEL_CACHE_DIR, "rknn_cache/") + + +class RknnDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + num_cores: int = Field(default=0, ge=0, le=3, title="Number of NPU cores to use.") + + +class Rknn(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, config: RknnDetectorConfig): + super().__init__(config) + self.height = config.model.height + self.width = config.model.width + core_mask = 2**config.num_cores - 1 + soc = self.get_soc() + + model_path = config.model.path or "deci-fp16-yolonas_s" + + model_props = self.parse_model_input(model_path, soc) + + if self.detector_config.model.model_type == ModelTypeEnum.yolox: + self.calculate_grids_strides(expanded=False) + + if model_props["preset"]: + config.model.model_type = model_props["model_type"] + + if model_props["model_type"] == ModelTypeEnum.yolonas: + logger.info( + "You are using yolo-nas with weights from DeciAI. " + "These weights are subject to their license and can't be used commercially. " + "For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html" + ) + + self.runner = RKNNModelRunner( + model_path=model_props["path"], + model_type=config.model.model_type.value + if config.model.model_type + else None, + core_mask=core_mask, + ) + + def __del__(self): + if hasattr(self, "runner") and self.runner: + # The runner's __del__ method will handle cleanup + pass + + def get_soc(self): + try: + with open("/proc/device-tree/compatible") as file: + soc = file.read().split(",")[-1].strip("\x00") + except FileNotFoundError: + raise Exception("Make sure to run docker in privileged mode.") + + if soc not in supported_socs: + raise Exception( + f"Your SoC is not supported. Your SoC is: {soc}. Currently these SoCs are supported: {supported_socs}." + ) + + return soc + + def parse_model_input(self, model_path, soc): + model_props = {} + + # find out if user provides his own model + # user provided models should be a path and contain a "/" + if "/" in model_path: + model_props["preset"] = False + + # Check if this is an ONNX model or model without extension that needs conversion + if model_path.endswith(".onnx") or not os.path.splitext(model_path)[1]: + # Try to auto-convert to RKNN format + logger.info( + f"Attempting to auto-convert {model_path} to RKNN format..." + ) + + # Determine model type from config + model_type = self.detector_config.model.model_type + + # Convert enum to string if needed + model_type_str = model_type.value if model_type else None + + # Auto-convert the model + converted_path = auto_convert_model(model_path, model_type_str) + + if converted_path: + model_props["path"] = converted_path + logger.info(f"Successfully converted model to: {converted_path}") + else: + # Fall back to original path if conversion fails + logger.warning( + f"Failed to convert {model_path} to RKNN format, using original path" + ) + model_props["path"] = model_path + else: + model_props["path"] = model_path + else: + model_props["preset"] = True + + """ + Filenames follow this pattern: + origin-quant-basename-soc-tk_version-rev.rknn + origin: From where comes the model? default: upstream repo; rknn: modifications from airockchip + quant: i8 or fp16 + basename: e.g. yolonas_s + soc: e.g. rk3588 + tk_version: e.g. v2.0.0 + rev: e.g. 1 + + Full name could be: default-fp16-yolonas_s-rk3588-v2.0.0-1.rknn + """ + + model_matched = False + + for model_type, pattern in supported_models.items(): + if re.match(pattern, model_path): + model_matched = True + model_props["model_type"] = model_type + + if model_matched: + model_props["filename"] = model_path + f"-{soc}-v2.3.2-2.rknn" + + model_props["path"] = model_cache_dir + model_props["filename"] + + if not os.path.isfile(model_props["path"]): + self.download_model(model_props["filename"]) + else: + supported_models_str = ", ".join( + model[1:-1] for model in supported_models + ) + raise Exception( + f"Model {model_path} is unsupported. Provide your own model or choose one of the following: {supported_models_str}" + ) + + return model_props + + def download_model(self, filename): + if not os.path.isdir(model_cache_dir): + os.mkdir(model_cache_dir) + + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + urllib.request.urlretrieve( + f"{GITHUB_ENDPOINT}/MarcA711/rknn-models/releases/download/v2.3.2-2/{filename}", + model_cache_dir + filename, + ) + + def post_process_yolonas(self, output: list[np.ndarray]): + """ + @param output: output of inference + expected shape: [np.array(1, N, 4), np.array(1, N, 80)] + where N depends on the input size e.g. N=2100 for 320x320 images + + @return: best results: np.array(20, 6) where each row is + in this order (class_id, score, y1/height, x1/width, y2/height, x2/width) + """ + + N = output[0].shape[1] + + boxes = output[0].reshape(N, 4) + scores = output[1].reshape(N, 80) + + class_ids = np.argmax(scores, axis=1) + scores = scores[np.arange(N), class_ids] + + args_best = np.argwhere(scores > self.thresh)[:, 0] + + num_matches = len(args_best) + if num_matches == 0: + return np.zeros((20, 6), np.float32) + elif num_matches > 20: + args_best20 = np.argpartition(scores[args_best], -20)[-20:] + args_best = args_best[args_best20] + + boxes = boxes[args_best] + class_ids = class_ids[args_best] + scores = scores[args_best] + + boxes = np.transpose( + np.vstack( + ( + boxes[:, 1] / self.height, + boxes[:, 0] / self.width, + boxes[:, 3] / self.height, + boxes[:, 2] / self.width, + ) + ) + ) + + results = np.hstack( + (class_ids[..., np.newaxis], scores[..., np.newaxis], boxes) + ) + + return np.resize(results, (20, 6)) + + def post_process_yolox( + self, + predictions: list[np.ndarray], + grids: np.ndarray, + expanded_strides: np.ndarray, + ) -> np.ndarray: + def sp_flatten(_in: np.ndarray): + ch = _in.shape[1] + _in = _in.transpose(0, 2, 3, 1) + return _in.reshape(-1, ch) + + boxes, scores, classes_conf = [], [], [] + + input_data = [ + _in.reshape([1, -1] + list(_in.shape[-2:])) for _in in predictions + ] + + for i in range(len(input_data)): + unprocessed_box = input_data[i][:, :4, :, :] + box_xy = unprocessed_box[:, :2, :, :] + box_wh = np.exp(unprocessed_box[:, 2:4, :, :]) * expanded_strides[i] + + box_xy += grids[i] + box_xy *= expanded_strides[i] + box = np.concatenate((box_xy, box_wh), axis=1) + + # Convert [c_x, c_y, w, h] to [x1, y1, x2, y2] + xyxy = np.copy(box) + xyxy[:, 0, :, :] = box[:, 0, :, :] - box[:, 2, :, :] / 2 # top left x + xyxy[:, 1, :, :] = box[:, 1, :, :] - box[:, 3, :, :] / 2 # top left y + xyxy[:, 2, :, :] = box[:, 0, :, :] + box[:, 2, :, :] / 2 # bottom right x + xyxy[:, 3, :, :] = box[:, 1, :, :] + box[:, 3, :, :] / 2 # bottom right y + + boxes.append(xyxy) + scores.append(input_data[i][:, 4:5, :, :]) + classes_conf.append(input_data[i][:, 5:, :, :]) + + # flatten data + boxes = np.concatenate([sp_flatten(_v) for _v in boxes]) + classes_conf = np.concatenate([sp_flatten(_v) for _v in classes_conf]) + scores = np.concatenate([sp_flatten(_v) for _v in scores]) + + # reshape and filter boxes + box_confidences = scores.reshape(-1) + class_max_score = np.max(classes_conf, axis=-1) + classes = np.argmax(classes_conf, axis=-1) + _class_pos = np.where(class_max_score * box_confidences >= 0.4) + scores = (class_max_score * box_confidences)[_class_pos] + boxes = boxes[_class_pos] + classes = classes[_class_pos] + + # run nms + indices = cv2.dnn.NMSBoxes( + bboxes=boxes, + scores=scores, + score_threshold=0.4, + nms_threshold=0.4, + ) + + results = np.zeros((20, 6), np.float32) + + if len(indices) > 0: + for i, idx in enumerate(indices.flatten()[:20]): + box = boxes[idx] + results[i] = [ + classes[idx], + scores[idx], + box[1] / self.height, + box[0] / self.width, + box[3] / self.height, + box[2] / self.width, + ] + + return results + + def post_process(self, output): + if self.detector_config.model.model_type == ModelTypeEnum.yolonas: + return self.post_process_yolonas(output) + elif self.detector_config.model.model_type == ModelTypeEnum.yologeneric: + return post_process_yolo(output, self.width, self.height) + elif self.detector_config.model.model_type == ModelTypeEnum.yolox: + return self.post_process_yolox(output, self.grids, self.expanded_strides) + else: + raise ValueError( + f'Model type "{self.detector_config.model.model_type}" is currently not supported.' + ) + + def detect_raw(self, tensor_input): + # Prepare input for the runner + inputs = {"input": tensor_input} + output = self.runner.run(inputs) + return self.post_process(output) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/synaptics.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/synaptics.py new file mode 100644 index 0000000..6181b16 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/synaptics.py @@ -0,0 +1,103 @@ +import logging +import os + +import numpy as np +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + InputTensorEnum, + ModelTypeEnum, +) + +try: + from synap import Network + from synap.postprocessor import Detector + from synap.preprocessor import Preprocessor + from synap.types import Layout, Shape + + SYNAP_SUPPORT = True +except ImportError: + SYNAP_SUPPORT = False + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "synaptics" + + +class SynapDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + + +class SynapDetector(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: SynapDetectorConfig): + if not SYNAP_SUPPORT: + logger.error( + "Error importing Synaptics SDK modules. You must use the -synaptics Docker image variant for Synaptics detector support." + ) + return + + try: + _, ext = os.path.splitext(detector_config.model.path) + if ext and ext != ".synap": + raise ValueError("Model path config for Synap1680 is incorrect.") + + synap_network = Network(detector_config.model.path) + logger.info(f"Synap NPU loaded model: {detector_config.model.path}") + except ValueError as ve: + logger.error(f"Synap1680 setup has failed: {ve}") + raise + except Exception as e: + logger.error(f"Failed to init Synap NPU: {e}") + raise + + self.width = detector_config.model.width + self.height = detector_config.model.height + self.model_type = detector_config.model.model_type + self.network = synap_network + self.network_input_details = self.network.inputs[0] + self.input_tensor_layout = detector_config.model.input_tensor + + # Create Inference Engine + self.preprocessor = Preprocessor() + self.detector = Detector(score_threshold=0.4, iou_threshold=0.4) + + def detect_raw(self, tensor_input: np.ndarray): + # It has only been testing for pre-converted mobilenet80 .tflite -> .synap model currently + layout = Layout.nhwc # default layout + detections = np.zeros((20, 6), np.float32) + + if self.input_tensor_layout == InputTensorEnum.nhwc: + layout = Layout.nhwc + + postprocess_data = self.preprocessor.assign( + self.network.inputs, tensor_input, Shape(tensor_input.shape), layout + ) + output_tensor_obj = self.network.predict() + output = self.detector.process(output_tensor_obj, postprocess_data) + + if self.model_type == ModelTypeEnum.ssd: + for i, item in enumerate(output.items): + if i == 20: + break + + bb = item.bounding_box + # Convert corner coordinates to normalized [0,1] range + x1 = bb.origin.x / self.width # Top-left X + y1 = bb.origin.y / self.height # Top-left Y + x2 = (bb.origin.x + bb.size.x) / self.width # Bottom-right X + y2 = (bb.origin.y + bb.size.y) / self.height # Bottom-right Y + detections[i] = [ + item.class_index, + float(item.confidence), + y1, + x1, + y2, + x2, + ] + else: + logger.error(f"Unsupported model type: {self.model_type}") + return detections diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/teflon_tfl.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/teflon_tfl.py new file mode 100644 index 0000000..7e29d66 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/teflon_tfl.py @@ -0,0 +1,38 @@ +import logging + +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +from ..detector_utils import ( + tflite_detect_raw, + tflite_init, + tflite_load_delegate_interpreter, +) + +logger = logging.getLogger(__name__) + +# Use _tfl suffix to default tflite model +DETECTOR_KEY = "teflon_tfl" + + +class TeflonDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + + +class TeflonTfl(DetectionApi): + type_key = DETECTOR_KEY + + def __init__(self, detector_config: TeflonDetectorConfig): + # Location in Debian's mesa-teflon-delegate + delegate_library = "/usr/lib/teflon/libteflon.so" + device_config = {} + + interpreter = tflite_load_delegate_interpreter( + delegate_library, detector_config, device_config + ) + tflite_init(self, interpreter) + + def detect_raw(self, tensor_input): + return tflite_detect_raw(self, tensor_input) diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/tensorrt.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/tensorrt.py new file mode 100644 index 0000000..bf0eb6f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/tensorrt.py @@ -0,0 +1,346 @@ +import ctypes +import logging +import platform + +import numpy as np + +try: + import tensorrt as trt + from cuda import cuda + + TRT_VERSION = int(trt.__version__[0 : trt.__version__.find(".")]) + + TRT_SUPPORT = True +except ModuleNotFoundError: + TRT_SUPPORT = False + +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "tensorrt" + +if TRT_SUPPORT: + + class TrtLogger(trt.ILogger): + def log(self, severity, msg): + logger.log(self.getSeverity(severity), msg) + + def getSeverity(self, sev: trt.ILogger.Severity) -> int: + if sev == trt.ILogger.VERBOSE: + return logging.DEBUG + elif sev == trt.ILogger.INFO: + return logging.INFO + elif sev == trt.ILogger.WARNING: + return logging.WARNING + elif sev == trt.ILogger.ERROR: + return logging.ERROR + elif sev == trt.ILogger.INTERNAL_ERROR: + return logging.CRITICAL + else: + return logging.DEBUG + + +class TensorRTDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + device: int = Field(default=0, title="GPU Device Index") + + +class HostDeviceMem(object): + """Simple helper data class that's a little nicer to use than a 2-tuple.""" + + def __init__(self, host_mem, device_mem, nbytes, size): + self.host = host_mem + err, self.host_dev = cuda.cuMemHostGetDevicePointer(self.host, 0) + self.device = device_mem + self.nbytes = nbytes + self.size = size + + def __str__(self): + return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device) + + def __repr__(self): + return self.__str__() + + def __del__(self): + cuda.cuMemFreeHost(self.host) + cuda.cuMemFree(self.device) + + +class TensorRtDetector(DetectionApi): + type_key = DETECTOR_KEY + + def _load_engine(self, model_path): + try: + trt.init_libnvinfer_plugins(self.trt_logger, "") + + ctypes.cdll.LoadLibrary("/usr/local/lib/libyolo_layer.so") + except OSError as e: + logger.error( + "ERROR: failed to load libraries. %s", + e, + ) + + with open(model_path, "rb") as f, trt.Runtime(self.trt_logger) as runtime: + return runtime.deserialize_cuda_engine(f.read()) + + def _binding_is_input(self, binding): + if TRT_VERSION < 10: + return self.engine.binding_is_input(binding) + else: + return binding == "input" + + def _get_binding_dims(self, binding): + if TRT_VERSION < 10: + return self.engine.get_binding_shape(binding) + else: + return self.engine.get_tensor_shape(binding) + + def _get_binding_dtype(self, binding): + if TRT_VERSION < 10: + return self.engine.get_binding_dtype(binding) + else: + return self.engine.get_tensor_dtype(binding) + + def _execute(self): + if TRT_VERSION < 10: + return self.context.execute_async_v2( + bindings=self.bindings, stream_handle=self.stream + ) + else: + return self.context.execute_v2(self.bindings) + + def _get_input_shape(self): + """Get input shape of the TensorRT YOLO engine.""" + binding = self.engine[0] + assert self._binding_is_input(binding) + binding_dims = self._get_binding_dims(binding) + if len(binding_dims) == 4: + return ( + tuple(binding_dims[2:]), + trt.nptype(self._get_binding_dtype(binding)), + ) + elif len(binding_dims) == 3: + return ( + tuple(binding_dims[1:]), + trt.nptype(self._get_binding_dtype(binding)), + ) + else: + raise ValueError( + "bad dims of binding %s: %s" % (binding, str(binding_dims)) + ) + + def _allocate_buffers(self): + """Allocates all host/device in/out buffers required for an engine.""" + inputs = [] + outputs = [] + bindings = [] + output_idx = 0 + for binding in self.engine: + binding_dims = self._get_binding_dims(binding) + if len(binding_dims) == 4: + # explicit batch case (TensorRT 7+) + size = trt.volume(binding_dims) + elif len(binding_dims) == 3: + # implicit batch case (TensorRT 6 or older) + size = trt.volume(binding_dims) * self.engine.max_batch_size + else: + raise ValueError( + "bad dims of binding %s: %s" % (binding, str(binding_dims)) + ) + nbytes = size * self._get_binding_dtype(binding).itemsize + # Allocate host and device buffers + err, host_mem = cuda.cuMemHostAlloc( + nbytes, Flags=cuda.CU_MEMHOSTALLOC_DEVICEMAP + ) + assert err is cuda.CUresult.CUDA_SUCCESS, f"cuMemAllocHost returned {err}" + logger.debug( + f"Allocated Tensor Binding {binding} Memory {nbytes} Bytes ({size} * {self._get_binding_dtype(binding)})" + ) + err, device_mem = cuda.cuMemAlloc(nbytes) + assert err is cuda.CUresult.CUDA_SUCCESS, f"cuMemAlloc returned {err}" + # Append the device buffer to device bindings. + bindings.append(int(device_mem)) + # Append to the appropriate list. + if self._binding_is_input(binding): + logger.debug(f"Input has Shape {binding_dims}") + inputs.append(HostDeviceMem(host_mem, device_mem, nbytes, size)) + else: + # each grid has 3 anchors, each anchor generates a detection + # output of 7 float32 values + assert size % 7 == 0, f"output size was {size}" + logger.debug(f"Output has Shape {binding_dims}") + outputs.append(HostDeviceMem(host_mem, device_mem, nbytes, size)) + output_idx += 1 + assert len(inputs) == 1, f"inputs len was {len(inputs)}" + assert len(outputs) == 1, f"output len was {len(outputs)}" + return inputs, outputs, bindings + + def _do_inference(self): + """do_inference (for TensorRT 7.0+) + This function is generalized for multiple inputs/outputs for full + dimension networks. + Inputs and outputs are expected to be lists of HostDeviceMem objects. + """ + # Push CUDA Context + cuda.cuCtxPushCurrent(self.cu_ctx) + + # Transfer input data to the GPU. + [ + cuda.cuMemcpyHtoDAsync(inp.device, inp.host, inp.nbytes, self.stream) + for inp in self.inputs + ] + + # Run inference. + if not self._execute(): + logger.warning("Execute returned false") + + # Transfer predictions back from the GPU. + [ + cuda.cuMemcpyDtoHAsync(out.host, out.device, out.nbytes, self.stream) + for out in self.outputs + ] + + # Synchronize the stream + cuda.cuStreamSynchronize(self.stream) + + # Pop CUDA Context + cuda.cuCtxPopCurrent() + + # Return only the host outputs. + return [ + np.array( + (ctypes.c_float * out.size).from_address(out.host), dtype=np.float32 + ) + for out in self.outputs + ] + + def __init__(self, detector_config: TensorRTDetectorConfig): + if platform.machine() == "x86_64": + logger.error( + "TensorRT detector is no longer supported on amd64 system. Please use ONNX detector instead, see https://docs.frigate.video/configuration/object_detectors#onnx for more information." + ) + raise ImportError( + "TensorRT detector is no longer supported on amd64 system. Please use ONNX detector instead, see https://docs.frigate.video/configuration/object_detectors#onnx for more information." + ) + + assert TRT_SUPPORT, ( + f"TensorRT libraries not found, {DETECTOR_KEY} detector not present" + ) + + (cuda_err,) = cuda.cuInit(0) + assert cuda_err == cuda.CUresult.CUDA_SUCCESS, ( + f"Failed to initialize cuda {cuda_err}" + ) + err, dev_count = cuda.cuDeviceGetCount() + logger.debug(f"Num Available Devices: {dev_count}") + assert detector_config.device < dev_count, ( + f"Invalid TensorRT Device Config. Device {detector_config.device} Invalid." + ) + err, self.cu_ctx = cuda.cuCtxCreate( + cuda.CUctx_flags.CU_CTX_MAP_HOST, detector_config.device + ) + + self.conf_th = 0.4 ##TODO: model config parameter + self.nms_threshold = 0.4 + err, self.stream = cuda.cuStreamCreate(0) + self.trt_logger = TrtLogger() + self.engine = self._load_engine(detector_config.model.path) + self.input_shape = self._get_input_shape() + + try: + self.context = self.engine.create_execution_context() + ( + self.inputs, + self.outputs, + self.bindings, + ) = self._allocate_buffers() + except Exception as e: + logger.error(e) + raise RuntimeError("fail to allocate CUDA resources") from e + + logger.debug("TensorRT loaded. Input shape is %s", self.input_shape) + logger.debug("TensorRT version is %s", TRT_VERSION) + + def __del__(self): + """Free CUDA memories.""" + if self.outputs is not None: + del self.outputs + if self.inputs is not None: + del self.inputs + if self.stream is not None: + cuda.cuStreamDestroy(self.stream) + del self.stream + del self.engine + del self.context + del self.trt_logger + cuda.cuCtxDestroy(self.cu_ctx) + + def _postprocess_yolo(self, trt_outputs, conf_th): + """Postprocess TensorRT outputs. + # Args + trt_outputs: a list of 2 or 3 tensors, where each tensor + contains a multiple of 7 float32 numbers in + the order of [x, y, w, h, box_confidence, class_id, class_prob] + conf_th: confidence threshold + # Returns + boxes, scores, classes + """ + # filter low-conf detections and concatenate results of all yolo layers + detection_list = [] + for o in trt_outputs: + detections = o.reshape((-1, 7)) + detections = detections[detections[:, 4] * detections[:, 6] >= conf_th] + detection_list.append(detections) + detection_list = np.concatenate(detection_list, axis=0) + + return detection_list + + def detect_raw(self, tensor_input): + # Input tensor has the shape of the [height, width, 3] + # Output tensor of float32 of shape [20, 6] where: + # O - class id + # 1 - score + # 2..5 - a value between 0 and 1 of the box: [top, left, bottom, right] + + # normalize + if self.input_shape[-1] != trt.int8: + tensor_input = tensor_input.astype(self.input_shape[-1]) + tensor_input /= 255.0 + + self.inputs[0].host = np.ascontiguousarray( + tensor_input.astype(self.input_shape[-1]) + ) + trt_outputs = self._do_inference() + + raw_detections = self._postprocess_yolo(trt_outputs, self.conf_th) + + if len(raw_detections) == 0: + return np.zeros((20, 6), np.float32) + + # raw_detections: Nx7 numpy arrays of + # [[x, y, w, h, box_confidence, class_id, class_prob], + + # Calculate score as box_confidence x class_prob + raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6] + # Reorder elements by the score, best on top, remove class_prob + ordered = raw_detections[raw_detections[:, 4].argsort()[::-1]][:, 0:6] + # transform width to right with clamp to 0..1 + ordered[:, 2] = np.clip(ordered[:, 2] + ordered[:, 0], 0, 1) + # transform height to bottom with clamp to 0..1 + ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1) + # put result into the correct order and limit to top 20 + detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20] + + # pad to 20x6 shape + append_cnt = 20 - len(detections) + if append_cnt > 0: + detections = np.append( + detections, np.zeros((append_cnt, 6), np.float32), axis=0 + ) + + return detections diff --git a/sam2-cpu/frigate-dev/frigate/detectors/plugins/zmq_ipc.py b/sam2-cpu/frigate-dev/frigate/detectors/plugins/zmq_ipc.py new file mode 100644 index 0000000..cd397ae --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/detectors/plugins/zmq_ipc.py @@ -0,0 +1,331 @@ +import json +import logging +import os +from typing import Any, List + +import numpy as np +import zmq +from pydantic import Field +from typing_extensions import Literal + +from frigate.detectors.detection_api import DetectionApi +from frigate.detectors.detector_config import BaseDetectorConfig + +logger = logging.getLogger(__name__) + +DETECTOR_KEY = "zmq" + + +class ZmqDetectorConfig(BaseDetectorConfig): + type: Literal[DETECTOR_KEY] + endpoint: str = Field( + default="ipc:///tmp/cache/zmq_detector", title="ZMQ IPC endpoint" + ) + request_timeout_ms: int = Field( + default=200, title="ZMQ request timeout in milliseconds" + ) + linger_ms: int = Field(default=0, title="ZMQ socket linger in milliseconds") + + +class ZmqIpcDetector(DetectionApi): + """ + ZMQ-based detector plugin using a REQ/REP socket over an IPC endpoint. + + Protocol: + - Request is sent as a multipart message: + [ header_json_bytes, tensor_bytes ] + where header is a JSON object containing: + { + "shape": List[int], + "dtype": str, # numpy dtype string, e.g. "uint8", "float32" + } + tensor_bytes are the raw bytes of the numpy array in C-order. + + - Response is expected to be either: + a) Multipart [ header_json_bytes, tensor_bytes ] with header specifying + shape [20,6] and dtype "float32"; or + b) Single frame tensor_bytes of length 20*6*4 bytes (float32). + + On any error or timeout, this detector returns a zero array of shape (20, 6). + + Model Management: + - On initialization, sends model request to check if model is available + - If model not available, sends model data via ZMQ + - Only starts inference after model is ready + """ + + type_key = DETECTOR_KEY + + def __init__(self, detector_config: ZmqDetectorConfig): + super().__init__(detector_config) + + self._context = zmq.Context() + self._endpoint = detector_config.endpoint + self._request_timeout_ms = detector_config.request_timeout_ms + self._linger_ms = detector_config.linger_ms + self._socket = None + self._create_socket() + + # Model management + self._model_ready = False + self._model_name = self._get_model_name() + + # Initialize model if needed + self._initialize_model() + + # Preallocate zero result for error paths + self._zero_result = np.zeros((20, 6), np.float32) + + def _create_socket(self) -> None: + if self._socket is not None: + try: + self._socket.close(linger=self._linger_ms) + except Exception: + pass + self._socket = self._context.socket(zmq.REQ) + # Apply timeouts and linger so calls don't block indefinitely + self._socket.setsockopt(zmq.RCVTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.SNDTIMEO, self._request_timeout_ms) + self._socket.setsockopt(zmq.LINGER, self._linger_ms) + + logger.debug(f"ZMQ detector connecting to {self._endpoint}") + self._socket.connect(self._endpoint) + + def _get_model_name(self) -> str: + """Get the model filename from the detector config.""" + model_path = self.detector_config.model.path + return os.path.basename(model_path) + + def _initialize_model(self) -> None: + """Initialize the model by checking availability and transferring if needed.""" + try: + logger.info(f"Initializing model: {self._model_name}") + + # Check if model is available and transfer if needed + if self._check_and_transfer_model(): + logger.info(f"Model {self._model_name} is ready") + self._model_ready = True + else: + logger.error(f"Failed to initialize model {self._model_name}") + + except Exception as e: + logger.error(f"Failed to initialize model: {e}") + + def _check_and_transfer_model(self) -> bool: + """Check if model is available and transfer if needed in one atomic operation.""" + try: + # Send model availability request + header = {"model_request": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes]) + + # Temporarily increase timeout for model operations + original_timeout = self._socket.getsockopt(zmq.RCVTIMEO) + self._socket.setsockopt(zmq.RCVTIMEO, 30000) + + try: + response_frames = self._socket.recv_multipart() + finally: + self._socket.setsockopt(zmq.RCVTIMEO, original_timeout) + + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_available = response.get("model_available", False) + model_loaded = response.get("model_loaded", False) + + if model_available and model_loaded: + return True + elif model_available and not model_loaded: + logger.error("Model exists but failed to load") + return False + else: + return self._send_model_data() + + except json.JSONDecodeError: + logger.warning( + "Received non-JSON response for model availability check" + ) + return False + else: + logger.warning( + "Received unexpected response format for model availability check" + ) + return False + + except Exception as e: + logger.error(f"Failed to check and transfer model: {e}") + return False + + def _check_model_availability(self) -> bool: + """Check if the model is available on the detector.""" + try: + # Send model availability request + header = {"model_request": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes]) + + # Receive response + response_frames = self._socket.recv_multipart() + + # Check if this is a JSON response (model management) + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_available = response.get("model_available", False) + model_loaded = response.get("model_loaded", False) + logger.debug( + f"Model availability check: available={model_available}, loaded={model_loaded}" + ) + return model_available and model_loaded + except json.JSONDecodeError: + logger.warning( + "Received non-JSON response for model availability check" + ) + return False + else: + logger.warning( + "Received unexpected response format for model availability check" + ) + return False + + except Exception as e: + logger.error(f"Failed to check model availability: {e}") + return False + + def _send_model_data(self) -> bool: + """Send model data to the detector.""" + try: + model_path = self.detector_config.model.path + + if not os.path.exists(model_path): + logger.error(f"Model file not found: {model_path}") + return False + + logger.info(f"Transferring model to detector: {self._model_name}") + with open(model_path, "rb") as f: + model_data = f.read() + + header = {"model_data": True, "model_name": self._model_name} + header_bytes = json.dumps(header).encode("utf-8") + + self._socket.send_multipart([header_bytes, model_data]) + + # Temporarily increase timeout for model loading (can take several seconds) + original_timeout = self._socket.getsockopt(zmq.RCVTIMEO) + self._socket.setsockopt(zmq.RCVTIMEO, 30000) + + try: + # Receive response + response_frames = self._socket.recv_multipart() + finally: + # Restore original timeout + self._socket.setsockopt(zmq.RCVTIMEO, original_timeout) + + # Check if this is a JSON response (model management) + if len(response_frames) == 1: + try: + response = json.loads(response_frames[0].decode("utf-8")) + model_saved = response.get("model_saved", False) + model_loaded = response.get("model_loaded", False) + if model_saved and model_loaded: + logger.info( + f"Model {self._model_name} transferred and loaded successfully" + ) + else: + logger.error( + f"Model transfer failed: saved={model_saved}, loaded={model_loaded}" + ) + return model_saved and model_loaded + except json.JSONDecodeError: + logger.warning("Received non-JSON response for model data transfer") + return False + else: + logger.warning( + "Received unexpected response format for model data transfer" + ) + return False + + except Exception as e: + logger.error(f"Failed to send model data: {e}") + return False + + def _build_header(self, tensor_input: np.ndarray) -> bytes: + header: dict[str, Any] = { + "shape": list(tensor_input.shape), + "dtype": str(tensor_input.dtype.name), + "model_type": str(self.detector_config.model.model_type.name), + } + return json.dumps(header).encode("utf-8") + + def _decode_response(self, frames: List[bytes]) -> np.ndarray: + try: + if len(frames) == 1: + # Single-frame raw float32 (20x6) + buf = frames[0] + if len(buf) != 20 * 6 * 4: + logger.warning( + f"ZMQ detector received unexpected payload size: {len(buf)}" + ) + return self._zero_result + return np.frombuffer(buf, dtype=np.float32).reshape((20, 6)) + + if len(frames) >= 2: + header = json.loads(frames[0].decode("utf-8")) + shape = tuple(header.get("shape", [])) + dtype = np.dtype(header.get("dtype", "float32")) + return np.frombuffer(frames[1], dtype=dtype).reshape(shape) + + logger.warning("ZMQ detector received empty reply") + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector failed to decode response: {exc}") + return self._zero_result + + def detect_raw(self, tensor_input: np.ndarray) -> np.ndarray: + if not self._model_ready: + logger.warning("Model not ready, returning zero detections") + return self._zero_result + + try: + header_bytes = self._build_header(tensor_input) + payload_bytes = memoryview(tensor_input.tobytes(order="C")) + + # Send request + self._socket.send_multipart([header_bytes, payload_bytes]) + + # Receive reply + reply_frames = self._socket.recv_multipart() + detections = self._decode_response(reply_frames) + + # Ensure output shape and dtype are exactly as expected + return detections + except zmq.Again: + # Timeout + logger.debug("ZMQ detector request timed out; resetting socket") + try: + self._create_socket() + self._initialize_model() + except Exception: + pass + return self._zero_result + except zmq.ZMQError as exc: + logger.error(f"ZMQ detector ZMQError: {exc}; resetting socket") + try: + self._create_socket() + self._initialize_model() + except Exception: + pass + return self._zero_result + except Exception as exc: # noqa: BLE001 + logger.error(f"ZMQ detector unexpected error: {exc}") + return self._zero_result + + def __del__(self) -> None: # pragma: no cover - best-effort cleanup + try: + if self._socket is not None: + self._socket.close(linger=self.detector_config.linger_ms) + except Exception: + pass diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/__init__.py b/sam2-cpu/frigate-dev/frigate/embeddings/__init__.py new file mode 100644 index 0000000..0a854fc --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/__init__.py @@ -0,0 +1,321 @@ +"""SQLite-vec embeddings database.""" + +import base64 +import json +import logging +import os +import threading +from json.decoder import JSONDecodeError +from multiprocessing.synchronize import Event as MpEvent +from typing import Any, Union + +import regex +from pathvalidate import ValidationError, sanitize_filename + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor +from frigate.config import FrigateConfig +from frigate.const import CONFIG_DIR, FACE_DIR, PROCESS_PRIORITY_HIGH +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event +from frigate.util.builtin import serialize +from frigate.util.classification import kickoff_model_training +from frigate.util.process import FrigateProcess + +from .maintainer import EmbeddingMaintainer +from .util import ZScoreNormalization + +logger = logging.getLogger(__name__) + + +class EmbeddingProcess(FrigateProcess): + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics | None, + stop_event: MpEvent, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name="frigate.embeddings_manager", + daemon=True, + ) + self.config = config + self.metrics = metrics + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = EmbeddingMaintainer( + self.config, + self.metrics, + self.stop_event, + ) + maintainer.start() + + +class EmbeddingsContext: + def __init__(self, db: SqliteVecQueueDatabase): + self.db = db + self.thumb_stats = ZScoreNormalization() + self.desc_stats = ZScoreNormalization() + self.requestor = EmbeddingsRequestor() + + # load stats from disk + stats_file = os.path.join(CONFIG_DIR, ".search_stats.json") + try: + with open(stats_file, "r") as f: + data = json.loads(f.read()) + self.thumb_stats.from_dict(data["thumb_stats"]) + self.desc_stats.from_dict(data["desc_stats"]) + except FileNotFoundError: + pass + except JSONDecodeError: + logger.warning("Failed to decode semantic search stats, clearing file") + try: + with open(stats_file, "w") as f: + f.write("") + except OSError as e: + logger.error(f"Failed to clear corrupted stats file: {e}") + + def stop(self): + """Write the stats to disk as JSON on exit.""" + contents = { + "thumb_stats": self.thumb_stats.to_dict(), + "desc_stats": self.desc_stats.to_dict(), + } + with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "w") as f: + json.dump(contents, f) + self.requestor.stop() + + def search_thumbnail( + self, query: Union[Event, str], event_ids: list[str] = None + ) -> list[tuple[str, float]]: + if query.__class__ == Event: + cursor = self.db.execute_sql( + """ + SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ? + """, + [query.id], + ) + + row = cursor.fetchone() if cursor else None + + if row: + query_embedding = row[0] + else: + # If no embedding found, generate it and return it + data = self.requestor.send_data( + EmbeddingsRequestEnum.embed_thumbnail.value, + {"id": str(query.id), "thumbnail": str(query.thumbnail)}, + ) + + if not data: + return [] + + query_embedding = serialize(data) + else: + data = self.requestor.send_data( + EmbeddingsRequestEnum.generate_search.value, query + ) + + if not data: + return [] + + query_embedding = serialize(data) + + sql_query = """ + SELECT + id, + distance + FROM vec_thumbnails + WHERE thumbnail_embedding MATCH ? + AND k = 100 + """ + + # Add the IN clause if event_ids is provided and not empty + # this is the only filter supported by sqlite-vec as of 0.1.3 + # but it seems to be broken in this version + if event_ids: + sql_query += " AND id IN ({})".format(",".join("?" * len(event_ids))) + + # order by distance DESC is not implemented in this version of sqlite-vec + # when it's implemented, we can use cosine similarity + sql_query += " ORDER BY distance" + + parameters = [query_embedding] + event_ids if event_ids else [query_embedding] + + results = self.db.execute_sql(sql_query, parameters).fetchall() + + return results + + def search_description( + self, query_text: str, event_ids: list[str] = None + ) -> list[tuple[str, float]]: + data = self.requestor.send_data( + EmbeddingsRequestEnum.generate_search.value, query_text + ) + + if not data: + return [] + + query_embedding = serialize(data) + + # Prepare the base SQL query + sql_query = """ + SELECT + id, + distance + FROM vec_descriptions + WHERE description_embedding MATCH ? + AND k = 100 + """ + + # Add the IN clause if event_ids is provided and not empty + # this is the only filter supported by sqlite-vec as of 0.1.3 + # but it seems to be broken in this version + if event_ids: + sql_query += " AND id IN ({})".format(",".join("?" * len(event_ids))) + + # order by distance DESC is not implemented in this version of sqlite-vec + # when it's implemented, we can use cosine similarity + sql_query += " ORDER BY distance" + + parameters = [query_embedding] + event_ids if event_ids else [query_embedding] + + results = self.db.execute_sql(sql_query, parameters).fetchall() + + return results + + def register_face(self, face_name: str, image_data: bytes) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.register_face.value, + { + "face_name": face_name, + "image": base64.b64encode(image_data).decode("ASCII"), + }, + ) + + def recognize_face(self, image_data: bytes) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.recognize_face.value, + { + "image": base64.b64encode(image_data).decode("ASCII"), + }, + ) + + def get_face_ids(self, name: str) -> list[str]: + sql_query = f""" + SELECT + id + FROM vec_descriptions + WHERE id LIKE '%{name}%' + """ + + return self.db.execute_sql(sql_query).fetchall() + + def reprocess_face(self, face_file: str) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.reprocess_face.value, {"image_file": face_file} + ) + + def clear_face_classifier(self) -> None: + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def delete_face_ids(self, face: str, ids: list[str]) -> None: + folder = os.path.join(FACE_DIR, face) + for id in ids: + file_path = os.path.join(folder, id) + + if os.path.isfile(file_path): + os.unlink(file_path) + + if face != "train" and len(os.listdir(folder)) == 0: + os.rmdir(folder) + + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def rename_face(self, old_name: str, new_name: str) -> None: + valid_name_pattern = r"^[\p{L}\p{N}\s'_-]{1,50}$" + + try: + sanitized_old_name = sanitize_filename(old_name, replacement_text="_") + sanitized_new_name = sanitize_filename(new_name, replacement_text="_") + except ValidationError as e: + raise ValueError(f"Invalid face name: {str(e)}") + + if not regex.match(valid_name_pattern, old_name): + raise ValueError(f"Invalid old face name: {old_name}") + if not regex.match(valid_name_pattern, new_name): + raise ValueError(f"Invalid new face name: {new_name}") + if sanitized_old_name != old_name: + raise ValueError(f"Old face name contains invalid characters: {old_name}") + if sanitized_new_name != new_name: + raise ValueError(f"New face name contains invalid characters: {new_name}") + + old_path = os.path.normpath(os.path.join(FACE_DIR, old_name)) + new_path = os.path.normpath(os.path.join(FACE_DIR, new_name)) + + # Prevent path traversal + if not old_path.startswith( + os.path.normpath(FACE_DIR) + ) or not new_path.startswith(os.path.normpath(FACE_DIR)): + raise ValueError("Invalid path detected") + + if not os.path.exists(old_path): + raise ValueError(f"Face {old_name} not found.") + + os.rename(old_path, new_path) + + self.requestor.send_data( + EmbeddingsRequestEnum.clear_face_classifier.value, None + ) + + def update_description(self, event_id: str, description: str) -> None: + self.requestor.send_data( + EmbeddingsRequestEnum.embed_description.value, + {"id": event_id, "description": description}, + ) + + def reprocess_plate(self, event: dict[str, Any]) -> dict[str, Any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.reprocess_plate.value, {"event": event} + ) + + def reindex_embeddings(self) -> dict[str, Any]: + return self.requestor.send_data(EmbeddingsRequestEnum.reindex.value, {}) + + def start_classification_training(self, model_name: str) -> dict[str, Any]: + threading.Thread( + target=kickoff_model_training, + args=(self.requestor, model_name), + daemon=True, + ).start() + return {"success": True, "message": f"Began training {model_name} model."} + + def transcribe_audio(self, event: dict[str, any]) -> dict[str, any]: + return self.requestor.send_data( + EmbeddingsRequestEnum.transcribe_audio.value, {"event": event} + ) + + def generate_description_embedding(self, text: str) -> None: + return self.requestor.send_data( + EmbeddingsRequestEnum.embed_description.value, + {"id": None, "description": text, "upsert": False}, + ) + + def generate_image_embedding(self, event_id: str, thumbnail: bytes) -> None: + return self.requestor.send_data( + EmbeddingsRequestEnum.embed_thumbnail.value, + {"id": str(event_id), "thumbnail": str(thumbnail), "upsert": False}, + ) + + def generate_review_summary(self, start_ts: float, end_ts: float) -> str | None: + return self.requestor.send_data( + EmbeddingsRequestEnum.summarize_review.value, + {"start_ts": start_ts, "end_ts": end_ts}, + ) diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/embeddings.py b/sam2-cpu/frigate-dev/frigate/embeddings/embeddings.py new file mode 100644 index 0000000..8d7bcd2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/embeddings.py @@ -0,0 +1,643 @@ +"""SQLite-vec embeddings database.""" + +import datetime +import io +import logging +import os +import threading +import time + +import numpy as np +from peewee import DoesNotExist, IntegrityError +from PIL import Image +from playhouse.shortcuts import model_to_dict + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.config.classification import SemanticSearchModelEnum +from frigate.const import ( + CONFIG_DIR, + TRIGGER_DIR, + UPDATE_EMBEDDINGS_REINDEX_PROGRESS, + UPDATE_MODEL_STATE, +) +from frigate.data_processing.types import DataProcessorMetrics +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event, Trigger +from frigate.types import ModelStatusTypesEnum +from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize +from frigate.util.file import get_event_thumbnail_bytes + +from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding +from .onnx.jina_v2_embedding import JinaV2Embedding + +logger = logging.getLogger(__name__) + + +def get_metadata(event: Event) -> dict: + """Extract valid event metadata.""" + event_dict = model_to_dict(event) + return ( + { + k: v + for k, v in event_dict.items() + if k not in ["thumbnail"] + and v is not None + and isinstance(v, (str, int, float, bool)) + } + | { + k: v + for k, v in event_dict["data"].items() + if k not in ["description"] + and v is not None + and isinstance(v, (str, int, float, bool)) + } + | { + # Metadata search doesn't support $contains + # and an event can have multiple zones, so + # we need to create a key for each zone + f"{k}_{x}": True + for k, v in event_dict.items() + if isinstance(v, list) and len(v) > 0 + for x in v + if isinstance(x, str) + } + ) + + +class Embeddings: + """SQLite-vec embeddings database.""" + + def __init__( + self, + config: FrigateConfig, + db: SqliteVecQueueDatabase, + metrics: DataProcessorMetrics, + ) -> None: + self.config = config + self.db = db + self.metrics = metrics + self.requestor = InterProcessRequestor() + + self.image_inference_speed = InferenceSpeed(self.metrics.image_embeddings_speed) + self.image_eps = EventsPerSecond() + self.image_eps.start() + self.text_inference_speed = InferenceSpeed(self.metrics.text_embeddings_speed) + self.text_eps = EventsPerSecond() + self.text_eps.start() + + self.reindex_lock = threading.Lock() + self.reindex_thread = None + self.reindex_running = False + + # Create tables if they don't exist + self.db.create_embeddings_tables() + + models = self.get_model_definitions() + + for model in models: + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model, + "state": ModelStatusTypesEnum.not_downloaded, + }, + ) + + if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2: + # Single JinaV2Embedding instance for both text and vision + self.embedding = JinaV2Embedding( + model_size=self.config.semantic_search.model_size, + requestor=self.requestor, + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), + ) + self.text_embedding = lambda input_data: self.embedding( + input_data, embedding_type="text" + ) + self.vision_embedding = lambda input_data: self.embedding( + input_data, embedding_type="vision" + ) + else: # Default to jinav1 + self.text_embedding = JinaV1TextEmbedding( + model_size=config.semantic_search.model_size, + requestor=self.requestor, + device="CPU", + ) + self.vision_embedding = JinaV1ImageEmbedding( + model_size=config.semantic_search.model_size, + requestor=self.requestor, + device=config.semantic_search.device + or ("GPU" if config.semantic_search.model_size == "large" else "CPU"), + ) + + def update_stats(self) -> None: + self.metrics.image_embeddings_eps.value = self.image_eps.eps() + self.metrics.text_embeddings_eps.value = self.text_eps.eps() + + def get_model_definitions(self): + # Version-specific models + if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2: + models = [ + "jinaai/jina-clip-v2-tokenizer", + "jinaai/jina-clip-v2-model_fp16.onnx" + if self.config.semantic_search.model_size == "large" + else "jinaai/jina-clip-v2-model_quantized.onnx", + "jinaai/jina-clip-v2-preprocessor_config.json", + ] + else: # Default to jinav1 + models = [ + "jinaai/jina-clip-v1-text_model_fp16.onnx", + "jinaai/jina-clip-v1-tokenizer", + "jinaai/jina-clip-v1-vision_model_fp16.onnx" + if self.config.semantic_search.model_size == "large" + else "jinaai/jina-clip-v1-vision_model_quantized.onnx", + "jinaai/jina-clip-v1-preprocessor_config.json", + ] + + # Add common models + models.extend( + [ + "facenet-facenet.onnx", + "paddleocr-onnx-detection.onnx", + "paddleocr-onnx-classification.onnx", + "paddleocr-onnx-recognition.onnx", + ] + ) + + return models + + def embed_thumbnail( + self, event_id: str, thumbnail: bytes, upsert: bool = True + ) -> np.ndarray: + """Embed thumbnail and optionally insert into DB. + + @param: event_id in Events DB + @param: thumbnail bytes in jpg format + @param: upsert If embedding should be upserted into vec DB + """ + start = datetime.datetime.now().timestamp() + # Convert thumbnail bytes to PIL Image + embedding = self.vision_embedding([thumbnail])[0] + + if upsert: + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_thumbnails(id, thumbnail_embedding) + VALUES(?, ?) + """, + (event_id, serialize(embedding)), + ) + + self.image_inference_speed.update(datetime.datetime.now().timestamp() - start) + self.image_eps.update() + + return embedding + + def batch_embed_thumbnail( + self, event_thumbs: dict[str, bytes], upsert: bool = True + ) -> list[np.ndarray]: + """Embed thumbnails and optionally insert into DB. + + @param: event_thumbs Map of Event IDs in DB to thumbnail bytes in jpg format + @param: upsert If embedding should be upserted into vec DB + """ + start = datetime.datetime.now().timestamp() + valid_ids = [] + valid_thumbs = [] + for eid, thumb in event_thumbs.items(): + try: + img = Image.open(io.BytesIO(thumb)) + img.verify() # Will raise if corrupt + valid_ids.append(eid) + valid_thumbs.append(thumb) + except Exception as e: + logger.warning( + f"Embeddings reindexing: Skipping corrupt thumbnail for event {eid}: {e}" + ) + + if not valid_thumbs: + logger.warning( + "Embeddings reindexing: No valid thumbnails to embed in this batch." + ) + return [] + + embeddings = self.vision_embedding(valid_thumbs) + + if upsert: + items = [] + for i in range(len(valid_ids)): + items.append(valid_ids[i]) + items.append(serialize(embeddings[i])) + self.image_eps.update() + + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_thumbnails(id, thumbnail_embedding) + VALUES {} + """.format(", ".join(["(?, ?)"] * len(valid_ids))), + items, + ) + + duration = datetime.datetime.now().timestamp() - start + self.text_inference_speed.update(duration / len(valid_ids)) + + return embeddings + + def embed_description( + self, event_id: str, description: str, upsert: bool = True + ) -> np.ndarray: + start = datetime.datetime.now().timestamp() + embedding = self.text_embedding([description])[0] + + if upsert: + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_descriptions(id, description_embedding) + VALUES(?, ?) + """, + (event_id, serialize(embedding)), + ) + + self.text_inference_speed.update(datetime.datetime.now().timestamp() - start) + self.text_eps.update() + + return embedding + + def batch_embed_description( + self, event_descriptions: dict[str, str], upsert: bool = True + ) -> np.ndarray: + start = datetime.datetime.now().timestamp() + # upsert embeddings one by one to avoid token limit + embeddings = [] + + for desc in event_descriptions.values(): + embeddings.append(self.text_embedding([desc])[0]) + + if upsert: + ids = list(event_descriptions.keys()) + items = [] + + for i in range(len(ids)): + items.append(ids[i]) + items.append(serialize(embeddings[i])) + self.text_eps.update() + + self.db.execute_sql( + """ + INSERT OR REPLACE INTO vec_descriptions(id, description_embedding) + VALUES {} + """.format(", ".join(["(?, ?)"] * len(ids))), + items, + ) + + self.text_inference_speed.update(datetime.datetime.now().timestamp() - start) + + return embeddings + + def reindex(self) -> None: + logger.info("Indexing tracked object embeddings...") + + self.db.drop_embeddings_tables() + logger.debug("Dropped embeddings tables.") + self.db.create_embeddings_tables() + logger.debug("Created embeddings tables.") + + # Delete the saved stats file + if os.path.exists(os.path.join(CONFIG_DIR, ".search_stats.json")): + os.remove(os.path.join(CONFIG_DIR, ".search_stats.json")) + + st = time.time() + + # Get total count of events to process + total_events = Event.select().count() + + batch_size = ( + 4 + if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2 + else 32 + ) + current_page = 1 + + totals = { + "thumbnails": 0, + "descriptions": 0, + "processed_objects": total_events - 1 if total_events < batch_size else 0, + "total_objects": total_events, + "time_remaining": 0 if total_events < batch_size else -1, + "status": "indexing", + } + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + events = ( + Event.select() + .order_by(Event.start_time.desc()) + .paginate(current_page, batch_size) + ) + + while events: + event: Event + batch_thumbs = {} + batch_descs = {} + for event in events: + totals["processed_objects"] += 1 + + if description := event.data.get("description", "").strip(): + batch_descs[event.id] = description + totals["descriptions"] += 1 + + if thumbnail := get_event_thumbnail_bytes(event): + batch_thumbs[event.id] = thumbnail + totals["thumbnails"] += 1 + + # run batch embedding + if batch_thumbs: + self.batch_embed_thumbnail(batch_thumbs) + + if batch_descs: + self.batch_embed_description(batch_descs) + + # report progress every batch so we don't spam the logs + progress = (totals["processed_objects"] / total_events) * 100 + logger.debug( + "Processed %d/%d events (%.2f%% complete) | Thumbnails: %d, Descriptions: %d", + totals["processed_objects"], + total_events, + progress, + totals["thumbnails"], + totals["descriptions"], + ) + + # Calculate time remaining + elapsed_time = time.time() - st + avg_time_per_event = elapsed_time / totals["processed_objects"] + remaining_events = total_events - totals["processed_objects"] + time_remaining = avg_time_per_event * remaining_events + totals["time_remaining"] = int(time_remaining) + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + # Move to the next page + current_page += 1 + events = ( + Event.select() + .order_by(Event.start_time.desc()) + .paginate(current_page, batch_size) + ) + + logger.info( + "Embedded %d thumbnails and %d descriptions in %s seconds", + totals["thumbnails"], + totals["descriptions"], + round(time.time() - st, 1), + ) + totals["status"] = "completed" + + self.requestor.send_data(UPDATE_EMBEDDINGS_REINDEX_PROGRESS, totals) + + def start_reindex(self) -> bool: + """Start reindexing in a separate thread if not already running.""" + with self.reindex_lock: + if self.reindex_running: + logger.warning("Reindex embeddings is already running.") + return False + + # Mark as running and start the thread + self.reindex_running = True + self.reindex_thread = threading.Thread( + target=self._reindex_wrapper, daemon=True + ) + self.reindex_thread.start() + return True + + def _reindex_wrapper(self) -> None: + """Wrapper to run reindex and reset running flag when done.""" + try: + self.reindex() + finally: + with self.reindex_lock: + self.reindex_running = False + self.reindex_thread = None + + def sync_triggers(self) -> None: + for camera in self.config.cameras.values(): + # Get all existing triggers for this camera + existing_triggers = { + trigger.name: trigger + for trigger in Trigger.select().where(Trigger.camera == camera.name) + } + + # Get all configured trigger names + configured_trigger_names = set(camera.semantic_search.triggers or {}) + + # Create or update triggers from config + for trigger_name, trigger in ( + camera.semantic_search.triggers or {} + ).items(): + if trigger_name in existing_triggers: + existing_trigger = existing_triggers[trigger_name] + needs_embedding_update = False + thumbnail_missing = False + + # Check if data has changed or thumbnail is missing for thumbnail type + if trigger.type == "thumbnail": + thumbnail_path = os.path.join( + TRIGGER_DIR, camera.name, f"{trigger.data}.webp" + ) + try: + event = Event.get(Event.id == trigger.data) + if event.data.get("type") != "object": + logger.warning( + f"Event {trigger.data} is not a tracked object for {trigger.type} trigger" + ) + continue # Skip if not an object + + # Check if thumbnail needs to be updated (data changed or missing) + if ( + existing_trigger.data != trigger.data + or not os.path.exists(thumbnail_path) + ): + thumbnail = get_event_thumbnail_bytes(event) + if not thumbnail: + logger.warning( + f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}." + ) + continue + self.write_trigger_thumbnail( + camera.name, trigger.data, thumbnail + ) + thumbnail_missing = True + except DoesNotExist: + logger.debug( + f"Event ID {trigger.data} for trigger {trigger_name} does not exist." + ) + continue + + # Update existing trigger if data has changed + if ( + existing_trigger.type != trigger.type + or existing_trigger.data != trigger.data + or existing_trigger.threshold != trigger.threshold + ): + existing_trigger.type = trigger.type + existing_trigger.data = trigger.data + existing_trigger.threshold = trigger.threshold + needs_embedding_update = True + + # Check if embedding is missing or needs update + if ( + not existing_trigger.embedding + or needs_embedding_update + or thumbnail_missing + ): + existing_trigger.embedding = self._calculate_trigger_embedding( + trigger, trigger_name, camera.name + ) + needs_embedding_update = True + + if needs_embedding_update: + existing_trigger.save() + continue + else: + # Create new trigger + try: + # For thumbnail triggers, validate the event exists + if trigger.type == "thumbnail": + try: + event: Event = Event.get(Event.id == trigger.data) + except DoesNotExist: + logger.warning( + f"Event ID {trigger.data} for trigger {trigger_name} does not exist." + ) + continue + + # Skip the event if not an object + if event.data.get("type") != "object": + logger.warning( + f"Event ID {trigger.data} for trigger {trigger_name} is not a tracked object." + ) + continue + + thumbnail = get_event_thumbnail_bytes(event) + + if not thumbnail: + logger.warning( + f"Unable to retrieve thumbnail for event ID {trigger.data} for {trigger_name}." + ) + continue + + self.write_trigger_thumbnail( + camera.name, trigger.data, thumbnail + ) + + # Calculate embedding for new trigger + embedding = self._calculate_trigger_embedding( + trigger, trigger_name, camera.name + ) + + Trigger.create( + camera=camera.name, + name=trigger_name, + type=trigger.type, + data=trigger.data, + threshold=trigger.threshold, + model=self.config.semantic_search.model, + embedding=embedding, + triggering_event_id="", + last_triggered=None, + ) + + except IntegrityError: + pass # Handle duplicate creation attempts + + # Remove triggers that are no longer in config + triggers_to_remove = ( + set(existing_triggers.keys()) - configured_trigger_names + ) + if triggers_to_remove: + Trigger.delete().where( + Trigger.camera == camera.name, Trigger.name.in_(triggers_to_remove) + ).execute() + for trigger_name in triggers_to_remove: + # Only remove thumbnail files for thumbnail triggers + if existing_triggers[trigger_name].type == "thumbnail": + self.remove_trigger_thumbnail( + camera.name, existing_triggers[trigger_name].data + ) + + def write_trigger_thumbnail( + self, camera: str, event_id: str, thumbnail: bytes + ) -> None: + """Write the thumbnail to the trigger directory.""" + try: + os.makedirs(os.path.join(TRIGGER_DIR, camera), exist_ok=True) + with open(os.path.join(TRIGGER_DIR, camera, f"{event_id}.webp"), "wb") as f: + f.write(thumbnail) + logger.debug( + f"Writing thumbnail for trigger with data {event_id} in {camera}." + ) + except Exception as e: + logger.error( + f"Failed to write thumbnail for trigger with data {event_id} in {camera}: {e}" + ) + + def remove_trigger_thumbnail(self, camera: str, event_id: str) -> None: + """Write the thumbnail to the trigger directory.""" + try: + os.remove(os.path.join(TRIGGER_DIR, camera, f"{event_id}.webp")) + logger.debug( + f"Deleted thumbnail for trigger with data {event_id} in {camera}." + ) + except Exception as e: + logger.error( + f"Failed to delete thumbnail for trigger with data {event_id} in {camera}: {e}" + ) + + def _calculate_trigger_embedding( + self, trigger, trigger_name: str, camera_name: str + ) -> bytes: + """Calculate embedding for a trigger based on its type and data.""" + if trigger.type == "description": + logger.debug(f"Generating embedding for trigger description {trigger_name}") + embedding = self.embed_description(None, trigger.data, upsert=False) + return embedding.astype(np.float32).tobytes() + + elif trigger.type == "thumbnail": + # For image triggers, trigger.data should be an image ID + # Try to get embedding from vec_thumbnails table first + cursor = self.db.execute_sql( + "SELECT thumbnail_embedding FROM vec_thumbnails WHERE id = ?", + [trigger.data], + ) + row = cursor.fetchone() if cursor else None + if row: + return row[0] # Already in bytes format + else: + logger.debug( + f"No thumbnail embedding found for image ID: {trigger.data}, generating from saved trigger thumbnail" + ) + + try: + with open( + os.path.join(TRIGGER_DIR, camera_name, f"{trigger.data}.webp"), + "rb", + ) as f: + thumbnail = f.read() + except Exception as e: + logger.error( + f"Failed to read thumbnail for trigger {trigger_name} with ID {trigger.data}: {e}" + ) + return b"" + + logger.debug( + f"Generating embedding for trigger thumbnail {trigger_name} with ID {trigger.data}" + ) + embedding = self.embed_thumbnail( + str(trigger.data), thumbnail, upsert=False + ) + return embedding.astype(np.float32).tobytes() + + else: + logger.warning(f"Unknown trigger type: {trigger.type}") + return b"" diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/maintainer.py b/sam2-cpu/frigate-dev/frigate/embeddings/maintainer.py new file mode 100644 index 0000000..78a251c --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/maintainer.py @@ -0,0 +1,674 @@ +"""Maintain embeddings in SQLite-vec.""" + +import base64 +import datetime +import logging +import threading +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from peewee import DoesNotExist + +from frigate.comms.config_updater import ConfigSubscriber +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.embeddings_updater import ( + EmbeddingsRequestEnum, + EmbeddingsResponder, +) +from frigate.comms.event_metadata_updater import ( + EventMetadataPublisher, + EventMetadataSubscriber, + EventMetadataTypeEnum, +) +from frigate.comms.events_updater import EventEndSubscriber, EventUpdateSubscriber +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataSubscriber, + RecordingsDataTypeEnum, +) +from frigate.comms.review_updater import ReviewDataSubscriber +from frigate.config import FrigateConfig +from frigate.config.camera.camera import CameraTypeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.data_processing.common.license_plate.model import ( + LicensePlateModelRunner, +) +from frigate.data_processing.post.api import PostProcessorApi +from frigate.data_processing.post.audio_transcription import ( + AudioTranscriptionPostProcessor, +) +from frigate.data_processing.post.license_plate import ( + LicensePlatePostProcessor, +) +from frigate.data_processing.post.object_descriptions import ObjectDescriptionProcessor +from frigate.data_processing.post.review_descriptions import ReviewDescriptionProcessor +from frigate.data_processing.post.semantic_trigger import SemanticTriggerProcessor +from frigate.data_processing.real_time.api import RealTimeProcessorApi +from frigate.data_processing.real_time.bird import BirdRealTimeProcessor +from frigate.data_processing.real_time.custom_classification import ( + CustomObjectClassificationProcessor, + CustomStateClassificationProcessor, +) +from frigate.data_processing.real_time.face import FaceRealTimeProcessor +from frigate.data_processing.real_time.license_plate import ( + LicensePlateRealTimeProcessor, +) +from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum +from frigate.genai import get_genai_client +from frigate.models import Event, Recordings, ReviewSegment, Trigger +from frigate.util.builtin import serialize +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import SharedMemoryFrameManager + +from .embeddings import Embeddings + +logger = logging.getLogger(__name__) + +MAX_THUMBNAILS = 10 + + +class EmbeddingMaintainer(threading.Thread): + """Handle embedding queue and post event updates.""" + + def __init__( + self, + config: FrigateConfig, + metrics: DataProcessorMetrics | None, + stop_event: MpEvent, + ) -> None: + super().__init__(name="embeddings_maintainer") + self.config = config + self.metrics = metrics + self.embeddings = None + self.config_updater = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.object_genai, + CameraConfigUpdateEnum.review_genai, + CameraConfigUpdateEnum.semantic_search, + ], + ) + self.classification_config_subscriber = ConfigSubscriber( + "config/classification/custom/" + ) + + # Configure Frigate DB + db = SqliteVecQueueDatabase( + config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, 10 * len([c for c in config.cameras.values() if c.enabled]) + ), + load_vec_extension=True, + ) + models = [Event, Recordings, ReviewSegment, Trigger] + db.bind(models) + + if config.semantic_search.enabled: + self.embeddings = Embeddings(config, db, metrics) + + # Check if we need to re-index events + if config.semantic_search.reindex: + self.embeddings.reindex() + + # Sync semantic search triggers in db with config + self.embeddings.sync_triggers() + + # create communication for updating event descriptions + self.requestor = InterProcessRequestor() + + self.event_subscriber = EventUpdateSubscriber() + self.event_end_subscriber = EventEndSubscriber() + self.event_metadata_publisher = EventMetadataPublisher() + self.event_metadata_subscriber = EventMetadataSubscriber( + EventMetadataTypeEnum.regenerate_description + ) + self.recordings_subscriber = RecordingsDataSubscriber( + RecordingsDataTypeEnum.saved + ) + self.review_subscriber = ReviewDataSubscriber("") + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + self.embeddings_responder = EmbeddingsResponder() + self.frame_manager = SharedMemoryFrameManager() + + self.detected_license_plates: dict[str, dict[str, Any]] = {} + self.genai_client = get_genai_client(config) + + # model runners to share between realtime and post processors + if self.config.lpr.enabled: + lpr_model_runner = LicensePlateModelRunner( + self.requestor, + device=self.config.lpr.device, + model_size=self.config.lpr.model_size, + ) + + # realtime processors + self.realtime_processors: list[RealTimeProcessorApi] = [] + + if self.config.face_recognition.enabled: + logger.debug("Face recognition enabled, initializing FaceRealTimeProcessor") + self.realtime_processors.append( + FaceRealTimeProcessor( + self.config, self.requestor, self.event_metadata_publisher, metrics + ) + ) + logger.debug("FaceRealTimeProcessor initialized successfully") + + if self.config.classification.bird.enabled: + self.realtime_processors.append( + BirdRealTimeProcessor( + self.config, self.event_metadata_publisher, metrics + ) + ) + + if self.config.lpr.enabled: + self.realtime_processors.append( + LicensePlateRealTimeProcessor( + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + lpr_model_runner, + self.detected_license_plates, + ) + ) + + for model_config in self.config.classification.custom.values(): + self.realtime_processors.append( + CustomStateClassificationProcessor( + self.config, model_config, self.requestor, self.metrics + ) + if model_config.state_config != None + else CustomObjectClassificationProcessor( + self.config, + model_config, + self.event_metadata_publisher, + self.requestor, + self.metrics, + ) + ) + + # post processors + self.post_processors: list[PostProcessorApi] = [] + + if any(c.review.genai.enabled_in_config for c in self.config.cameras.values()): + self.post_processors.append( + ReviewDescriptionProcessor( + self.config, self.requestor, self.metrics, self.genai_client + ) + ) + + if self.config.lpr.enabled: + self.post_processors.append( + LicensePlatePostProcessor( + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + lpr_model_runner, + self.detected_license_plates, + ) + ) + + if self.config.audio_transcription.enabled and any( + c.enabled_in_config and c.audio_transcription.enabled + for c in self.config.cameras.values() + ): + self.post_processors.append( + AudioTranscriptionPostProcessor( + self.config, self.requestor, self.embeddings, metrics + ) + ) + + semantic_trigger_processor: SemanticTriggerProcessor | None = None + if self.config.semantic_search.enabled: + semantic_trigger_processor = SemanticTriggerProcessor( + db, + self.config, + self.requestor, + self.event_metadata_publisher, + metrics, + self.embeddings, + ) + self.post_processors.append(semantic_trigger_processor) + + if any(c.objects.genai.enabled_in_config for c in self.config.cameras.values()): + self.post_processors.append( + ObjectDescriptionProcessor( + self.config, + self.embeddings, + self.requestor, + self.metrics, + self.genai_client, + semantic_trigger_processor, + ) + ) + + self.stop_event = stop_event + + # recordings data + self.recordings_available_through: dict[str, float] = {} + + def run(self) -> None: + """Maintain a SQLite-vec database for semantic search.""" + while not self.stop_event.is_set(): + self.config_updater.check_for_updates() + self._check_classification_config_updates() + self._process_requests() + self._process_updates() + self._process_recordings_updates() + self._process_review_updates() + self._process_frame_updates() + self._expire_dedicated_lpr() + self._process_finalized() + self._process_event_metadata() + + self.config_updater.stop() + self.classification_config_subscriber.stop() + self.event_subscriber.stop() + self.event_end_subscriber.stop() + self.recordings_subscriber.stop() + self.detection_subscriber.stop() + self.event_metadata_publisher.stop() + self.event_metadata_subscriber.stop() + self.embeddings_responder.stop() + self.requestor.stop() + logger.info("Exiting embeddings maintenance...") + + def _check_classification_config_updates(self) -> None: + """Check for classification config updates and add/remove processors.""" + topic, model_config = self.classification_config_subscriber.check_for_update() + + if topic: + model_name = topic.split("/")[-1] + + if model_config is None: + self.realtime_processors = [ + processor + for processor in self.realtime_processors + if not ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name + ) + ] + + logger.info( + f"Successfully removed classification processor for model: {model_name}" + ) + else: + self.config.classification.custom[model_name] = model_config + + # Check if processor already exists + for processor in self.realtime_processors: + if isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ): + if processor.model_config.name == model_name: + logger.debug( + f"Classification processor for model {model_name} already exists, skipping" + ) + return + + if model_config.state_config is not None: + processor = CustomStateClassificationProcessor( + self.config, model_config, self.requestor, self.metrics + ) + else: + processor = CustomObjectClassificationProcessor( + self.config, + model_config, + self.event_metadata_publisher, + self.requestor, + self.metrics, + ) + + self.realtime_processors.append(processor) + logger.info( + f"Added classification processor for model: {model_name} (type: {type(processor).__name__})" + ) + + def _process_requests(self) -> None: + """Process embeddings requests""" + + def _handle_request(topic: str, data: dict[str, Any]) -> str: + try: + # First handle the embedding-specific topics when semantic search is enabled + if self.config.semantic_search.enabled: + if topic == EmbeddingsRequestEnum.embed_description.value: + return serialize( + self.embeddings.embed_description( + data["id"], data["description"] + ), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.embed_thumbnail.value: + thumbnail = base64.b64decode(data["thumbnail"]) + return serialize( + self.embeddings.embed_thumbnail(data["id"], thumbnail), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.generate_search.value: + return serialize( + self.embeddings.embed_description("", data, upsert=False), + pack=False, + ) + elif topic == EmbeddingsRequestEnum.reindex.value: + response = self.embeddings.start_reindex() + return "started" if response else "in_progress" + + processors = [self.realtime_processors, self.post_processors] + for processor_list in processors: + for processor in processor_list: + resp = processor.handle_request(topic, data) + if resp is not None: + return resp + + logger.error(f"No processor handled the topic {topic}") + return None + except Exception as e: + logger.error(f"Unable to handle embeddings request {e}", exc_info=True) + + self.embeddings_responder.check_for_request(_handle_request) + + def _process_updates(self) -> None: + """Process event updates""" + update = self.event_subscriber.check_for_update() + + if update is None: + return + + source_type, _, camera, frame_name, data = update + + logger.debug( + f"Received update - source_type: {source_type}, camera: {camera}, data label: {data.get('label') if data else 'None'}" + ) + + if not camera or source_type != EventTypeEnum.tracked_object: + logger.debug( + f"Skipping update - camera: {camera}, source_type: {source_type}" + ) + return + + if self.config.semantic_search.enabled: + self.embeddings.update_stats() + + camera_config = self.config.cameras[camera] + + # no need to process updated objects if no processors are active + if len(self.realtime_processors) == 0 and len(self.post_processors) == 0: + logger.debug( + f"No processors active - realtime: {len(self.realtime_processors)}, post: {len(self.post_processors)}" + ) + return + + # Create our own thumbnail based on the bounding box and the frame time + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + except FileNotFoundError: + logger.debug(f"Frame {frame_name} not found for camera {camera}") + pass + + if yuv_frame is None: + logger.debug( + "Unable to process object update because frame is unavailable." + ) + return + + logger.debug( + f"Processing {len(self.realtime_processors)} realtime processors for object {data.get('id')} (label: {data.get('label')})" + ) + for processor in self.realtime_processors: + logger.debug(f"Calling process_frame on {processor.__class__.__name__}") + processor.process_frame(data, yuv_frame) + + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.process_data( + { + "camera": camera, + "data": data, + "state": "update", + "yuv_frame": yuv_frame, + }, + PostProcessDataEnum.tracked_object, + ) + + self.frame_manager.close(frame_name) + + def _process_finalized(self) -> None: + """Process the end of an event.""" + while True: + ended = self.event_end_subscriber.check_for_update() + + if ended == None: + break + + event_id, camera, updated_db = ended + + # expire in realtime processors + for processor in self.realtime_processors: + processor.expire_object(event_id, camera) + + thumbnail: bytes | None = None + + if updated_db: + try: + event: Event = Event.get(Event.id == event_id) + except DoesNotExist: + continue + + # Skip the event if not an object + if event.data.get("type") != "object": + continue + + # Extract valid thumbnail + thumbnail = get_event_thumbnail_bytes(event) + + # Embed the thumbnail + self._embed_thumbnail(event_id, thumbnail) + + # call any defined post processors + for processor in self.post_processors: + if isinstance(processor, LicensePlatePostProcessor): + recordings_available = self.recordings_available_through.get(camera) + if ( + recordings_available is not None + and event_id in self.detected_license_plates + and self.config.cameras[camera].type != "lpr" + ): + processor.process_data( + { + "event_id": event_id, + "camera": camera, + "recordings_available": self.recordings_available_through[ + camera + ], + "obj_data": self.detected_license_plates[event_id][ + "obj_data" + ], + }, + PostProcessDataEnum.recording, + ) + elif isinstance(processor, AudioTranscriptionPostProcessor): + continue + elif isinstance(processor, SemanticTriggerProcessor): + processor.process_data( + {"event_id": event_id, "camera": camera, "type": "image"}, + PostProcessDataEnum.tracked_object, + ) + elif isinstance(processor, ObjectDescriptionProcessor): + if not updated_db: + continue + + processor.process_data( + { + "event": event, + "camera": camera, + "state": "finalize", + "thumbnail": thumbnail, + }, + PostProcessDataEnum.tracked_object, + ) + else: + processor.process_data( + {"event_id": event_id, "camera": camera}, + PostProcessDataEnum.tracked_object, + ) + + def _expire_dedicated_lpr(self) -> None: + """Remove plates not seen for longer than expiration timeout for dedicated lpr cameras.""" + now = datetime.datetime.now().timestamp() + + to_remove = [] + + for id, data in self.detected_license_plates.items(): + last_seen = data.get("last_seen", 0) + if not last_seen: + continue + + if now - last_seen > self.config.cameras[data["camera"]].lpr.expire_time: + to_remove.append(id) + for id in to_remove: + self.event_metadata_publisher.publish( + (id, now), + EventMetadataTypeEnum.manual_event_end.value, + ) + self.detected_license_plates.pop(id) + + def _process_recordings_updates(self) -> None: + """Process recordings updates.""" + while True: + update = self.recordings_subscriber.check_for_update() + + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: + break + + topic = str(raw_topic) + + if topic.endswith(RecordingsDataTypeEnum.saved.value): + camera, recordings_available_through_timestamp, _ = payload + + self.recordings_available_through[camera] = ( + recordings_available_through_timestamp + ) + + logger.debug( + f"{camera} now has recordings available through {recordings_available_through_timestamp}" + ) + + def _process_review_updates(self) -> None: + """Process review updates.""" + while True: + review_updates = self.review_subscriber.check_for_update() + + if review_updates == None: + break + + for processor in self.post_processors: + if isinstance(processor, ReviewDescriptionProcessor): + processor.process_data(review_updates, PostProcessDataEnum.review) + + def _process_event_metadata(self): + # Check for regenerate description requests + (topic, payload) = self.event_metadata_subscriber.check_for_update() + + if topic is None: + return + + event_id, source, force = payload + + if event_id: + for processor in self.post_processors: + if isinstance(processor, ObjectDescriptionProcessor): + processor.handle_request( + "regenerate_description", + { + "event_id": event_id, + "source": RegenerateDescriptionEnum(source), + "force": force, + }, + ) + + def _process_frame_updates(self) -> None: + """Process event updates""" + (topic, data) = self.detection_subscriber.check_for_update() + + if topic is None: + return + + camera, frame_name, _, _, motion_boxes, _ = data + + if not camera or len(motion_boxes) == 0: + return + + camera_config = self.config.cameras[camera] + dedicated_lpr_enabled = ( + camera_config.type == CameraTypeEnum.lpr + and "license_plate" not in camera_config.objects.track + ) + + if not dedicated_lpr_enabled and len(self.config.classification.custom) == 0: + # no active features that use this data + return + + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + except FileNotFoundError: + pass + + if yuv_frame is None: + logger.debug( + "Unable to process dedicated LPR update because frame is unavailable." + ) + return + + for processor in self.realtime_processors: + if dedicated_lpr_enabled and isinstance( + processor, LicensePlateRealTimeProcessor + ): + processor.process_frame(camera, yuv_frame, True) + + if isinstance(processor, CustomStateClassificationProcessor): + processor.process_frame( + {"camera": camera, "motion": motion_boxes}, yuv_frame + ) + + self.frame_manager.close(frame_name) + + def _embed_thumbnail(self, event_id: str, thumbnail: bytes) -> None: + """Embed the thumbnail for an event.""" + if not self.config.semantic_search.enabled: + return + + self.embeddings.embed_thumbnail(event_id, thumbnail) diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/onnx/base_embedding.py b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/base_embedding.py new file mode 100644 index 0000000..c0bd584 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/base_embedding.py @@ -0,0 +1,97 @@ +"""Base class for onnx embedding implementations.""" + +import logging +import os +from abc import ABC, abstractmethod +from io import BytesIO +from typing import Any + +import numpy as np +import requests +from PIL import Image + +from frigate.const import UPDATE_MODEL_STATE +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +logger = logging.getLogger(__name__) + + +class BaseEmbedding(ABC): + """Base embedding class.""" + + def __init__(self, model_name: str, model_file: str, download_urls: dict[str, str]): + self.model_name = model_name + self.model_file = model_file + self.download_urls = download_urls + self.downloader: ModelDownloader = None + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + @abstractmethod + def _load_model_and_utils(self): + pass + + @abstractmethod + def _preprocess_inputs(self, raw_inputs: Any) -> Any: + pass + + def _process_image(self, image, output: str = "RGB") -> Image.Image: + if isinstance(image, str): + if image.startswith("http"): + response = requests.get(image) + image = Image.open(BytesIO(response.content)).convert(output) + elif isinstance(image, bytes): + image = Image.open(BytesIO(image)).convert(output) + elif isinstance(image, np.ndarray): + image = Image.fromarray(image) + + return image + + def _postprocess_outputs(self, outputs: Any) -> Any: + return outputs + + def __call__( + self, inputs: list[str] | list[Image.Image] | list[str] + ) -> list[np.ndarray]: + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + input_names = self.runner.get_input_names() + onnx_inputs = {name: [] for name in input_names} + input: dict[str, Any] + for input in processed: + for key, value in input.items(): + if key in input_names: + onnx_inputs[key].append(value[0]) + + for key in input_names: + if onnx_inputs.get(key): + onnx_inputs[key] = np.stack(onnx_inputs[key]) + else: + logger.warning(f"Expected input '{key}' not found in onnx_inputs") + + outputs = self.runner.run(onnx_inputs)[0] + embeddings = self._postprocess_outputs(outputs) + + return [embedding for embedding in embeddings] diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/onnx/face_embedding.py b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/face_embedding.py new file mode 100644 index 0000000..e661f8d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/face_embedding.py @@ -0,0 +1,193 @@ +"""Facenet Embeddings.""" + +import logging +import os + +import numpy as np + +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.log import redirect_output_to_logger +from frigate.util.downloader import ModelDownloader + +from ...config import FaceRecognitionConfig +from .base_embedding import BaseEmbedding + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + +logger = logging.getLogger(__name__) + +ARCFACE_INPUT_SIZE = 112 +FACENET_INPUT_SIZE = 160 + + +class FaceNetEmbedding(BaseEmbedding): + def __init__(self): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="facedet", + model_file="facenet.tflite", + download_urls={ + "facenet.tflite": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/facenet.tflite", + }, + ) + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + files_names = list(self.download_urls.keys()) + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + @redirect_output_to_logger(logger, logging.DEBUG) + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = Interpreter( + model_path=os.path.join(MODEL_CACHE_DIR, "facedet/facenet.tflite"), + num_threads=2, + ) + self.runner.allocate_tensors() + self.tensor_input_details = self.runner.get_input_details() + self.tensor_output_details = self.runner.get_output_details() + + def _preprocess_inputs(self, raw_inputs): + pil = self._process_image(raw_inputs[0]) + + # handle images larger than input size + width, height = pil.size + if width != FACENET_INPUT_SIZE or height != FACENET_INPUT_SIZE: + if width > height: + new_height = int(((height / width) * FACENET_INPUT_SIZE) // 4 * 4) + pil = pil.resize((FACENET_INPUT_SIZE, new_height)) + else: + new_width = int(((width / height) * FACENET_INPUT_SIZE) // 4 * 4) + pil = pil.resize((new_width, FACENET_INPUT_SIZE)) + + og = np.array(pil).astype(np.float32) + + # Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE + og_h, og_w, channels = og.shape + frame = np.zeros( + (FACENET_INPUT_SIZE, FACENET_INPUT_SIZE, channels), dtype=np.float32 + ) + + # compute center offset + x_center = (FACENET_INPUT_SIZE - og_w) // 2 + y_center = (FACENET_INPUT_SIZE - og_h) // 2 + + # copy img image into center of result image + frame[y_center : y_center + og_h, x_center : x_center + og_w] = og + + # run facenet normalization + frame = (frame / 127.5) - 1.0 + + frame = np.expand_dims(frame, axis=0) + return frame + + def __call__(self, inputs): + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + self.runner.set_tensor(self.tensor_input_details[0]["index"], processed) + self.runner.invoke() + return self.runner.get_tensor(self.tensor_output_details[0]["index"]) + + +class ArcfaceEmbedding(BaseEmbedding): + def __init__(self, config: FaceRecognitionConfig): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="facedet", + model_file="arcface.onnx", + download_urls={ + "arcface.onnx": f"{GITHUB_ENDPOINT}/NickM-27/facenet-onnx/releases/download/v1.0/arcface.onnx", + }, + ) + self.config = config + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + files_names = list(self.download_urls.keys()) + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + device=self.config.device or "GPU", + model_type=EnrichmentModelTypeEnum.arcface.value, + ) + + def _preprocess_inputs(self, raw_inputs): + pil = self._process_image(raw_inputs[0]) + + # handle images larger than input size + width, height = pil.size + if width != ARCFACE_INPUT_SIZE or height != ARCFACE_INPUT_SIZE: + if width > height: + new_height = int(((height / width) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((ARCFACE_INPUT_SIZE, new_height)) + else: + new_width = int(((width / height) * ARCFACE_INPUT_SIZE) // 4 * 4) + pil = pil.resize((new_width, ARCFACE_INPUT_SIZE)) + + og = np.array(pil).astype(np.float32) + + # Image must be FACE_EMBEDDING_SIZExFACE_EMBEDDING_SIZE + og_h, og_w, channels = og.shape + frame = np.zeros( + (ARCFACE_INPUT_SIZE, ARCFACE_INPUT_SIZE, channels), dtype=np.float32 + ) + + # compute center offset + x_center = (ARCFACE_INPUT_SIZE - og_w) // 2 + y_center = (ARCFACE_INPUT_SIZE - og_h) // 2 + + # copy img image into center of result image + frame[y_center : y_center + og_h, x_center : x_center + og_w] = og + + # run arcface normalization + frame = (frame / 127.5) - 1.0 + + frame = np.transpose(frame, (2, 0, 1)) + frame = np.expand_dims(frame, axis=0) + return [{"data": frame}] diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v1_embedding.py b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v1_embedding.py new file mode 100644 index 0000000..519247f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v1_embedding.py @@ -0,0 +1,223 @@ +"""JinaV1 Embeddings.""" + +import logging +import os +import warnings + +from transformers import AutoFeatureExtractor, AutoTokenizer +from transformers.utils.logging import disable_progress_bar + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR, UPDATE_MODEL_STATE +from frigate.detectors.detection_runners import BaseModelRunner, get_optimized_runner + +# importing this without pytorch or others causes a warning +# https://github.com/huggingface/transformers/issues/27214 +# suppressed by setting env TRANSFORMERS_NO_ADVISORY_WARNINGS=1 +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +warnings.filterwarnings( + "ignore", + category=FutureWarning, + message="The class CLIPFeatureExtractor is deprecated", +) + +# disables the progress bar for downloading tokenizers and feature extractors +disable_progress_bar() +logger = logging.getLogger(__name__) + + +class JinaV1TextEmbedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v1", + model_file="text_model_fp16.onnx", + download_urls={ + "text_model_fp16.onnx": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/text_model_fp16.onnx", + }, + ) + self.tokenizer_file = "tokenizer" + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.feature_extractor = None + self.runner = None + files_names = list(self.download_urls.keys()) + [self.tokenizer_file] + + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + elif file_name == self.tokenizer_file: + if not os.path.exists(path + "/" + self.model_name): + logger.info(f"Downloading {self.model_name} tokenizer") + + tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + trust_remote_code=True, + cache_dir=f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer", + clean_up_tokenization_spaces=True, + ) + tokenizer.save_pretrained(path) + + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.downloader.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + tokenizer_path = os.path.join( + f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer" + ) + self.tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + cache_dir=tokenizer_path, + trust_remote_code=True, + clean_up_tokenization_spaces=True, + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v1.value, + ) + + def _preprocess_inputs(self, raw_inputs): + max_length = max(len(self.tokenizer.encode(text)) for text in raw_inputs) + return [ + self.tokenizer( + text, + padding="max_length", + truncation=True, + max_length=max_length, + return_tensors="np", + ) + for text in raw_inputs + ] + + +class JinaV1ImageEmbedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + model_file = ( + "vision_model_fp16.onnx" + if model_size == "large" + else "vision_model_quantized.onnx" + ) + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v1", + model_file=model_file, + download_urls={ + model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/onnx/{model_file}", + "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v1/resolve/main/preprocessor_config.json", + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.feature_extractor = None + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + # Avoid lazy loading in worker threads: block until downloads complete + # and load the model on the main thread during initialization. + self._load_model_and_utils() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.feature_extractor = AutoFeatureExtractor.from_pretrained( + f"{MODEL_CACHE_DIR}/{self.model_name}", + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v1.value, + ) + + def _preprocess_inputs(self, raw_inputs): + processed_images = [self._process_image(img) for img in raw_inputs] + return [ + self.feature_extractor(images=image, return_tensors="np") + for image in processed_images + ] diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v2_embedding.py b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v2_embedding.py new file mode 100644 index 0000000..fd4323f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/jina_v2_embedding.py @@ -0,0 +1,236 @@ +"""JinaV2 Embeddings.""" + +import io +import logging +import os + +import numpy as np +from PIL import Image +from transformers import AutoTokenizer +from transformers.utils.logging import disable_progress_bar, set_verbosity_error + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR, UPDATE_MODEL_STATE +from frigate.detectors.detection_runners import get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +# disables the progress bar and download logging for downloading tokenizers and image processors +disable_progress_bar() +set_verbosity_error() +logger = logging.getLogger(__name__) + + +class JinaV2Embedding(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + embedding_type: str = None, + ): + model_file = ( + "model_fp16.onnx" if model_size == "large" else "model_quantized.onnx" + ) + HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co") + super().__init__( + model_name="jinaai/jina-clip-v2", + model_file=model_file, + download_urls={ + model_file: f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/onnx/{model_file}", + "preprocessor_config.json": f"{HF_ENDPOINT}/jinaai/jina-clip-v2/resolve/main/preprocessor_config.json", + }, + ) + self.tokenizer_file = "tokenizer" + self.embedding_type = embedding_type + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.tokenizer = None + self.image_processor = None + self.runner = None + files_names = list(self.download_urls.keys()) + [self.tokenizer_file] + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + # Avoid lazy loading in worker threads: block until downloads complete + # and load the model on the main thread during initialization. + self._load_model_and_utils() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _download_model(self, path: str): + try: + file_name = os.path.basename(path) + + if file_name in self.download_urls: + ModelDownloader.download_from_url(self.download_urls[file_name], path) + elif file_name == self.tokenizer_file: + if not os.path.exists(os.path.join(path, self.model_name)): + logger.info(f"Downloading {self.model_name} tokenizer") + + tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + trust_remote_code=True, + cache_dir=os.path.join( + MODEL_CACHE_DIR, self.model_name, "tokenizer" + ), + clean_up_tokenization_spaces=True, + ) + tokenizer.save_pretrained(path) + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + except Exception: + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.error, + }, + ) + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + tokenizer_path = os.path.join( + f"{MODEL_CACHE_DIR}/{self.model_name}/tokenizer" + ) + self.tokenizer = AutoTokenizer.from_pretrained( + self.model_name, + cache_dir=tokenizer_path, + trust_remote_code=True, + clean_up_tokenization_spaces=True, + ) + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.jina_v2.value, + ) + + def _preprocess_image(self, image_data: bytes | Image.Image) -> np.ndarray: + """ + Manually preprocess a single image from bytes or PIL.Image to (3, 512, 512). + """ + if isinstance(image_data, bytes): + image = Image.open(io.BytesIO(image_data)) + else: + image = image_data + + if image.mode != "RGB": + image = image.convert("RGB") + + image = image.resize((512, 512), Image.Resampling.LANCZOS) + + # Convert to numpy array, normalize to [0, 1], and transpose to (channels, height, width) + image_array = np.array(image, dtype=np.float32) / 255.0 + image_array = np.transpose(image_array, (2, 0, 1)) # (H, W, C) -> (C, H, W) + + return image_array + + def _preprocess_inputs(self, raw_inputs): + """ + Preprocess inputs into a list of real input tensors (no dummies). + - For text: Returns list of input_ids. + - For vision: Returns list of pixel_values. + """ + if not isinstance(raw_inputs, list): + raw_inputs = [raw_inputs] + + processed = [] + if self.embedding_type == "text": + for text in raw_inputs: + input_ids = self.tokenizer([text], return_tensors="np")["input_ids"] + processed.append(input_ids) + elif self.embedding_type == "vision": + for img in raw_inputs: + pixel_values = self._preprocess_image(img) + processed.append( + pixel_values[np.newaxis, ...] + ) # Add batch dim: (1, 3, 512, 512) + else: + raise ValueError( + f"Invalid embedding_type: {self.embedding_type}. Must be 'text' or 'vision'." + ) + return processed + + def _postprocess_outputs(self, outputs): + """ + Process ONNX model outputs, truncating each embedding in the array to truncate_dim. + - outputs: NumPy array of embeddings. + - Returns: List of truncated embeddings. + """ + # size of vector in database + truncate_dim = 768 + + # jina v2 defaults to 1024 and uses Matryoshka representation, so + # truncating only causes an extremely minor decrease in retrieval accuracy + if outputs.shape[-1] > truncate_dim: + outputs = outputs[..., :truncate_dim] + + return outputs + + def __call__( + self, inputs: list[str] | list[Image.Image] | list[str], embedding_type=None + ) -> list[np.ndarray]: + self.embedding_type = embedding_type + if not self.embedding_type: + raise ValueError( + "embedding_type must be specified either in __init__ or __call__" + ) + + self._load_model_and_utils() + processed = self._preprocess_inputs(inputs) + batch_size = len(processed) + + # Prepare ONNX inputs with matching batch sizes + onnx_inputs = {} + if self.embedding_type == "text": + onnx_inputs["input_ids"] = np.stack([x[0] for x in processed]) + onnx_inputs["pixel_values"] = np.zeros( + (batch_size, 3, 512, 512), dtype=np.float32 + ) + elif self.embedding_type == "vision": + onnx_inputs["input_ids"] = np.zeros((batch_size, 16), dtype=np.int64) + onnx_inputs["pixel_values"] = np.stack([x[0] for x in processed]) + else: + raise ValueError("Invalid embedding type") + + # Run inference + outputs = self.runner.run(onnx_inputs) + if self.embedding_type == "text": + embeddings = outputs[2] # text embeddings + elif self.embedding_type == "vision": + embeddings = outputs[3] # image embeddings + else: + raise ValueError("Invalid embedding type") + + embeddings = self._postprocess_outputs(embeddings) + return [embedding for embedding in embeddings] diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/onnx/lpr_embedding.py b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/lpr_embedding.py new file mode 100644 index 0000000..ad20999 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/onnx/lpr_embedding.py @@ -0,0 +1,308 @@ +import logging +import os +import warnings + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors.detection_runners import BaseModelRunner, get_optimized_runner +from frigate.embeddings.types import EnrichmentModelTypeEnum +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader + +from .base_embedding import BaseEmbedding + +warnings.filterwarnings( + "ignore", + category=FutureWarning, + message="The class CLIPFeatureExtractor is deprecated", +) + +logger = logging.getLogger(__name__) + +LPR_EMBEDDING_SIZE = 256 + + +class PaddleOCRDetection(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + model_file = ( + "detection_v3-large.onnx" + if model_size == "large" + else "detection_v5-small.onnx" + ) + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file=model_file, + download_urls={ + model_file: f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/{'v3' if model_size == 'large' else 'v5'}/{model_file}" + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + preprocessed = [] + for x in raw_inputs: + preprocessed.append(x) + return [{"x": preprocessed[0]}] + + +class PaddleOCRClassification(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file="classification.onnx", + download_urls={ + "classification.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/classification.onnx" + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + processed = [] + for img in raw_inputs: + processed.append({"x": img}) + return processed + + +class PaddleOCRRecognition(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="paddleocr-onnx", + model_file="recognition_v4.onnx", + download_urls={ + "recognition_v4.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/v4/recognition_v4.onnx", + "ppocr_keys_v1.txt": f"{GITHUB_ENDPOINT}/hawkeye217/paddleocr-onnx/raw/refs/heads/master/models/v4/ppocr_keys_v1.txt", + }, + ) + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.paddleocr.value, + ) + + def _preprocess_inputs(self, raw_inputs): + processed = [] + for img in raw_inputs: + processed.append({"x": img}) + return processed + + +class LicensePlateDetector(BaseEmbedding): + def __init__( + self, + model_size: str, + requestor: InterProcessRequestor, + device: str = "AUTO", + ): + GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com") + super().__init__( + model_name="yolov9_license_plate", + model_file="yolov9-256-license-plates.onnx", + download_urls={ + "yolov9-256-license-plates.onnx": f"{GITHUB_ENDPOINT}/hawkeye217/yolov9-license-plates/raw/refs/heads/master/models/yolov9-256-license-plates.onnx" + }, + ) + + self.requestor = requestor + self.model_size = model_size + self.device = device + self.download_path = os.path.join(MODEL_CACHE_DIR, self.model_name) + self.runner: BaseModelRunner | None = None + files_names = list(self.download_urls.keys()) + if not all( + os.path.exists(os.path.join(self.download_path, n)) for n in files_names + ): + logger.debug(f"starting model download for {self.model_name}") + self.downloader = ModelDownloader( + model_name=self.model_name, + download_path=self.download_path, + file_names=files_names, + download_func=self._download_model, + ) + self.downloader.ensure_model_files() + else: + self.downloader = None + ModelDownloader.mark_files_state( + self.requestor, + self.model_name, + files_names, + ModelStatusTypesEnum.downloaded, + ) + self._load_model_and_utils() + logger.debug(f"models are already downloaded for {self.model_name}") + + def _load_model_and_utils(self): + if self.runner is None: + if self.downloader: + self.downloader.wait_for_download() + + self.runner = get_optimized_runner( + os.path.join(self.download_path, self.model_file), + self.device, + model_type=EnrichmentModelTypeEnum.yolov9_license_plate.value, + ) + + def _preprocess_inputs(self, raw_inputs): + if isinstance(raw_inputs, list): + raise ValueError("License plate embedding does not support batch inputs.") + + img = raw_inputs + height, width, channels = img.shape + + # Resize maintaining aspect ratio + if width > height: + new_height = int(((height / width) * LPR_EMBEDDING_SIZE) // 4 * 4) + img = cv2.resize(img, (LPR_EMBEDDING_SIZE, new_height)) + else: + new_width = int(((width / height) * LPR_EMBEDDING_SIZE) // 4 * 4) + img = cv2.resize(img, (new_width, LPR_EMBEDDING_SIZE)) + + # Get new dimensions after resize + og_h, og_w, channels = img.shape + + # Create black square frame + frame = np.full( + (LPR_EMBEDDING_SIZE, LPR_EMBEDDING_SIZE, channels), + (0, 0, 0), + dtype=np.float32, + ) + + # Center the resized image in the square frame + x_center = (LPR_EMBEDDING_SIZE - og_w) // 2 + y_center = (LPR_EMBEDDING_SIZE - og_h) // 2 + frame[y_center : y_center + og_h, x_center : x_center + og_w] = img + + # Normalize to 0-1 + frame = frame / 255.0 + + # Convert from HWC to CHW format and add batch dimension + frame = np.transpose(frame, (2, 0, 1)) + frame = np.expand_dims(frame, axis=0) + return [{"images": frame}] diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/types.py b/sam2-cpu/frigate-dev/frigate/embeddings/types.py new file mode 100644 index 0000000..32cbe5d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/types.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class EmbeddingTypeEnum(str, Enum): + thumbnail = "thumbnail" + description = "description" + + +class EnrichmentModelTypeEnum(str, Enum): + arcface = "arcface" + facenet = "facenet" + jina_v1 = "jina_v1" + jina_v2 = "jina_v2" + paddleocr = "paddleocr" + yolov9_license_plate = "yolov9_license_plate" diff --git a/sam2-cpu/frigate-dev/frigate/embeddings/util.py b/sam2-cpu/frigate-dev/frigate/embeddings/util.py new file mode 100644 index 0000000..bc1a952 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/embeddings/util.py @@ -0,0 +1,54 @@ +"""Z-score normalization for search distance.""" + +import math + + +class ZScoreNormalization: + def __init__(self, scale_factor: float = 1.0, bias: float = 0.0): + """Initialize with optional scaling and bias adjustments.""" + """scale_factor adjusts the magnitude of each score""" + """bias will artificially shift the entire distribution upwards""" + self.n = 0 + self.mean = 0 + self.m2 = 0 + self.scale_factor = scale_factor + self.bias = bias + + @property + def variance(self): + return self.m2 / (self.n - 1) if self.n > 1 else 0.0 + + @property + def stddev(self): + return math.sqrt(self.variance) if self.variance > 0 else 0.0 + + def normalize(self, distances: list[float], save_stats: bool): + if save_stats: + self._update(distances) + if self.stddev == 0: + return distances + return [ + (x - self.mean) / self.stddev * self.scale_factor + self.bias + for x in distances + ] + + def _update(self, distances: list[float]): + for x in distances: + self.n += 1 + delta = x - self.mean + self.mean += delta / self.n + delta2 = x - self.mean + self.m2 += delta * delta2 + + def to_dict(self): + return { + "n": self.n, + "mean": self.mean, + "m2": self.m2, + } + + def from_dict(self, data: dict): + self.n = data["n"] + self.mean = data["mean"] + self.m2 = data["m2"] + return self diff --git a/sam2-cpu/frigate-dev/frigate/events/__init__.py b/sam2-cpu/frigate-dev/frigate/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/events/audio.py b/sam2-cpu/frigate-dev/frigate/events/audio.py new file mode 100644 index 0000000..1aa2277 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/events/audio.py @@ -0,0 +1,427 @@ +"""Handle creating audio events.""" + +import datetime +import logging +import threading +import time +from multiprocessing.managers import DictProxy +from multiprocessing.synchronize import Event as MpEvent +from typing import Tuple + +import numpy as np + +from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, CameraInput, FfmpegConfig, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + AUDIO_DURATION, + AUDIO_FORMAT, + AUDIO_MAX_BIT_RANGE, + AUDIO_MIN_CONFIDENCE, + AUDIO_SAMPLE_RATE, + EXPIRE_AUDIO_ACTIVITY, + PROCESS_PRIORITY_HIGH, + UPDATE_AUDIO_ACTIVITY, +) +from frigate.data_processing.common.audio_transcription.model import ( + AudioTranscriptionModelRunner, +) +from frigate.data_processing.real_time.audio_transcription import ( + AudioTranscriptionRealTimeProcessor, +) +from frigate.ffmpeg_presets import parse_preset_input +from frigate.log import LogPipe, redirect_output_to_logger +from frigate.object_detection.base import load_labels +from frigate.util.builtin import get_ffmpeg_arg_list +from frigate.util.process import FrigateProcess +from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg + +try: + from tflite_runtime.interpreter import Interpreter +except ModuleNotFoundError: + from tensorflow.lite.python.interpreter import Interpreter + + +logger = logging.getLogger(__name__) + + +def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> list[str]: + ffmpeg_input: CameraInput = [i for i in ffmpeg.inputs if "audio" in i.roles][0] + input_args = get_ffmpeg_arg_list(ffmpeg.global_args) + ( + parse_preset_input(ffmpeg_input.input_args, 1) + or get_ffmpeg_arg_list(ffmpeg_input.input_args) + or parse_preset_input(ffmpeg.input_args, 1) + or get_ffmpeg_arg_list(ffmpeg.input_args) + ) + return ( + [ffmpeg.ffmpeg_path, "-vn", "-threads", "1"] + + input_args + + ["-i"] + + [ffmpeg_input.path] + + [ + "-threads", + "1", + "-f", + f"{AUDIO_FORMAT}", + "-ar", + f"{AUDIO_SAMPLE_RATE}", + "-ac", + "1", + "-y", + "pipe:", + ] + ) + + +class AudioProcessor(FrigateProcess): + name = "frigate.audio_manager" + + def __init__( + self, + config: FrigateConfig, + cameras: list[CameraConfig], + camera_metrics: DictProxy, + stop_event: MpEvent, + ): + super().__init__( + stop_event, PROCESS_PRIORITY_HIGH, name="frigate.audio_manager", daemon=True + ) + + self.camera_metrics = camera_metrics + self.cameras = cameras + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + audio_threads: list[AudioEventMaintainer] = [] + + threading.current_thread().name = "process:audio_manager" + + if self.config.audio_transcription.enabled: + self.transcription_model_runner = AudioTranscriptionModelRunner( + self.config.audio_transcription.device, + self.config.audio_transcription.model_size, + ) + else: + self.transcription_model_runner = None + + if len(self.cameras) == 0: + return + + for camera in self.cameras: + audio_thread = AudioEventMaintainer( + camera, + self.config, + self.camera_metrics, + self.transcription_model_runner, + self.stop_event, + ) + audio_threads.append(audio_thread) + audio_thread.start() + + self.logger.info(f"Audio processor started (pid: {self.pid})") + + while not self.stop_event.wait(): + pass + + for thread in audio_threads: + thread.join(1) + if thread.is_alive(): + self.logger.info(f"Waiting for thread {thread.name:s} to exit") + thread.join(10) + + for thread in audio_threads: + if thread.is_alive(): + self.logger.warning(f"Thread {thread.name} is still alive") + + self.logger.info("Exiting audio processor") + + +class AudioEventMaintainer(threading.Thread): + def __init__( + self, + camera: CameraConfig, + config: FrigateConfig, + camera_metrics: DictProxy, + audio_transcription_model_runner: AudioTranscriptionModelRunner | None, + stop_event: threading.Event, + ) -> None: + super().__init__(name=f"{camera.name}_audio_event_processor") + + self.config = config + self.camera_config = camera + self.camera_metrics = camera_metrics + self.stop_event = stop_event + self.detector = AudioTfl(stop_event, self.camera_config.audio.num_threads) + self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),) + self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2)) + self.logger = logging.getLogger(f"audio.{self.camera_config.name}") + self.ffmpeg_cmd = get_ffmpeg_command(self.camera_config.ffmpeg) + self.logpipe = LogPipe(f"ffmpeg.{self.camera_config.name}.audio") + self.audio_listener = None + self.audio_transcription_model_runner = audio_transcription_model_runner + self.transcription_processor = None + self.transcription_thread = None + + # create communication for audio detections + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {self.camera_config.name: self.camera_config}, + [ + CameraConfigUpdateEnum.audio, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.audio_transcription, + ], + ) + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.audio.value) + + if self.config.audio_transcription.enabled: + # init the transcription processor for this camera + self.transcription_processor = AudioTranscriptionRealTimeProcessor( + config=self.config, + camera_config=self.camera_config, + requestor=self.requestor, + model_runner=self.audio_transcription_model_runner, + metrics=self.camera_metrics[self.camera_config.name], + stop_event=self.stop_event, + ) + + self.transcription_thread = threading.Thread( + target=self.transcription_processor.run, + name=f"{self.camera_config.name}_transcription_processor", + daemon=True, + ) + self.transcription_thread.start() + + self.was_enabled = camera.enabled + + def detect_audio(self, audio) -> None: + if not self.camera_config.audio.enabled or self.stop_event.is_set(): + return + + audio_as_float = audio.astype(np.float32) + rms, dBFS = self.calculate_audio_levels(audio_as_float) + + self.camera_metrics[self.camera_config.name].audio_rms.value = rms + self.camera_metrics[self.camera_config.name].audio_dBFS.value = dBFS + + audio_detections: list[Tuple[str, float]] = [] + + # only run audio detection when volume is above min_volume + if rms >= self.camera_config.audio.min_volume: + # create waveform relative to max range and look for detections + waveform = (audio / AUDIO_MAX_BIT_RANGE).astype(np.float32) + model_detections = self.detector.detect(waveform) + + for label, score, _ in model_detections: + self.logger.debug( + f"{self.camera_config.name} heard {label} with a score of {score}" + ) + + if label not in self.camera_config.audio.listen: + continue + + if score > dict( + (self.camera_config.audio.filters or {}).get(label, {}) + ).get("threshold", 0.8): + audio_detections.append((label, score)) + + # send audio detection data + self.detection_publisher.publish( + ( + self.camera_config.name, + datetime.datetime.now().timestamp(), + dBFS, + [label for label, _ in audio_detections], + ) + ) + + # send audio activity update + self.requestor.send_data( + UPDATE_AUDIO_ACTIVITY, + {self.camera_config.name: {"detections": audio_detections}}, + ) + + # run audio transcription + if self.transcription_processor is not None: + if self.camera_config.audio_transcription.live_enabled: + # process audio until we've reached the endpoint + self.transcription_processor.process_audio( + { + "id": f"{self.camera_config.name}_audio", + "camera": self.camera_config.name, + }, + audio, + ) + else: + self.transcription_processor.check_unload_model() + + def calculate_audio_levels(self, audio_as_float: np.float32) -> Tuple[float, float]: + # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude + # Note: np.float32 isn't serializable, we must use np.float64 to publish the message + rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float)))) + + # Transform RMS to dBFS (decibels relative to full scale) + if rms > 0: + dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE) + else: + dBFS = 0 + + self.requestor.send_data(f"{self.camera_config.name}/audio/dBFS", float(dBFS)) + self.requestor.send_data(f"{self.camera_config.name}/audio/rms", float(rms)) + + return float(rms), float(dBFS) + + def start_or_restart_ffmpeg(self) -> None: + self.audio_listener = start_or_restart_ffmpeg( + self.ffmpeg_cmd, + self.logger, + self.logpipe, + self.chunk_size, + self.audio_listener, + ) + self.requestor.send_data(f"{self.camera_config.name}/status/audio", "online") + + def read_audio(self) -> None: + def log_and_restart() -> None: + if self.stop_event.is_set(): + return + + time.sleep(self.camera_config.ffmpeg.retry_interval) + self.logpipe.dump() + self.start_or_restart_ffmpeg() + + try: + chunk = self.audio_listener.stdout.read(self.chunk_size) + + if not chunk: + if self.audio_listener.poll() is not None: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "offline" + ) + self.logger.error("ffmpeg process is not running, restarting...") + log_and_restart() + return + + return + + audio = np.frombuffer(chunk, dtype=np.int16) + self.detect_audio(audio) + except Exception as e: + self.logger.error(f"Error reading audio data from ffmpeg process: {e}") + log_and_restart() + + def run(self) -> None: + if self.camera_config.enabled: + self.start_or_restart_ffmpeg() + + while not self.stop_event.is_set(): + enabled = self.camera_config.enabled + if enabled != self.was_enabled: + if enabled: + self.logger.debug( + f"Enabling audio detections for {self.camera_config.name}" + ) + self.start_or_restart_ffmpeg() + else: + self.requestor.send_data( + f"{self.camera_config.name}/status/audio", "disabled" + ) + self.logger.debug( + f"Disabling audio detections for {self.camera_config.name}, ending events" + ) + self.requestor.send_data( + EXPIRE_AUDIO_ACTIVITY, self.camera_config.name + ) + stop_ffmpeg(self.audio_listener, self.logger) + self.audio_listener = None + self.was_enabled = enabled + continue + + if not enabled: + time.sleep(0.1) + continue + + # check if there is an updated config + self.config_subscriber.check_for_updates() + + self.read_audio() + + if self.audio_listener: + stop_ffmpeg(self.audio_listener, self.logger) + if self.transcription_thread: + self.transcription_thread.join(timeout=2) + if self.transcription_thread.is_alive(): + self.logger.warning( + f"Audio transcription thread {self.transcription_thread.name} is still alive" + ) + self.logpipe.close() + self.requestor.stop() + self.config_subscriber.stop() + self.detection_publisher.stop() + + +class AudioTfl: + @redirect_output_to_logger(logger, logging.DEBUG) + def __init__(self, stop_event: threading.Event, num_threads=2): + self.stop_event = stop_event + self.num_threads = num_threads + self.labels = load_labels("/audio-labelmap.txt", prefill=521) + self.interpreter = Interpreter( + model_path="/cpu_audio_model.tflite", + num_threads=self.num_threads, + ) + + self.interpreter.allocate_tensors() + + self.tensor_input_details = self.interpreter.get_input_details() + self.tensor_output_details = self.interpreter.get_output_details() + + def _detect_raw(self, tensor_input): + self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input) + self.interpreter.invoke() + detections = np.zeros((20, 6), np.float32) + + res = self.interpreter.get_tensor(self.tensor_output_details[0]["index"])[0] + non_zero_indices = res > 0 + class_ids = np.argpartition(-res, 20)[:20] + class_ids = class_ids[np.argsort(-res[class_ids])] + class_ids = class_ids[non_zero_indices[class_ids]] + scores = res[class_ids] + boxes = np.full((scores.shape[0], 4), -1, np.float32) + count = len(scores) + + for i in range(count): + if scores[i] < AUDIO_MIN_CONFIDENCE or i == 20: + break + detections[i] = [ + class_ids[i], + float(scores[i]), + boxes[i][0], + boxes[i][1], + boxes[i][2], + boxes[i][3], + ] + + return detections + + def detect(self, tensor_input, threshold=AUDIO_MIN_CONFIDENCE): + detections = [] + + if self.stop_event.is_set(): + return detections + + raw_detections = self._detect_raw(tensor_input) + + for d in raw_detections: + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + return detections diff --git a/sam2-cpu/frigate-dev/frigate/events/cleanup.py b/sam2-cpu/frigate-dev/frigate/events/cleanup.py new file mode 100644 index 0000000..1ac03b2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/events/cleanup.py @@ -0,0 +1,366 @@ +"""Cleanup events based on configured retention.""" + +import datetime +import logging +import os +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any + +from frigate.config import FrigateConfig +from frigate.const import CLIPS_DIR +from frigate.db.sqlitevecq import SqliteVecQueueDatabase +from frigate.models import Event, Timeline +from frigate.util.file import delete_event_snapshot, delete_event_thumbnail + +logger = logging.getLogger(__name__) + + +CHUNK_SIZE = 50 + + +class EventCleanup(threading.Thread): + def __init__( + self, config: FrigateConfig, stop_event: MpEvent, db: SqliteVecQueueDatabase + ): + super().__init__(name="event_cleanup") + self.config = config + self.stop_event = stop_event + self.db = db + self.camera_keys = list(self.config.cameras.keys()) + self.removed_camera_labels: list[str] = None + self.camera_labels: dict[str, dict[str, Any]] = {} + + def get_removed_camera_labels(self) -> list[Event]: + """Get a list of distinct labels for removed cameras.""" + if self.removed_camera_labels is None: + self.removed_camera_labels = list( + Event.select(Event.label) + .where(Event.camera.not_in(self.camera_keys)) + .distinct() + .execute() + ) + + return self.removed_camera_labels + + def get_camera_labels(self, camera: str) -> list[Event]: + """Get a list of distinct labels for each camera, updating once a day.""" + if ( + self.camera_labels.get(camera) is None + or self.camera_labels[camera]["last_update"] + < (datetime.datetime.now() - datetime.timedelta(days=1)).timestamp() + ): + self.camera_labels[camera] = { + "last_update": datetime.datetime.now().timestamp(), + "labels": list( + Event.select(Event.label) + .where(Event.camera == camera) + .distinct() + .execute() + ), + } + + return self.camera_labels[camera]["labels"] + + def expire_snapshots(self) -> list[str]: + ## Expire events from unlisted cameras based on the global config + retain_config = self.config.snapshots.retain + update_params = {"has_snapshot": False} + + distinct_labels = self.get_removed_camera_labels() + + ## Expire events from cameras no longer in the config + # loop over object types in db + for event in distinct_labels: + # get expiration time for this label + expire_days = retain_config.objects.get(event.label, retain_config.default) + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events: list[Event] = ( + Event.select( + Event.id, + Event.camera, + Event.thumbnail, + ) + .where( + Event.camera.not_in(self.camera_keys), + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + logger.debug(f"{len(list(expired_events))} events can be expired") + + # delete the media from disk + for expired in expired_events: + deleted = delete_event_snapshot(expired) + + if not deleted: + logger.warning( + f"Unable to delete event images for {expired.camera}: {expired.id}" + ) + + # update the clips attribute for the db entry + query = Event.select(Event.id).where( + Event.camera.not_in(self.camera_keys), + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + + events_to_update = [] + + for event in query.iterator(): + events_to_update.append(event.id) + if len(events_to_update) >= CHUNK_SIZE: + logger.debug( + f"Updating {update_params} for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + events_to_update = [] + + # Update any remaining events + if events_to_update: + logger.debug( + f"Updating clips/snapshots attribute for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + + events_to_update = [] + + ## Expire events from cameras based on the camera config + for name, camera in self.config.cameras.items(): + retain_config = camera.snapshots.retain + + # get distinct objects in database for this camera + distinct_labels = self.get_camera_labels(name) + + # loop over object types in db + for event in distinct_labels: + # get expiration time for this label + expire_days = retain_config.objects.get( + event.label, retain_config.default + ) + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events = ( + Event.select( + Event.id, + Event.camera, + Event.thumbnail, + ) + .where( + Event.camera == name, + Event.start_time < expire_after, + Event.label == event.label, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + + # delete the grabbed clips from disk + # only snapshots are stored in /clips + # so no need to delete mp4 files + for event in expired_events: + events_to_update.append(event.id) + deleted = delete_event_snapshot(event) + + if not deleted: + logger.warning( + f"Unable to delete event images for {event.camera}: {event.id}" + ) + + # update the clips attribute for the db entry + for i in range(0, len(events_to_update), CHUNK_SIZE): + batch = events_to_update[i : i + CHUNK_SIZE] + logger.debug(f"Updating {update_params} for {len(batch)} events") + Event.update(update_params).where(Event.id << batch).execute() + + return events_to_update + + def expire_clips(self) -> list[str]: + ## Expire events from unlisted cameras based on the global config + expire_days = max( + self.config.record.alerts.retain.days, + self.config.record.detections.retain.days, + ) + file_extension = None # mp4 clips are no longer stored in /clips + update_params = {"has_clip": False} + + # get expiration time for this label + + expire_after = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + # grab all events after specific time + expired_events: list[Event] = ( + Event.select( + Event.id, + Event.camera, + ) + .where( + Event.camera.not_in(self.camera_keys), + Event.start_time < expire_after, + Event.retain_indefinitely == False, + ) + .namedtuples() + .iterator() + ) + logger.debug(f"{len(list(expired_events))} events can be expired") + # delete the media from disk + for expired in expired_events: + media_name = f"{expired.camera}-{expired.id}" + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}") + + try: + media_path.unlink(missing_ok=True) + if file_extension == "jpg": + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp" + ) + media_path.unlink(missing_ok=True) + # Also delete clean.png (legacy) for backward compatibility + media_path = Path( + f"{os.path.join(CLIPS_DIR, media_name)}-clean.png" + ) + media_path.unlink(missing_ok=True) + except OSError as e: + logger.warning(f"Unable to delete event images: {e}") + + # update the clips attribute for the db entry + query = Event.select(Event.id).where( + Event.camera.not_in(self.camera_keys), + Event.start_time < expire_after, + Event.retain_indefinitely == False, + ) + + events_to_update = [] + + for event in query.iterator(): + events_to_update.append(event.id) + + if len(events_to_update) >= CHUNK_SIZE: + logger.debug( + f"Updating {update_params} for {len(events_to_update)} events" + ) + Event.update(update_params).where( + Event.id << events_to_update + ).execute() + events_to_update = [] + + # Update any remaining events + if events_to_update: + logger.debug( + f"Updating clips/snapshots attribute for {len(events_to_update)} events" + ) + Event.update(update_params).where(Event.id << events_to_update).execute() + + events_to_update = [] + now = datetime.datetime.now() + + ## Expire events from cameras based on the camera config + for name, camera in self.config.cameras.items(): + expire_days = max( + camera.record.alerts.retain.days, + camera.record.detections.retain.days, + ) + alert_expire_date = ( + now - datetime.timedelta(days=camera.record.alerts.retain.days) + ).timestamp() + detection_expire_date = ( + now - datetime.timedelta(days=camera.record.detections.retain.days) + ).timestamp() + # grab all events after specific time + expired_events = ( + Event.select( + Event.id, + Event.camera, + ) + .where( + Event.camera == name, + Event.retain_indefinitely == False, + ( + ( + (Event.data["max_severity"] != "detection") + | (Event.data["max_severity"].is_null()) + ) + & (Event.end_time < alert_expire_date) + ) + | ( + (Event.data["max_severity"] == "detection") + & (Event.end_time < detection_expire_date) + ), + ) + .namedtuples() + .iterator() + ) + + # delete the grabbed clips from disk + # only snapshots are stored in /clips + # so no need to delete mp4 files + for event in expired_events: + events_to_update.append(event.id) + + # update the clips attribute for the db entry + for i in range(0, len(events_to_update), CHUNK_SIZE): + batch = events_to_update[i : i + CHUNK_SIZE] + logger.debug(f"Updating {update_params} for {len(batch)} events") + Event.update(update_params).where(Event.id << batch).execute() + + return events_to_update + + def run(self) -> None: + # only expire events every 5 minutes + while not self.stop_event.wait(300): + events_with_expired_clips = self.expire_clips() + + # delete timeline entries for events that have expired recordings + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_events_list = list(events_with_expired_clips) + for i in range(0, len(deleted_events_list), max_deletes): + Timeline.delete().where( + Timeline.source_id << deleted_events_list[i : i + max_deletes] + ).execute() + + self.expire_snapshots() + + # drop events from db where has_clip and has_snapshot are false + events = ( + Event.select() + .where(Event.has_clip == False, Event.has_snapshot == False) + .iterator() + ) + events_to_delete: list[Event] = [e for e in events] + + for e in events_to_delete: + delete_event_thumbnail(e) + + logger.debug(f"Found {len(events_to_delete)} events that can be expired") + if len(events_to_delete) > 0: + ids_to_delete = [e.id for e in events_to_delete] + for i in range(0, len(ids_to_delete), CHUNK_SIZE): + chunk = ids_to_delete[i : i + CHUNK_SIZE] + logger.debug(f"Deleting {len(chunk)} events from the database") + Event.delete().where(Event.id << chunk).execute() + + if self.config.semantic_search.enabled: + self.db.delete_embeddings_description(event_ids=chunk) + self.db.delete_embeddings_thumbnail(event_ids=chunk) + logger.debug(f"Deleted {len(ids_to_delete)} embeddings") + + logger.info("Exiting event cleanup...") diff --git a/sam2-cpu/frigate-dev/frigate/events/maintainer.py b/sam2-cpu/frigate-dev/frigate/events/maintainer.py new file mode 100644 index 0000000..2b0fc41 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/events/maintainer.py @@ -0,0 +1,298 @@ +import logging +import threading +from multiprocessing import Queue +from multiprocessing.synchronize import Event as MpEvent +from typing import Dict + +from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber +from frigate.config import FrigateConfig +from frigate.events.types import EventStateEnum, EventTypeEnum +from frigate.models import Event +from frigate.util.builtin import to_relative_box + +logger = logging.getLogger(__name__) + + +def should_update_db(prev_event: Event, current_event: Event) -> bool: + """If current_event has updated fields and (clip or snapshot).""" + if current_event["has_clip"] or current_event["has_snapshot"]: + # if this is the first time has_clip or has_snapshot turned true + if not prev_event["has_clip"] and not prev_event["has_snapshot"]: + return True + # or if any of the following values changed + if ( + prev_event["top_score"] != current_event["top_score"] + or prev_event["entered_zones"] != current_event["entered_zones"] + or prev_event["end_time"] != current_event["end_time"] + or prev_event["average_estimated_speed"] + != current_event["average_estimated_speed"] + or prev_event["velocity_angle"] != current_event["velocity_angle"] + or prev_event["recognized_license_plate"] + != current_event["recognized_license_plate"] + or prev_event["path_data"] != current_event["path_data"] + ): + return True + return False + + +def should_update_state(prev_event: Event, current_event: Event) -> bool: + """If current event should update state, but not necessarily update the db.""" + if prev_event["stationary"] != current_event["stationary"]: + return True + + if prev_event["attributes"] != current_event["attributes"]: + return True + + if prev_event["sub_label"] != current_event["sub_label"]: + return True + + if len(prev_event["current_zones"]) < len(current_event["current_zones"]): + return True + + return False + + +class EventProcessor(threading.Thread): + def __init__( + self, + config: FrigateConfig, + timeline_queue: Queue, + stop_event: MpEvent, + ): + super().__init__(name="event_processor") + self.config = config + self.timeline_queue = timeline_queue + self.events_in_process: Dict[str, Event] = {} + self.stop_event = stop_event + + self.event_receiver = EventUpdateSubscriber() + self.event_end_publisher = EventEndPublisher() + + def run(self) -> None: + # set an end_time on events without an end_time on startup + Event.update(end_time=Event.start_time + 30).where( + Event.end_time == None + ).execute() + + while not self.stop_event.is_set(): + update = self.event_receiver.check_for_update(timeout=1) + + if update == None: + continue + + source_type, event_type, camera, _, event_data = update + + logger.debug( + f"Event received: {source_type} {event_type} {camera} {event_data['id']}" + ) + + if source_type == EventTypeEnum.tracked_object: + id = event_data["id"] + self.timeline_queue.put( + ( + camera, + source_type, + event_type, + self.events_in_process.get(id), + event_data, + ) + ) + + # if this is the first message, just store it and continue, its not time to insert it in the db + if ( + event_type == EventStateEnum.start + or id not in self.events_in_process + ): + self.events_in_process[id] = event_data + continue + + self.handle_object_detection(event_type, camera, event_data) + elif source_type == EventTypeEnum.api: + self.timeline_queue.put( + ( + camera, + source_type, + event_type, + {}, + event_data, + ) + ) + + self.handle_external_detection(event_type, event_data) + + self.event_receiver.stop() + self.event_end_publisher.stop() + logger.info("Exiting event processor...") + + def handle_object_detection( + self, + event_type: str, + camera: str, + event_data: Event, + ) -> None: + """handle tracked object event updates.""" + updated_db = False + + if should_update_db(self.events_in_process[event_data["id"]], event_data): + updated_db = True + camera_config = self.config.cameras[camera] + width = camera_config.detect.width + height = camera_config.detect.height + first_detector = list(self.config.detectors.values())[0] + + start_time = event_data["start_time"] + end_time = ( + None if event_data["end_time"] is None else event_data["end_time"] + ) + # score of the snapshot + score = ( + None + if event_data["snapshot"] is None + else event_data["snapshot"]["score"] + ) + # detection region in the snapshot + region = ( + None + if event_data["snapshot"] is None + else to_relative_box( + width, + height, + event_data["snapshot"]["region"], + ) + ) + # bounding box for the snapshot + box = ( + None + if event_data["snapshot"] is None + else to_relative_box( + width, + height, + event_data["snapshot"]["box"], + ) + ) + + attributes = ( + None + if event_data["snapshot"] is None + else [ + { + "box": to_relative_box( + width, + height, + a["box"], + ), + "label": a["label"], + "score": a["score"], + } + for a in event_data["snapshot"]["attributes"] + ] + ) + + # keep these from being set back to false because the event + # may have started while recordings/snapshots/alerts/detections were enabled + # this would be an issue for long running events + if self.events_in_process[event_data["id"]]["has_clip"]: + event_data["has_clip"] = True + if self.events_in_process[event_data["id"]]["has_snapshot"]: + event_data["has_snapshot"] = True + + event = { + Event.id: event_data["id"], + Event.label: event_data["label"], + Event.camera: camera, + Event.start_time: start_time, + Event.end_time: end_time, + Event.zones: list(event_data["entered_zones"]), + Event.thumbnail: event_data.get("thumbnail"), + Event.has_clip: event_data["has_clip"], + Event.has_snapshot: event_data["has_snapshot"], + Event.model_hash: first_detector.model.model_hash, + Event.model_type: first_detector.model.model_type, + Event.detector_type: first_detector.type, + Event.data: { + "box": box, + "region": region, + "score": score, + "top_score": event_data["top_score"], + "attributes": attributes, + "average_estimated_speed": event_data["average_estimated_speed"], + "velocity_angle": event_data["velocity_angle"], + "type": "object", + "max_severity": event_data.get("max_severity"), + "path_data": event_data.get("path_data"), + }, + } + + # only overwrite the sub_label in the database if it's set + if event_data.get("sub_label") is not None: + event[Event.sub_label] = event_data["sub_label"][0] + event[Event.data]["sub_label_score"] = event_data["sub_label"][1] + + # only overwrite the recognized_license_plate in the database if it's set + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ][0] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "recognized_license_plate" + ][1] + + ( + Event.insert(event) + .on_conflict( + conflict_target=[Event.id], + update=event, + ) + .execute() + ) + + # check if the stored event_data should be updated + if updated_db or should_update_state( + self.events_in_process[event_data["id"]], event_data + ): + # update the stored copy for comparison on future update messages + self.events_in_process[event_data["id"]] = event_data + + if event_type == EventStateEnum.end: + del self.events_in_process[event_data["id"]] + self.event_end_publisher.publish((event_data["id"], camera, updated_db)) + + def handle_external_detection( + self, event_type: EventStateEnum, event_data: Event + ) -> None: + if event_type == EventStateEnum.start: + event = { + Event.id: event_data["id"], + Event.label: event_data["label"], + Event.sub_label: event_data["sub_label"], + Event.camera: event_data["camera"], + Event.start_time: event_data["start_time"], + Event.end_time: event_data["end_time"], + Event.thumbnail: event_data.get("thumbnail"), + Event.has_clip: event_data["has_clip"], + Event.has_snapshot: event_data["has_snapshot"], + Event.zones: [], + Event.data: { + "type": event_data["type"], + "score": event_data["score"], + "top_score": event_data["score"], + }, + } + if event_data.get("recognized_license_plate") is not None: + event[Event.data]["recognized_license_plate"] = event_data[ + "recognized_license_plate" + ] + event[Event.data]["recognized_license_plate_score"] = event_data[ + "score" + ] + Event.insert(event).execute() + elif event_type == EventStateEnum.end: + event = { + Event.id: event_data["id"], + Event.end_time: event_data["end_time"], + } + + try: + Event.update(event).where(Event.id == event_data["id"]).execute() + except Exception: + logger.warning(f"Failed to update manual event: {event_data['id']}") diff --git a/sam2-cpu/frigate-dev/frigate/events/types.py b/sam2-cpu/frigate-dev/frigate/events/types.py new file mode 100644 index 0000000..1461c1f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/events/types.py @@ -0,0 +1,19 @@ +"""Types for event management.""" + +from enum import Enum + + +class EventTypeEnum(str, Enum): + api = "api" + tracked_object = "tracked_object" + + +class EventStateEnum(str, Enum): + start = "start" + update = "update" + end = "end" + + +class RegenerateDescriptionEnum(str, Enum): + thumbnails = "thumbnails" + snapshot = "snapshot" diff --git a/sam2-cpu/frigate-dev/frigate/ffmpeg_presets.py b/sam2-cpu/frigate-dev/frigate/ffmpeg_presets.py new file mode 100644 index 0000000..43272a6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/ffmpeg_presets.py @@ -0,0 +1,556 @@ +"""Handles inserting and maintaining ffmpeg presets.""" + +import logging +import os +from enum import Enum +from typing import Any + +from frigate.const import ( + FFMPEG_HVC1_ARGS, + FFMPEG_HWACCEL_AMF, + FFMPEG_HWACCEL_NVIDIA, + FFMPEG_HWACCEL_RKMPP, + FFMPEG_HWACCEL_VAAPI, + FFMPEG_HWACCEL_VULKAN, + LIBAVFORMAT_VERSION_MAJOR, +) +from frigate.util.services import vainfo_hwaccel +from frigate.version import VERSION + +logger = logging.getLogger(__name__) + + +class LibvaGpuSelector: + "Automatically selects the correct libva GPU." + + _valid_gpus: list[str] | None = None + + def __get_valid_gpus(self) -> None: + """Get valid libva GPUs.""" + if not os.path.exists("/dev/dri"): + self._valid_gpus = [] + return + + if self._valid_gpus: + return + + devices = list(filter(lambda d: d.startswith("render"), os.listdir("/dev/dri"))) + + if not devices: + self._valid_gpus = ["/dev/dri/renderD128"] + return + + if len(devices) < 2: + self._valid_gpus = [f"/dev/dri/{devices[0]}"] + return + + self._valid_gpus = [] + for device in devices: + check = vainfo_hwaccel(device_name=device) + + logger.debug(f"{device} return vainfo status code: {check.returncode}") + + if check.returncode == 0: + self._valid_gpus.append(f"/dev/dri/{device}") + + def get_gpu_arg(self, preset: str, gpu: int) -> str: + if "nvidia" in preset: + return str(gpu) + + if self._valid_gpus is None: + self.__get_valid_gpus() + + if not self._valid_gpus: + return "" + + if gpu <= len(self._valid_gpus): + return self._valid_gpus[gpu] + else: + logger.warning(f"Invalid GPU index {gpu}, using first valid GPU") + return self._valid_gpus[0] + + +FPS_VFR_PARAM = "-fps_mode vfr" if LIBAVFORMAT_VERSION_MAJOR >= 59 else "-vsync 2" +TIMEOUT_PARAM = "-timeout" if LIBAVFORMAT_VERSION_MAJOR >= 59 else "-stimeout" + +_gpu_selector = LibvaGpuSelector() +_user_agent_args = [ + "-user_agent", + f"FFmpeg Frigate/{VERSION}", +] + +# Presets for FFMPEG Stream Decoding (detect role) + +PRESETS_HW_ACCEL_DECODE = { + "preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m", + "preset-rpi-64-h265": "-c:v:1 hevc_v4l2m2m", + FFMPEG_HWACCEL_VAAPI: "-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {3} -hwaccel_output_format vaapi", + "preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv -c:v h264_qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17 + "preset-intel-qsv-h265": f"-load_plugin hevc_hw -hwaccel qsv -qsv_device {{3}} -hwaccel_output_format qsv{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}", # https://trac.ffmpeg.org/ticket/9766#comment:17 + FFMPEG_HWACCEL_NVIDIA: "-hwaccel_device {3} -hwaccel cuda -hwaccel_output_format cuda", + "preset-jetson-h264": "-c:v h264_nvmpi -resize {1}x{2}", + "preset-jetson-h265": "-c:v hevc_nvmpi -resize {1}x{2}", + f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra": "-hwaccel rkmpp -hwaccel_output_format drm_prime", + # experimental presets + FFMPEG_HWACCEL_VULKAN: "-hwaccel vulkan -init_hw_device vulkan=gpu:0 -filter_hw_device gpu -hwaccel_output_format vulkan", + FFMPEG_HWACCEL_AMF: "-hwaccel amf -init_hw_device amf=gpu:0 -filter_hw_device gpu -hwaccel_output_format amf", +} +PRESETS_HW_ACCEL_DECODE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_DECODE["preset-nvidia-h265"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_DECODE["preset-nvidia-mjpeg"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_NVIDIA +] + +PRESETS_HW_ACCEL_DECODE[FFMPEG_HWACCEL_RKMPP] = ( + f"{PRESETS_HW_ACCEL_DECODE[f'{FFMPEG_HWACCEL_RKMPP}-no-dump_extra']}{' -bsf:v dump_extra' if LIBAVFORMAT_VERSION_MAJOR >= 61 else ''}" +) +PRESETS_HW_ACCEL_DECODE["preset-rk-h264"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_RKMPP +] +PRESETS_HW_ACCEL_DECODE["preset-rk-h265"] = PRESETS_HW_ACCEL_DECODE[ + FFMPEG_HWACCEL_RKMPP +] + +# Presets for FFMPEG Stream Scaling (detect role) + +PRESETS_HW_ACCEL_SCALE = { + "preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}", + "preset-rpi-64-h265": "-r {0} -vf fps={0},scale={1}:{2}", + FFMPEG_HWACCEL_VAAPI: "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5", + "preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", + "preset-intel-qsv-h265": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p", + FFMPEG_HWACCEL_NVIDIA: "-r {0} -vf fps={0},scale_cuda=w={1}:h={2},hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5", + "preset-jetson-h264": "-r {0}", # scaled in decoder + "preset-jetson-h265": "-r {0}", # scaled in decoder + FFMPEG_HWACCEL_RKMPP: "-r {0} -vf scale_rkrga=w={1}:h={2}:format=yuv420p:force_original_aspect_ratio=0,hwmap=mode=read,format=yuv420p", + "default": "-r {0} -vf fps={0},scale={1}:{2}", + # experimental presets + FFMPEG_HWACCEL_VULKAN: "-r {0} -vf fps={0},hwupload,scale_vulkan=w={1}:h={2},hwdownload", + FFMPEG_HWACCEL_AMF: "-r {0} -vf fps={0},hwupload,scale_amf=w={1}:h={2},hwdownload", +} +PRESETS_HW_ACCEL_SCALE["preset-nvidia-h264"] = PRESETS_HW_ACCEL_SCALE[ + FFMPEG_HWACCEL_NVIDIA +] +PRESETS_HW_ACCEL_SCALE["preset-nvidia-h265"] = PRESETS_HW_ACCEL_SCALE[ + FFMPEG_HWACCEL_NVIDIA +] + +PRESETS_HW_ACCEL_SCALE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_SCALE["preset-rk-h264"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] +PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL_RKMPP] + +# Presets for FFMPEG Stream Encoding (birdseye feature) + +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile main {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -g 50 -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency {2}", +} +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-nvidia-h265"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_NVIDIA] +) + +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_BIRDSEYE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE[ + FFMPEG_HWACCEL_RKMPP +] + +# Presets for FFMPEG Stream Encoding (timelapse feature) + +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -pix_fmt yuv420p {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m -pix_fmt yuv420p {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v hevc_qsv -profile:v main -level:v 4.1 -async_depth:v 1 {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc {2}", + "preset-nvidia-h265": "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v hevc_nvenc {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v hevc_nvmpi -profile main {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high {2}", + "preset-rk-h265": "{0} -hide_banner {1} -c:v hevc_rkmpp -profile:v main {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -profile:v high {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -tune:v zerolatency {2}", +} +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_NVIDIA] +) + +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_TIMELAPSE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE[ + FFMPEG_HWACCEL_RKMPP +] + +# encoding of previews is only done on CPU due to comparable encode times and better quality from libx264 +PRESETS_HW_ACCEL_ENCODE_PREVIEW = { + "default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}", +} + + +def parse_preset_hardware_acceleration_decode( + arg: Any, + fps: int, + width: int, + height: int, + gpu: int, +) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + decode = PRESETS_HW_ACCEL_DECODE.get(arg, None) + + if not decode: + return None + + gpu_arg = _gpu_selector.get_gpu_arg(arg, gpu) + return decode.format(fps, width, height, gpu_arg).split(" ") + + +def parse_preset_hardware_acceleration_scale( + arg: Any, + detect_args: list[str], + fps: int, + width: int, + height: int, +) -> list[str]: + """Return the correct scaling preset or default preset if none is set.""" + if not isinstance(arg, str) or " " in arg: + scale = PRESETS_HW_ACCEL_SCALE["default"] + else: + scale = PRESETS_HW_ACCEL_SCALE.get(arg, PRESETS_HW_ACCEL_SCALE["default"]) + + if ( + ",hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5" in scale + and os.environ.get("FFMPEG_DISABLE_GAMMA_EQUALIZER") is not None + ): + scale = scale.replace( + ",hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5", + ":format=nv12,hwdownload,format=nv12,format=yuv420p", + ) + + scale = scale.format(fps, width, height).split(" ") + scale.extend(detect_args) + return scale + + +class EncodeTypeEnum(str, Enum): + birdseye = "birdseye" + preview = "preview" + timelapse = "timelapse" + + +def parse_preset_hardware_acceleration_encode( + ffmpeg_path: str, + arg: Any, + input: str, + output: str, + type: EncodeTypeEnum = EncodeTypeEnum.birdseye, +) -> str: + """Return the correct scaling preset or default preset if none is set.""" + if type == EncodeTypeEnum.birdseye: + arg_map = PRESETS_HW_ACCEL_ENCODE_BIRDSEYE + elif type == EncodeTypeEnum.preview: + arg_map = PRESETS_HW_ACCEL_ENCODE_PREVIEW + elif type == EncodeTypeEnum.timelapse: + arg_map = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE + + if not isinstance(arg, str): + return arg_map["default"].format(input, output) + + # Not all jetsons have HW encoders, so fall back to default SW encoder if not + if arg.startswith("preset-jetson-") and not os.path.exists("/dev/nvhost-msenc"): + arg = "default" + + return arg_map.get(arg, arg_map["default"]).format( + ffmpeg_path, + input, + output, + _gpu_selector.get_gpu_arg(arg, 0), + ) + + +PRESETS_INPUT = { + "preset-http-jpeg-generic": [ + "-r", + "{}", + "-stream_loop", + "-1", + "-f", + "image2", + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-mjpeg-generic": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-http-reolink": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-flags", + "low_delay", + "-strict", + "experimental", + "-analyzeduration", + "1000M", + "-probesize", + "1000M", + "-rw_timeout", + "10000000", + ], + "preset-rtmp-generic": [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rw_timeout", + "10000000", + "-use_wallclock_as_timestamps", + "1", + "-f", + "live_flv", + ], + "preset-rtsp-generic": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-restream": _user_agent_args + + [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + ], + "preset-rtsp-restream-low-latency": _user_agent_args + + [ + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-fflags", + "nobuffer", + "-flags", + "low_delay", + ], + "preset-rtsp-udp": _user_agent_args + + [ + "-avoid_negative_ts", + "make_zero", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "udp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], + "preset-rtsp-blue-iris": _user_agent_args + + [ + "-user_agent", + f"FFmpeg Frigate/{VERSION}", + "-avoid_negative_ts", + "make_zero", + "-flags", + "low_delay", + "-strict", + "experimental", + "-fflags", + "+genpts+discardcorrupt", + "-rtsp_transport", + "tcp", + TIMEOUT_PARAM, + "10000000", + "-use_wallclock_as_timestamps", + "1", + ], +} + + +def parse_preset_input(arg: Any, detect_fps: int) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + if arg == "preset-http-jpeg-generic": + input = PRESETS_INPUT[arg].copy() + input[len(_user_agent_args) + 1] = str(detect_fps) + return input + + return PRESETS_INPUT.get(arg, None) + + +PRESETS_RECORD_OUTPUT = { + "preset-record-generic": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + "-an", + ], + "preset-record-generic-audio-aac": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-c:a", + "aac", + ], + "preset-record-generic-audio-copy": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c", + "copy", + ], + "preset-record-mjpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-jpeg": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "libx264", + "-an", + ], + "preset-record-ubiquiti": [ + "-f", + "segment", + "-segment_time", + "10", + "-segment_format", + "mp4", + "-reset_timestamps", + "1", + "-strftime", + "1", + "-c:v", + "copy", + "-ar", + "44100", + "-c:a", + "aac", + ], +} + + +def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str]: + """Return the correct preset if in preset format otherwise return None.""" + if not isinstance(arg, str): + return None + + preset = PRESETS_RECORD_OUTPUT.get(arg, None) + + if not preset: + return None + + if force_record_hvc1: + # Apple only supports HEVC if it is hvc1 (vs. hev1) + return preset + FFMPEG_HVC1_ARGS + + return preset diff --git a/sam2-cpu/frigate-dev/frigate/genai/__init__.py b/sam2-cpu/frigate-dev/frigate/genai/__init__.py new file mode 100644 index 0000000..910fc13 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/genai/__init__.py @@ -0,0 +1,307 @@ +"""Generative AI module for Frigate.""" + +import datetime +import importlib +import logging +import os +import re +from typing import Any, Optional + +from playhouse.shortcuts import model_to_dict + +from frigate.config import CameraConfig, FrigateConfig, GenAIConfig, GenAIProviderEnum +from frigate.const import CLIPS_DIR +from frigate.data_processing.post.types import ReviewMetadata +from frigate.models import Event + +logger = logging.getLogger(__name__) + +PROVIDERS = {} + + +def register_genai_provider(key: GenAIProviderEnum): + """Register a GenAI provider.""" + + def decorator(cls): + PROVIDERS[key] = cls + return cls + + return decorator + + +class GenAIClient: + """Generative AI client for Frigate.""" + + def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None: + self.genai_config: GenAIConfig = genai_config + self.timeout = timeout + self.provider = self._init_provider() + + def generate_review_description( + self, + review_data: dict[str, Any], + thumbnails: list[bytes], + concerns: list[str], + preferred_language: str | None, + debug_save: bool, + activity_context_prompt: str, + ) -> ReviewMetadata | None: + """Generate a description for the review item activity.""" + + def get_concern_prompt() -> str: + if concerns: + concern_list = "\n - ".join(concerns) + return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring: + - {concern_list}""" + else: + return "" + + def get_language_prompt() -> str: + if preferred_language: + return f"Provide your answer in {preferred_language}" + else: + return "" + + def get_objects_list() -> str: + if review_data["unified_objects"]: + return "\n- " + "\n- ".join(review_data["unified_objects"]) + else: + return "\n- (No objects detected)" + + context_prompt = f""" +Your task is to analyze the sequence of images ({len(thumbnails)} total) taken in chronological order from the perspective of the {review_data["camera"]} security camera. + +## Normal Activity Patterns for This Property + +{activity_context_prompt} + +## Task Instructions + +Your task is to provide a clear, accurate description of the scene that: +1. States exactly what is happening based on observable actions and movements. +2. Evaluates the activity against the Normal and Suspicious Activity Indicators above. +3. Assigns a potential_threat_level (0, 1, or 2) based on the threat level indicators defined above, applying them consistently. + +**Use the activity patterns above as guidance to calibrate your assessment. Match the activity against both normal and suspicious indicators, then use your judgment based on the complete context.** + +## Analysis Guidelines + +When forming your description: +- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list. +- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence. +- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity). +- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects. +- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved. +- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour. +- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible. +- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases. + +## Response Format + +Your response MUST be a flat JSON object with: +- `title` (string): A concise, direct title that describes the primary action or event in the sequence, not just what you literally see. Use spatial context when available to make titles more meaningful. When multiple objects/actions are present, prioritize whichever is most prominent or occurs first. Use names from "Objects in Scene" based on what you visually observe. If you see both a name and an unidentified object of the same type but visually observe only one person/object, use ONLY the name. Examples: "Joe walking dog", "Person taking out trash", "Vehicle arriving in driveway", "Joe accessing vehicle", "Person leaving porch for driveway". +- `scene` (string): A narrative description of what happens across the sequence from start to finish, in chronological order. Start by describing how the sequence begins, then describe the progression of events. **Describe all significant movements and actions in the order they occur.** For example, if a vehicle arrives and then a person exits, describe both actions sequentially. **Only describe actions you can actually observe happening in the frames provided.** Do not infer or assume actions that aren't visible (e.g., if you see someone walking but never see them sit, don't say they sat down). Include setting, detected objects, and their observable actions. Avoid speculation or filling in assumed behaviors. Your description should align with and support the threat level you assign. +- `confidence` (float): 0-1 confidence in your analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. Lower confidence when the sequence is unclear, objects are partially obscured, or context is ambiguous. +- `potential_threat_level` (integer): 0, 1, or 2 as defined in "Normal Activity Patterns for This Property" above. Your threat level must be consistent with your scene description and the guidance above. +{get_concern_prompt()} + +## Sequence Details + +- Frame 1 = earliest, Frame {len(thumbnails)} = latest +- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds +- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"} + +## Objects in Scene + +Each line represents a detection state, not necessarily unique individuals. Parentheses indicate object type or category, use only the name/label in your response, not the parentheses. + +**CRITICAL: When you see both recognized and unrecognized entries of the same type (e.g., "Joe (person)" and "Person"), visually count how many distinct people/objects you actually see based on appearance and clothing. If you observe only ONE person throughout the sequence, use ONLY the recognized name (e.g., "Joe"). The same person may be recognized in some frames but not others. Only describe both if you visually see MULTIPLE distinct people with clearly different appearances.** + +**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.** +{get_objects_list()} + +## Important Notes +- Values must be plain strings, floats, or integers — no nested objects, no extra commentary. +- Only describe objects from the "Objects in Scene" list above. Do not hallucinate additional objects. +- When describing people or vehicles, use the exact names provided. +{get_language_prompt()} +""" + logger.debug( + f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}" + ) + + if debug_save: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", review_data["id"], "prompt.txt" + ), + "w", + ) as f: + f.write(context_prompt) + + response = self._send(context_prompt, thumbnails) + + if debug_save and response: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", review_data["id"], "response.txt" + ), + "w", + ) as f: + f.write(response) + + if response: + clean_json = re.sub( + r"\n?```$", "", re.sub(r"^```[a-zA-Z0-9]*\n?", "", response) + ) + + try: + metadata = ReviewMetadata.model_validate_json(clean_json) + + # If any verified objects (contain parentheses with name), set to 0 + if any("(" in obj for obj in review_data["unified_objects"]): + metadata.potential_threat_level = 0 + + metadata.time = review_data["start"] + return metadata + except Exception as e: + # rarely LLMs can fail to follow directions on output format + logger.warning( + f"Failed to parse review description as the response did not match expected format. {e}" + ) + return None + else: + return None + + def generate_review_summary( + self, + start_ts: float, + end_ts: float, + events: list[dict[str, Any]], + debug_save: bool, + ) -> str | None: + """Generate a summary of review item descriptions over a period of time.""" + time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}" + timeline_summary_prompt = f""" +You are a security officer writing a concise security report. + +Time range: {time_range} + +Input format: Each event is a JSON object with: +- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time" +- "context": array of related events from other cameras that occurred during overlapping time periods + +Report Structure - Use this EXACT format: + +# Security Summary - {time_range} + +## Overview +[Write 1-2 sentences summarizing the overall activity pattern during this period.] + +--- + +## Timeline + +[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.] + +### [Time Block Name] + +**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator] +- [Event title]: [Clear description incorporating contextual information from the "context" array] +- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"] +- Assessment: [Brief assessment incorporating context - if context explains the event, note it here] + +[Repeat for each event in chronological order within the time block] + +--- + +## Summary +[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."] + +Guidelines: +- List ALL events in chronological order, grouped by time blocks +- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern +- Integrate contextual information naturally - use the "context" array to enrich each event's description +- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person") +- Be concise but informative - focus on what happened and what it means +- If contextual information makes an event clearly normal, reflect that in your assessment +- Only create time blocks that have events - don't create empty sections +""" + + timeline_summary_prompt += "\n\nEvents:\n" + for event in events: + timeline_summary_prompt += f"\n{event}\n" + + if debug_save: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "prompt.txt" + ), + "w", + ) as f: + f.write(timeline_summary_prompt) + + response = self._send(timeline_summary_prompt, []) + + if debug_save and response: + with open( + os.path.join( + CLIPS_DIR, "genai-requests", f"{start_ts}-{end_ts}", "response.txt" + ), + "w", + ) as f: + f.write(response) + + return response + + def generate_object_description( + self, + camera_config: CameraConfig, + thumbnails: list[bytes], + event: Event, + ) -> Optional[str]: + """Generate a description for the frame.""" + try: + prompt = camera_config.objects.genai.object_prompts.get( + event.label, + camera_config.objects.genai.prompt, + ).format(**model_to_dict(event)) + except KeyError as e: + logger.error(f"Invalid key in GenAI prompt: {e}") + return None + + logger.debug(f"Sending images to genai provider with prompt: {prompt}") + return self._send(prompt, thumbnails) + + def _init_provider(self): + """Initialize the client.""" + return None + + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to the provider.""" + return None + + def get_context_size(self) -> int: + """Get the context window size for this provider in tokens.""" + return 4096 + + +def get_genai_client(config: FrigateConfig) -> Optional[GenAIClient]: + """Get the GenAI client.""" + if not config.genai.provider: + return None + + load_providers() + provider = PROVIDERS.get(config.genai.provider) + if provider: + return provider(config.genai) + + return None + + +def load_providers(): + package_dir = os.path.dirname(__file__) + for filename in os.listdir(package_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = f"frigate.genai.{filename[:-3]}" + importlib.import_module(module_name) diff --git a/sam2-cpu/frigate-dev/frigate/genai/azure-openai.py b/sam2-cpu/frigate-dev/frigate/genai/azure-openai.py new file mode 100644 index 0000000..eba8b47 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/genai/azure-openai.py @@ -0,0 +1,77 @@ +"""Azure OpenAI Provider for Frigate AI.""" + +import base64 +import logging +from typing import Optional +from urllib.parse import parse_qs, urlparse + +from openai import AzureOpenAI + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.azure_openai) +class OpenAIClient(GenAIClient): + """Generative AI client for Frigate using Azure OpenAI.""" + + provider: AzureOpenAI + + def _init_provider(self): + """Initialize the client.""" + try: + parsed_url = urlparse(self.genai_config.base_url) + query_params = parse_qs(parsed_url.query) + api_version = query_params.get("api-version", [None])[0] + azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/" + + if not api_version: + logger.warning("Azure OpenAI url is missing API version.") + return None + + except Exception as e: + logger.warning("Error parsing Azure OpenAI url: %s", str(e)) + return None + + return AzureOpenAI( + api_key=self.genai_config.api_key, + api_version=api_version, + azure_endpoint=azure_endpoint, + ) + + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to Azure OpenAI.""" + encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] + try: + result = self.provider.chat.completions.create( + model=self.genai_config.model, + messages=[ + { + "role": "user", + "content": [{"type": "text", "text": prompt}] + + [ + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image}", + "detail": "low", + }, + } + for image in encoded_images + ], + }, + ], + timeout=self.timeout, + ) + except Exception as e: + logger.warning("Azure OpenAI returned an error: %s", str(e)) + return None + if len(result.choices) > 0: + return result.choices[0].message.content.strip() + return None + + def get_context_size(self) -> int: + """Get the context window size for Azure OpenAI.""" + return 128000 diff --git a/sam2-cpu/frigate-dev/frigate/genai/gemini.py b/sam2-cpu/frigate-dev/frigate/genai/gemini.py new file mode 100644 index 0000000..f94448d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/genai/gemini.py @@ -0,0 +1,60 @@ +"""Gemini Provider for Frigate AI.""" + +import logging +from typing import Optional + +import google.generativeai as genai +from google.api_core.exceptions import GoogleAPICallError + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.gemini) +class GeminiClient(GenAIClient): + """Generative AI client for Frigate using Gemini.""" + + provider: genai.GenerativeModel + + def _init_provider(self): + """Initialize the client.""" + genai.configure(api_key=self.genai_config.api_key) + return genai.GenerativeModel( + self.genai_config.model, **self.genai_config.provider_options + ) + + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to Gemini.""" + data = [ + { + "mime_type": "image/jpeg", + "data": img, + } + for img in images + ] + [prompt] + try: + response = self.provider.generate_content( + data, + generation_config=genai.types.GenerationConfig( + candidate_count=1, + ), + request_options=genai.types.RequestOptions( + timeout=self.timeout, + ), + ) + except GoogleAPICallError as e: + logger.warning("Gemini returned an error: %s", str(e)) + return None + try: + description = response.text.strip() + except ValueError: + # No description was generated + return None + return description + + def get_context_size(self) -> int: + """Get the context window size for Gemini.""" + # Gemini Pro Vision has a 1M token context window + return 1000000 diff --git a/sam2-cpu/frigate-dev/frigate/genai/ollama.py b/sam2-cpu/frigate-dev/frigate/genai/ollama.py new file mode 100644 index 0000000..9f9c8a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/genai/ollama.py @@ -0,0 +1,79 @@ +"""Ollama Provider for Frigate AI.""" + +import logging +from typing import Any, Optional + +from httpx import TimeoutException +from ollama import Client as ApiClient +from ollama import ResponseError + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.ollama) +class OllamaClient(GenAIClient): + """Generative AI client for Frigate using Ollama.""" + + LOCAL_OPTIMIZED_OPTIONS = { + "options": { + "temperature": 0.5, + "repeat_penalty": 1.05, + "presence_penalty": 0.3, + }, + } + + provider: ApiClient + provider_options: dict[str, Any] + + def _init_provider(self): + """Initialize the client.""" + self.provider_options = { + **self.LOCAL_OPTIMIZED_OPTIONS, + **self.genai_config.provider_options, + } + + try: + client = ApiClient(host=self.genai_config.base_url, timeout=self.timeout) + # ensure the model is available locally + response = client.show(self.genai_config.model) + if response.get("error"): + logger.error( + "Ollama error: %s", + response["error"], + ) + return None + return client + except Exception as e: + logger.warning("Error initializing Ollama: %s", str(e)) + return None + + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to Ollama""" + if self.provider is None: + logger.warning( + "Ollama provider has not been initialized, a description will not be generated. Check your Ollama configuration." + ) + return None + try: + result = self.provider.generate( + self.genai_config.model, + prompt, + images=images if images else None, + **self.provider_options, + ) + logger.debug( + f"Ollama tokens used: eval_count={result.get('eval_count')}, prompt_eval_count={result.get('prompt_eval_count')}" + ) + return result["response"].strip() + except (TimeoutException, ResponseError, ConnectionError) as e: + logger.warning("Ollama returned an error: %s", str(e)) + return None + + def get_context_size(self) -> int: + """Get the context window size for Ollama.""" + return self.genai_config.provider_options.get("options", {}).get( + "num_ctx", 4096 + ) diff --git a/sam2-cpu/frigate-dev/frigate/genai/openai.py b/sam2-cpu/frigate-dev/frigate/genai/openai.py new file mode 100644 index 0000000..631cb34 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/genai/openai.py @@ -0,0 +1,102 @@ +"""OpenAI Provider for Frigate AI.""" + +import base64 +import logging +from typing import Optional + +from httpx import TimeoutException +from openai import OpenAI + +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient, register_genai_provider + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.openai) +class OpenAIClient(GenAIClient): + """Generative AI client for Frigate using OpenAI.""" + + provider: OpenAI + context_size: Optional[int] = None + + def _init_provider(self): + """Initialize the client.""" + return OpenAI( + api_key=self.genai_config.api_key, **self.genai_config.provider_options + ) + + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to OpenAI.""" + encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] + messages_content = [] + for image in encoded_images: + messages_content.append( + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image}", + "detail": "low", + }, + } + ) + messages_content.append( + { + "type": "text", + "text": prompt, + } + ) + try: + result = self.provider.chat.completions.create( + model=self.genai_config.model, + messages=[ + { + "role": "user", + "content": messages_content, + }, + ], + timeout=self.timeout, + ) + if ( + result is not None + and hasattr(result, "choices") + and len(result.choices) > 0 + ): + return result.choices[0].message.content.strip() + return None + except (TimeoutException, Exception) as e: + logger.warning("OpenAI returned an error: %s", str(e)) + return None + + def get_context_size(self) -> int: + """Get the context window size for OpenAI.""" + if self.context_size is not None: + return self.context_size + + try: + models = self.provider.models.list() + for model in models.data: + if model.id == self.genai_config.model: + if hasattr(model, "max_model_len") and model.max_model_len: + self.context_size = model.max_model_len + logger.debug( + f"Retrieved context size {self.context_size} for model {self.genai_config.model}" + ) + return self.context_size + + except Exception as e: + logger.debug( + f"Failed to fetch model context size from API: {e}, using default" + ) + + # Default to 128K for ChatGPT models, 8K for others + model_name = self.genai_config.model.lower() + if "gpt" in model_name: + self.context_size = 128000 + else: + self.context_size = 8192 + + logger.debug( + f"Using default context size {self.context_size} for model {self.genai_config.model}" + ) + return self.context_size diff --git a/sam2-cpu/frigate-dev/frigate/images/birdseye.png b/sam2-cpu/frigate-dev/frigate/images/birdseye.png new file mode 100644 index 0000000..3f7f768 Binary files /dev/null and b/sam2-cpu/frigate-dev/frigate/images/birdseye.png differ diff --git a/sam2-cpu/frigate-dev/frigate/images/camera-error.jpg b/sam2-cpu/frigate-dev/frigate/images/camera-error.jpg new file mode 100644 index 0000000..ace648f Binary files /dev/null and b/sam2-cpu/frigate-dev/frigate/images/camera-error.jpg differ diff --git a/sam2-cpu/frigate-dev/frigate/log.py b/sam2-cpu/frigate-dev/frigate/log.py new file mode 100644 index 0000000..f2171ff --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/log.py @@ -0,0 +1,320 @@ +# In log.py +import atexit +import io +import logging +import os +import sys +import threading +from collections import deque +from contextlib import contextmanager +from enum import Enum +from functools import wraps +from logging.handlers import QueueHandler, QueueListener +from multiprocessing.managers import SyncManager +from queue import Empty, Queue +from typing import Any, Callable, Deque, Generator, Optional + +from frigate.util.builtin import clean_camera_user_pass + +LOG_HANDLER = logging.StreamHandler() +LOG_HANDLER.setFormatter( + logging.Formatter( + "[%(asctime)s] %(name)-30s %(levelname)-8s: %(message)s", + "%Y-%m-%d %H:%M:%S", + ) +) + +# filter out norfair warning +LOG_HANDLER.addFilter( + lambda record: not record.getMessage().startswith( + "You are using a scalar distance function" + ) +) + +# filter out tflite logging +LOG_HANDLER.addFilter( + lambda record: "Created TensorFlow Lite XNNPACK delegate for CPU." + not in record.getMessage() +) + + +class LogLevel(str, Enum): + debug = "debug" + info = "info" + warning = "warning" + error = "error" + critical = "critical" + + +log_listener: Optional[QueueListener] = None +log_queue: Optional[Queue] = None + + +def setup_logging(manager: SyncManager) -> None: + global log_listener, log_queue + log_queue = manager.Queue() + log_listener = QueueListener(log_queue, LOG_HANDLER, respect_handler_level=True) + + atexit.register(_stop_logging) + log_listener.start() + + logging.basicConfig( + level=logging.INFO, + handlers=[], + force=True, + ) + + logging.getLogger().addHandler(QueueHandler(log_listener.queue)) + + +def _stop_logging() -> None: + global log_listener + if log_listener is not None: + log_listener.stop() + log_listener = None + + +def apply_log_levels(default: str, log_levels: dict[str, LogLevel]) -> None: + logging.getLogger().setLevel(default) + + log_levels = { + "absl": LogLevel.error, + "httpx": LogLevel.error, + "matplotlib": LogLevel.error, + "tensorflow": LogLevel.error, + "werkzeug": LogLevel.error, + "ws4py": LogLevel.error, + **log_levels, + } + + for log, level in log_levels.items(): + logging.getLogger(log).setLevel(level.value.upper()) + + +# When a multiprocessing.Process exits, python tries to flush stdout and stderr. However, if the +# process is created after a thread (for example a logging thread) is created and the process fork +# happens while an internal lock is held, the stdout/err flush can cause a deadlock. +# +# https://github.com/python/cpython/issues/91776 +def reopen_std_streams() -> None: + sys.stdout = os.fdopen(1, "w") + sys.stderr = os.fdopen(2, "w") + + +os.register_at_fork(after_in_child=reopen_std_streams) + + +# based on https://codereview.stackexchange.com/a/17959 +class LogPipe(threading.Thread): + def __init__(self, log_name: str, level: int = logging.ERROR): + """Setup the object with a logger and start the thread""" + super().__init__(daemon=False) + self.logger = logging.getLogger(log_name) + self.level = level + self.deque: Deque[str] = deque(maxlen=100) + self.fdRead, self.fdWrite = os.pipe() + self.pipeReader = os.fdopen(self.fdRead) + self.start() + + def cleanup_log(self, log: str) -> str: + """Cleanup the log line to remove sensitive info and string tokens.""" + log = clean_camera_user_pass(log).strip("\n") + return log + + def fileno(self) -> int: + """Return the write file descriptor of the pipe""" + return self.fdWrite + + def run(self) -> None: + """Run the thread, logging everything.""" + for line in iter(self.pipeReader.readline, ""): + self.deque.append(self.cleanup_log(line)) + + self.pipeReader.close() + + def dump(self) -> None: + while len(self.deque) > 0: + self.logger.log(self.level, self.deque.popleft()) + + def close(self) -> None: + """Close the write end of the pipe.""" + os.close(self.fdWrite) + + +class LogRedirect(io.StringIO): + """ + A custom file-like object to capture stdout and process it. + It extends io.StringIO to capture output and then processes it + line by line. + """ + + def __init__(self, logger_instance: logging.Logger, level: int): + super().__init__() + self.logger = logger_instance + self.log_level = level + self._line_buffer: list[str] = [] + + def write(self, s: Any) -> int: + if not isinstance(s, str): + s = str(s) + + self._line_buffer.append(s) + + # Process output line by line if a newline is present + if "\n" in s: + full_output = "".join(self._line_buffer) + lines = full_output.splitlines(keepends=True) + self._line_buffer = [] + + for line in lines: + if line.endswith("\n"): + self._process_line(line.rstrip("\n")) + else: + self._line_buffer.append(line) + + return len(s) + + def _process_line(self, line: str) -> None: + self.logger.log(self.log_level, line) + + def flush(self) -> None: + if self._line_buffer: + full_output = "".join(self._line_buffer) + self._line_buffer = [] + if full_output: # Only process if there's content + self._process_line(full_output) + + def __enter__(self) -> "LogRedirect": + """Context manager entry point.""" + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Context manager exit point. Ensures buffered content is flushed.""" + self.flush() + + +@contextmanager +def __redirect_fd_to_queue(queue: Queue[str]) -> Generator[None, None, None]: + """Redirect file descriptor 1 (stdout) to a pipe and capture output in a queue.""" + stdout_fd = os.dup(1) + read_fd, write_fd = os.pipe() + os.dup2(write_fd, 1) + os.close(write_fd) + + stop_event = threading.Event() + + def reader() -> None: + """Read from pipe and put lines in queue until stop_event is set.""" + try: + with os.fdopen(read_fd, "r") as pipe: + while not stop_event.is_set(): + line = pipe.readline() + if not line: # EOF + break + queue.put(line.strip()) + except OSError as e: + queue.put(f"Reader error: {e}") + finally: + if not stop_event.is_set(): + stop_event.set() + + reader_thread = threading.Thread(target=reader, daemon=False) + reader_thread.start() + + try: + yield + finally: + os.dup2(stdout_fd, 1) + os.close(stdout_fd) + stop_event.set() + reader_thread.join(timeout=1.0) + try: + os.close(read_fd) + except OSError: + pass + + +def redirect_output_to_logger(logger: logging.Logger, level: int) -> Any: + """Decorator to redirect both Python sys.stdout/stderr and C-level stdout to logger.""" + + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + queue: Queue[str] = Queue() + + log_redirect = LogRedirect(logger, level) + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = log_redirect + sys.stderr = log_redirect + + try: + # Redirect C-level stdout + with __redirect_fd_to_queue(queue): + result = func(*args, **kwargs) + finally: + # Restore Python stdout/stderr + sys.stdout = old_stdout + sys.stderr = old_stderr + log_redirect.flush() + + # Log C-level output from queue + while True: + try: + logger.log(level, queue.get_nowait()) + except Empty: + break + + return result + + return wrapper + + return decorator + + +def suppress_os_output(func: Callable) -> Callable: + """ + A decorator that suppresses all output (stdout and stderr) + at the operating system file descriptor level for the decorated function. + This is useful for silencing noisy C/C++ libraries. + Note: This is a Unix-specific solution using os.dup2 and os.pipe. + It temporarily redirects file descriptors 1 (stdout) and 2 (stderr) + to a non-read pipe, effectively discarding their output. + """ + + @wraps(func) + def wrapper(*args: tuple, **kwargs: dict[str, Any]) -> Any: + # Save the original file descriptors for stdout (1) and stderr (2) + original_stdout_fd = os.dup(1) + original_stderr_fd = os.dup(2) + + # Create dummy pipes. We only need the write ends to redirect to. + # The data written to these pipes will be discarded as nothing + # will read from the read ends. + devnull_read_fd, devnull_write_fd = os.pipe() + + try: + # Redirect stdout (FD 1) and stderr (FD 2) to the write end of our dummy pipe + os.dup2(devnull_write_fd, 1) # Redirect stdout to devnull pipe + os.dup2(devnull_write_fd, 2) # Redirect stderr to devnull pipe + + # Execute the original function + result = func(*args, **kwargs) + + finally: + # Restore original stdout and stderr file descriptors (1 and 2) + # This is crucial to ensure normal printing resumes after the decorated function. + os.dup2(original_stdout_fd, 1) + os.dup2(original_stderr_fd, 2) + + # Close all duplicated and pipe file descriptors to prevent resource leaks. + # It's important to close the read end of the dummy pipe too, + # as nothing is explicitly reading from it. + os.close(original_stdout_fd) + os.close(original_stderr_fd) + os.close(devnull_read_fd) + os.close(devnull_write_fd) + + return result + + return wrapper diff --git a/sam2-cpu/frigate-dev/frigate/models.py b/sam2-cpu/frigate-dev/frigate/models.py new file mode 100644 index 0000000..93f6cb5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/models.py @@ -0,0 +1,164 @@ +from peewee import ( + BlobField, + BooleanField, + CharField, + CompositeKey, + DateTimeField, + FloatField, + ForeignKeyField, + IntegerField, + Model, + TextField, +) +from playhouse.sqlite_ext import JSONField + + +class Event(Model): + id = CharField(null=False, primary_key=True, max_length=30) + label = CharField(index=True, max_length=20) + sub_label = CharField(max_length=100, null=True) + camera = CharField(index=True, max_length=20) + start_time = DateTimeField() + end_time = DateTimeField() + top_score = ( + FloatField() + ) # TODO remove when columns can be dropped without rebuilding table + score = ( + FloatField() + ) # TODO remove when columns can be dropped without rebuilding table + false_positive = BooleanField() + zones = JSONField() + thumbnail = TextField() + has_clip = BooleanField(default=True) + has_snapshot = BooleanField(default=True) + region = ( + JSONField() + ) # TODO remove when columns can be dropped without rebuilding table + box = ( + JSONField() + ) # TODO remove when columns can be dropped without rebuilding table + area = ( + IntegerField() + ) # TODO remove when columns can be dropped without rebuilding table + retain_indefinitely = BooleanField(default=False) + ratio = FloatField( + default=1.0 + ) # TODO remove when columns can be dropped without rebuilding table + plus_id = CharField(max_length=30) + model_hash = CharField(max_length=32) + detector_type = CharField(max_length=32) + model_type = CharField(max_length=32) + data = JSONField() # ex: tracked object box, region, etc. + + +class Timeline(Model): + timestamp = DateTimeField() + camera = CharField(index=True, max_length=20) + source = CharField(index=True, max_length=20) # ex: tracked object, audio, external + source_id = CharField(index=True, max_length=30) + class_type = CharField(max_length=50) # ex: entered_zone, audio_heard + data = JSONField() # ex: tracked object id, region, box, etc. + + +class Regions(Model): + camera = CharField(null=False, primary_key=True, max_length=20) + grid = JSONField() # json blob of grid + last_update = DateTimeField() + + +class Recordings(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + path = CharField(unique=True) + start_time = DateTimeField() + end_time = DateTimeField() + duration = FloatField() + motion = IntegerField(null=True) + objects = IntegerField(null=True) + dBFS = IntegerField(null=True) + segment_size = FloatField(default=0) # this should be stored as MB + regions = IntegerField(null=True) + + +class Export(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + name = CharField(index=True, max_length=100) + date = DateTimeField() + video_path = CharField(unique=True) + thumb_path = CharField(unique=True) + in_progress = BooleanField() + + +class ReviewSegment(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + start_time = DateTimeField() + end_time = DateTimeField() + severity = CharField(max_length=30) # alert, detection + thumb_path = CharField(unique=True) + data = JSONField() # additional data about detection like list of labels, zone, areas of significant motion + + +class UserReviewStatus(Model): + user_id = CharField(max_length=30) + review_segment = ForeignKeyField(ReviewSegment, backref="user_reviews") + has_been_reviewed = BooleanField(default=False) + + class Meta: + indexes = ((("user_id", "review_segment"), True),) + + +class Previews(Model): + id = CharField(null=False, primary_key=True, max_length=30) + camera = CharField(index=True, max_length=20) + path = CharField(unique=True) + start_time = DateTimeField() + end_time = DateTimeField() + duration = FloatField() + + +# Used for temporary table in record/cleanup.py +class RecordingsToDelete(Model): + id = CharField(null=False, primary_key=False, max_length=30) + + class Meta: + temporary = True + + +class User(Model): + username = CharField(null=False, primary_key=True, max_length=30) + role = CharField( + max_length=20, + default="admin", + ) + password_hash = CharField(null=False, max_length=120) + password_changed_at = DateTimeField(null=True) + notification_tokens = JSONField() + + @classmethod + def get_allowed_cameras( + cls, role: str, roles_dict: dict[str, list[str]], all_camera_names: set[str] + ) -> list[str]: + if role not in roles_dict: + return [] # Invalid role grants no access + allowed = roles_dict[role] + if not allowed: # Empty list means all cameras + return list(all_camera_names) + + return [cam for cam in allowed if cam in all_camera_names] + + +class Trigger(Model): + camera = CharField(max_length=20) + name = CharField() + type = CharField(max_length=10) + data = TextField() + threshold = FloatField() + model = CharField(max_length=30) + embedding = BlobField() + triggering_event_id = CharField(max_length=30) + last_triggered = DateTimeField() + + class Meta: + primary_key = CompositeKey("camera", "name") diff --git a/sam2-cpu/frigate-dev/frigate/motion/__init__.py b/sam2-cpu/frigate-dev/frigate/motion/__init__.py new file mode 100644 index 0000000..1f6785d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/motion/__init__.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +from typing import Tuple + +from numpy import ndarray + +from frigate.config import MotionConfig + + +class MotionDetector(ABC): + @abstractmethod + def __init__( + self, + frame_shape: Tuple[int, int, int], + config: MotionConfig, + fps: int, + improve_contrast, + threshold, + contour_area, + ): + pass + + @abstractmethod + def detect(self, frame: ndarray) -> list: + """Detect motion and return motion boxes.""" + pass + + @abstractmethod + def is_calibrating(self): + """Return if motion is recalibrating.""" + pass + + @abstractmethod + def update_mask(self) -> None: + """Update the motion mask after a config change.""" + pass + + @abstractmethod + def stop(self): + """Stop any ongoing work and processes.""" + pass diff --git a/sam2-cpu/frigate-dev/frigate/motion/frigate_motion.py b/sam2-cpu/frigate-dev/frigate/motion/frigate_motion.py new file mode 100644 index 0000000..fd362de --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/motion/frigate_motion.py @@ -0,0 +1,157 @@ +import cv2 +import numpy as np + +from frigate.config import MotionConfig +from frigate.motion import MotionDetector +from frigate.util.image import grab_cv2_contours + + +class FrigateMotionDetector(MotionDetector): + def __init__( + self, + frame_shape, + config: MotionConfig, + fps: int, + improve_contrast, + threshold, + contour_area, + ): + self.config = config + self.frame_shape = frame_shape + self.resize_factor = frame_shape[0] / config.frame_height + self.motion_frame_size = ( + config.frame_height, + config.frame_height * frame_shape[1] // frame_shape[0], + ) + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.avg_delta = np.zeros(self.motion_frame_size, np.float32) + self.motion_frame_count = 0 + self.frame_counter = 0 + resized_mask = cv2.resize( + config.mask, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_LINEAR, + ) + self.mask = np.where(resized_mask == [0]) + self.save_images = False + self.improve_contrast = improve_contrast + self.threshold = threshold + self.contour_area = contour_area + + def is_calibrating(self): + return False + + def detect(self, frame): + motion_boxes = [] + + gray = frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] + + # resize frame + resized_frame = cv2.resize( + gray, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_LINEAR, + ) + + # Improve contrast + if self.improve_contrast.value: + min_value = np.percentile(resized_frame, 4) + max_value = np.percentile(resized_frame, 96) + # don't adjust if the image is a single color + if min_value < max_value: + resized_frame = np.clip(resized_frame, min_value, max_value) + resized_frame = ( + ((resized_frame - min_value) / (max_value - min_value)) * 255 + ).astype(np.uint8) + + # mask frame + resized_frame[self.mask] = [255] + + # it takes ~30 frames to establish a baseline + # dont bother looking for motion + if self.frame_counter < 30: + self.frame_counter += 1 + else: + if self.save_images: + self.frame_counter += 1 + # compare to average + frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) + + # compute the average delta over the past few frames + # higher values mean the current frame impacts the delta a lot, and a single raindrop may + # register as motion, too low and a fast moving person wont be detected as motion + cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha) + + # compute the threshold image for the current frame + current_thresh = cv2.threshold( + frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY + )[1] + + # black out everything in the avg_delta where there isn't motion in the current frame + avg_delta_image = cv2.convertScaleAbs(self.avg_delta) + avg_delta_image = cv2.bitwise_and(avg_delta_image, current_thresh) + + # then look for deltas above the threshold, but only in areas where there is a delta + # in the current frame. this prevents deltas from previous frames from being included + thresh = cv2.threshold( + avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY + )[1] + + # dilate the thresholded image to fill in holes, then find contours + # on thresholded image + thresh_dilated = cv2.dilate(thresh, None, iterations=2) + contours = cv2.findContours( + thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + contours = grab_cv2_contours(contours) + + # loop over the contours + for c in contours: + # if the contour is big enough, count it as motion + contour_area = cv2.contourArea(c) + if contour_area > self.contour_area.value: + x, y, w, h = cv2.boundingRect(c) + motion_boxes.append( + ( + int(x * self.resize_factor), + int(y * self.resize_factor), + int((x + w) * self.resize_factor), + int((y + h) * self.resize_factor), + ) + ) + + if self.save_images: + thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR) + # print("--------") + # print(self.frame_counter) + for c in contours: + contour_area = cv2.contourArea(c) + if contour_area > self.contour_area.value: + x, y, w, h = cv2.boundingRect(c) + cv2.rectangle( + thresh_dilated, + (x, y), + (x + w, y + h), + (0, 0, 255), + 2, + ) + + cv2.imwrite( + f"debug/frames/frigate-{self.frame_counter}.jpg", thresh_dilated + ) + + if len(motion_boxes) > 0: + self.motion_frame_count += 1 + if self.motion_frame_count >= 10: + # only average in the current frame if the difference persists for a bit + cv2.accumulateWeighted( + resized_frame, self.avg_frame, self.config.frame_alpha + ) + else: + # when no motion, just keep averaging the frames together + cv2.accumulateWeighted( + resized_frame, self.avg_frame, self.config.frame_alpha + ) + self.motion_frame_count = 0 + + return motion_boxes diff --git a/sam2-cpu/frigate-dev/frigate/motion/improved_motion.py b/sam2-cpu/frigate-dev/frigate/motion/improved_motion.py new file mode 100644 index 0000000..b081d37 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/motion/improved_motion.py @@ -0,0 +1,250 @@ +import logging + +import cv2 +import numpy as np +from scipy.ndimage import gaussian_filter + +from frigate.camera import PTZMetrics +from frigate.config import MotionConfig +from frigate.motion import MotionDetector +from frigate.util.image import grab_cv2_contours + +logger = logging.getLogger(__name__) + + +class ImprovedMotionDetector(MotionDetector): + def __init__( + self, + frame_shape, + config: MotionConfig, + fps: int, + ptz_metrics: PTZMetrics = None, + name="improved", + blur_radius=1, + interpolation=cv2.INTER_NEAREST, + contrast_frame_history=50, + ): + self.name = name + self.config = config + self.frame_shape = frame_shape + self.resize_factor = frame_shape[0] / config.frame_height + self.motion_frame_size = ( + config.frame_height, + config.frame_height * frame_shape[1] // frame_shape[0], + ) + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.motion_frame_count = 0 + self.frame_counter = 0 + self.update_mask() + self.save_images = False + self.calibrating = True + self.blur_radius = blur_radius + self.interpolation = interpolation + self.contrast_values = np.zeros((contrast_frame_history, 2), np.uint8) + self.contrast_values[:, 1:2] = 255 + self.contrast_values_index = 0 + self.ptz_metrics = ptz_metrics + self.last_stop_time = None + + def is_calibrating(self): + return self.calibrating + + def detect(self, frame): + motion_boxes = [] + + if not self.config.enabled: + return motion_boxes + + # if ptz motor is moving from autotracking, quickly return + # a single box that is 80% of the frame + if ( + self.ptz_metrics.autotracker_enabled.value + and not self.ptz_metrics.motor_stopped.is_set() + ): + return [ + ( + int(self.frame_shape[1] * 0.1), + int(self.frame_shape[0] * 0.1), + int(self.frame_shape[1] * 0.9), + int(self.frame_shape[0] * 0.9), + ) + ] + + gray = frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] + + # resize frame + resized_frame = cv2.resize( + gray, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=self.interpolation, + ) + + if self.save_images: + resized_saved = resized_frame.copy() + + # Improve contrast + if self.config.improve_contrast: + # TODO tracking moving average of min/max to avoid sudden contrast changes + min_value = np.percentile(resized_frame, 4).astype(np.uint8) + max_value = np.percentile(resized_frame, 96).astype(np.uint8) + # skip contrast calcs if the image is a single color + if min_value < max_value: + # keep track of the last 50 contrast values + self.contrast_values[self.contrast_values_index] = [ + min_value, + max_value, + ] + self.contrast_values_index += 1 + if self.contrast_values_index == len(self.contrast_values): + self.contrast_values_index = 0 + + avg_min, avg_max = np.mean(self.contrast_values, axis=0) + + resized_frame = np.clip(resized_frame, avg_min, avg_max) + resized_frame = ( + ((resized_frame - avg_min) / (avg_max - avg_min)) * 255 + ).astype(np.uint8) + + if self.save_images: + contrasted_saved = resized_frame.copy() + + # mask frame + # this has to come after contrast improvement + # Setting masked pixels to zero, to match the average frame at startup + resized_frame[self.mask] = [0] + + resized_frame = gaussian_filter(resized_frame, sigma=1, radius=self.blur_radius) + + if self.save_images: + blurred_saved = resized_frame.copy() + + if self.save_images or self.calibrating: + self.frame_counter += 1 + # compare to average + frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) + + # compute the threshold image for the current frame + thresh = cv2.threshold( + frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY + )[1] + + # dilate the thresholded image to fill in holes, then find contours + # on thresholded image + thresh_dilated = cv2.dilate(thresh, None, iterations=1) + contours = cv2.findContours( + thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) + contours = grab_cv2_contours(contours) + + # loop over the contours + total_contour_area = 0 + for c in contours: + # if the contour is big enough, count it as motion + contour_area = cv2.contourArea(c) + total_contour_area += contour_area + if contour_area > self.config.contour_area: + x, y, w, h = cv2.boundingRect(c) + motion_boxes.append( + ( + int(x * self.resize_factor), + int(y * self.resize_factor), + int((x + w) * self.resize_factor), + int((y + h) * self.resize_factor), + ) + ) + + pct_motion = total_contour_area / ( + self.motion_frame_size[0] * self.motion_frame_size[1] + ) + + # check if the motor has just stopped from autotracking + # if so, reassign the average to the current frame so we begin with a new baseline + if ( + # ensure we only do this for cameras with autotracking enabled + self.ptz_metrics.autotracker_enabled.value + and self.ptz_metrics.motor_stopped.is_set() + and ( + self.last_stop_time is None + or self.ptz_metrics.stop_time.value != self.last_stop_time + ) + # value is 0 on startup or when motor is moving + and self.ptz_metrics.stop_time.value != 0 + ): + self.last_stop_time = self.ptz_metrics.stop_time.value + + self.avg_frame = resized_frame.astype(np.float32) + motion_boxes = [] + pct_motion = 0 + + # once the motion is less than 5% and the number of contours is < 4, assume its calibrated + if pct_motion < 0.05 and len(motion_boxes) <= 4: + self.calibrating = False + + # if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate + if self.calibrating or pct_motion > self.config.lightning_threshold: + self.calibrating = True + + if self.save_images: + thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR) + for b in motion_boxes: + cv2.rectangle( + thresh_dilated, + (int(b[0] / self.resize_factor), int(b[1] / self.resize_factor)), + (int(b[2] / self.resize_factor), int(b[3] / self.resize_factor)), + (0, 0, 255), + 2, + ) + frames = [ + cv2.cvtColor(resized_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(contrasted_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(blurred_saved, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR), + cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), + thresh_dilated, + ] + cv2.imwrite( + f"debug/frames/{self.name}-{self.frame_counter}.jpg", + ( + cv2.hconcat(frames) + if self.frame_shape[0] > self.frame_shape[1] + else cv2.vconcat(frames) + ), + ) + + if len(motion_boxes) > 0: + self.motion_frame_count += 1 + if self.motion_frame_count >= 10: + # only average in the current frame if the difference persists for a bit + cv2.accumulateWeighted( + resized_frame, + self.avg_frame, + 0.2 if self.calibrating else self.config.frame_alpha, + ) + else: + # when no motion, just keep averaging the frames together + cv2.accumulateWeighted( + resized_frame, + self.avg_frame, + 0.2 if self.calibrating else self.config.frame_alpha, + ) + self.motion_frame_count = 0 + + return motion_boxes + + def update_mask(self) -> None: + resized_mask = cv2.resize( + self.config.mask, + dsize=(self.motion_frame_size[1], self.motion_frame_size[0]), + interpolation=cv2.INTER_AREA, + ) + self.mask = np.where(resized_mask == [0]) + + # Reset motion detection state when mask changes + # so motion detection can quickly recalibrate with the new mask + self.avg_frame = np.zeros(self.motion_frame_size, np.float32) + self.calibrating = True + self.motion_frame_count = 0 + + def stop(self) -> None: + """stop the motion detector.""" + pass diff --git a/sam2-cpu/frigate-dev/frigate/mypy.ini b/sam2-cpu/frigate-dev/frigate/mypy.ini new file mode 100644 index 0000000..5bad10f --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/mypy.ini @@ -0,0 +1,71 @@ +[mypy] +python_version = 3.11 +show_error_codes = true +follow_imports = normal +ignore_missing_imports = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +enable_error_code = ignore-without-code +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +# https://github.com/python/mypy/issues/10757 +disallow_untyped_calls = false +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true +no_implicit_reexport = true + +[mypy-frigate.*] +ignore_errors = true + +[mypy-frigate.__main__] +ignore_errors = false +disallow_untyped_calls = false + +[mypy-frigate.app] +ignore_errors = false +disallow_untyped_calls = false + +[mypy-frigate.const] +ignore_errors = false + +[mypy-frigate.comms.*] +ignore_errors = false + +[mypy-frigate.events] +ignore_errors = false + +[mypy-frigate.log] +ignore_errors = false + +[mypy-frigate.models] +ignore_errors = false + +[mypy-frigate.plus] +ignore_errors = false + +[mypy-frigate.stats] +ignore_errors = false + +[mypy-frigate.track.*] +ignore_errors = false + +[mypy-frigate.types] +ignore_errors = false + +[mypy-frigate.version] +ignore_errors = false + +[mypy-frigate.watchdog] +ignore_errors = false +disallow_untyped_calls = false + + +[mypy-frigate.service_manager.*] +ignore_errors = false diff --git a/sam2-cpu/frigate-dev/frigate/object_detection/base.py b/sam2-cpu/frigate-dev/frigate/object_detection/base.py new file mode 100644 index 0000000..d2a54af --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/object_detection/base.py @@ -0,0 +1,437 @@ +import datetime +import logging +import queue +import threading +import time +from abc import ABC, abstractmethod +from collections import deque +from multiprocessing import Queue, Value +from multiprocessing.synchronize import Event as MpEvent + +import numpy as np +import zmq + +from frigate.comms.object_detector_signaler import ( + ObjectDetectorPublisher, + ObjectDetectorSubscriber, +) +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_HIGH +from frigate.detectors import create_detector +from frigate.detectors.detector_config import ( + BaseDetectorConfig, + InputDTypeEnum, + ModelConfig, +) +from frigate.util.builtin import EventsPerSecond, load_labels +from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory +from frigate.util.process import FrigateProcess + +from .util import tensor_transform + +logger = logging.getLogger(__name__) + + +class ObjectDetector(ABC): + @abstractmethod + def detect(self, tensor_input, threshold: float = 0.4): + pass + + +class BaseLocalDetector(ObjectDetector): + def __init__( + self, + detector_config: BaseDetectorConfig = None, + labels: str = None, + stop_event: MpEvent = None, + ): + self.fps = EventsPerSecond() + if labels is None: + self.labels = {} + else: + self.labels = load_labels(labels) + + if detector_config: + self.input_transform = tensor_transform(detector_config.model.input_tensor) + + self.dtype = detector_config.model.input_dtype + else: + self.input_transform = None + self.dtype = InputDTypeEnum.int + + self.detect_api = create_detector(detector_config) + + # If the detector supports stop_event, pass it + if hasattr(self.detect_api, "set_stop_event") and stop_event: + self.detect_api.set_stop_event(stop_event) + + def _transform_input(self, tensor_input: np.ndarray) -> np.ndarray: + if self.input_transform: + tensor_input = np.transpose(tensor_input, self.input_transform) + + if self.dtype == InputDTypeEnum.float: + tensor_input = tensor_input.astype(np.float32) + tensor_input /= 255 + elif self.dtype == InputDTypeEnum.float_denorm: + tensor_input = tensor_input.astype(np.float32) + + return tensor_input + + def detect(self, tensor_input: np.ndarray, threshold=0.4): + detections = [] + + raw_detections = self.detect_raw(tensor_input) + + for d in raw_detections: + if int(d[0]) < 0 or int(d[0]) >= len(self.labels): + logger.warning(f"Raw Detect returned invalid label: {d}") + continue + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + self.fps.update() + return detections + + +class LocalObjectDetector(BaseLocalDetector): + def detect_raw(self, tensor_input: np.ndarray): + tensor_input = self._transform_input(tensor_input) + return self.detect_api.detect_raw(tensor_input=tensor_input) + + +class AsyncLocalObjectDetector(BaseLocalDetector): + def async_send_input(self, tensor_input: np.ndarray, connection_id: str): + tensor_input = self._transform_input(tensor_input) + return self.detect_api.send_input(connection_id, tensor_input) + + def async_receive_output(self): + return self.detect_api.receive_output() + + +class DetectorRunner(FrigateProcess): + def __init__( + self, + name, + detection_queue: Queue, + cameras: list[str], + avg_speed: Value, + start_time: Value, + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ) -> None: + super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True) + self.detection_queue = detection_queue + self.cameras = cameras + self.avg_speed = avg_speed + self.start_time = start_time + self.config = config + self.detector_config = detector_config + self.outputs: dict = {} + + def create_output_shm(self, name: str): + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) + self.outputs[name] = {"shm": out_shm, "np": out_np} + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + frame_manager = SharedMemoryFrameManager() + object_detector = LocalObjectDetector(detector_config=self.detector_config) + detector_publisher = ObjectDetectorPublisher() + + for name in self.cameras: + self.create_output_shm(name) + + while not self.stop_event.is_set(): + try: + connection_id = self.detection_queue.get(timeout=1) + except queue.Empty: + continue + input_frame = frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, + self.detector_config.model.width, + 3, + ), + ) + + if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") + continue + + # detect and send the output + self.start_time.value = datetime.datetime.now().timestamp() + detections = object_detector.detect_raw(input_frame) + duration = datetime.datetime.now().timestamp() - self.start_time.value + frame_manager.close(connection_id) + + if connection_id not in self.outputs: + self.create_output_shm(connection_id) + + self.outputs[connection_id]["np"][:] = detections[:] + detector_publisher.publish(connection_id) + self.start_time.value = 0.0 + + self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10 + + detector_publisher.stop() + logger.info("Exited detection process...") + + +class AsyncDetectorRunner(FrigateProcess): + def __init__( + self, + name, + detection_queue: Queue, + cameras: list[str], + avg_speed: Value, + start_time: Value, + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ) -> None: + super().__init__(stop_event, PROCESS_PRIORITY_HIGH, name=name, daemon=True) + self.detection_queue = detection_queue + self.cameras = cameras + self.avg_speed = avg_speed + self.start_time = start_time + self.config = config + self.detector_config = detector_config + self.outputs: dict = {} + self._frame_manager: SharedMemoryFrameManager | None = None + self._publisher: ObjectDetectorPublisher | None = None + self._detector: AsyncLocalObjectDetector | None = None + self.send_times = deque() + + def create_output_shm(self, name: str): + out_shm = UntrackedSharedMemory(name=f"out-{name}", create=False) + out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf) + self.outputs[name] = {"shm": out_shm, "np": out_np} + + def _detect_worker(self) -> None: + logger.info("Starting Detect Worker Thread") + while not self.stop_event.is_set(): + try: + connection_id = self.detection_queue.get(timeout=1) + except queue.Empty: + continue + + input_frame = self._frame_manager.get( + connection_id, + ( + 1, + self.detector_config.model.height, + self.detector_config.model.width, + 3, + ), + ) + + if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") + continue + + # mark start time and send to accelerator + self.send_times.append(time.perf_counter()) + self._detector.async_send_input(input_frame, connection_id) + + def _result_worker(self) -> None: + logger.info("Starting Result Worker Thread") + while not self.stop_event.is_set(): + connection_id, detections = self._detector.async_receive_output() + + # Handle timeout case (queue.Empty) - just continue + if connection_id is None: + continue + + if not self.send_times: + # guard; shouldn't happen if send/recv are balanced + continue + ts = self.send_times.popleft() + duration = time.perf_counter() - ts + + # release input buffer + self._frame_manager.close(connection_id) + + if connection_id not in self.outputs: + self.create_output_shm(connection_id) + + # write results and publish + if detections is not None: + self.outputs[connection_id]["np"][:] = detections[:] + self._publisher.publish(connection_id) + + # update timers + self.avg_speed.value = (self.avg_speed.value * 9 + duration) / 10 + self.start_time.value = 0.0 + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + self._frame_manager = SharedMemoryFrameManager() + self._publisher = ObjectDetectorPublisher() + self._detector = AsyncLocalObjectDetector( + detector_config=self.detector_config, stop_event=self.stop_event + ) + + for name in self.cameras: + self.create_output_shm(name) + + t_detect = threading.Thread(target=self._detect_worker, daemon=False) + t_result = threading.Thread(target=self._result_worker, daemon=False) + t_detect.start() + t_result.start() + + try: + while not self.stop_event.is_set(): + time.sleep(0.5) + + logger.info( + "Stop event detected, waiting for detector threads to finish..." + ) + + # Wait for threads to finish processing + t_detect.join(timeout=5) + t_result.join(timeout=5) + + # Shutdown the AsyncDetector + self._detector.detect_api.shutdown() + + self._publisher.stop() + except Exception as e: + logger.error(f"Error during async detector shutdown: {e}") + finally: + logger.info("Exited Async detection process...") + + +class ObjectDetectProcess: + def __init__( + self, + name: str, + detection_queue: Queue, + cameras: list[str], + config: FrigateConfig, + detector_config: BaseDetectorConfig, + stop_event: MpEvent, + ): + self.name = name + self.cameras = cameras + self.detection_queue = detection_queue + self.avg_inference_speed = Value("d", 0.01) + self.detection_start = Value("d", 0.0) + self.detect_process: FrigateProcess | None = None + self.config = config + self.detector_config = detector_config + self.stop_event = stop_event + self.start_or_restart() + + def stop(self): + # if the process has already exited on its own, just return + if self.detect_process and self.detect_process.exitcode: + return + + logging.info("Waiting for detection process to exit gracefully...") + self.detect_process.join(timeout=30) + if self.detect_process.exitcode is None: + logging.info("Detection process didn't exit. Force killing...") + self.detect_process.kill() + self.detect_process.join() + logging.info("Detection process has exited...") + + def start_or_restart(self): + self.detection_start.value = 0.0 + if (self.detect_process is not None) and self.detect_process.is_alive(): + self.stop() + + # Async path for MemryX + if self.detector_config.type == "memryx": + self.detect_process = AsyncDetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) + else: + self.detect_process = DetectorRunner( + f"frigate.detector:{self.name}", + self.detection_queue, + self.cameras, + self.avg_inference_speed, + self.detection_start, + self.config, + self.detector_config, + self.stop_event, + ) + self.detect_process.start() + + +class RemoteObjectDetector: + def __init__( + self, + name: str, + labels: dict[int, str], + detection_queue: Queue, + model_config: ModelConfig, + stop_event: MpEvent, + ): + self.labels = labels + self.name = name + self.fps = EventsPerSecond() + self.detection_queue = detection_queue + self.stop_event = stop_event + self.shm = UntrackedSharedMemory(name=self.name, create=False) + self.np_shm = np.ndarray( + (1, model_config.height, model_config.width, 3), + dtype=np.uint8, + buffer=self.shm.buf, + ) + self.out_shm = UntrackedSharedMemory(name=f"out-{self.name}", create=False) + self.out_np_shm = np.ndarray((20, 6), dtype=np.float32, buffer=self.out_shm.buf) + self.detector_subscriber = ObjectDetectorSubscriber(name) + + def detect(self, tensor_input, threshold=0.4): + detections = [] + + if self.stop_event.is_set(): + return detections + + # Drain any stale detection results from the ZMQ buffer before making a new request + # This prevents reading detection results from a previous request + # NOTE: This should never happen, but can in some rare cases + while True: + try: + self.detector_subscriber.socket.recv_string(flags=zmq.NOBLOCK) + except zmq.Again: + break + + # copy input to shared memory + self.np_shm[:] = tensor_input[:] + self.detection_queue.put(self.name) + result = self.detector_subscriber.check_for_update() + + # if it timed out + if result is None: + return detections + + for d in self.out_np_shm: + if d[1] < threshold: + break + detections.append( + (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5])) + ) + self.fps.update() + return detections + + def cleanup(self): + self.detector_subscriber.stop() + self.shm.unlink() + self.out_shm.unlink() diff --git a/sam2-cpu/frigate-dev/frigate/object_detection/util.py b/sam2-cpu/frigate-dev/frigate/object_detection/util.py new file mode 100644 index 0000000..ea8bd42 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/object_detection/util.py @@ -0,0 +1,77 @@ +"""Object detection utilities.""" + +import queue +import threading + +from numpy import ndarray + +from frigate.detectors.detector_config import InputTensorEnum + + +class RequestStore: + """ + A thread-safe hash-based response store that handles creating requests. + """ + + def __init__(self): + self.request_counter = 0 + self.request_counter_lock = threading.Lock() + self.input_queue = queue.Queue() + + def __get_request_id(self) -> int: + with self.request_counter_lock: + request_id = self.request_counter + self.request_counter += 1 + if self.request_counter > 1000000: + self.request_counter = 0 + return request_id + + def put(self, tensor_input: ndarray) -> int: + request_id = self.__get_request_id() + self.input_queue.put((request_id, tensor_input)) + return request_id + + def get(self) -> tuple[int, ndarray] | None: + try: + return self.input_queue.get() + except Exception: + return None + + +class ResponseStore: + """ + A thread-safe hash-based response store that maps request IDs + to their results. Threads can wait on the condition variable until + their request's result appears. + """ + + def __init__(self): + self.responses = {} # Maps request_id -> (original_input, infer_results) + self.lock = threading.Lock() + self.cond = threading.Condition(self.lock) + + def put(self, request_id: int, response: ndarray): + with self.cond: + self.responses[request_id] = response + self.cond.notify_all() + + def get(self, request_id: int, timeout=None) -> ndarray: + with self.cond: + if not self.cond.wait_for( + lambda: request_id in self.responses, timeout=timeout + ): + raise TimeoutError(f"Timeout waiting for response {request_id}") + + return self.responses.pop(request_id) + + +def tensor_transform(desired_shape: InputTensorEnum): + # Currently this function only supports BHWC permutations + if desired_shape == InputTensorEnum.nhwc: + return None + elif desired_shape == InputTensorEnum.nchw: + return (0, 3, 1, 2) + elif desired_shape == InputTensorEnum.hwnc: + return (1, 2, 0, 3) + elif desired_shape == InputTensorEnum.hwcn: + return (1, 2, 3, 0) diff --git a/sam2-cpu/frigate-dev/frigate/output/birdseye.py b/sam2-cpu/frigate-dev/frigate/output/birdseye.py new file mode 100644 index 0000000..eb23c25 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/output/birdseye.py @@ -0,0 +1,868 @@ +"""Handle outputting birdseye frames via jsmpeg and go2rtc.""" + +import datetime +import glob +import logging +import math +import multiprocessing as mp +import os +import queue +import subprocess as sp +import threading +import time +import traceback +from typing import Any, Optional + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig +from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT +from frigate.util.image import ( + SharedMemoryFrameManager, + copy_yuv_to_position, + get_yuv_crop, +) + +logger = logging.getLogger(__name__) + + +def get_standard_aspect_ratio(width: int, height: int) -> tuple[int, int]: + """Ensure that only standard aspect ratios are used.""" + # it is important that all ratios have the same scale + known_aspects = [ + (16, 9), + (9, 16), + (20, 10), + (16, 3), # max wide camera + (16, 6), # reolink duo 2 + (32, 9), # panoramic cameras + (12, 9), + (9, 12), + (22, 15), # Amcrest, NTSC DVT + (1, 1), # fisheye + ] # aspects are scaled to have common relative size + known_aspects_ratios = list( + map(lambda aspect: aspect[0] / aspect[1], known_aspects) + ) + closest = min( + known_aspects_ratios, + key=lambda x: abs(x - (width / height)), + ) + return known_aspects[known_aspects_ratios.index(closest)] + + +def get_canvas_shape(width: int, height: int) -> tuple[int, int]: + """Get birdseye canvas shape.""" + canvas_width = width + canvas_height = height + a_w, a_h = get_standard_aspect_ratio(width, height) + + if round(a_w / a_h, 2) != round(width / height, 2): + canvas_width = int(width // 4 * 4) + canvas_height = int((canvas_width / a_w * a_h) // 4 * 4) + logger.warning( + f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}" + ) + + return (canvas_width, canvas_height) + + +class Canvas: + def __init__( + self, + canvas_width: int, + canvas_height: int, + scaling_factor: int, + ) -> None: + self.scaling_factor = scaling_factor + gcd = math.gcd(canvas_width, canvas_height) + self.aspect = get_standard_aspect_ratio( + (canvas_width / gcd), (canvas_height / gcd) + ) + self.width = canvas_width + self.height = (self.width * self.aspect[1]) / self.aspect[0] + self.coefficient_cache: dict[int, int] = {} + self.aspect_cache: dict[str, tuple[int, int]] = {} + + def get_aspect(self, coefficient: int) -> tuple[int, int]: + return (self.aspect[0] * coefficient, self.aspect[1] * coefficient) + + def get_coefficient(self, camera_count: int) -> int: + return self.coefficient_cache.get(camera_count, self.scaling_factor) + + def set_coefficient(self, camera_count: int, coefficient: int) -> None: + self.coefficient_cache[camera_count] = coefficient + + def get_camera_aspect( + self, cam_name: str, camera_width: int, camera_height: int + ) -> tuple[int, int]: + cached = self.aspect_cache.get(cam_name) + + if cached: + return cached + + gcd = math.gcd(camera_width, camera_height) + camera_aspect = get_standard_aspect_ratio( + camera_width / gcd, camera_height / gcd + ) + self.aspect_cache[cam_name] = camera_aspect + return camera_aspect + + +class FFMpegConverter(threading.Thread): + def __init__( + self, + ffmpeg: FfmpegConfig, + input_queue: queue.Queue, + stop_event: mp.Event, + in_width: int, + in_height: int, + out_width: int, + out_height: int, + quality: int, + birdseye_rtsp: bool = False, + ): + super().__init__(name="birdseye_output_converter") + self.camera = "birdseye" + self.input_queue = input_queue + self.stop_event = stop_event + self.bd_pipe = None + + if birdseye_rtsp: + self.recreate_birdseye_pipe() + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-threads", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-video_size", + f"{in_width}x{in_height}", + "-i", + "pipe:", + "-threads", + "1", + "-f", + "mpegts", + "-s", + f"{out_width}x{out_height}", + "-codec:v", + "mpeg1video", + "-q", + f"{quality}", + "-bf", + "0", + "pipe:", + ] + + self.process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=sp.DEVNULL, + stdin=sp.PIPE, + start_new_session=True, + ) + + def recreate_birdseye_pipe(self) -> None: + if self.bd_pipe: + os.close(self.bd_pipe) + + if os.path.exists(BIRDSEYE_PIPE): + os.remove(BIRDSEYE_PIPE) + + os.mkfifo(BIRDSEYE_PIPE, mode=0o777) + stdin = os.open(BIRDSEYE_PIPE, os.O_RDONLY | os.O_NONBLOCK) + self.bd_pipe = os.open(BIRDSEYE_PIPE, os.O_WRONLY) + os.close(stdin) + self.reading_birdseye = False + + def __write(self, b) -> None: + self.process.stdin.write(b) + + if self.bd_pipe: + try: + os.write(self.bd_pipe, b) + self.reading_birdseye = True + except BrokenPipeError: + if self.reading_birdseye: + # we know the pipe was being read from and now it is not + # so we should recreate the pipe to ensure no partially-read + # frames exist + logger.debug( + "Recreating the birdseye pipe because it was read from and now is not" + ) + self.recreate_birdseye_pipe() + + return + + def read(self, length): + try: + return self.process.stdout.read1(length) + except ValueError: + return False + + def exit(self): + if self.bd_pipe: + os.close(self.bd_pipe) + + self.process.terminate() + try: + self.process.communicate(timeout=30) + except sp.TimeoutExpired: + self.process.kill() + self.process.communicate() + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + frame = self.input_queue.get(True, timeout=1) + self.__write(frame) + except queue.Empty: + pass + + self.exit() + + +class BroadcastThread(threading.Thread): + def __init__( + self, + camera: str, + converter: FFMpegConverter, + websocket_server, + stop_event: mp.Event, + ): + super().__init__() + self.camera = camera + self.converter = converter + self.websocket_server = websocket_server + self.stop_event = stop_event + + def run(self): + while not self.stop_event.is_set(): + buf = self.converter.read(65536) + if buf: + manager = self.websocket_server.manager + with manager.lock: + websockets = manager.websockets.copy() + ws_iter = iter(websockets.values()) + + for ws in ws_iter: + if ( + not ws.terminated + and ws.environ["PATH_INFO"] == f"/{self.camera}" + ): + try: + ws.send(buf, binary=True) + except ValueError: + pass + except (BrokenPipeError, ConnectionResetError, OSError) as e: + logger.debug(f"Websocket unexpectedly closed {e}") + elif self.converter.process.poll() is not None: + break + + +class BirdsEyeFrameManager: + def __init__( + self, + config: FrigateConfig, + stop_event: mp.Event, + ): + self.config = config + self.mode = config.birdseye.mode + width, height = get_canvas_shape(config.birdseye.width, config.birdseye.height) + self.frame_shape = (height, width) + self.yuv_shape = (height * 3 // 2, width) + self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8) + self.canvas = Canvas(width, height, config.birdseye.layout.scaling_factor) + self.stop_event = stop_event + self.inactivity_threshold = config.birdseye.inactivity_threshold + + if config.birdseye.layout.max_cameras: + self.last_refresh_time = 0 + + # initialize the frame as black and with the Frigate logo + self.blank_frame = np.zeros(self.yuv_shape, np.uint8) + self.blank_frame[:] = 128 + self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16 + + # find and copy the logo on the blank frame + birdseye_logo = None + + custom_logo_files = glob.glob(f"{BASE_DIR}/custom.png") + + if len(custom_logo_files) > 0: + birdseye_logo = cv2.imread(custom_logo_files[0], cv2.IMREAD_UNCHANGED) + + if birdseye_logo is None: + logo_files = glob.glob( + os.path.join(INSTALL_DIR, "frigate/images/birdseye.png") + ) + + if len(logo_files) > 0: + birdseye_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) + + if birdseye_logo is not None: + transparent_layer = birdseye_logo[:, :, 3] + y_offset = height // 2 - transparent_layer.shape[0] // 2 + x_offset = width // 2 - transparent_layer.shape[1] // 2 + self.blank_frame[ + y_offset : y_offset + transparent_layer.shape[1], + x_offset : x_offset + transparent_layer.shape[0], + ] = transparent_layer + else: + logger.warning("Unable to read Frigate logo") + + self.frame[:] = self.blank_frame + + self.cameras = {} + for camera in self.config.cameras.keys(): + self.add_camera(camera) + + self.camera_layout = [] + self.active_cameras = set() + self.last_output_time = 0.0 + + def add_camera(self, cam: str): + """Add a camera to self.cameras with the correct structure.""" + settings = self.config.cameras[cam] + # precalculate the coordinates for all the channels + y, u1, u2, v1, v2 = get_yuv_crop( + settings.frame_shape_yuv, + ( + 0, + 0, + settings.frame_shape[1], + settings.frame_shape[0], + ), + ) + self.cameras[cam] = { + "dimensions": [ + settings.detect.width, + settings.detect.height, + ], + "last_active_frame": 0.0, + "current_frame": 0.0, + "layout_frame": 0.0, + "channel_dims": { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + }, + } + + def remove_camera(self, cam: str): + """Remove a camera from self.cameras.""" + if cam in self.cameras: + del self.cameras[cam] + + def clear_frame(self): + logger.debug("Clearing the birdseye frame") + self.frame[:] = self.blank_frame + + def copy_to_position(self, position, camera=None, frame: np.ndarray = None): + if camera is None: + frame = None + channel_dims = None + else: + if frame is None: + logger.debug(f"Unable to copy frame {camera} to birdseye.") + return + + channel_dims = self.cameras[camera]["channel_dims"] + + copy_yuv_to_position( + self.frame, + [position[1], position[0]], + [position[3], position[2]], + frame, + channel_dims, + ) + + def camera_active(self, mode, object_box_count, motion_box_count): + if mode == BirdseyeModeEnum.continuous: + return True + + if mode == BirdseyeModeEnum.motion and motion_box_count > 0: + return True + + if mode == BirdseyeModeEnum.objects and object_box_count > 0: + return True + + def get_camera_coordinates(self) -> dict[str, dict[str, int]]: + """Return the coordinates of each camera in the current layout.""" + coordinates = {} + for row in self.camera_layout: + for position in row: + camera_name, (x, y, width, height) = position + coordinates[camera_name] = { + "x": x, + "y": y, + "width": width, + "height": height, + } + return coordinates + + def update_frame(self, frame: Optional[np.ndarray] = None) -> tuple[bool, bool]: + """ + Update birdseye, optionally with a new frame. + Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. + """ + + # determine how many cameras are tracking objects within the last inactivity_threshold seconds + active_cameras: set[str] = set( + [ + cam + for cam, cam_data in self.cameras.items() + if self.config.cameras[cam].birdseye.enabled + and self.config.cameras[cam].enabled_in_config + and self.config.cameras[cam].enabled + and cam_data["last_active_frame"] > 0 + and cam_data["current_frame_time"] - cam_data["last_active_frame"] + < self.inactivity_threshold + ] + ) + logger.debug(f"Active cameras: {active_cameras}") + + max_cameras = self.config.birdseye.layout.max_cameras + max_camera_refresh = False + if max_cameras: + now = datetime.datetime.now().timestamp() + + if len(active_cameras) == max_cameras and now - self.last_refresh_time < 10: + # don't refresh cameras too often + active_cameras = self.active_cameras + else: + limited_active_cameras = sorted( + active_cameras, + key=lambda active_camera: ( + self.cameras[active_camera]["current_frame_time"] + - self.cameras[active_camera]["last_active_frame"] + ), + ) + active_cameras = limited_active_cameras[:max_cameras] + max_camera_refresh = True + self.last_refresh_time = now + + # Track if the frame or layout changes + frame_changed = False + layout_changed = False + + # If no active cameras and layout is already empty, no update needed + if len(active_cameras) == 0: + # if the layout is already cleared + if len(self.camera_layout) == 0: + return False, False + # if the layout needs to be cleared + self.camera_layout = [] + self.active_cameras = set() + self.clear_frame() + frame_changed = True + layout_changed = True + else: + # Determine if layout needs resetting + if len(self.active_cameras) - len(active_cameras) == 0: + if ( + len(self.active_cameras) == 1 + and self.active_cameras != active_cameras + ): + reset_layout = True + elif max_camera_refresh: + reset_layout = True + else: + reset_layout = False + else: + reset_layout = True + + if reset_layout: + logger.debug("Resetting Birdseye layout...") + self.clear_frame() + self.active_cameras = active_cameras + layout_changed = True # Layout is changing due to reset + # this also converts added_cameras from a set to a list since we need + # to pop elements in order + active_cameras_to_add = sorted( + active_cameras, + # sort cameras by order and by name if the order is the same + key=lambda active_camera: ( + self.config.cameras[active_camera].birdseye.order, + active_camera, + ), + ) + if len(active_cameras) == 1: + # show single camera as fullscreen + camera = active_cameras_to_add[0] + camera_dims = self.cameras[camera]["dimensions"].copy() + scaled_width = int( + self.canvas.height * camera_dims[0] / camera_dims[1] + ) + + # center camera view in canvas and ensure that it fits + if scaled_width < self.canvas.width: + coefficient = 1 + x_offset = int((self.canvas.width - scaled_width) / 2) + else: + coefficient = self.canvas.width / scaled_width + x_offset = int( + (self.canvas.width - (scaled_width * coefficient)) / 2 + ) + + self.camera_layout = [ + [ + ( + camera, + ( + x_offset, + 0, + int(scaled_width * coefficient), + int(self.canvas.height * coefficient), + ), + ) + ] + ] + else: + # calculate optimal layout + coefficient = self.canvas.get_coefficient(len(active_cameras)) + calculating = True + + # decrease scaling coefficient until height of all cameras can fit into the birdseye canvas + while calculating: + if self.stop_event.is_set(): + return frame_changed, layout_changed + + layout_candidate = self.calculate_layout( + active_cameras_to_add, coefficient + ) + + if not layout_candidate: + if coefficient < 10: + coefficient += 1 + continue + else: + logger.error( + "Error finding appropriate birdseye layout" + ) + return frame_changed, layout_changed + calculating = False + self.canvas.set_coefficient(len(active_cameras), coefficient) + + self.camera_layout = layout_candidate + frame_changed = True + + # Draw the layout + for row in self.camera_layout: + for position in row: + src_frame = self.cameras[position[0]]["current_frame"] + if src_frame is None or src_frame.size == 0: + logger.debug(f"Skipping invalid frame for {position[0]}") + continue + self.copy_to_position(position[1], position[0], src_frame) + if frame is not None: # Frame presence indicates a potential change + frame_changed = True + + return frame_changed, layout_changed + + def calculate_layout( + self, + cameras_to_add: list[str], + coefficient: float, + ) -> tuple[Any]: + """Calculate the optimal layout for 2+ cameras.""" + + def map_layout(camera_layout: list[list[Any]], row_height: int): + """Map the calculated layout.""" + candidate_layout = [] + starting_x = 0 + x = 0 + max_width = 0 + y = 0 + + for row in camera_layout: + final_row = [] + max_width = max(max_width, x) + x = starting_x + for cameras in row: + camera_dims = self.cameras[cameras[0]]["dimensions"].copy() + camera_aspect = cameras[1] + + if camera_dims[1] > camera_dims[0]: + scaled_height = int(row_height * 2) + scaled_width = int(scaled_height * camera_aspect) + starting_x = scaled_width + else: + scaled_height = row_height + scaled_width = int(scaled_height * camera_aspect) + + # layout is too large + if ( + x + scaled_width > self.canvas.width + or y + scaled_height > self.canvas.height + ): + return x + scaled_width, y + scaled_height, None + + final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) + x += scaled_width + + y += row_height + candidate_layout.append(final_row) + + if max_width == 0: + max_width = x + + return max_width, y, candidate_layout + + canvas_aspect_x, canvas_aspect_y = self.canvas.get_aspect(coefficient) + camera_layout: list[list[Any]] = [] + camera_layout.append([]) + starting_x = 0 + x = starting_x + y = 0 + y_i = 0 + max_y = 0 + for camera in cameras_to_add: + camera_dims = self.cameras[camera]["dimensions"].copy() + camera_aspect_x, camera_aspect_y = self.canvas.get_camera_aspect( + camera, camera_dims[0], camera_dims[1] + ) + + if camera_dims[1] > camera_dims[0]: + portrait = True + else: + portrait = False + + if (x + camera_aspect_x) <= canvas_aspect_x: + # insert if camera can fit on current row + camera_layout[y_i].append( + ( + camera, + camera_aspect_x / camera_aspect_y, + ) + ) + + if portrait: + starting_x = camera_aspect_x + else: + max_y = max( + max_y, + camera_aspect_y, + ) + + x += camera_aspect_x + else: + # move on to the next row and insert + y += max_y + y_i += 1 + camera_layout.append([]) + x = starting_x + + if x + camera_aspect_x > canvas_aspect_x: + return None + + camera_layout[y_i].append( + ( + camera, + camera_aspect_x / camera_aspect_y, + ) + ) + x += camera_aspect_x + + if y + max_y > canvas_aspect_y: + return None + + row_height = int(self.canvas.height / coefficient) + total_width, total_height, standard_candidate_layout = map_layout( + camera_layout, row_height + ) + + if not standard_candidate_layout: + # if standard layout didn't work + # try reducing row_height by the % overflow + scale_down_percent = max( + total_width / self.canvas.width, + total_height / self.canvas.height, + ) + row_height = int(row_height / scale_down_percent) + total_width, total_height, standard_candidate_layout = map_layout( + camera_layout, row_height + ) + + if not standard_candidate_layout: + return None + + # layout can't be optimized more + if total_width / self.canvas.width >= 0.99: + return standard_candidate_layout + + scale_up_percent = min( + 1 / (total_width / self.canvas.width), + 1 / (total_height / self.canvas.height), + ) + row_height = int(row_height * scale_up_percent) + _, _, scaled_layout = map_layout(camera_layout, row_height) + + if scaled_layout: + return scaled_layout + else: + return standard_candidate_layout + + def update( + self, + camera: str, + object_count: int, + motion_count: int, + frame_time: float, + frame: np.ndarray, + ) -> tuple[bool, bool]: + """ + Update birdseye for a specific camera with new frame data. + Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. + """ + # don't process if birdseye is disabled for this camera + camera_config = self.config.cameras[camera] + force_update = False + + # disabling birdseye is a little tricky + if not camera_config.birdseye.enabled or not camera_config.enabled: + # if we've rendered a frame (we have a value for last_active_frame) + # then we need to set it to zero + if self.cameras[camera]["last_active_frame"] > 0: + self.cameras[camera]["last_active_frame"] = 0 + force_update = True + else: + return False, False + + # update the last active frame for the camera + self.cameras[camera]["current_frame"] = frame.copy() + self.cameras[camera]["current_frame_time"] = frame_time + if self.camera_active(camera_config.birdseye.mode, object_count, motion_count): + self.cameras[camera]["last_active_frame"] = frame_time + + now = datetime.datetime.now().timestamp() + + # limit output to 10 fps + if not force_update and (now - self.last_output_time) < 1 / 10: + return False, False + + try: + frame_changed, layout_changed = self.update_frame(frame) + except Exception: + frame_changed, layout_changed = False, False + self.active_cameras = [] + self.camera_layout = [] + print(traceback.format_exc()) + + # if the frame was updated or the fps is too low, send frame + if force_update or frame_changed or (now - self.last_output_time) > 1: + self.last_output_time = now + return True, layout_changed + + return False, layout_changed + + +class Birdseye: + def __init__( + self, + config: FrigateConfig, + stop_event: mp.Event, + websocket_server, + ) -> None: + self.config = config + self.input = queue.Queue(maxsize=10) + self.converter = FFMpegConverter( + config.ffmpeg, + self.input, + stop_event, + config.birdseye.width, + config.birdseye.height, + config.birdseye.width, + config.birdseye.height, + config.birdseye.quality, + config.birdseye.restream, + ) + self.broadcaster = BroadcastThread( + "birdseye", self.converter, websocket_server, stop_event + ) + self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event) + self.frame_manager = SharedMemoryFrameManager() + self.stop_event = stop_event + self.requestor = InterProcessRequestor() + self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps + self._idle_interval: Optional[float] = ( + (1.0 / self.idle_fps) if self.idle_fps > 0 else None + ) + + if config.birdseye.restream: + self.birdseye_buffer = self.frame_manager.create( + "birdseye", + self.birdseye_manager.yuv_shape[0] * self.birdseye_manager.yuv_shape[1], + ) + + self.converter.start() + self.broadcaster.start() + + def __send_new_frame(self) -> None: + frame_bytes = self.birdseye_manager.frame.tobytes() + + if self.config.birdseye.restream: + self.birdseye_buffer[:] = frame_bytes + + try: + self.input.put_nowait(frame_bytes) + except queue.Full: + # drop frames if queue is full + pass + + def all_cameras_disabled(self) -> None: + self.birdseye_manager.clear_frame() + self.__send_new_frame() + + def add_camera(self, camera: str) -> None: + """Add a camera to the birdseye manager.""" + self.birdseye_manager.add_camera(camera) + logger.debug(f"Added camera {camera} to birdseye") + + def remove_camera(self, camera: str) -> None: + """Remove a camera from the birdseye manager.""" + self.birdseye_manager.remove_camera(camera) + logger.debug(f"Removed camera {camera} from birdseye") + + def write_data( + self, + camera: str, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + frame: np.ndarray, + ) -> None: + frame_changed, frame_layout_changed = self.birdseye_manager.update( + camera, + len([o for o in current_tracked_objects if not o["stationary"]]), + len(motion_boxes), + frame_time, + frame, + ) + if frame_changed: + self.__send_new_frame() + + if frame_layout_changed: + coordinates = self.birdseye_manager.get_camera_coordinates() + self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates) + if self._idle_interval: + now = time.monotonic() + is_idle = len(self.birdseye_manager.camera_layout) == 0 + if ( + is_idle + and (now - self.birdseye_manager.last_output_time) + >= self._idle_interval + ): + self.__send_new_frame() + + def stop(self) -> None: + self.converter.join() + self.broadcaster.join() diff --git a/sam2-cpu/frigate-dev/frigate/output/camera.py b/sam2-cpu/frigate-dev/frigate/output/camera.py new file mode 100644 index 0000000..2311ec6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/output/camera.py @@ -0,0 +1,170 @@ +"""Handle outputting individual cameras via jsmpeg.""" + +import logging +import multiprocessing as mp +import queue +import subprocess as sp +import threading + +from frigate.config import CameraConfig, FfmpegConfig + +logger = logging.getLogger(__name__) + + +class FFMpegConverter(threading.Thread): + def __init__( + self, + camera: str, + ffmpeg: FfmpegConfig, + input_queue: queue.Queue, + stop_event: mp.Event, + in_width: int, + in_height: int, + out_width: int, + out_height: int, + quality: int, + ): + super().__init__(name=f"{camera}_output_converter") + self.camera = camera + self.input_queue = input_queue + self.stop_event = stop_event + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-threads", + "1", + "-f", + "rawvideo", + "-pix_fmt", + "yuv420p", + "-video_size", + f"{in_width}x{in_height}", + "-i", + "pipe:", + "-threads", + "1", + "-f", + "mpegts", + "-s", + f"{out_width}x{out_height}", + "-codec:v", + "mpeg1video", + "-q", + f"{quality}", + "-bf", + "0", + "pipe:", + ] + + self.process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=sp.DEVNULL, + stdin=sp.PIPE, + start_new_session=True, + ) + + def __write(self, b) -> None: + self.process.stdin.write(b) + + def read(self, length): + try: + return self.process.stdout.read1(length) + except ValueError: + return False + + def exit(self): + self.process.terminate() + + try: + self.process.communicate(timeout=30) + except sp.TimeoutExpired: + self.process.kill() + self.process.communicate() + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + frame = self.input_queue.get(True, timeout=1) + self.__write(frame) + except queue.Empty: + pass + + self.exit() + + +class BroadcastThread(threading.Thread): + def __init__( + self, + camera: str, + converter: FFMpegConverter, + websocket_server, + stop_event: mp.Event, + ): + super().__init__() + self.camera = camera + self.converter = converter + self.websocket_server = websocket_server + self.stop_event = stop_event + + def run(self): + while not self.stop_event.is_set(): + buf = self.converter.read(65536) + if buf: + manager = self.websocket_server.manager + with manager.lock: + websockets = manager.websockets.copy() + ws_iter = iter(websockets.values()) + + for ws in ws_iter: + if ( + not ws.terminated + and ws.environ["PATH_INFO"] == f"/{self.camera}" + ): + try: + ws.send(buf, binary=True) + except ValueError: + pass + except (BrokenPipeError, ConnectionResetError) as e: + logger.debug(f"Websocket unexpectedly closed {e}") + elif self.converter.process.poll() is not None: + break + + +class JsmpegCamera: + def __init__( + self, config: CameraConfig, stop_event: mp.Event, websocket_server + ) -> None: + self.config = config + self.input = queue.Queue(maxsize=config.detect.fps) + width = int( + config.live.height * (config.frame_shape[1] / config.frame_shape[0]) + ) + self.converter = FFMpegConverter( + config.name, + config.ffmpeg, + self.input, + stop_event, + config.frame_shape[1], + config.frame_shape[0], + width, + config.live.height, + config.live.quality, + ) + self.broadcaster = BroadcastThread( + config.name, self.converter, websocket_server, stop_event + ) + + self.converter.start() + self.broadcaster.start() + + def write_frame(self, frame_bytes) -> None: + try: + self.input.put_nowait(frame_bytes) + except queue.Full: + # drop frames if queue is full + pass + + def stop(self) -> None: + self.converter.join() + self.broadcaster.join() diff --git a/sam2-cpu/frigate-dev/frigate/output/output.py b/sam2-cpu/frigate-dev/frigate/output/output.py new file mode 100644 index 0000000..674c02b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/output/output.py @@ -0,0 +1,285 @@ +"""Handle outputting raw frigate frames""" + +import datetime +import logging +import os +import shutil +import threading +from multiprocessing.synchronize import Event as MpEvent +from wsgiref.simple_server import make_server + +from ws4py.server.wsgirefserver import ( + WebSocketWSGIHandler, + WebSocketWSGIRequestHandler, + WSGIServer, +) +from ws4py.server.wsgiutils import WebSocketWSGIApplication + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.ws import WebSocket +from frigate.config import FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import CACHE_DIR, CLIPS_DIR, PROCESS_PRIORITY_MED +from frigate.output.birdseye import Birdseye +from frigate.output.camera import JsmpegCamera +from frigate.output.preview import PreviewRecorder +from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +def check_disabled_camera_update( + config: FrigateConfig, + birdseye: Birdseye | None, + previews: dict[str, PreviewRecorder], + write_times: dict[str, float], +) -> None: + """Check if camera is disabled / offline and needs an update.""" + now = datetime.datetime.now().timestamp() + has_enabled_camera = False + + for camera, last_update in write_times.items(): + offline_time = now - last_update + + if config.cameras[camera].enabled: + has_enabled_camera = True + else: + # flag camera as offline when it is disabled + previews[camera].flag_offline(now) + + if offline_time > 1: + # last camera update was more than 1 second ago + # need to send empty data to birdseye because current + # frame is now out of date + if birdseye and offline_time < 10: + # we only need to send blank frames to birdseye at the beginning of a camera being offline + birdseye.write_data( + camera, + [], + [], + now, + get_blank_yuv_frame( + config.cameras[camera].detect.width, + config.cameras[camera].detect.height, + ), + ) + + if not has_enabled_camera and birdseye: + birdseye.all_cameras_disabled() + + +class OutputProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, PROCESS_PRIORITY_MED, name="frigate.output", daemon=True + ) + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + + frame_manager = SharedMemoryFrameManager() + + # start a websocket server on 8082 + WebSocketWSGIHandler.http_version = "1.1" + websocket_server = make_server( + "127.0.0.1", + 8082, + server_class=WSGIServer, + handler_class=WebSocketWSGIRequestHandler, + app=WebSocketWSGIApplication(handler_cls=WebSocket), + ) + websocket_server.initialize_websockets_manager() + websocket_thread = threading.Thread(target=websocket_server.serve_forever) + + detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value) + config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.birdseye, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.record, + ], + ) + + jsmpeg_cameras: dict[str, JsmpegCamera] = {} + birdseye: Birdseye | None = None + preview_recorders: dict[str, PreviewRecorder] = {} + preview_write_times: dict[str, float] = {} + failed_frame_requests: dict[str, int] = {} + last_disabled_cam_check = datetime.datetime.now().timestamp() + + move_preview_frames("cache") + + for camera, cam_config in self.config.cameras.items(): + if not cam_config.enabled_in_config: + continue + + jsmpeg_cameras[camera] = JsmpegCamera( + cam_config, self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder(cam_config) + preview_write_times[camera] = 0 + + if self.config.birdseye.enabled: + birdseye = Birdseye(self.config, self.stop_event, websocket_server) + + websocket_thread.start() + + while not self.stop_event.is_set(): + # check if there is an updated config + updates = config_subscriber.check_for_updates() + + if CameraConfigUpdateEnum.add in updates: + for camera in updates["add"]: + jsmpeg_cameras[camera] = JsmpegCamera( + cam_config, self.stop_event, websocket_server + ) + preview_recorders[camera] = PreviewRecorder(cam_config) + preview_write_times[camera] = 0 + + if ( + self.config.birdseye.enabled + and self.config.cameras[camera].birdseye.enabled + ): + birdseye.add_camera(camera) + + (topic, data) = detection_subscriber.check_for_update(timeout=1) + now = datetime.datetime.now().timestamp() + + if now - last_disabled_cam_check > 5: + # check disabled cameras every 5 seconds + last_disabled_cam_check = now + check_disabled_camera_update( + self.config, birdseye, preview_recorders, preview_write_times + ) + + if not topic: + continue + + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + _, + ) = data + + if not self.config.cameras[camera].enabled: + continue + + frame = frame_manager.get( + frame_name, self.config.cameras[camera].frame_shape_yuv + ) + + if frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + failed_frame_requests[camera] = failed_frame_requests.get(camera, 0) + 1 + + if ( + failed_frame_requests[camera] + > self.config.cameras[camera].detect.fps + ): + logger.warning( + f"Failed to retrieve many frames for {camera} from SHM, consider increasing SHM size if this continues." + ) + + continue + else: + failed_frame_requests[camera] = 0 + + # send frames for low fps recording + preview_recorders[camera].write_data( + current_tracked_objects, motion_boxes, frame_time, frame + ) + preview_write_times[camera] = frame_time + + # send camera frame to ffmpeg process if websockets are connected + if any( + ws.environ["PATH_INFO"].endswith(camera) + for ws in websocket_server.manager + ): + # write to the converter for the camera if clients are listening to the specific camera + jsmpeg_cameras[camera].write_frame(frame.tobytes()) + + # send output data to birdseye if websocket is connected or restreaming + if self.config.birdseye.enabled and ( + self.config.birdseye.restream + or any( + ws.environ["PATH_INFO"].endswith("birdseye") + for ws in websocket_server.manager + ) + ): + birdseye.write_data( + camera, + current_tracked_objects, + motion_boxes, + frame_time, + frame, + ) + + frame_manager.close(frame_name) + + move_preview_frames("clips") + + while True: + (topic, data) = detection_subscriber.check_for_update(timeout=0) + + if not topic: + break + + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = data + + frame = frame_manager.get( + frame_name, self.config.cameras[camera].frame_shape_yuv + ) + frame_manager.close(frame_name) + + detection_subscriber.stop() + + for jsmpeg in jsmpeg_cameras.values(): + jsmpeg.stop() + + for preview in preview_recorders.values(): + preview.stop() + + if birdseye is not None: + birdseye.stop() + + config_subscriber.stop() + websocket_server.manager.close_all() + websocket_server.manager.stop() + websocket_server.manager.join() + websocket_server.shutdown() + websocket_thread.join() + logger.info("exiting output process...") + + +def move_preview_frames(loc: str): + preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache") + preview_cache = os.path.join(CACHE_DIR, "preview_frames") + + try: + if loc == "clips": + shutil.move(preview_cache, preview_holdover) + elif loc == "cache": + if not os.path.exists(preview_holdover): + return + + shutil.move(preview_holdover, preview_cache) + except shutil.Error: + logger.error("Failed to restore preview cache.") diff --git a/sam2-cpu/frigate-dev/frigate/output/preview.py b/sam2-cpu/frigate-dev/frigate/output/preview.py new file mode 100644 index 0000000..6dfd909 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/output/preview.py @@ -0,0 +1,411 @@ +"""Handle outputting low res / fps preview segments from decoded frames.""" + +import datetime +import logging +import os +import shutil +import subprocess as sp +import threading +import time +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import CameraConfig, RecordQualityEnum +from frigate.const import CACHE_DIR, CLIPS_DIR, INSERT_PREVIEW, PREVIEW_FRAME_TYPE +from frigate.ffmpeg_presets import ( + FPS_VFR_PARAM, + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) +from frigate.models import Previews +from frigate.track.object_processing import TrackedObject +from frigate.util.image import copy_yuv_to_position, get_blank_yuv_frame, get_yuv_crop + +logger = logging.getLogger(__name__) + +FOLDER_PREVIEW_FRAMES = "preview_frames" +PREVIEW_CACHE_DIR = os.path.join(CACHE_DIR, FOLDER_PREVIEW_FRAMES) +PREVIEW_SEGMENT_DURATION = 3600 # one hour +# important to have lower keyframe to maintain scrubbing performance +PREVIEW_KEYFRAME_INTERVAL = 40 +PREVIEW_HEIGHT = 180 +PREVIEW_QUALITY_WEBP = { + RecordQualityEnum.very_low: 70, + RecordQualityEnum.low: 80, + RecordQualityEnum.medium: 80, + RecordQualityEnum.high: 80, + RecordQualityEnum.very_high: 86, +} +PREVIEW_QUALITY_BIT_RATES = { + RecordQualityEnum.very_low: 7168, + RecordQualityEnum.low: 8196, + RecordQualityEnum.medium: 9216, + RecordQualityEnum.high: 9864, + RecordQualityEnum.very_high: 10096, +} + + +def get_cache_image_name(camera: str, frame_time: float) -> str: + """Get the image name in cache.""" + return os.path.join( + CACHE_DIR, + f"{FOLDER_PREVIEW_FRAMES}/preview_{camera}-{frame_time}.{PREVIEW_FRAME_TYPE}", + ) + + +class FFMpegConverter(threading.Thread): + """Convert a list of still frames into a vfr mp4.""" + + def __init__( + self, + config: CameraConfig, + frame_times: list[float], + requestor: InterProcessRequestor, + ): + super().__init__(name=f"{config.name}_preview_converter") + self.config = config + self.frame_times = frame_times + self.requestor = requestor + self.path = os.path.join( + CLIPS_DIR, + f"previews/{self.config.name}/{self.frame_times[0]}-{self.frame_times[-1]}.mp4", + ) + + # write a PREVIEW at fps and 1 key frame per clip + self.ffmpeg_cmd = parse_preset_hardware_acceleration_encode( + config.ffmpeg.ffmpeg_path, + "default", + input="-f concat -y -protocol_whitelist pipe,file -safe 0 -threads 1 -i /dev/stdin", + output=f"-threads 1 -g {PREVIEW_KEYFRAME_INTERVAL} -bf 0 -b:v {PREVIEW_QUALITY_BIT_RATES[self.config.record.preview.quality]} {FPS_VFR_PARAM} -movflags +faststart -pix_fmt yuv420p {self.path}", + type=EncodeTypeEnum.preview, + ) + + def run(self) -> None: + # generate input list + item_count = len(self.frame_times) + playlist = [] + + for t_idx in range(0, item_count): + if t_idx == item_count - 1: + # last frame does not get a duration + playlist.append( + f"file '{get_cache_image_name(self.config.name, self.frame_times[t_idx])}'" + ) + continue + + playlist.append( + f"file '{get_cache_image_name(self.config.name, self.frame_times[t_idx])}'" + ) + playlist.append( + f"duration {self.frame_times[t_idx + 1] - self.frame_times[t_idx]}" + ) + + try: + p = sp.run( + self.ffmpeg_cmd.split(" "), + input="\n".join(playlist), + encoding="ascii", + capture_output=True, + ) + except BlockingIOError: + logger.warning( + f"Failed to create preview for {self.config.name}, retrying..." + ) + time.sleep(2) + p = sp.run( + self.ffmpeg_cmd.split(" "), + input="\n".join(playlist), + encoding="ascii", + capture_output=True, + ) + + start = self.frame_times[0] + end = self.frame_times[-1] + + if p.returncode == 0: + logger.debug("successfully saved preview") + self.requestor.send_data( + INSERT_PREVIEW, + { + Previews.id.name: f"{self.config.name}_{end}", + Previews.camera.name: self.config.name, + Previews.path.name: self.path, + Previews.start_time.name: start, + Previews.end_time.name: end, + Previews.duration.name: end - start, + }, + ) + else: + logger.error(f"Error saving preview for {self.config.name} :: {p.stderr}") + + # unlink files from cache + # don't delete last frame as it will be used as first frame in next segment + for t in self.frame_times[0:-1]: + Path(get_cache_image_name(self.config.name, t)).unlink(missing_ok=True) + + +class PreviewRecorder: + def __init__(self, config: CameraConfig) -> None: + self.config = config + self.start_time = 0 + self.last_output_time = 0 + self.offline = False + self.output_frames = [] + + if config.detect.width > config.detect.height: + self.out_height = PREVIEW_HEIGHT + self.out_width = ( + int((config.detect.width / config.detect.height) * self.out_height) + // 4 + * 4 + ) + else: + self.out_width = PREVIEW_HEIGHT + self.out_height = ( + int((config.detect.height / config.detect.width) * self.out_width) + // 4 + * 4 + ) + + # create communication for finished previews + self.requestor = InterProcessRequestor() + + y, u1, u2, v1, v2 = get_yuv_crop( + self.config.frame_shape_yuv, + ( + 0, + 0, + self.config.frame_shape[1], + self.config.frame_shape[0], + ), + ) + self.channel_dims = { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + } + + # end segment at end of hour + self.segment_end = ( + (datetime.datetime.now() + datetime.timedelta(hours=1)) + .astimezone(datetime.timezone.utc) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + + Path(PREVIEW_CACHE_DIR).mkdir(exist_ok=True) + Path(os.path.join(CLIPS_DIR, f"previews/{config.name}")).mkdir( + parents=True, exist_ok=True + ) + + # check for existing items in cache + start_ts = ( + datetime.datetime.now() + .astimezone(datetime.timezone.utc) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + + file_start = f"preview_{config.name}" + start_file = f"{file_start}-{start_ts}.webp" + + for file in sorted(os.listdir(os.path.join(CACHE_DIR, FOLDER_PREVIEW_FRAMES))): + if not file.startswith(file_start): + continue + + if file < start_file: + os.unlink(os.path.join(PREVIEW_CACHE_DIR, file)) + continue + + try: + file_time = file.split("-")[-1][: -(len(PREVIEW_FRAME_TYPE) + 1)] + + if not file_time: + continue + + ts = float(file_time) + except ValueError: + continue + + if self.start_time == 0: + self.start_time = ts + + self.last_output_time = ts + self.output_frames.append(ts) + + def reset_frame_cache(self, frame_time: float) -> None: + self.segment_end = ( + (datetime.datetime.now() + datetime.timedelta(hours=1)) + .astimezone(datetime.timezone.utc) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + self.start_time = frame_time + self.last_output_time = frame_time + self.output_frames: list[float] = [] + + def should_write_frame( + self, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + ) -> bool: + """Decide if this frame should be added to PREVIEW.""" + if not self.config.record.enabled: + return False + + active_objs = get_active_objects( + frame_time, self.config, current_tracked_objects + ) + + preview_output_fps = 2 if any(o["label"] == "car" for o in active_objs) else 1 + + # limit output to 1 fps + if (frame_time - self.last_output_time) < 1 / preview_output_fps: + return False + + # send frame if a non-stationary object is in a zone + if len(active_objs) > 0: + self.last_output_time = frame_time + return True + + if len(motion_boxes) > 0: + self.last_output_time = frame_time + return True + + # ensure that at least 2 frames are written every minute + if frame_time - self.last_output_time > 30: + self.last_output_time = frame_time + return True + + return False + + def write_frame_to_cache(self, frame_time: float, frame: np.ndarray) -> None: + # resize yuv frame + small_frame = np.zeros((self.out_height * 3 // 2, self.out_width), np.uint8) + copy_yuv_to_position( + small_frame, + (0, 0), + (self.out_height, self.out_width), + frame, + self.channel_dims, + cv2.INTER_AREA, + ) + small_frame = cv2.cvtColor( + small_frame, + cv2.COLOR_YUV2BGR_I420, + ) + cv2.imwrite( + get_cache_image_name(self.config.name, frame_time), + small_frame, + [ + int(cv2.IMWRITE_WEBP_QUALITY), + PREVIEW_QUALITY_WEBP[self.config.record.preview.quality], + ], + ) + + def write_data( + self, + current_tracked_objects: list[dict[str, Any]], + motion_boxes: list[list[int]], + frame_time: float, + frame: np.ndarray, + ) -> None: + self.offline = False + + # always write the first frame + if self.start_time == 0: + self.start_time = frame_time + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + return + + # check if PREVIEW clip should be generated and cached frames reset + if frame_time >= self.segment_end: + if len(self.output_frames) > 0: + # save last frame to ensure consistent duration + if self.config.record: + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + + # write the preview if any frames exist for this hour + FFMpegConverter( + self.config, + self.output_frames, + self.requestor, + ).start() + else: + logger.debug( + f"Not saving preview for {self.config.name} because there are no saved frames." + ) + + self.reset_frame_cache(frame_time) + + # include first frame to ensure consistent duration + if self.config.record.enabled: + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + + return + elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time): + self.output_frames.append(frame_time) + self.write_frame_to_cache(frame_time, frame) + return + + def flag_offline(self, frame_time: float) -> None: + if not self.offline: + self.write_frame_to_cache( + frame_time, + get_blank_yuv_frame( + self.config.detect.width, self.config.detect.height + ), + ) + self.offline = True + + # check if PREVIEW clip should be generated and cached frames reset + if frame_time >= self.segment_end: + if len(self.output_frames) == 0: + # camera has been offline for entire hour + # we have no preview to create + self.reset_frame_cache(frame_time) + return + + old_frame_path = get_cache_image_name( + self.config.name, self.output_frames[-1] + ) + new_frame_path = get_cache_image_name(self.config.name, frame_time) + shutil.copy(old_frame_path, new_frame_path) + + # save last frame to ensure consistent duration + self.output_frames.append(frame_time) + FFMpegConverter( + self.config, + self.output_frames, + self.requestor, + ).start() + + self.reset_frame_cache(frame_time) + + def stop(self) -> None: + self.config_subscriber.stop() + self.requestor.stop() + + +def get_active_objects( + frame_time: float, camera_config: CameraConfig, all_objects: list[TrackedObject] +) -> list[TrackedObject]: + """get active objects for detection.""" + return [ + o + for o in all_objects + if o["motionless_count"] < camera_config.detect.stationary.threshold + and o["position_changes"] > 0 + and o["frame_time"] == frame_time + and not o["false_positive"] + ] diff --git a/sam2-cpu/frigate-dev/frigate/plus.py b/sam2-cpu/frigate-dev/frigate/plus.py new file mode 100644 index 0000000..197b6e4 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/plus.py @@ -0,0 +1,244 @@ +import datetime +import json +import logging +import os +import re +from pathlib import Path +from typing import Any, List + +import cv2 +import requests +from numpy import ndarray +from requests.models import Response + +from frigate.const import PLUS_API_HOST, PLUS_ENV_VAR + +logger = logging.getLogger(__name__) + + +def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes: + if image.shape[1] >= image.shape[0]: + width = min(max_dim, image.shape[1]) + height = int(width * image.shape[0] / image.shape[1]) + else: + height = min(max_dim, image.shape[0]) + width = int(height * image.shape[1] / image.shape[0]) + + original = cv2.resize(image, dsize=(width, height), interpolation=cv2.INTER_AREA) + + ret, jpg = cv2.imencode(".jpg", original, [int(cv2.IMWRITE_JPEG_QUALITY), quality]) + jpg_bytes = jpg.tobytes() + return jpg_bytes if isinstance(jpg_bytes, bytes) else b"" + + +class PlusApi: + def __init__(self) -> None: + self.host = PLUS_API_HOST + self.key = None + if PLUS_ENV_VAR in os.environ: + self.key = os.environ.get(PLUS_ENV_VAR) + elif ( + os.path.isdir("/run/secrets") + and os.access("/run/secrets", os.R_OK) + and PLUS_ENV_VAR in os.listdir("/run/secrets") + ): + self.key = ( + Path(os.path.join("/run/secrets", PLUS_ENV_VAR)).read_text().strip() + ) + # check for the add-on options file + elif os.path.isfile("/data/options.json"): + with open("/data/options.json") as f: + raw_options = f.read() + options = json.loads(raw_options) + self.key = options.get("plus_api_key") + + if self.key is not None and not re.match( + r"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}", + self.key, + ): + logger.error("Plus API Key is not formatted correctly.") + self.key = None + + self._is_active: bool = self.key is not None + self._token_data: dict = {} + + def _refresh_token_if_needed(self) -> None: + if ( + self._token_data.get("expires") is None + or self._token_data["expires"] - datetime.datetime.now().timestamp() < 60 + ): + if self.key is None: + raise Exception( + "Plus API key not set. See https://docs.frigate.video/integrations/plus#set-your-api-key" + ) + parts = self.key.split(":") + r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1])) + if not r.ok: + raise Exception(f"Unable to refresh API token: {r.text}") + self._token_data = r.json() + + def _get_authorization_header(self) -> dict: + self._refresh_token_if_needed() + return {"authorization": f"Bearer {self._token_data.get('accessToken')}"} + + def _get(self, path: str) -> Response: + return requests.get( + f"{self.host}/v1/{path}", headers=self._get_authorization_header() + ) + + def _post(self, path: str, data: dict) -> Response: + return requests.post( + f"{self.host}/v1/{path}", + headers=self._get_authorization_header(), + json=data, + ) + + def _put(self, path: str, data: dict) -> Response: + return requests.put( + f"{self.host}/v1/{path}", + headers=self._get_authorization_header(), + json=data, + ) + + def is_active(self) -> bool: + return self._is_active + + def upload_image(self, image: ndarray, camera: str) -> str: + r = self._get("image/signed_urls") + presigned_urls = r.json() + if not r.ok: + raise Exception("Unable to get signed urls") + + # resize and submit original + files = {"file": get_jpg_bytes(image, 1920, 85)} + data = presigned_urls["original"]["fields"] + data["content-type"] = "image/jpeg" + r = requests.post(presigned_urls["original"]["url"], files=files, data=data) + if not r.ok: + logger.error(f"Failed to upload original: {r.status_code} {r.text}") + raise Exception(r.text) + + # resize and submit thumbnail + files = {"file": get_jpg_bytes(image, 200, 70)} + data = presigned_urls["thumbnail"]["fields"] + data["content-type"] = "image/jpeg" + r = requests.post(presigned_urls["thumbnail"]["url"], files=files, data=data) + if not r.ok: + logger.error(f"Failed to upload thumbnail: {r.status_code} {r.text}") + raise Exception(r.text) + + # create image + r = self._post( + "image/create", {"id": presigned_urls["imageId"], "camera": camera} + ) + if not r.ok: + raise Exception(r.text) + + # return image id + return str(presigned_urls.get("imageId")) + + def add_false_positive( + self, + plus_id: str, + region: List[float], + bbox: List[float], + score: float, + label: str, + model_hash: str, + model_type: str, + detector_type: str, + ) -> None: + r = self._put( + f"image/{plus_id}/false_positive", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "regionX": region[0], + "regionY": region[1], + "regionW": region[2], + "regionH": region[3], + "score": score, + "model_hash": model_hash, + "model_type": model_type, + "detector_type": detector_type, + }, + ) + + if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e + raise Exception(r.text) + + def add_annotation( + self, + plus_id: str, + bbox: List[float], + label: str, + difficult: bool = False, + ) -> None: + r = self._put( + f"image/{plus_id}/annotation", + { + "label": label, + "x": bbox[0], + "y": bbox[1], + "w": bbox[2], + "h": bbox[3], + "difficult": difficult, + }, + ) + + if not r.ok: + try: + error_response = r.json() + errors = error_response.get("errors", []) + for error in errors: + if ( + error.get("param") == "label" + and error.get("type") == "invalid_enum_value" + ): + raise ValueError(f"Unsupported label value provided: {label}") + except ValueError as e: + raise e + raise Exception(r.text) + + def get_model_download_url( + self, + model_id: str, + ) -> str: + r = self._get(f"model/{model_id}/signed_url") + + if not r.ok: + raise Exception(r.text) + + presigned_url = r.json() + + return str(presigned_url.get("url")) + + def get_model_info(self, model_id: str) -> Any: + r = self._get(f"model/{model_id}") + + if not r.ok: + raise Exception(r.text) + + return r.json() + + def get_models(self) -> Any: + r = self._get("model/list") + + if not r.ok: + raise Exception(r.text) + + return r.json() diff --git a/sam2-cpu/frigate-dev/frigate/ptz/autotrack.py b/sam2-cpu/frigate-dev/frigate/ptz/autotrack.py new file mode 100644 index 0000000..6e86ecb --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/ptz/autotrack.py @@ -0,0 +1,1505 @@ +"""Automatically pan, tilt, and zoom on detected objects via onvif.""" + +import asyncio +import copy +import logging +import threading +import time +from collections import deque +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 +import numpy as np +from norfair.camera_motion import ( + HomographyTransformationGetter, + MotionEstimator, + TranslationTransformationGetter, +) + +from frigate.camera import PTZMetrics +from frigate.comms.dispatcher import Dispatcher +from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum +from frigate.const import ( + AUTOTRACKING_MAX_AREA_RATIO, + AUTOTRACKING_MAX_MOVE_METRICS, + AUTOTRACKING_MOTION_MAX_POINTS, + AUTOTRACKING_MOTION_MIN_DISTANCE, + AUTOTRACKING_ZOOM_EDGE_THRESHOLD, + AUTOTRACKING_ZOOM_IN_HYSTERESIS, + AUTOTRACKING_ZOOM_OUT_HYSTERESIS, +) +from frigate.ptz.onvif import OnvifController +from frigate.track.tracked_object import TrackedObject +from frigate.util.builtin import update_yaml_file_bulk +from frigate.util.config import find_config_file +from frigate.util.image import SharedMemoryFrameManager, intersection_over_union + +logger = logging.getLogger(__name__) + + +def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time): + # Determine if the PTZ was in motion at the set frame time + # for non ptz/autotracking cameras, this will always return False + # ptz_start_time is initialized to 0 on startup and only changes + # when autotracking movements are made + return (ptz_start_time != 0.0 and frame_time > ptz_start_time) and ( + ptz_stop_time == 0.0 or (ptz_start_time <= frame_time <= ptz_stop_time) + ) + + +class PtzMotionEstimator: + def __init__(self, config: CameraConfig, ptz_metrics: PTZMetrics) -> None: + self.frame_manager = SharedMemoryFrameManager() + self.norfair_motion_estimator = None + self.camera_config = config + self.coord_transformations = None + self.ptz_metrics = ptz_metrics + self.ptz_metrics.reset.set() + logger.debug(f"{config.name}: Motion estimator init") + + def motion_estimator( + self, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + frame_name: str, + frame_time: float, + camera: str | None, + ): + # If we've just started up or returned to our preset, reset motion estimator for new tracking session + if self.ptz_metrics.reset.is_set(): + self.ptz_metrics.reset.clear() + + # homography is nice (zooming) but slow, translation is pan/tilt only but fast. + if ( + self.camera_config.onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + logger.debug(f"{camera}: Motion estimator reset - homography") + transformation_type = HomographyTransformationGetter() + else: + logger.debug(f"{camera}: Motion estimator reset - translation") + transformation_type = TranslationTransformationGetter() + + self.norfair_motion_estimator = MotionEstimator( + transformations_getter=transformation_type, + min_distance=AUTOTRACKING_MOTION_MIN_DISTANCE, + max_points=AUTOTRACKING_MOTION_MAX_POINTS, + ) + + self.coord_transformations = None + + if ptz_moving_at_frame_time( + frame_time, + self.ptz_metrics.start_time.value, + self.ptz_metrics.stop_time.value, + ): + logger.debug( + f"{camera}: Motion estimator running - frame time: {frame_time}" + ) + + yuv_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + self.coord_transformations = None + return None + + frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2GRAY_I420) + + # mask out detections for better motion estimation + mask = np.ones(frame.shape[:2], frame.dtype) + + detection_boxes = [x[2] for x in detections] + for detection in detection_boxes: + x1, y1, x2, y2 = detection + mask[y1:y2, x1:x2] = 0 + + # merge camera config motion mask with detections. Norfair function needs 0,1 mask + mask = np.bitwise_and(mask, self.camera_config.motion.mask).clip(max=1) + + # Norfair estimator function needs color so it can convert it right back to gray + frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGRA) + + try: + self.coord_transformations = self.norfair_motion_estimator.update( + frame, mask + ) + except Exception: + # sometimes opencv can't find enough features in the image to find homography, so catch this error + # https://github.com/tryolabs/norfair/pull/278 + logger.warning( + f"Autotracker: motion estimator couldn't get transformations for {camera} at frame time {frame_time}" + ) + self.coord_transformations = None + + try: + logger.debug( + f"{camera}: Motion estimator transformation: {self.coord_transformations.rel_to_abs([[0, 0]])}" + ) + except Exception: + pass + + self.frame_manager.close(frame_name) + + return self.coord_transformations + + +class PtzAutoTrackerThread(threading.Thread): + def __init__( + self, + config: FrigateConfig, + onvif: OnvifController, + ptz_metrics: dict[str, PTZMetrics], + dispatcher: Dispatcher, + stop_event: MpEvent, + ) -> None: + super().__init__(name="ptz_autotracker") + self.ptz_autotracker = PtzAutoTracker( + config, onvif, ptz_metrics, dispatcher, stop_event + ) + self.stop_event = stop_event + self.config = config + + def run(self): + while not self.stop_event.wait(1): + for camera, camera_config in self.config.cameras.items(): + if not camera_config.enabled: + continue + + if camera_config.onvif.autotracking.enabled: + future = asyncio.run_coroutine_threadsafe( + self.ptz_autotracker.camera_maintenance(camera), + self.ptz_autotracker.onvif.loop, + ) + # Wait for the coroutine to complete + future.result() + else: + # disabled dynamically by mqtt + if self.ptz_autotracker.tracked_object.get(camera): + self.ptz_autotracker.tracked_object[camera] = None + self.ptz_autotracker.tracked_object_history[camera].clear() + + logger.info("Exiting autotracker...") + + +class PtzAutoTracker: + def __init__( + self, + config: FrigateConfig, + onvif: OnvifController, + ptz_metrics: PTZMetrics, + dispatcher: Dispatcher, + stop_event: MpEvent, + ) -> None: + self.config = config + self.onvif = onvif + self.ptz_metrics = ptz_metrics + self.dispatcher = dispatcher + self.stop_event = stop_event + self.tracked_object: dict[str, object] = {} + self.tracked_object_history: dict[str, object] = {} + self.tracked_object_metrics: dict[str, object] = {} + self.object_types: dict[str, object] = {} + self.required_zones: dict[str, object] = {} + self.move_queues: dict[str, object] = {} + self.move_queue_locks: dict[str, object] = {} + self.move_threads: dict[str, object] = {} + self.autotracker_init: dict[str, object] = {} + self.move_metrics: dict[str, object] = {} + self.calibrating: dict[str, object] = {} + self.intercept: dict[str, object] = {} + self.move_coefficients: dict[str, object] = {} + self.zoom_time: dict[str, float] = {} + self.zoom_factor: dict[str, object] = {} + + # if cam is set to autotrack, onvif should be set up + for camera, camera_config in self.config.cameras.items(): + if not camera_config.enabled: + continue + + self.autotracker_init[camera] = False + if ( + camera_config.onvif.autotracking.enabled + and camera_config.onvif.autotracking.enabled_in_config + ): + future = asyncio.run_coroutine_threadsafe( + self._autotracker_setup(camera_config, camera), self.onvif.loop + ) + # Wait for the coroutine to complete + future.result() + + async def _autotracker_setup(self, camera_config: CameraConfig, camera: str): + logger.debug(f"{camera}: Autotracker init") + + self.object_types[camera] = camera_config.onvif.autotracking.track + self.required_zones[camera] = camera_config.onvif.autotracking.required_zones + self.zoom_factor[camera] = camera_config.onvif.autotracking.zoom_factor + + self.tracked_object[camera] = None + self.tracked_object_history[camera] = deque( + maxlen=round(camera_config.detect.fps * 1.5) + ) + self.tracked_object_metrics[camera] = { + "max_target_box": AUTOTRACKING_MAX_AREA_RATIO + ** (1 / self.zoom_factor[camera]) + } + + self.calibrating[camera] = False + self.move_metrics[camera] = [] + self.intercept[camera] = None + self.move_coefficients[camera] = [] + + self.move_queues[camera] = asyncio.Queue() + self.move_queue_locks[camera] = asyncio.Lock() + + # handle onvif constructor failing due to no connection + if camera not in self.onvif.cams: + logger.warning( + f"Disabling autotracking for {camera}: onvif connection failed" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if not self.onvif.cams[camera]["init"]: + if not await self.onvif._init_onvif(camera): + logger.warning( + f"Disabling autotracking for {camera}: Unable to initialize onvif" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if "pt-r-fov" not in self.onvif.cams[camera]["features"]: + logger.warning( + f"Disabling autotracking for {camera}: FOV relative movement not supported" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + move_status_supported = await self.onvif.get_service_capabilities(camera) + + if not ( + isinstance(move_status_supported, bool) and move_status_supported + ) and not ( + isinstance(move_status_supported, str) + and move_status_supported.lower() == "true" + ): + logger.warning( + f"Disabling autotracking for {camera}: ONVIF MoveStatus not supported" + ) + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + return + + if self.onvif.cams[camera]["init"]: + await self.onvif.get_camera_status(camera) + + # movement queue with asyncio on OnvifController loop + asyncio.run_coroutine_threadsafe( + self._process_move_queue(camera), self.onvif.loop + ) + + if camera_config.onvif.autotracking.movement_weights: + if len(camera_config.onvif.autotracking.movement_weights) == 6: + camera_config.onvif.autotracking.movement_weights = [ + float(val) + for val in camera_config.onvif.autotracking.movement_weights + ] + self.ptz_metrics[ + camera + ].min_zoom.value = ( + camera_config.onvif.autotracking.movement_weights[0] + ) + self.ptz_metrics[ + camera + ].max_zoom.value = ( + camera_config.onvif.autotracking.movement_weights[1] + ) + self.intercept[camera] = ( + camera_config.onvif.autotracking.movement_weights[2] + ) + self.move_coefficients[camera] = ( + camera_config.onvif.autotracking.movement_weights[3:5] + ) + self.zoom_time[camera] = ( + camera_config.onvif.autotracking.movement_weights[5] + ) + else: + camera_config.onvif.autotracking.enabled = False + self.ptz_metrics[camera].autotracker_enabled.value = False + logger.warning( + f"Autotracker recalibration is required for {camera}. Disabling autotracking." + ) + + if camera_config.onvif.autotracking.calibrate_on_startup: + await self._calibrate_camera(camera) + + self.ptz_metrics[camera].tracking_active.clear() + self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) + self.autotracker_init[camera] = True + + def _write_config(self, camera): + config_file = find_config_file() + + logger.debug( + f"{camera}: Writing new config with autotracker motion coefficients: {self.config.cameras[camera].onvif.autotracking.movement_weights}" + ) + + update_yaml_file_bulk( + config_file, + { + f"cameras.{camera}.onvif.autotracking.movement_weights": self.config.cameras[ + camera + ].onvif.autotracking.movement_weights + }, + ) + + async def _calibrate_camera(self, camera): + # move the camera from the preset in steps and measure the time it takes to move that amount + # this will allow us to predict movement times with a simple linear regression + # start with 0 so we can determine a baseline (to be used as the intercept in the regression calc) + # TODO: take zooming into account too + num_steps = 30 + step_sizes = np.linspace(0, 1, num_steps) + zoom_in_values = [] + zoom_out_values = [] + + self.calibrating[camera] = True + + logger.info(f"Camera calibration for {camera} in progress") + + # zoom levels test + self.zoom_time[camera] = 0 + + if ( + self.config.cameras[camera].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + logger.info(f"Calibration for {camera} in progress: 0% complete") + + for i in range(2): + # absolute move to 0 - fully zoomed out + await self.onvif._zoom_absolute( + camera, + self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Min"], + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value) + + await self.onvif._zoom_absolute( + camera, + self.onvif.cams[camera]["absolute_zoom_range"]["XRange"]["Max"], + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value) + + if ( + self.config.cameras[camera].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + # relative move to -0.01 + await self.onvif._move_relative( + camera, + 0, + 0, + -1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_out_values.append(self.ptz_metrics[camera].zoom_level.value) + + zoom_start_time = time.time() + # relative move to 0.01 + await self.onvif._move_relative( + camera, + 0, + 0, + 1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + zoom_stop_time = time.time() + + full_relative_start_time = time.time() + + await self.onvif._move_relative( + camera, + -1, + -1, + -1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + full_relative_stop_time = time.time() + + await self.onvif._move_relative( + camera, + 1, + 1, + 1e-2, + 1, + ) + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + self.zoom_time[camera] = ( + full_relative_stop_time - full_relative_start_time + ) - (zoom_stop_time - zoom_start_time) + + zoom_in_values.append(self.ptz_metrics[camera].zoom_level.value) + + self.ptz_metrics[camera].max_zoom.value = max(zoom_in_values) + self.ptz_metrics[camera].min_zoom.value = min(zoom_out_values) + + logger.debug( + f"{camera}: Calibration values: max zoom: {self.ptz_metrics[camera].max_zoom.value}, min zoom: {self.ptz_metrics[camera].min_zoom.value}, zoom time: {self.zoom_time[camera]}" + ) + + else: + self.ptz_metrics[camera].max_zoom.value = 1 + self.ptz_metrics[camera].min_zoom.value = 0 + + await self.onvif._move_to_preset( + camera, + self.config.cameras[camera].onvif.autotracking.return_preset.lower(), + ) + self.ptz_metrics[camera].reset.set() + self.ptz_metrics[camera].motor_stopped.clear() + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + for step in range(num_steps): + pan = step_sizes[step] + tilt = step_sizes[step] + + start_time = time.time() + await self.onvif._move_relative(camera, pan, tilt, 0, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + stop_time = time.time() + + self.move_metrics[camera].append( + { + "pan": pan, + "tilt": tilt, + "start_timestamp": start_time, + "end_timestamp": stop_time, + } + ) + + await self.onvif._move_to_preset( + camera, + self.config.cameras[camera].onvif.autotracking.return_preset.lower(), + ) + self.ptz_metrics[camera].reset.set() + self.ptz_metrics[camera].motor_stopped.clear() + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + logger.info( + f"Calibration for {camera} in progress: {round((step / num_steps) * 100)}% complete" + ) + + self.calibrating[camera] = False + + logger.info(f"Calibration for {camera} complete") + + # calculate and save new intercept and coefficients + self._calculate_move_coefficients(camera, True) + + def _calculate_move_coefficients(self, camera, calibration=False): + # calculate new coefficients when we have 50 more new values. Save up to 500 + if calibration or ( + len(self.move_metrics[camera]) % 50 == 0 + and len(self.move_metrics[camera]) != 0 + and len(self.move_metrics[camera]) <= AUTOTRACKING_MAX_MOVE_METRICS + ): + X = np.array( + [abs(d["pan"]) + abs(d["tilt"]) for d in self.move_metrics[camera]] + ) + y = np.array( + [ + d["end_timestamp"] - d["start_timestamp"] + for d in self.move_metrics[camera] + ] + ) + + # simple linear regression with intercept + X_with_intercept = np.column_stack((np.ones(X.shape[0]), X)) + coefficients = np.linalg.lstsq(X_with_intercept, y, rcond=None)[0] + + intercept, slope = coefficients + + # Define reasonable bounds for PTZ movement times + MIN_MOVEMENT_TIME = 0.1 # Minimum time for any movement (100ms) + MAX_MOVEMENT_TIME = 10.0 # Maximum time for any movement + MAX_SLOPE = 2.0 # Maximum seconds per unit of movement + + coefficients_valid = ( + MIN_MOVEMENT_TIME <= intercept <= MAX_MOVEMENT_TIME + and 0 < slope <= MAX_SLOPE + ) + + if not coefficients_valid: + logger.warning( + f"{camera}: Autotracking calibration failed. See the Frigate documentation." + ) + return False + + # If coefficients are valid, proceed with updates + self.move_coefficients[camera] = coefficients + + # only assign a new intercept if we're calibrating + if calibration: + self.intercept[camera] = y[0] + + # write the min zoom, max zoom, intercept, and coefficients + # back to the config file as a comma separated string + self.config.cameras[camera].onvif.autotracking.movement_weights = ", ".join( + str(v) + for v in [ + self.ptz_metrics[camera].min_zoom.value, + self.ptz_metrics[camera].max_zoom.value, + self.intercept[camera], + *self.move_coefficients[camera], + self.zoom_time[camera], + ] + ) + + logger.debug( + f"{camera}: New regression parameters - intercept: {self.intercept[camera]}, coefficients: {self.move_coefficients[camera]}" + ) + + self._write_config(camera) + + def _predict_movement_time(self, camera, pan, tilt): + combined_movement = abs(pan) + abs(tilt) + input_data = np.array([self.intercept[camera], combined_movement]) + + return np.dot(self.move_coefficients[camera], input_data) + + def _predict_area_after_time(self, camera, time): + return np.dot( + self.tracked_object_metrics[camera]["area_coefficients"], + [self.tracked_object_history[camera][-1]["frame_time"] + time], + ) + + def _calculate_tracked_object_metrics(self, camera, obj): + def remove_outliers(data): + areas = [item["area"] for item in data] + + Q1 = np.percentile(areas, 25) + Q3 = np.percentile(areas, 75) + IQR = Q3 - Q1 + lower_bound = Q1 - 1.5 * IQR + upper_bound = Q3 + 1.5 * IQR + + filtered_data = [ + item for item in data if lower_bound <= item["area"] <= upper_bound + ] + + # Find and log the removed values + removed_values = [item for item in data if item not in filtered_data] + logger.debug(f"{camera}: Removed area outliers: {removed_values}") + + return filtered_data + + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + + # Extract areas and calculate weighted average + # grab the largest dimension of the bounding box and create a square from that + # Filter out the initial frame and use a recent time window + current_time = obj.obj_data["frame_time"] + time_window = 1.5 # seconds + history = [ + entry + for entry in self.tracked_object_history[camera] + if not entry.get("is_initial_frame", False) + and current_time - entry["frame_time"] <= time_window + ] + if not history: # Fallback to latest if no recent entries + history = [self.tracked_object_history[camera][-1]] + + areas = [ + { + "frame_time": entry["frame_time"], + "box": entry["box"], + "area": max( + entry["box"][2] - entry["box"][0], entry["box"][3] - entry["box"][1] + ) + ** 2, + } + for entry in history + ] + + filtered_areas = remove_outliers(areas) if len(areas) > 3 else areas + + # Filter entries that are not touching the frame edge + filtered_areas_not_touching_edge = [ + entry + for entry in filtered_areas + if self._touching_frame_edges(camera, entry["box"]) == 0 + ] + + # Calculate regression for area change predictions + if len(filtered_areas_not_touching_edge): + X = np.array( + [item["frame_time"] for item in filtered_areas_not_touching_edge] + ) + y = np.array([item["area"] for item in filtered_areas_not_touching_edge]) + + self.tracked_object_metrics[camera]["area_coefficients"] = np.linalg.lstsq( + X.reshape(-1, 1), y, rcond=None + )[0] + else: + self.tracked_object_metrics[camera]["area_coefficients"] = np.array([0]) + + weights = np.arange(1, len(filtered_areas) + 1) + weighted_area = np.average( + [item["area"] for item in filtered_areas], weights=weights + ) + + self.tracked_object_metrics[camera]["target_box"] = ( + weighted_area / (camera_width * camera_height) + ) ** self.zoom_factor[camera] + + if "original_target_box" not in self.tracked_object_metrics[camera]: + self.tracked_object_metrics[camera]["original_target_box"] = ( + self.tracked_object_metrics[camera]["target_box"] + ) + + ( + self.tracked_object_metrics[camera]["valid_velocity"], + self.tracked_object_metrics[camera]["velocity"], + ) = self._get_valid_velocity(camera, obj) + self.tracked_object_metrics[camera]["distance"] = self._get_distance_threshold( + camera, obj + ) + + centroid_distance = np.linalg.norm( + [ + obj.obj_data["centroid"][0] - camera_config.detect.width / 2, + obj.obj_data["centroid"][1] - camera_config.detect.height / 2, + ] + ) + + logger.debug(f"{camera}: Centroid distance: {centroid_distance}") + + self.tracked_object_metrics[camera]["below_distance_threshold"] = ( + centroid_distance < self.tracked_object_metrics[camera]["distance"] + ) + + async def _process_move_queue(self, camera): + move_queue = self.move_queues[camera] + + while not self.stop_event.is_set(): + try: + # Asynchronously wait for move data with a timeout + move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1) + except asyncio.TimeoutError: + continue + + async with self.move_queue_locks[camera]: + frame_time, pan, tilt, zoom = move_data + + # if we're receiving move requests during a PTZ move, ignore them + if ptz_moving_at_frame_time( + frame_time, + self.ptz_metrics[camera].start_time.value, + self.ptz_metrics[camera].stop_time.value, + ): + logger.debug( + f"{camera}: Move queue: PTZ moving, dequeueing move request - frame time: {frame_time}, final pan: {pan}, final tilt: {tilt}, final zoom: {zoom}" + ) + continue + + else: + if ( + self.config.cameras[camera].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + await self.onvif._move_relative(camera, pan, tilt, zoom, 1) + else: + if pan != 0 or tilt != 0: + await self.onvif._move_relative(camera, pan, tilt, 0, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + if ( + zoom > 0 + and self.ptz_metrics[camera].zoom_level.value != zoom + ): + await self.onvif._zoom_absolute(camera, zoom, 1) + + # Wait until the camera finishes moving + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + if self.config.cameras[camera].onvif.autotracking.movement_weights: + logger.debug( + f"{camera}: Predicted movement time: {self._predict_movement_time(camera, pan, tilt)}" + ) + logger.debug( + f"{camera}: Actual movement time: {self.ptz_metrics[camera].stop_time.value - self.ptz_metrics[camera].start_time.value}" + ) + + # save metrics for better estimate calculations + if ( + self.intercept[camera] is not None + and len(self.move_metrics[camera]) + < AUTOTRACKING_MAX_MOVE_METRICS + and (pan != 0 or tilt != 0) + and self.config.cameras[ + camera + ].onvif.autotracking.calibrate_on_startup + ): + logger.debug(f"{camera}: Adding new values to move metrics") + self.move_metrics[camera].append( + { + "pan": pan, + "tilt": tilt, + "start_timestamp": self.ptz_metrics[ + camera + ].start_time.value, + "end_timestamp": self.ptz_metrics[ + camera + ].stop_time.value, + } + ) + + # calculate new coefficients if we have enough data + self._calculate_move_coefficients(camera) + + # Clean up the queue on exit + while not move_queue.empty(): + await move_queue.get() + + def _enqueue_move(self, camera, frame_time, pan, tilt, zoom): + def split_value(value, suppress_diff=True): + clipped = np.clip(value, -1, 1) + + # don't make small movements + if -0.05 < clipped < 0.05 and suppress_diff: + diff = 0.0 + else: + diff = value - clipped + + return clipped, diff + + if ( + frame_time > self.ptz_metrics[camera].start_time.value + and frame_time > self.ptz_metrics[camera].stop_time.value + and not self.move_queue_locks[camera].locked() + ): + # we can split up any large moves caused by velocity estimated movements if necessary + # get an excess amount and assign it instead of 0 below + while pan != 0 or tilt != 0 or zoom != 0: + pan, _ = split_value(pan) + tilt, _ = split_value(tilt) + zoom, _ = split_value(zoom, False) + + logger.debug( + f"{camera}: Enqueue movement for frame time: {frame_time} pan: {pan}, tilt: {tilt}, zoom: {zoom}" + ) + move_data = (frame_time, pan, tilt, zoom) + self.onvif.loop.call_soon_threadsafe( + self.move_queues[camera].put_nowait, move_data + ) + + # reset values to not split up large movements + pan = 0 + tilt = 0 + zoom = 0 + + def _touching_frame_edges(self, camera, box): + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + bb_left, bb_top, bb_right, bb_bottom = box + + edge_threshold = AUTOTRACKING_ZOOM_EDGE_THRESHOLD + + return int( + (bb_left < edge_threshold * camera_width) + + (bb_right > (1 - edge_threshold) * camera_width) + + (bb_top < edge_threshold * camera_height) + + (bb_bottom > (1 - edge_threshold) * camera_height) + ) + + def _get_valid_velocity(self, camera, obj): + # returns a tuple and euclidean distance if the estimated velocity is valid + # if invalid, returns [0, 0] and -1 + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + + # estimate_velocity is a numpy array of bbox top,left and bottom,right velocities + velocities = obj.obj_data["estimate_velocity"] + logger.debug( + f"{camera}: Velocity (Norfair): {tuple(np.round(velocities).flatten().astype(int))}" + ) + + # if we are close enough to zero, return right away + if np.all(np.round(velocities) == 0): + return True, np.zeros((4,)) + + # Thresholds + x_mags_thresh = camera_width / camera_fps / 2 + y_mags_thresh = camera_height / camera_fps / 2 + dir_thresh = 0.93 + delta_thresh = 20 + var_thresh = 10 + + # Check magnitude + x_mags = np.abs(velocities[:, 0]) + y_mags = np.abs(velocities[:, 1]) + invalid_x_mags = np.any(x_mags > x_mags_thresh) + invalid_y_mags = np.any(y_mags > y_mags_thresh) + + # Check delta + delta = np.abs(velocities[0] - velocities[1]) + invalid_delta = np.any(delta > delta_thresh) + + # Check variance + stdev_list = np.std(velocities, axis=0) + high_variances = np.any(stdev_list > var_thresh) + + # Check direction difference + velocities = np.round(velocities) + invalid_dirs = False + if not np.any(np.linalg.norm(velocities, axis=1)): + cosine_sim = np.dot(velocities[0], velocities[1]) / ( + np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1]) + ) + dir_thresh = 0.6 if np.all(delta < delta_thresh / 2) else dir_thresh + invalid_dirs = cosine_sim < dir_thresh + + # Combine + invalid = ( + invalid_x_mags + or invalid_y_mags + or invalid_dirs + or invalid_delta + or high_variances + ) + + if invalid: + logger.debug( + f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: " + + ", ".join( + [ + var_name + for var_name, is_invalid in [ + ("invalid_x_mags", invalid_x_mags), + ("invalid_y_mags", invalid_y_mags), + ("invalid_dirs", invalid_dirs), + ("invalid_delta", invalid_delta), + ("high_variances", high_variances), + ] + if is_invalid + ] + ) + ) + # invalid velocity + return False, np.zeros((4,)) + else: + logger.debug(f"{camera}: Valid velocity ") + return True, velocities.flatten() + + def _get_distance_threshold(self, camera: str, obj: TrackedObject): + # Returns true if Euclidean distance from object to center of frame is + # less than 10% of the of the larger dimension (width or height) of the frame, + # multiplied by a scaling factor for object size. + # Distance is increased if object is not moving to prevent small ptz moves + # Adjusting this percentage slightly lower will effectively cause the camera to move + # more often to keep the object in the center. Raising the percentage will cause less + # movement and will be more flexible with objects not quite being centered. + # TODO: there's probably a better way to approach this + camera_config = self.config.cameras[camera] + + obj_width = obj.obj_data["box"][2] - obj.obj_data["box"][0] + obj_height = obj.obj_data["box"][3] - obj.obj_data["box"][1] + + max_obj = max(obj_width, obj_height) + max_frame = ( + camera_config.detect.width + if max_obj == obj_width + else camera_config.detect.height + ) + + # larger objects should lower the threshold, smaller objects should raise it + scaling_factor = 1 - np.log(max_obj / max_frame) + + percentage = ( + 0.08 + if camera_config.onvif.autotracking.movement_weights + and self.tracked_object_metrics[camera]["valid_velocity"] + else 0.03 + ) + distance_threshold = percentage * max_frame * scaling_factor + + logger.debug(f"{camera}: Distance threshold: {distance_threshold}") + + return distance_threshold + + def _should_zoom_in( + self, camera: str, obj: TrackedObject, box, predicted_time, debug_zooming=False + ): + # returns True if we should zoom in, False if we should zoom out, None to do nothing + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + + average_velocity = self.tracked_object_metrics[camera]["velocity"] + + bb_left, bb_top, bb_right, bb_bottom = box + + # calculate a velocity threshold based on movement coefficients if available + if camera_config.onvif.autotracking.movement_weights: + predicted_movement_time = self._predict_movement_time(camera, 1, 1) + velocity_threshold_x = camera_width / predicted_movement_time / camera_fps + velocity_threshold_y = camera_height / predicted_movement_time / camera_fps + else: + # use a generic velocity threshold + velocity_threshold_x = camera_width * 0.02 + velocity_threshold_y = camera_height * 0.02 + + # return a count of the number of frame edges the bounding box is touching + touching_frame_edges = self._touching_frame_edges(camera, box) + + # make sure object is centered in the frame + below_distance_threshold = self.tracked_object_metrics[camera][ + "below_distance_threshold" + ] + + below_dimension_threshold = (bb_right - bb_left) <= camera_width * ( + self.zoom_factor[camera] + 0.1 + ) and (bb_bottom - bb_top) <= camera_height * (self.zoom_factor[camera] + 0.1) + + # ensure object is not moving quickly + below_velocity_threshold = np.all( + np.abs(average_velocity) + < np.tile([velocity_threshold_x, velocity_threshold_y], 2) + ) or np.all(average_velocity == 0) + + if not predicted_time: + calculated_target_box = self.tracked_object_metrics[camera]["target_box"] + else: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + self._predict_area_after_time(camera, predicted_time) / ( + camera_width * camera_height + ) + + below_area_threshold = ( + calculated_target_box + < self.tracked_object_metrics[camera]["max_target_box"] + ) + + # introduce some hysteresis to prevent a yo-yo zooming effect + zoom_out_hysteresis = ( + calculated_target_box + > self.tracked_object_metrics[camera]["max_target_box"] + * AUTOTRACKING_ZOOM_OUT_HYSTERESIS + ) + zoom_in_hysteresis = ( + calculated_target_box + < self.tracked_object_metrics[camera]["max_target_box"] + * AUTOTRACKING_ZOOM_IN_HYSTERESIS + ) + + at_max_zoom = ( + self.ptz_metrics[camera].zoom_level.value + == self.ptz_metrics[camera].max_zoom.value + ) + at_min_zoom = ( + self.ptz_metrics[camera].zoom_level.value + == self.ptz_metrics[camera].min_zoom.value + ) + + # debug zooming + if debug_zooming: + logger.debug( + f"{camera}: Zoom test: touching edges: count: {touching_frame_edges} left: {bb_left < AUTOTRACKING_ZOOM_EDGE_THRESHOLD * camera_width}, right: {bb_right > (1 - AUTOTRACKING_ZOOM_EDGE_THRESHOLD) * camera_width}, top: {bb_top < AUTOTRACKING_ZOOM_EDGE_THRESHOLD * camera_height}, bottom: {bb_bottom > (1 - AUTOTRACKING_ZOOM_EDGE_THRESHOLD) * camera_height}" + ) + logger.debug( + f"{camera}: Zoom test: below distance threshold: {(below_distance_threshold)}" + ) + logger.debug( + f"{camera}: Zoom test: below area threshold: {(below_area_threshold)} target: {self.tracked_object_metrics[camera]['target_box']}, calculated: {calculated_target_box}, max: {self.tracked_object_metrics[camera]['max_target_box']}" + ) + logger.debug( + f"{camera}: Zoom test: below dimension threshold: {below_dimension_threshold} width: {bb_right - bb_left}, max width: {camera_width * (self.zoom_factor[camera] + 0.1)}, height: {bb_bottom - bb_top}, max height: {camera_height * (self.zoom_factor[camera] + 0.1)}" + ) + logger.debug( + f"{camera}: Zoom test: below velocity threshold: {below_velocity_threshold} velocity x: {abs(average_velocity[0])}, x threshold: {velocity_threshold_x}, velocity y: {abs(average_velocity[0])}, y threshold: {velocity_threshold_y}" + ) + logger.debug(f"{camera}: Zoom test: at max zoom: {at_max_zoom}") + logger.debug(f"{camera}: Zoom test: at min zoom: {at_min_zoom}") + logger.debug( + f"{camera}: Zoom test: zoom in hysteresis limit: {zoom_in_hysteresis} value: {AUTOTRACKING_ZOOM_IN_HYSTERESIS} original: {self.tracked_object_metrics[camera]['original_target_box']} max: {self.tracked_object_metrics[camera]['max_target_box']} target: {calculated_target_box if calculated_target_box else self.tracked_object_metrics[camera]['target_box']}" + ) + logger.debug( + f"{camera}: Zoom test: zoom out hysteresis limit: {zoom_out_hysteresis} value: {AUTOTRACKING_ZOOM_OUT_HYSTERESIS} original: {self.tracked_object_metrics[camera]['original_target_box']} max: {self.tracked_object_metrics[camera]['max_target_box']} target: {calculated_target_box if calculated_target_box else self.tracked_object_metrics[camera]['target_box']}" + ) + + # Zoom in conditions (and) + if ( + zoom_in_hysteresis + and touching_frame_edges == 0 + and below_velocity_threshold + and below_dimension_threshold + and below_area_threshold + and not at_max_zoom + ): + return True + + # Zoom out conditions (or) + if ( + ( + zoom_out_hysteresis + and not at_max_zoom + and (not below_area_threshold or not below_dimension_threshold) + ) + or (zoom_out_hysteresis and not below_area_threshold and at_max_zoom) + or ( + touching_frame_edges == 1 + and (below_distance_threshold or not below_dimension_threshold) + ) + or touching_frame_edges > 1 + or not below_velocity_threshold + ) and not at_min_zoom: + return False + + # Don't zoom at all + return None + + def _autotrack_move_ptz(self, camera: str, obj: TrackedObject): + camera_config = self.config.cameras[camera] + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + camera_fps = camera_config.detect.fps + predicted_movement_time = 0 + zoom_distance = 0 + + average_velocity = np.zeros((4,)) + predicted_box = obj.obj_data["box"] + zoom_predicted_box = obj.obj_data["box"] + + centroid_x = obj.obj_data["centroid"][0] + centroid_y = obj.obj_data["centroid"][1] + + # Normalize coordinates. top right of the fov is (1,1), center is (0,0), bottom left is (-1, -1). + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + _, average_velocity = ( + self._get_valid_velocity(camera, obj) + if "velocity" not in self.tracked_object_metrics[camera] + else ( + self.tracked_object_metrics[camera]["valid_velocity"], + self.tracked_object_metrics[camera]["velocity"], + ) + ) + + if ( + camera_config.onvif.autotracking.movement_weights + ): # use estimates if we have available coefficients + predicted_movement_time = self._predict_movement_time(camera, pan, tilt) + + if np.any(average_velocity): + # this box could exceed the frame boundaries if velocity is high + # but we'll handle that in _enqueue_move() as two separate moves + current_box = np.array(obj.obj_data["box"]) + predicted_box = ( + current_box + + camera_fps * predicted_movement_time * average_velocity + ) + + predicted_box = np.round(predicted_box).astype(int) + + centroid_x = round((predicted_box[0] + predicted_box[2]) / 2) + centroid_y = round((predicted_box[1] + predicted_box[3]) / 2) + + # recalculate pan and tilt with new centroid + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + logger.debug(f"{camera}: Original box: {obj.obj_data['box']}") + logger.debug(f"{camera}: Predicted box: {tuple(predicted_box)}") + logger.debug( + f"{camera}: Velocity: {tuple(np.round(average_velocity).flatten().astype(int))}" + ) + + zoom = self._get_zoom_amount( + camera, obj, predicted_box, predicted_movement_time, debug_zoom=True + ) + + if ( + camera_config.onvif.autotracking.movement_weights + and camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative + and zoom != 0 + ): + zoom_predicted_movement_time = 0 + + if np.any(average_velocity): + # Calculate the intended change in zoom level + zoom_change = (1 - abs(zoom)) * (1 if zoom >= 0 else -1) + + # Calculate new zoom level and clamp to [0, 1] + new_zoom = max( + 0, min(1, self.ptz_metrics[camera].zoom_level.value + zoom_change) + ) + + # Calculate the actual zoom distance + zoom_distance = abs( + new_zoom - self.ptz_metrics[camera].zoom_level.value + ) + + zoom_predicted_movement_time = zoom_distance * self.zoom_time[camera] + + zoom_predicted_box = ( + predicted_box + + camera_fps * zoom_predicted_movement_time * average_velocity + ) + + zoom_predicted_box = np.round(zoom_predicted_box).astype(int) + + centroid_x = round((zoom_predicted_box[0] + zoom_predicted_box[2]) / 2) + centroid_y = round((zoom_predicted_box[1] + zoom_predicted_box[3]) / 2) + + # recalculate pan and tilt with new centroid + pan = ((centroid_x / camera_width) - 0.5) * 2 + tilt = (0.5 - (centroid_y / camera_height)) * 2 + + logger.debug( + f"{camera}: Zoom amount: {zoom}, zoom distance: {zoom_distance}, zoom predicted time: {zoom_predicted_movement_time}, zoom predicted box: {tuple(zoom_predicted_box)}" + ) + + self._enqueue_move(camera, obj.obj_data["frame_time"], pan, tilt, zoom) + + def _autotrack_move_zoom_only(self, camera, obj): + camera_config = self.config.cameras[camera] + + if camera_config.onvif.autotracking.zooming != ZoomingModeEnum.disabled: + zoom = self._get_zoom_amount(camera, obj, obj.obj_data["box"], 0) + + if zoom != 0: + self._enqueue_move(camera, obj.obj_data["frame_time"], 0, 0, zoom) + + def _get_zoom_amount( + self, + camera: str, + obj: TrackedObject, + predicted_box, + predicted_movement_time, + debug_zoom=True, + ): + camera_config = self.config.cameras[camera] + + # frame width and height + camera_width = camera_config.frame_shape[1] + camera_height = camera_config.frame_shape[0] + + zoom = 0 + result = None + current_zoom_level = self.ptz_metrics[camera].zoom_level.value + target_box = max( + obj.obj_data["box"][2] - obj.obj_data["box"][0], + obj.obj_data["box"][3] - obj.obj_data["box"][1], + ) ** 2 / (camera_width * camera_height) + + # absolute zooming separately from pan/tilt + if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.absolute: + # don't zoom on initial move + if "target_box" not in self.tracked_object_metrics[camera]: + zoom = current_zoom_level + else: + if ( + result := self._should_zoom_in( + camera, + obj, + obj.obj_data["box"], + predicted_movement_time, + debug_zoom, + ) + ) is not None: + # divide zoom in 10 increments and always zoom out more than in + level = ( + self.ptz_metrics[camera].max_zoom.value + - self.ptz_metrics[camera].min_zoom.value + ) / 20 + if result: + zoom = min(1.0, current_zoom_level + level) + else: + zoom = max(0.0, current_zoom_level - 2 * level) + + # relative zooming concurrently with pan/tilt + if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.relative: + # this is our initial zoom in on a new object + if "target_box" not in self.tracked_object_metrics[camera]: + zoom = target_box ** self.zoom_factor[camera] + if zoom > self.tracked_object_metrics[camera]["max_target_box"]: + zoom = -(1 - zoom) + logger.debug( + f"{camera}: target box: {target_box}, max: {self.tracked_object_metrics[camera]['max_target_box']}, calc zoom: {zoom}" + ) + else: + if ( + result := self._should_zoom_in( + camera, + obj, + predicted_box + if camera_config.onvif.autotracking.movement_weights + else obj.obj_data["box"], + predicted_movement_time, + debug_zoom, + ) + ) is not None: + if predicted_movement_time: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + self._predict_area_after_time( + camera, predicted_movement_time + ) / (camera_width * camera_height) + logger.debug( + f"{camera}: Zooming prediction: predicted movement time: {predicted_movement_time}, original box: {self.tracked_object_metrics[camera]['target_box']}, calculated box: {calculated_target_box}" + ) + else: + calculated_target_box = self.tracked_object_metrics[camera][ + "target_box" + ] + # zoom value + ratio = ( + self.tracked_object_metrics[camera]["max_target_box"] + / calculated_target_box + ) + zoom = (ratio - 1) / (ratio + 1) + logger.debug( + f"{camera}: limit: {self.tracked_object_metrics[camera]['max_target_box']}, ratio: {ratio} zoom calculation: {zoom}" + ) + if not result: + # zoom out with special condition if zooming out because of velocity, edges, etc. + zoom = -(1 - zoom) if zoom > 0 else -(zoom * 2 + 1) + if result: + # zoom in + zoom = 1 - zoom if zoom > 0 else (zoom * 2 + 1) + + logger.debug(f"{camera}: Zooming: {result} Zoom amount: {zoom}") + + return zoom + + def is_autotracking(self, camera: str): + return self.tracked_object[camera] is not None + + def autotracked_object_region(self, camera: str): + return self.tracked_object[camera]["region"] + + def autotrack_object(self, camera: str, obj: TrackedObject): + camera_config = self.config.cameras[camera] + + if camera_config.onvif.autotracking.enabled: + if not self.autotracker_init[camera]: + future = asyncio.run_coroutine_threadsafe( + self._autotracker_setup(camera_config, camera), self.onvif.loop + ) + # Wait for the coroutine to complete + future.result() + + if self.calibrating[camera]: + logger.debug(f"{camera}: Calibrating camera") + return + + # this is a brand new object that's on our camera, has our label, entered the zone, + # is not a false positive, and is active + if ( + # new object + self.tracked_object[camera] is None + and obj.camera_config.name == camera + and obj.obj_data["label"] in self.object_types[camera] + and set(obj.entered_zones) & set(self.required_zones[camera]) + and not obj.previous["false_positive"] + and not obj.false_positive + and not self.tracked_object_history[camera] + and obj.active + ): + logger.debug( + f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + self.ptz_metrics[camera].tracking_active.set() + self.dispatcher.publish( + f"{camera}/ptz_autotracker/active", "ON", retain=False + ) + self.tracked_object[camera] = obj + + self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data)) + self._autotrack_move_ptz(camera, obj) + + return + + if ( + # already tracking an object + self.tracked_object[camera] is not None + and self.tracked_object_history[camera] + and obj.obj_data["id"] == self.tracked_object[camera].obj_data["id"] + and obj.obj_data["frame_time"] + != self.tracked_object_history[camera][-1]["frame_time"] + ): + self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data)) + self._calculate_tracked_object_metrics(camera, obj) + + if not ptz_moving_at_frame_time( + obj.obj_data["frame_time"], + self.ptz_metrics[camera].start_time.value, + self.ptz_metrics[camera].stop_time.value, + ): + if self.tracked_object_metrics[camera]["below_distance_threshold"]: + logger.debug( + f"{camera}: Existing object (do NOT move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + + # no need to move, but try zooming + self._autotrack_move_zoom_only(camera, obj) + else: + logger.debug( + f"{camera}: Existing object (need to move ptz): {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + + self._autotrack_move_ptz(camera, obj) + + return + + if ( + # The tracker lost an object, so let's check the previous object's region and compare it with the incoming object + # If it's within bounds, start tracking that object. + # Should we check region (maybe too broad) or expand the previous object's box a bit and check that? + self.tracked_object[camera] is None + and obj.camera_config.name == camera + and obj.obj_data["label"] in self.object_types[camera] + and not obj.previous["false_positive"] + and not obj.false_positive + and self.tracked_object_history[camera] + ): + if ( + intersection_over_union( + self.tracked_object_history[camera][-1]["region"], + obj.obj_data["box"], + ) + < 0.2 + ): + logger.debug( + f"{camera}: Reacquired object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" + ) + self.tracked_object[camera] = obj + + self.tracked_object_history[camera].clear() + self.tracked_object_history[camera].append( + copy.deepcopy(obj.obj_data) + ) + self._calculate_tracked_object_metrics(camera, obj) + self._autotrack_move_ptz(camera, obj) + + return + + def end_object(self, camera, obj): + if self.config.cameras[camera].onvif.autotracking.enabled: + if ( + self.tracked_object[camera] is not None + and obj.obj_data["id"] == self.tracked_object[camera].obj_data["id"] + ): + logger.debug( + f"{camera}: End object: {obj.obj_data['id']} {obj.obj_data['box']}" + ) + self.tracked_object[camera] = None + self.tracked_object_metrics[camera] = { + "max_target_box": AUTOTRACKING_MAX_AREA_RATIO + ** (1 / self.zoom_factor[camera]) + } + + async def camera_maintenance(self, camera): + # bail and don't check anything if we're calibrating or tracking an object + if ( + not self.autotracker_init[camera] + or self.calibrating[camera] + or self.tracked_object[camera] is not None + ): + return + + # calls get_camera_status to check/update ptz movement + # returns camera to preset after timeout when tracking is over + autotracker_config = self.config.cameras[camera].onvif.autotracking + + if not self.autotracker_init[camera]: + self._autotracker_setup(self.config.cameras[camera], camera) + # regularly update camera status + if not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + # return to preset if tracking is over + if ( + self.tracked_object[camera] is None + and self.tracked_object_history[camera] + and ( + # might want to use a different timestamp here? + self.ptz_metrics[camera].frame_time.value + - self.tracked_object_history[camera][-1]["frame_time"] + >= autotracker_config.timeout + ) + and autotracker_config.return_preset + ): + # clear tracked object and reset zoom level + self.tracked_object[camera] = None + self.tracked_object_history[camera].clear() + + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + logger.debug( + f"{camera}: Time is {self.ptz_metrics[camera].frame_time.value}, returning to preset: {autotracker_config.return_preset}" + ) + await self.onvif._move_to_preset( + camera, + autotracker_config.return_preset.lower(), + ) + + # update stored zoom level from preset + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) + + self.ptz_metrics[camera].tracking_active.clear() + self.dispatcher.publish( + f"{camera}/ptz_autotracker/active", "OFF", retain=False + ) + self.ptz_metrics[camera].reset.set() diff --git a/sam2-cpu/frigate-dev/frigate/ptz/onvif.py b/sam2-cpu/frigate-dev/frigate/ptz/onvif.py new file mode 100644 index 0000000..488dbd2 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/ptz/onvif.py @@ -0,0 +1,983 @@ +"""Configure and control camera via onvif.""" + +import asyncio +import logging +import threading +import time +from enum import Enum +from importlib.util import find_spec +from pathlib import Path +from typing import Any + +import numpy +from onvif import ONVIFCamera, ONVIFError, ONVIFService +from zeep.exceptions import Fault, TransportError + +from frigate.camera import PTZMetrics +from frigate.config import FrigateConfig, ZoomingModeEnum +from frigate.util.builtin import find_by_key + +logger = logging.getLogger(__name__) + + +class OnvifCommandEnum(str, Enum): + """Holds all possible move commands""" + + init = "init" + move_down = "move_down" + move_left = "move_left" + move_relative = "move_relative" + move_right = "move_right" + move_up = "move_up" + preset = "preset" + stop = "stop" + zoom_in = "zoom_in" + zoom_out = "zoom_out" + focus_in = "focus_in" + focus_out = "focus_out" + + +class OnvifController: + ptz_metrics: dict[str, PTZMetrics] + + def __init__( + self, config: FrigateConfig, ptz_metrics: dict[str, PTZMetrics] + ) -> None: + self.cams: dict[str, dict] = {} + self.failed_cams: dict[str, dict] = {} + self.max_retries = 5 + self.reset_timeout = 900 # 15 minutes + self.config = config + self.ptz_metrics = ptz_metrics + + self.status_locks: dict[str, asyncio.Lock] = {} + + # Create a dedicated event loop and run it in a separate thread + self.loop = asyncio.new_event_loop() + self.loop_thread = threading.Thread(target=self._run_event_loop, daemon=True) + self.loop_thread.start() + + self.camera_configs = {} + for cam_name, cam in config.cameras.items(): + if not cam.enabled: + continue + if cam.onvif.host: + self.camera_configs[cam_name] = cam + self.status_locks[cam_name] = asyncio.Lock() + + asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop) + + def _run_event_loop(self) -> None: + """Run the event loop in a separate thread.""" + asyncio.set_event_loop(self.loop) + try: + self.loop.run_forever() + except Exception as e: + logger.error(f"Onvif event loop terminated unexpectedly: {e}") + + async def _init_cameras(self) -> None: + """Initialize all configured cameras.""" + for cam_name in self.camera_configs: + await self._init_single_camera(cam_name) + + async def _init_single_camera(self, cam_name: str) -> bool: + """Initialize a single camera by name. + + Args: + cam_name: The name of the camera to initialize + + Returns: + bool: True if initialization succeeded, False otherwise + """ + if cam_name not in self.camera_configs: + logger.error(f"No configuration found for camera {cam_name}") + return False + + cam = self.camera_configs[cam_name] + try: + user = cam.onvif.user + password = cam.onvif.password + + if user is not None and isinstance(user, bytes): + user = user.decode("utf-8") + + if password is not None and isinstance(password, bytes): + password = password.decode("utf-8") + + self.cams[cam_name] = { + "onvif": ONVIFCamera( + cam.onvif.host, + cam.onvif.port, + user, + password, + wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"), + adjust_time=cam.onvif.ignore_time_mismatch, + encrypt=not cam.onvif.tls_insecure, + ), + "init": False, + "active": False, + "features": [], + "presets": {}, + } + return True + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error(f"Failed to create ONVIF camera instance for {cam_name}: {e}") + # track initial failures + self.failed_cams[cam_name] = { + "retry_attempts": 0, + "last_error": str(e), + "last_attempt": time.time(), + } + return False + + async def _init_onvif(self, camera_name: str) -> bool: + onvif: ONVIFCamera = self.cams[camera_name]["onvif"] + try: + await onvif.update_xaddrs() + except Exception as e: + logger.error(f"Onvif connection failed for {camera_name}: {e}") + return False + + # create init services + media: ONVIFService = await onvif.create_media_service() + logger.debug(f"Onvif media xaddr for {camera_name}: {media.xaddr}") + + try: + # this will fire an exception if camera is not a ptz + capabilities = onvif.get_definition("ptz") + logger.debug(f"Onvif capabilities for {camera_name}: {capabilities}") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error( + f"Unable to get Onvif capabilities for camera: {camera_name}: {e}" + ) + return False + + try: + profiles = await media.GetProfiles() + logger.debug(f"Onvif profiles for {camera_name}: {profiles}") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error( + f"Unable to get Onvif media profiles for camera: {camera_name}: {e}" + ) + return False + + profile = None + for _, onvif_profile in enumerate(profiles): + if ( + onvif_profile.VideoEncoderConfiguration + and onvif_profile.PTZConfiguration + and ( + onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace + is not None + or onvif_profile.PTZConfiguration.DefaultContinuousZoomVelocitySpace + is not None + ) + ): + # use the first profile that has a valid ptz configuration + profile = onvif_profile + logger.debug(f"Selected Onvif profile for {camera_name}: {profile}") + break + + if profile is None: + logger.error( + f"No appropriate Onvif profiles found for camera: {camera_name}." + ) + return False + + # get the PTZ config for the profile + try: + configs = profile.PTZConfiguration + logger.debug( + f"Onvif ptz config for media profile in {camera_name}: {configs}" + ) + except Exception as e: + logger.error( + f"Invalid Onvif PTZ configuration for camera: {camera_name}: {e}" + ) + return False + + ptz: ONVIFService = await onvif.create_ptz_service() + self.cams[camera_name]["ptz"] = ptz + + try: + imaging: ONVIFService = await onvif.create_imaging_service() + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Imaging service not supported for {camera_name}: {e}") + imaging = None + self.cams[camera_name]["imaging"] = imaging + try: + video_sources = await media.GetVideoSources() + if video_sources and len(video_sources) > 0: + self.cams[camera_name]["video_source_token"] = video_sources[0].token + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Unable to get video sources for {camera_name}: {e}") + self.cams[camera_name]["video_source_token"] = None + + # setup continuous moving request + move_request = ptz.create_type("ContinuousMove") + move_request.ProfileToken = profile.token + self.cams[camera_name]["move_request"] = move_request + + # extra setup for autotracking cameras + if ( + self.config.cameras[camera_name].onvif.autotracking.enabled_in_config + and self.config.cameras[camera_name].onvif.autotracking.enabled + ): + request = ptz.create_type("GetConfigurationOptions") + request.ConfigurationToken = profile.PTZConfiguration.token + ptz_config = await ptz.GetConfigurationOptions(request) + logger.debug(f"Onvif config for {camera_name}: {ptz_config}") + + service_capabilities_request = ptz.create_type("GetServiceCapabilities") + self.cams[camera_name]["service_capabilities_request"] = ( + service_capabilities_request + ) + + fov_space_id = next( + ( + i + for i, space in enumerate( + ptz_config.Spaces.RelativePanTiltTranslationSpace + ) + if "TranslationSpaceFov" in space["URI"] + ), + None, + ) + + # status request for autotracking and filling ptz-parameters + status_request = ptz.create_type("GetStatus") + status_request.ProfileToken = profile.token + self.cams[camera_name]["status_request"] = status_request + try: + status = await ptz.GetStatus(status_request) + logger.debug(f"Onvif status config for {camera_name}: {status}") + except Exception as e: + logger.warning(f"Unable to get status from camera: {camera_name}: {e}") + status = None + + # autotracking relative panning/tilting needs a relative zoom value set to 0 + # if camera supports relative movement + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + zoom_space_id = next( + ( + i + for i, space in enumerate( + ptz_config.Spaces.RelativeZoomTranslationSpace + ) + if "TranslationGenericSpace" in space["URI"] + ), + None, + ) + + # setup relative moving request for autotracking + move_request = ptz.create_type("RelativeMove") + move_request.ProfileToken = profile.token + logger.debug(f"{camera_name}: Relative move request: {move_request}") + if move_request.Translation is None and fov_space_id is not None: + move_request.Translation = status.Position + move_request.Translation.PanTilt.space = ptz_config["Spaces"][ + "RelativePanTiltTranslationSpace" + ][fov_space_id]["URI"] + + # try setting relative zoom translation space + try: + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + if zoom_space_id is not None: + move_request.Translation.Zoom.space = ptz_config["Spaces"][ + "RelativeZoomTranslationSpace" + ][zoom_space_id]["URI"] + else: + if ( + move_request["Translation"] is not None + and "Zoom" in move_request["Translation"] + ): + del move_request["Translation"]["Zoom"] + if ( + move_request["Speed"] is not None + and "Zoom" in move_request["Speed"] + ): + del move_request["Speed"]["Zoom"] + logger.debug( + f"{camera_name}: Relative move request after deleting zoom: {move_request}" + ) + except Exception as e: + self.config.cameras[ + camera_name + ].onvif.autotracking.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" + ) + + if move_request.Speed is None: + move_request.Speed = configs.DefaultPTZSpeed if configs else None + logger.debug( + f"{camera_name}: Relative move request after setup: {move_request}" + ) + self.cams[camera_name]["relative_move_request"] = move_request + + # setup absolute moving request for autotracking zooming + move_request = ptz.create_type("AbsoluteMove") + move_request.ProfileToken = profile.token + self.cams[camera_name]["absolute_move_request"] = move_request + + # setup existing presets + try: + presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token}) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Unable to get presets from camera: {camera_name}: {e}") + presets = [] + + for preset in presets: + # Ensure preset name is a Unicode string and handle UTF-8 characters correctly + preset_name = getattr(preset, "Name") or f"preset {preset['token']}" + + if isinstance(preset_name, bytes): + preset_name = preset_name.decode("utf-8") + + # Convert to lowercase while preserving UTF-8 characters + preset_name_lower = preset_name.lower() + self.cams[camera_name]["presets"][preset_name_lower] = preset["token"] + + # get list of supported features + supported_features = [] + + if configs.DefaultContinuousPanTiltVelocitySpace: + supported_features.append("pt") + + if configs.DefaultContinuousZoomVelocitySpace: + supported_features.append("zoom") + + if configs.DefaultRelativePanTiltTranslationSpace: + supported_features.append("pt-r") + + if configs.DefaultRelativeZoomTranslationSpace: + supported_features.append("zoom-r") + if ( + self.config.cameras[camera_name].onvif.autotracking.enabled_in_config + and self.config.cameras[camera_name].onvif.autotracking.enabled + ): + try: + # get camera's zoom limits from onvif config + self.cams[camera_name]["relative_zoom_range"] = ( + ptz_config.Spaces.RelativeZoomTranslationSpace[0] + ) + except Exception as e: + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + self.config.cameras[ + camera_name + ].onvif.autotracking.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" + ) + + if configs.DefaultAbsoluteZoomPositionSpace: + supported_features.append("zoom-a") + if ( + self.config.cameras[camera_name].onvif.autotracking.enabled_in_config + and self.config.cameras[camera_name].onvif.autotracking.enabled + ): + try: + # get camera's zoom limits from onvif config + self.cams[camera_name]["absolute_zoom_range"] = ( + ptz_config.Spaces.AbsoluteZoomPositionSpace[0] + ) + self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits + except Exception as e: + if self.config.cameras[camera_name].onvif.autotracking.zooming: + self.config.cameras[ + camera_name + ].onvif.autotracking.zooming = ZoomingModeEnum.disabled + logger.warning( + f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}" + ) + + if ( + self.cams[camera_name]["video_source_token"] is not None + and imaging is not None + ): + try: + imaging_capabilities = await imaging.GetImagingSettings( + {"VideoSourceToken": self.cams[camera_name]["video_source_token"]} + ) + if ( + hasattr(imaging_capabilities, "Focus") + and imaging_capabilities.Focus + ): + supported_features.append("focus") + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.debug(f"Focus not supported for {camera_name}: {e}") + + if ( + self.config.cameras[camera_name].onvif.autotracking.enabled_in_config + and self.config.cameras[camera_name].onvif.autotracking.enabled + and fov_space_id is not None + and configs.DefaultRelativePanTiltTranslationSpace is not None + ): + supported_features.append("pt-r-fov") + self.cams[camera_name]["relative_fov_range"] = ( + ptz_config.Spaces.RelativePanTiltTranslationSpace[fov_space_id] + ) + + self.cams[camera_name]["features"] = supported_features + self.cams[camera_name]["init"] = True + return True + + async def _stop(self, camera_name: str) -> None: + move_request = self.cams[camera_name]["move_request"] + await self.cams[camera_name]["ptz"].Stop( + { + "ProfileToken": move_request.ProfileToken, + "PanTilt": True, + "Zoom": True, + } + ) + if ( + "focus" in self.cams[camera_name]["features"] + and self.cams[camera_name]["video_source_token"] + and self.cams[camera_name]["imaging"] is not None + ): + try: + stop_request = self.cams[camera_name]["imaging"].create_type("Stop") + stop_request.VideoSourceToken = self.cams[camera_name][ + "video_source_token" + ] + await self.cams[camera_name]["imaging"].Stop(stop_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Failed to stop focus for {camera_name}: {e}") + self.cams[camera_name]["active"] = False + + async def _move(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + await self._stop(camera_name) + + if "pt" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF pan/tilt movement.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.move_left: + move_request.Velocity = {"PanTilt": {"x": -0.5, "y": 0}} + elif command == OnvifCommandEnum.move_right: + move_request.Velocity = {"PanTilt": {"x": 0.5, "y": 0}} + elif command == OnvifCommandEnum.move_up: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": 0.5, + } + } + elif command == OnvifCommandEnum.move_down: + move_request.Velocity = { + "PanTilt": { + "x": 0, + "y": -0.5, + } + } + + try: + await self.cams[camera_name]["ptz"].ContinuousMove(move_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Onvif sending move request to {camera_name} failed: {e}") + + async def _move_relative(self, camera_name: str, pan, tilt, zoom, speed) -> None: + if "pt-r-fov" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF RelativeMove (FOV).") + return + + logger.debug( + f"{camera_name} called RelativeMove: pan: {pan} tilt: {tilt} zoom: {zoom}" + ) + + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].motor_stopped.clear() + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["relative_move_request"] + + # function takes in -1 to 1 for pan and tilt, interpolate to the values of the camera. + # The onvif spec says this can report as +INF and -INF, so this may need to be modified + pan = numpy.interp( + pan, + [-1, 1], + [ + self.cams[camera_name]["relative_fov_range"]["XRange"]["Min"], + self.cams[camera_name]["relative_fov_range"]["XRange"]["Max"], + ], + ) + tilt = numpy.interp( + tilt, + [-1, 1], + [ + self.cams[camera_name]["relative_fov_range"]["YRange"]["Min"], + self.cams[camera_name]["relative_fov_range"]["YRange"]["Max"], + ], + ) + + move_request.Speed = { + "PanTilt": { + "x": speed, + "y": speed, + }, + } + + move_request.Translation.PanTilt.x = pan + move_request.Translation.PanTilt.y = tilt + + if ( + "zoom-r" in self.cams[camera_name]["features"] + and self.config.cameras[camera_name].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + move_request.Speed = { + "PanTilt": { + "x": speed, + "y": speed, + }, + "Zoom": {"x": speed}, + } + move_request.Translation.Zoom.x = zoom + + await self.cams[camera_name]["ptz"].RelativeMove(move_request) + + # reset after the move request + move_request.Translation.PanTilt.x = 0 + move_request.Translation.PanTilt.y = 0 + + if ( + "zoom-r" in self.cams[camera_name]["features"] + and self.config.cameras[camera_name].onvif.autotracking.zooming + == ZoomingModeEnum.relative + ): + move_request.Translation.Zoom.x = 0 + + self.cams[camera_name]["active"] = False + + async def _move_to_preset(self, camera_name: str, preset: str) -> None: + if isinstance(preset, bytes): + preset = preset.decode("utf-8") + + preset = preset.lower() + + if preset not in self.cams[camera_name]["presets"]: + logger.error(f"{preset} is not a valid preset for {camera_name}") + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].start_time.value = 0 + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["move_request"] + preset_token = self.cams[camera_name]["presets"][preset] + + await self.cams[camera_name]["ptz"].GotoPreset( + { + "ProfileToken": move_request.ProfileToken, + "PresetToken": preset_token, + } + ) + + self.cams[camera_name]["active"] = False + + async def _zoom(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, stopping..." + ) + await self._stop(camera_name) + + if "zoom" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF zooming.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["move_request"] + + if command == OnvifCommandEnum.zoom_in: + move_request.Velocity = {"Zoom": {"x": 0.5}} + elif command == OnvifCommandEnum.zoom_out: + move_request.Velocity = {"Zoom": {"x": -0.5}} + + await self.cams[camera_name]["ptz"].ContinuousMove(move_request) + + async def _zoom_absolute(self, camera_name: str, zoom, speed) -> None: + if "zoom-a" not in self.cams[camera_name]["features"]: + logger.error(f"{camera_name} does not support ONVIF AbsoluteMove zooming.") + return + + logger.debug(f"{camera_name} called AbsoluteMove: zoom: {zoom}") + + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + return + + self.cams[camera_name]["active"] = True + self.ptz_metrics[camera_name].motor_stopped.clear() + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + move_request = self.cams[camera_name]["absolute_move_request"] + + # function takes in 0 to 1 for zoom, interpolate to the values of the camera. + zoom = numpy.interp( + zoom, + [0, 1], + [ + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], + ], + ) + + move_request.Speed = {"Zoom": speed} + move_request.Position = {"Zoom": zoom} + + logger.debug(f"{camera_name}: Absolute zoom: {zoom}") + + await self.cams[camera_name]["ptz"].AbsoluteMove(move_request) + + self.cams[camera_name]["active"] = False + + async def _focus(self, camera_name: str, command: OnvifCommandEnum) -> None: + if self.cams[camera_name]["active"]: + logger.warning( + f"{camera_name} is already performing an action, not moving..." + ) + await self._stop(camera_name) + + if ( + "focus" not in self.cams[camera_name]["features"] + or not self.cams[camera_name]["video_source_token"] + or self.cams[camera_name]["imaging"] is None + ): + logger.error(f"{camera_name} does not support ONVIF continuous focus.") + return + + self.cams[camera_name]["active"] = True + move_request = self.cams[camera_name]["imaging"].create_type("Move") + move_request.VideoSourceToken = self.cams[camera_name]["video_source_token"] + move_request.Focus = { + "Continuous": { + "Speed": 0.5 if command == OnvifCommandEnum.focus_in else -0.5 + } + } + + try: + await self.cams[camera_name]["imaging"].Move(move_request) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.warning(f"Onvif sending focus request to {camera_name} failed: {e}") + self.cams[camera_name]["active"] = False + + async def handle_command_async( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + """Handle ONVIF commands asynchronously""" + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not await self._init_onvif(camera_name): + return + + try: + if command == OnvifCommandEnum.init: + # already init + return + elif command == OnvifCommandEnum.stop: + await self._stop(camera_name) + elif command == OnvifCommandEnum.preset: + await self._move_to_preset(camera_name, param) + elif command == OnvifCommandEnum.move_relative: + _, pan, tilt = param.split("_") + await self._move_relative(camera_name, float(pan), float(tilt), 0, 1) + elif command in (OnvifCommandEnum.zoom_in, OnvifCommandEnum.zoom_out): + await self._zoom(camera_name, command) + elif command in (OnvifCommandEnum.focus_in, OnvifCommandEnum.focus_out): + await self._focus(camera_name, command) + else: + await self._move(camera_name, command) + except (Fault, ONVIFError, TransportError, Exception) as e: + logger.error(f"Unable to handle onvif command: {e}") + + def handle_command( + self, camera_name: str, command: OnvifCommandEnum, param: str = "" + ) -> None: + """ + Handle ONVIF commands by scheduling them in the event loop. + """ + future = asyncio.run_coroutine_threadsafe( + self.handle_command_async(camera_name, command, param), self.loop + ) + + try: + # Wait with a timeout to prevent blocking indefinitely + future.result(timeout=10) + except asyncio.TimeoutError: + logger.error(f"Command {command} timed out for camera {camera_name}") + except Exception as e: + logger.error( + f"Error executing command {command} for camera {camera_name}: {e}" + ) + + async def get_camera_info(self, camera_name: str) -> dict[str, Any]: + """ + Get ptz capabilities and presets, attempting to reconnect if ONVIF is configured + but not initialized. + + Returns camera details including features and presets if available. + """ + if not self.config.cameras[camera_name].enabled: + logger.debug( + f"Camera {camera_name} disabled, won't try to initialize ONVIF" + ) + return {} + + if camera_name not in self.cams.keys() and ( + camera_name not in self.config.cameras + or not self.config.cameras[camera_name].onvif.host + ): + logger.debug(f"ONVIF is not configured for {camera_name}") + return {} + + if camera_name in self.cams.keys() and self.cams[camera_name]["init"]: + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + } + + if camera_name not in self.cams.keys() and camera_name in self.config.cameras: + success = await self._init_single_camera(camera_name) + if not success: + return {} + + # Reset retry count after timeout + attempts = self.failed_cams.get(camera_name, {}).get("retry_attempts", 0) + last_attempt = self.failed_cams.get(camera_name, {}).get("last_attempt", 0) + + if last_attempt and (time.time() - last_attempt) > self.reset_timeout: + logger.debug(f"Resetting retry count for {camera_name} after timeout") + attempts = 0 + self.failed_cams[camera_name]["retry_attempts"] = 0 + + # Attempt initialization/reconnection + if attempts < self.max_retries: + logger.info( + f"Attempting ONVIF initialization for {camera_name} (retry {attempts + 1}/{self.max_retries})" + ) + try: + if await self._init_onvif(camera_name): + if camera_name in self.failed_cams: + del self.failed_cams[camera_name] + return { + "name": camera_name, + "features": self.cams[camera_name]["features"], + "presets": list(self.cams[camera_name]["presets"].keys()), + } + else: + logger.warning(f"ONVIF initialization failed for {camera_name}") + except Exception as e: + logger.error( + f"Error during ONVIF initialization for {camera_name}: {e}" + ) + if camera_name not in self.failed_cams: + self.failed_cams[camera_name] = {"retry_attempts": 0} + self.failed_cams[camera_name].update( + { + "retry_attempts": attempts + 1, + "last_error": str(e), + "last_attempt": time.time(), + } + ) + + if attempts >= self.max_retries: + remaining_time = max( + 0, int((self.reset_timeout - (time.time() - last_attempt)) / 60) + ) + logger.error( + f"Too many ONVIF initialization attempts for {camera_name}, retry in {remaining_time} minute{'s' if remaining_time != 1 else ''}" + ) + + logger.debug(f"Could not initialize ONVIF for {camera_name}") + return {} + + async def get_service_capabilities(self, camera_name: str) -> None: + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return {} + + if not self.cams[camera_name]["init"]: + await self._init_onvif(camera_name) + + service_capabilities_request = self.cams[camera_name][ + "service_capabilities_request" + ] + try: + service_capabilities = await self.cams[camera_name][ + "ptz" + ].GetServiceCapabilities(service_capabilities_request) + + logger.debug( + f"Onvif service capabilities for {camera_name}: {service_capabilities}" + ) + + # MoveStatus is required for autotracking - should return "true" if supported + return find_by_key(vars(service_capabilities), "MoveStatus") + except Exception as e: + logger.warning( + f"Camera {camera_name} does not support the ONVIF GetServiceCapabilities method. Autotracking will not function correctly and must be disabled in your config. Exception: {e}" + ) + return False + + async def get_camera_status(self, camera_name: str) -> None: + async with self.status_locks[camera_name]: + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") + return + + if not self.cams[camera_name]["init"]: + if not await self._init_onvif(camera_name): + return + + status_request = self.cams[camera_name]["status_request"] + try: + status = await self.cams[camera_name]["ptz"].GetStatus(status_request) + except Exception: + pass # We're unsupported, that'll be reported in the next check. + + try: + pan_tilt_status = getattr(status.MoveStatus, "PanTilt", None) + zoom_status = getattr(status.MoveStatus, "Zoom", None) + + # if it's not an attribute, see if MoveStatus even exists in the status result + if pan_tilt_status is None: + pan_tilt_status = getattr(status, "MoveStatus", None) + + # we're unsupported + if pan_tilt_status is None or pan_tilt_status not in [ + "IDLE", + "MOVING", + ]: + raise Exception + except Exception: + logger.warning( + f"Camera {camera_name} does not support the ONVIF GetStatus method. Autotracking will not function correctly and must be disabled in your config." + ) + return + + logger.debug( + f"{camera_name}: Pan/tilt status: {pan_tilt_status}, Zoom status: {zoom_status}" + ) + + if pan_tilt_status == "IDLE" and ( + zoom_status is None or zoom_status == "IDLE" + ): + self.cams[camera_name]["active"] = False + if not self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.set() + + logger.debug( + f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + + self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + else: + self.cams[camera_name]["active"] = True + if self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.clear() + + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + # store absolute zoom level as 0 to 1 interpolated from the values of the camera + self.ptz_metrics[camera_name].zoom_level.value = numpy.interp( + round(status.Position.Zoom.x, 2), + [ + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], + ], + [0, 1], + ) + logger.debug( + f"{camera_name}: Camera zoom level: {self.ptz_metrics[camera_name].zoom_level.value}" + ) + + # some hikvision cams won't update MoveStatus, so warn if it hasn't changed + if ( + not self.ptz_metrics[camera_name].motor_stopped.is_set() + and not self.ptz_metrics[camera_name].reset.is_set() + and self.ptz_metrics[camera_name].start_time.value != 0 + and self.ptz_metrics[camera_name].frame_time.value + > (self.ptz_metrics[camera_name].start_time.value + 10) + and self.ptz_metrics[camera_name].stop_time.value == 0 + ): + logger.debug( + f"Start time: {self.ptz_metrics[camera_name].start_time.value}, Stop time: {self.ptz_metrics[camera_name].stop_time.value}, Frame time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + # set the stop time so we don't come back into this again and spam the logs + self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + logger.warning( + f"Camera {camera_name} is still in ONVIF 'MOVING' status." + ) + + def close(self) -> None: + """Gracefully shut down the ONVIF controller.""" + if not hasattr(self, "loop") or self.loop.is_closed(): + logger.debug("ONVIF controller already closed") + return + + logger.info("Exiting ONVIF controller...") + + def stop_and_cleanup(): + try: + self.loop.stop() + except Exception as e: + logger.error(f"Error during loop cleanup: {e}") + + # Schedule stop and cleanup in the loop thread + self.loop.call_soon_threadsafe(stop_and_cleanup) + + self.loop_thread.join() diff --git a/sam2-cpu/frigate-dev/frigate/record/__init__.py b/sam2-cpu/frigate-dev/frigate/record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/record/cleanup.py b/sam2-cpu/frigate-dev/frigate/record/cleanup.py new file mode 100644 index 0000000..94dd43e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/record/cleanup.py @@ -0,0 +1,378 @@ +"""Cleanup recordings that are expired based on retention config.""" + +import datetime +import itertools +import logging +import os +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path + +from playhouse.sqlite_ext import SqliteExtDatabase + +from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum +from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR +from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus +from frigate.record.util import remove_empty_directories, sync_recordings +from frigate.util.builtin import clear_and_unlink +from frigate.util.time import get_tomorrow_at_time + +logger = logging.getLogger(__name__) + + +class RecordingCleanup(threading.Thread): + """Cleanup existing recordings based on retention config.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__(name="recording_cleanup") + self.config = config + self.stop_event = stop_event + + def clean_tmp_previews(self) -> None: + """delete any previews in the cache that are more than 1 hour old.""" + for p in Path(CACHE_DIR).rglob("preview_*.mp4"): + logger.debug(f"Checking preview {p}.") + if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 60): + logger.debug("Deleting preview.") + clear_and_unlink(p) + + def clean_tmp_clips(self) -> None: + """delete any clips in the cache that are more than 1 hour old.""" + for p in Path(os.path.join(CLIPS_DIR, "cache")).rglob("clip_*.mp4"): + logger.debug(f"Checking tmp clip {p}.") + if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 60): + logger.debug("Deleting tmp clip.") + clear_and_unlink(p) + + def truncate_wal(self) -> None: + """check if the WAL needs to be manually truncated.""" + + # by default the WAL should be check-pointed automatically + # however, high levels of activity can prevent an opportunity + # for the checkpoint to be finished which means the WAL will grow + # without bound + + # with auto checkpoint most users should never hit this + + if ( + os.stat(f"{self.config.database.path}-wal").st_size / (1024 * 1024) + ) > MAX_WAL_SIZE: + db = SqliteExtDatabase(self.config.database.path) + db.execute_sql("PRAGMA wal_checkpoint(TRUNCATE);") + db.close() + + def expire_review_segments(self, config: CameraConfig, now: datetime) -> None: + """Delete review segments that are expired""" + alert_expire_date = ( + now - datetime.timedelta(days=config.record.alerts.retain.days) + ).timestamp() + detection_expire_date = ( + now - datetime.timedelta(days=config.record.detections.retain.days) + ).timestamp() + expired_reviews: ReviewSegment = ( + ReviewSegment.select(ReviewSegment.id, ReviewSegment.thumb_path) + .where(ReviewSegment.camera == config.name) + .where( + ( + (ReviewSegment.severity == "alert") + & (ReviewSegment.end_time < alert_expire_date) + ) + | ( + (ReviewSegment.severity == "detection") + & (ReviewSegment.end_time < detection_expire_date) + ) + ) + .namedtuples() + ) + + thumbs_to_delete = list(map(lambda x: x[1], expired_reviews)) + for thumb_path in thumbs_to_delete: + Path(thumb_path).unlink(missing_ok=True) + + max_deletes = 100000 + deleted_reviews_list = list(map(lambda x: x[0], expired_reviews)) + for i in range(0, len(deleted_reviews_list), max_deletes): + ReviewSegment.delete().where( + ReviewSegment.id << deleted_reviews_list[i : i + max_deletes] + ).execute() + UserReviewStatus.delete().where( + UserReviewStatus.review_segment + << deleted_reviews_list[i : i + max_deletes] + ).execute() + + def expire_existing_camera_recordings( + self, + continuous_expire_date: float, + motion_expire_date: float, + config: CameraConfig, + reviews: ReviewSegment, + ) -> None: + """Delete recordings for existing camera based on retention config.""" + # Get the timestamp for cutoff of retained days + + # Get recordings to check for expiration + recordings: Recordings = ( + Recordings.select( + Recordings.id, + Recordings.start_time, + Recordings.end_time, + Recordings.path, + Recordings.objects, + Recordings.motion, + Recordings.dBFS, + ) + .where( + (Recordings.camera == config.name) + & ( + ( + (Recordings.end_time < continuous_expire_date) + & (Recordings.motion == 0) + & (Recordings.dBFS == 0) + ) + | (Recordings.end_time < motion_expire_date) + ) + ) + .order_by(Recordings.start_time) + .namedtuples() + .iterator() + ) + + # loop over recordings and see if they overlap with any non-expired reviews + # TODO: expire segments based on segment stats according to config + review_start = 0 + deleted_recordings = set() + kept_recordings: list[tuple[float, float]] = [] + recording: Recordings + for recording in recordings: + keep = False + mode = None + # Now look for a reason to keep this recording segment + for idx in range(review_start, len(reviews)): + review: ReviewSegment = reviews[idx] + severity = review.severity + pre_capture = config.record.get_review_pre_capture(severity) + post_capture = config.record.get_review_post_capture(severity) + + # if the review starts in the future, stop checking reviews + # and let this recording segment expire + if review.start_time - pre_capture > recording.end_time: + keep = False + break + + # if the review is in progress or ends after the recording starts, keep it + # and stop looking at reviews + if ( + review.end_time is None + or review.end_time + post_capture >= recording.start_time + ): + keep = True + mode = ( + config.record.alerts.retain.mode + if review.severity == "alert" + else config.record.detections.retain.mode + ) + break + + # if the review ends before this recording segment starts, skip + # this review and check the next review for an overlap. + # since the review and recordings are sorted, we can skip review + # that end before the previous recording segment started on future segments + if review.end_time + post_capture < recording.start_time: + review_start = idx + + # Delete recordings outside of the retention window or based on the retention mode + if ( + not keep + or ( + mode == RetainModeEnum.motion + and recording.motion == 0 + and recording.objects == 0 + and recording.dBFS == 0 + ) + or (mode == RetainModeEnum.active_objects and recording.objects == 0) + ): + Path(recording.path).unlink(missing_ok=True) + deleted_recordings.add(recording.id) + else: + kept_recordings.append((recording.start_time, recording.end_time)) + + # expire recordings + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_recordings_list = list(deleted_recordings) + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + + previews: list[Previews] = ( + Previews.select( + Previews.id, + Previews.start_time, + Previews.end_time, + Previews.path, + ) + .where( + (Previews.camera == config.name) + & (Previews.end_time < continuous_expire_date) + & (Previews.end_time < motion_expire_date) + ) + .order_by(Previews.start_time) + .namedtuples() + .iterator() + ) + + # expire previews + recording_start = 0 + deleted_previews = set() + for preview in previews: + keep = False + # look for a reason to keep this preview + for idx in range(recording_start, len(kept_recordings)): + start_time, end_time = kept_recordings[idx] + + # if the recording starts in the future, stop checking recordings + # and let this preview expire + if start_time > preview.end_time: + keep = False + break + + # if the recording ends after the preview starts, keep it + # and stop looking at recordings + if end_time >= preview.start_time: + keep = True + break + + # if the recording ends before this preview starts, skip + # this recording and check the next recording for an overlap. + # since the kept recordings and previews are sorted, we can skip recordings + # that end before the current preview started + if end_time < preview.start_time: + recording_start = idx + + # Delete previews without any relevant recordings + if not keep: + Path(preview.path).unlink(missing_ok=True) + deleted_previews.add(preview.id) + + # expire previews + logger.debug(f"Expiring {len(deleted_previews)} previews") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_previews_list = list(deleted_previews) + for i in range(0, len(deleted_previews_list), max_deletes): + Previews.delete().where( + Previews.id << deleted_previews_list[i : i + max_deletes] + ).execute() + + def expire_recordings(self) -> None: + """Delete recordings based on retention config.""" + logger.debug("Start expire recordings.") + logger.debug("Start deleted cameras.") + + # Handle deleted cameras + expire_days = max( + self.config.record.continuous.days, self.config.record.motion.days + ) + expire_before = ( + datetime.datetime.now() - datetime.timedelta(days=expire_days) + ).timestamp() + no_camera_recordings: Recordings = ( + Recordings.select( + Recordings.id, + Recordings.path, + ) + .where( + Recordings.camera.not_in(list(self.config.cameras.keys())), + Recordings.end_time < expire_before, + ) + .namedtuples() + .iterator() + ) + + deleted_recordings = set() + for recording in no_camera_recordings: + Path(recording.path).unlink(missing_ok=True) + deleted_recordings.add(recording.id) + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + deleted_recordings_list = list(deleted_recordings) + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + logger.debug("End deleted cameras.") + + logger.debug("Start all cameras.") + for camera, config in self.config.cameras.items(): + logger.debug(f"Start camera: {camera}.") + now = datetime.datetime.now() + + self.expire_review_segments(config, now) + continuous_expire_date = ( + now - datetime.timedelta(days=config.record.continuous.days) + ).timestamp() + motion_expire_date = ( + now + - datetime.timedelta( + days=max( + config.record.motion.days, config.record.continuous.days + ) # can't keep motion for less than continuous + ) + ).timestamp() + + # Get all the reviews to check against + reviews: ReviewSegment = ( + ReviewSegment.select( + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ) + .where( + ReviewSegment.camera == camera, + # need to ensure segments for all reviews starting + # before the expire date are included + ReviewSegment.start_time < motion_expire_date, + ) + .order_by(ReviewSegment.start_time) + .namedtuples() + ) + + self.expire_existing_camera_recordings( + continuous_expire_date, motion_expire_date, config, reviews + ) + logger.debug(f"End camera: {camera}.") + + logger.debug("End all cameras.") + logger.debug("End expire recordings.") + + def run(self) -> None: + # on startup sync recordings with disk if enabled + if self.config.record.sync_recordings: + sync_recordings(limited=False) + next_sync = get_tomorrow_at_time(3) + + # Expire tmp clips every minute, recordings and clean directories every hour. + for counter in itertools.cycle(range(self.config.record.expire_interval)): + if self.stop_event.wait(60): + logger.info("Exiting recording cleanup...") + break + + self.clean_tmp_previews() + + if ( + self.config.record.sync_recordings + and datetime.datetime.now().astimezone(datetime.timezone.utc) + > next_sync + ): + sync_recordings(limited=True) + next_sync = get_tomorrow_at_time(3) + + if counter == 0: + self.clean_tmp_clips() + self.expire_recordings() + remove_empty_directories(RECORD_DIR) + self.truncate_wal() diff --git a/sam2-cpu/frigate-dev/frigate/record/export.py b/sam2-cpu/frigate-dev/frigate/record/export.py new file mode 100644 index 0000000..d4b49bb --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/record/export.py @@ -0,0 +1,451 @@ +"""Export recordings to storage.""" + +import datetime +import logging +import os +import random +import shutil +import string +import subprocess as sp +import threading +from enum import Enum +from pathlib import Path +from typing import Optional + +from peewee import DoesNotExist + +from frigate.config import FfmpegConfig, FrigateConfig +from frigate.const import ( + CACHE_DIR, + CLIPS_DIR, + EXPORT_DIR, + MAX_PLAYLIST_SECONDS, + PREVIEW_FRAME_TYPE, + PROCESS_PRIORITY_LOW, +) +from frigate.ffmpeg_presets import ( + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) +from frigate.models import Export, Previews, Recordings +from frigate.util.time import is_current_hour + +logger = logging.getLogger(__name__) + + +TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" + + +def lower_priority(): + os.nice(PROCESS_PRIORITY_LOW) + + +class PlaybackFactorEnum(str, Enum): + realtime = "realtime" + timelapse_25x = "timelapse_25x" + + +class PlaybackSourceEnum(str, Enum): + recordings = "recordings" + preview = "preview" + + +class RecordingExporter(threading.Thread): + """Exports a specific set of recordings for a camera to storage as a single file.""" + + def __init__( + self, + config: FrigateConfig, + id: str, + camera: str, + name: Optional[str], + image: Optional[str], + start_time: int, + end_time: int, + playback_factor: PlaybackFactorEnum, + playback_source: PlaybackSourceEnum, + ) -> None: + super().__init__() + self.config = config + self.export_id = id + self.camera = camera + self.user_provided_name = name + self.user_provided_image = image + self.start_time = start_time + self.end_time = end_time + self.playback_factor = playback_factor + self.playback_source = playback_source + + # ensure export thumb dir + Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) + + def get_datetime_from_timestamp(self, timestamp: int) -> str: + # return in iso format + return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + + def save_thumbnail(self, id: str) -> str: + thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp") + + if self.user_provided_image is not None and os.path.isfile( + self.user_provided_image + ): + shutil.copy(self.user_provided_image, thumb_path) + return thumb_path + + if ( + self.start_time + < datetime.datetime.now(datetime.timezone.utc) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ): + # has preview mp4 + try: + preview: Previews = ( + Previews.select( + Previews.camera, + Previews.path, + Previews.duration, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .limit(1) + .get() + ) + except DoesNotExist: + return "" + + diff = self.start_time - preview.start_time + minutes = int(diff / 60) + seconds = int(diff % 60) + ffmpeg_cmd = [ + "/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support + "-hide_banner", + "-loglevel", + "warning", + "-ss", + f"00:{minutes}:{seconds}", + "-i", + preview.path, + "-frames", + "1", + "-c:v", + "libwebp", + thumb_path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + return "" + + else: + # need to generate from existing images + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{self.camera}" + start_file = f"{file_start}-{self.start_time}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}-{self.end_time}.{PREVIEW_FRAME_TYPE}" + selected_preview = None + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + selected_preview = os.path.join(preview_dir, file) + break + + if not selected_preview: + return "" + + shutil.copyfile(selected_preview, thumb_path) + + return thumb_path + + def get_record_export_command(self, video_path: str) -> list[str]: + if (self.end_time - self.start_time) <= MAX_PLAYLIST_SECONDS: + playlist_lines = f"http://127.0.0.1:5000/vod/{self.camera}/start/{self.start_time}/end/{self.end_time}/index.m3u8" + ffmpeg_input = ( + f"-y -protocol_whitelist pipe,file,http,tcp -i {playlist_lines}" + ) + else: + playlist_lines = [] + + # get full set of recordings + export_recordings = ( + Recordings.select( + Recordings.start_time, + Recordings.end_time, + ) + .where( + Recordings.start_time.between(self.start_time, self.end_time) + | Recordings.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Recordings.start_time) + & (self.end_time < Recordings.end_time) + ) + ) + .where(Recordings.camera == self.camera) + .order_by(Recordings.start_time.asc()) + ) + + # Use pagination to process records in chunks + page_size = 1000 + num_pages = (export_recordings.count() + page_size - 1) // page_size + + for page in range(1, num_pages + 1): + playlist = export_recordings.paginate(page, page_size) + playlist_lines.append( + f"file 'http://127.0.0.1:5000/vod/{self.camera}/start/{float(playlist[0].start_time)}/end/{float(playlist[-1].end_time)}/index.m3u8'" + ) + + ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin" + + if self.playback_factor == PlaybackFactorEnum.realtime: + ffmpeg_cmd = ( + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} -c copy -movflags +faststart" + ).split(" ") + elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: + ffmpeg_cmd = ( + parse_preset_hardware_acceleration_encode( + self.config.ffmpeg.ffmpeg_path, + self.config.ffmpeg.hwaccel_args, + f"-an {ffmpeg_input}", + f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart", + EncodeTypeEnum.timelapse, + ) + ).split(" ") + + # add metadata + title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" + ffmpeg_cmd.extend(["-metadata", f"title={title}"]) + + ffmpeg_cmd.append(video_path) + + return ffmpeg_cmd, playlist_lines + + def get_preview_export_command(self, video_path: str) -> list[str]: + playlist_lines = [] + codec = "-c copy" + + if is_current_hour(self.start_time): + # get list of current preview frames + preview_dir = os.path.join(CACHE_DIR, "preview_frames") + file_start = f"preview_{self.camera}" + start_file = f"{file_start}-{self.start_time}.{PREVIEW_FRAME_TYPE}" + end_file = f"{file_start}-{self.end_time}.{PREVIEW_FRAME_TYPE}" + + for file in sorted(os.listdir(preview_dir)): + if not file.startswith(file_start): + continue + + if file < start_file: + continue + + if file > end_file: + break + + playlist_lines.append(f"file '{os.path.join(preview_dir, file)}'") + playlist_lines.append("duration 0.12") + + if playlist_lines: + last_file = playlist_lines[-2] + playlist_lines.append(last_file) + codec = "-c:v libx264" + + # get full set of previews + export_previews = ( + Previews.select( + Previews.path, + Previews.start_time, + Previews.end_time, + ) + .where( + Previews.start_time.between(self.start_time, self.end_time) + | Previews.end_time.between(self.start_time, self.end_time) + | ( + (self.start_time > Previews.start_time) + & (self.end_time < Previews.end_time) + ) + ) + .where(Previews.camera == self.camera) + .order_by(Previews.start_time.asc()) + .namedtuples() + .iterator() + ) + + preview: Previews + for preview in export_previews: + playlist_lines.append(f"file '{preview.path}'") + + if preview.start_time < self.start_time: + playlist_lines.append( + f"inpoint {int(self.start_time - preview.start_time)}" + ) + + if preview.end_time > self.end_time: + playlist_lines.append( + f"outpoint {int(preview.end_time - self.end_time)}" + ) + + ffmpeg_input = ( + "-y -protocol_whitelist pipe,file,tcp -f concat -safe 0 -i /dev/stdin" + ) + + if self.playback_factor == PlaybackFactorEnum.realtime: + ffmpeg_cmd = ( + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}" + ).split(" ") + elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: + ffmpeg_cmd = ( + parse_preset_hardware_acceleration_encode( + self.config.ffmpeg.ffmpeg_path, + self.config.ffmpeg.hwaccel_args, + f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}", + f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}", + EncodeTypeEnum.timelapse, + ) + ).split(" ") + + # add metadata + title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" + ffmpeg_cmd.extend(["-metadata", f"title={title}"]) + + return ffmpeg_cmd, playlist_lines + + def run(self) -> None: + logger.debug( + f"Beginning export for {self.camera} from {self.start_time} to {self.end_time}" + ) + export_name = ( + self.user_provided_name + or f"{self.camera.replace('_', ' ')} {self.get_datetime_from_timestamp(self.start_time)} {self.get_datetime_from_timestamp(self.end_time)}" + ) + filename_start_datetime = datetime.datetime.fromtimestamp( + self.start_time + ).strftime("%Y%m%d_%H%M%S") + filename_end_datetime = datetime.datetime.fromtimestamp(self.end_time).strftime( + "%Y%m%d_%H%M%S" + ) + cleaned_export_id = self.export_id.split("_")[-1] + video_path = f"{EXPORT_DIR}/{self.camera}_{filename_start_datetime}-{filename_end_datetime}_{cleaned_export_id}.mp4" + thumb_path = self.save_thumbnail(self.export_id) + + Export.insert( + { + Export.id: self.export_id, + Export.camera: self.camera, + Export.name: export_name, + Export.date: self.start_time, + Export.video_path: video_path, + Export.thumb_path: thumb_path, + Export.in_progress: True, + } + ).execute() + + try: + if self.playback_source == PlaybackSourceEnum.recordings: + ffmpeg_cmd, playlist_lines = self.get_record_export_command(video_path) + else: + ffmpeg_cmd, playlist_lines = self.get_preview_export_command(video_path) + except DoesNotExist: + return + + p = sp.run( + ffmpeg_cmd, + input="\n".join(playlist_lines), + encoding="ascii", + preexec_fn=lower_priority, + capture_output=True, + ) + + if p.returncode != 0: + logger.error( + f"Failed to export {self.playback_source.value} for command {' '.join(ffmpeg_cmd)}" + ) + logger.error(p.stderr) + Path(video_path).unlink(missing_ok=True) + Export.delete().where(Export.id == self.export_id).execute() + Path(thumb_path).unlink(missing_ok=True) + return + else: + Export.update({Export.in_progress: False}).where( + Export.id == self.export_id + ).execute() + + logger.debug(f"Finished exporting {video_path}") + + +def migrate_exports(ffmpeg: FfmpegConfig, camera_names: list[str]): + Path(os.path.join(CLIPS_DIR, "export")).mkdir(exist_ok=True) + + exports = [] + for export_file in os.listdir(EXPORT_DIR): + camera = "unknown" + + for cam_name in camera_names: + if cam_name in export_file: + camera = cam_name + break + + id = f"{camera}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" + video_path = os.path.join(EXPORT_DIR, export_file) + thumb_path = os.path.join( + CLIPS_DIR, f"export/{id}.jpg" + ) # use jpg because webp encoder can't get quality low enough + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-i", + video_path, + "-vf", + "scale=-1:180", + "-frames", + "1", + "-q:v", + "8", + thumb_path, + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode != 0: + logger.error(process.stderr) + continue + + exports.append( + { + Export.id: id, + Export.camera: camera, + Export.name: export_file.replace(".mp4", ""), + Export.date: os.path.getctime(video_path), + Export.video_path: video_path, + Export.thumb_path: thumb_path, + Export.in_progress: False, + } + ) + + Export.insert_many(exports).execute() diff --git a/sam2-cpu/frigate-dev/frigate/record/maintainer.py b/sam2-cpu/frigate-dev/frigate/record/maintainer.py new file mode 100644 index 0000000..d60d7cc --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/record/maintainer.py @@ -0,0 +1,672 @@ +"""Maintain recording segments in cache.""" + +import asyncio +import datetime +import logging +import os +import random +import string +import threading +import time +from collections import defaultdict +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any, Optional, Tuple + +import numpy as np +import psutil + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataPublisher, + RecordingsDataTypeEnum, +) +from frigate.config import FrigateConfig, RetainModeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + CACHE_DIR, + CACHE_SEGMENT_FORMAT, + FAST_QUEUE_TIMEOUT, + INSERT_MANY_RECORDINGS, + MAX_SEGMENT_DURATION, + MAX_SEGMENTS_IN_CACHE, + RECORD_DIR, +) +from frigate.models import Recordings, ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.util.services import get_video_properties + +logger = logging.getLogger(__name__) + + +class SegmentInfo: + def __init__( + self, + motion_count: int, + active_object_count: int, + region_count: int, + average_dBFS: int, + ) -> None: + self.motion_count = motion_count + self.active_object_count = active_object_count + self.region_count = region_count + self.average_dBFS = average_dBFS + + def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool: + keep = False + + # all mode should never discard + if retain_mode == RetainModeEnum.all: + keep = True + + # motion mode should keep if motion or audio is detected + if ( + not keep + and retain_mode == RetainModeEnum.motion + and (self.motion_count > 0 or self.average_dBFS != 0) + ): + keep = True + + # active objects mode should keep if any active objects are detected + if not keep and self.active_object_count > 0: + keep = True + + return not keep + + +class RecordingMaintainer(threading.Thread): + def __init__(self, config: FrigateConfig, stop_event: MpEvent): + super().__init__(name="recording_maintainer") + self.config = config + + # create communication for retained recordings + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.record], + ) + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value) + self.recordings_publisher = RecordingsDataPublisher() + + self.stop_event = stop_event + self.object_recordings_info: dict[str, list] = defaultdict(list) + self.audio_recordings_info: dict[str, list] = defaultdict(list) + self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} + + async def move_files(self) -> None: + cache_files = [ + d + for d in os.listdir(CACHE_DIR) + if os.path.isfile(os.path.join(CACHE_DIR, d)) + and d.endswith(".mp4") + and not d.startswith("preview_") + ] + + # publish newest cached segment per camera (including in use files) + newest_cache_segments: dict[str, dict[str, Any]] = {} + for cache in cache_files: + cache_path = os.path.join(CACHE_DIR, cache) + basename = os.path.splitext(cache)[0] + camera, date = basename.rsplit("@", maxsplit=1) + start_time = datetime.datetime.strptime( + date, CACHE_SEGMENT_FORMAT + ).astimezone(datetime.timezone.utc) + if ( + camera not in newest_cache_segments + or start_time > newest_cache_segments[camera]["start_time"] + ): + newest_cache_segments[camera] = { + "start_time": start_time, + "cache_path": cache_path, + } + + for camera, newest in newest_cache_segments.items(): + self.recordings_publisher.publish( + ( + camera, + newest["start_time"].timestamp(), + newest["cache_path"], + ), + RecordingsDataTypeEnum.latest.value, + ) + # publish None for cameras with no cache files (but only if we know the camera exists) + for camera_name in self.config.cameras: + if camera_name not in newest_cache_segments: + self.recordings_publisher.publish( + (camera_name, None, None), + RecordingsDataTypeEnum.latest.value, + ) + + files_in_use = [] + for process in psutil.process_iter(): + try: + if process.name() != "ffmpeg": + continue + file_list = process.open_files() + if file_list: + for nt in file_list: + if nt.path.startswith(CACHE_DIR): + files_in_use.append(nt.path.split("/")[-1]) + except psutil.Error: + continue + + # group recordings by camera (skip in-use for validation/moving) + grouped_recordings: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) + for cache in cache_files: + # Skip files currently in use + if cache in files_in_use: + continue + + cache_path = os.path.join(CACHE_DIR, cache) + basename = os.path.splitext(cache)[0] + camera, date = basename.rsplit("@", maxsplit=1) + + # important that start_time is utc because recordings are stored and compared in utc + start_time = datetime.datetime.strptime( + date, CACHE_SEGMENT_FORMAT + ).astimezone(datetime.timezone.utc) + + grouped_recordings[camera].append( + { + "cache_path": cache_path, + "start_time": start_time, + } + ) + + # delete all cached files past the most recent MAX_SEGMENTS_IN_CACHE + keep_count = MAX_SEGMENTS_IN_CACHE + for camera in grouped_recordings.keys(): + # sort based on start time + grouped_recordings[camera] = sorted( + grouped_recordings[camera], key=lambda s: s["start_time"] + ) + + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + + processed_segment_count = len( + list( + filter( + lambda r: r["start_time"].timestamp() + < most_recently_processed_frame_time, + grouped_recordings[camera], + ) + ) + ) + + # see if the recording mover is too slow and segments need to be deleted + if processed_segment_count > keep_count: + logger.warning( + f"Unable to keep up with recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {processed_segment_count} and discarding the rest..." + ) + to_remove = grouped_recordings[camera][:-keep_count] + for rec in to_remove: + cache_path = rec["cache_path"] + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] + + # see if detection has failed and unprocessed segments need to be deleted + unprocessed_segment_count = ( + len(grouped_recordings[camera]) - processed_segment_count + ) + if unprocessed_segment_count > keep_count: + logger.warning( + f"Too many unprocessed recording segments in cache for {camera}. This likely indicates an issue with the detect stream, keeping the {keep_count} most recent segments out of {unprocessed_segment_count} and discarding the rest..." + ) + to_remove = grouped_recordings[camera][:-keep_count] + for rec in to_remove: + cache_path = rec["cache_path"] + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] + + tasks = [] + for camera, recordings in grouped_recordings.items(): + # clear out all the object recording info for old frames + while ( + len(self.object_recordings_info[camera]) > 0 + and self.object_recordings_info[camera][0][0] + < recordings[0]["start_time"].timestamp() + ): + self.object_recordings_info[camera].pop(0) + + # clear out all the audio recording info for old frames + while ( + len(self.audio_recordings_info[camera]) > 0 + and self.audio_recordings_info[camera][0][0] + < recordings[0]["start_time"].timestamp() + ): + self.audio_recordings_info[camera].pop(0) + + # get all reviews with the end time after the start of the oldest cache file + # or with end_time None + reviews: ReviewSegment = ( + ReviewSegment.select( + ReviewSegment.start_time, + ReviewSegment.end_time, + ReviewSegment.severity, + ReviewSegment.data, + ) + .where( + ReviewSegment.camera == camera, + (ReviewSegment.end_time == None) + | ( + ReviewSegment.end_time + >= recordings[0]["start_time"].timestamp() + ), + ) + .order_by(ReviewSegment.start_time) + ) + + tasks.extend( + [self.validate_and_move_segment(camera, reviews, r) for r in recordings] + ) + + # publish most recently available recording time and None if disabled + self.recordings_publisher.publish( + ( + camera, + recordings[0]["start_time"].timestamp() + if self.config.cameras[camera].record.enabled + else None, + None, + ), + RecordingsDataTypeEnum.saved.value, + ) + + recordings_to_insert: list[Optional[Recordings]] = await asyncio.gather(*tasks) + + # fire and forget recordings entries + self.requestor.send_data( + INSERT_MANY_RECORDINGS, + [r for r in recordings_to_insert if r is not None], + ) + + def drop_segment(self, cache_path: str) -> None: + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + + async def validate_and_move_segment( + self, camera: str, reviews: list[ReviewSegment], recording: dict[str, Any] + ) -> Optional[Recordings]: + cache_path: str = recording["cache_path"] + start_time: datetime.datetime = recording["start_time"] + record_config = self.config.cameras[camera].record + + # Just delete files if recordings are turned off + if ( + camera not in self.config.cameras + or not self.config.cameras[camera].record.enabled + ): + self.drop_segment(cache_path) + return None + + if cache_path in self.end_time_cache: + end_time, duration = self.end_time_cache[cache_path] + else: + segment_info = await get_video_properties( + self.config.ffmpeg, cache_path, get_duration=True + ) + + if not segment_info.get("has_valid_video", False): + logger.warning( + f"Invalid or missing video stream in segment {cache_path}. Discarding." + ) + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.invalid.value, + ) + self.drop_segment(cache_path) + return None + + duration = float(segment_info.get("duration", -1)) + + # ensure duration is within expected length + if 0 < duration < MAX_SEGMENT_DURATION: + end_time = start_time + datetime.timedelta(seconds=duration) + self.end_time_cache[cache_path] = (end_time, duration) + else: + if duration == -1: + logger.warning(f"Failed to probe corrupt segment {cache_path}") + + logger.warning(f"Discarding a corrupt recording segment: {cache_path}") + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.invalid.value, + ) + self.drop_segment(cache_path) + return None + + # this segment has a valid duration and has video data, so publish an update + self.recordings_publisher.publish( + (camera, start_time.timestamp(), cache_path), + RecordingsDataTypeEnum.valid.value, + ) + + record_config = self.config.cameras[camera].record + highest = None + + if record_config.continuous.days > 0: + highest = "continuous" + elif record_config.motion.days > 0: + highest = "motion" + + # if we have continuous or motion recording enabled + # we should first just check if this segment matches that + # and avoid any DB calls + if highest is not None: + # assume that empty means the relevant recording info has not been received yet + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + + # ensure delayed segment info does not lead to lost segments + if ( + datetime.datetime.fromtimestamp( + most_recently_processed_frame_time + ).astimezone(datetime.timezone.utc) + >= end_time + ): + record_mode = ( + RetainModeEnum.all + if highest == "continuous" + else RetainModeEnum.motion + ) + return await self.move_segment( + camera, start_time, end_time, duration, cache_path, record_mode + ) + + # we fell through the continuous / motion check, so we need to check the review items + # if the cached segment overlaps with the review items: + overlaps = False + for review in reviews: + severity = SeverityEnum[review.severity] + + # if the review item starts in the future, stop checking review items + # and remove this segment + if ( + review.start_time - record_config.get_review_pre_capture(severity) + ) > end_time.timestamp(): + overlaps = False + break + + # if the review item is in progress or ends after the recording starts, keep it + # and stop looking at review items + if ( + review.end_time is None + or (review.end_time + record_config.get_review_post_capture(severity)) + >= start_time.timestamp() + ): + overlaps = True + break + + if overlaps: + record_mode = ( + record_config.alerts.retain.mode + if review.severity == "alert" + else record_config.detections.retain.mode + ) + # move from cache to recordings immediately + return await self.move_segment( + camera, + start_time, + end_time, + duration, + cache_path, + record_mode, + ) + # if it doesn't overlap with an review item, go ahead and drop the segment + # if it ends more than the configured pre_capture for the camera + # BUT only if continuous/motion is NOT enabled (otherwise wait for processing) + elif highest is None: + camera_info = self.object_recordings_info[camera] + most_recently_processed_frame_time = ( + camera_info[-1][0] if len(camera_info) > 0 else 0 + ) + retain_cutoff = datetime.datetime.fromtimestamp( + most_recently_processed_frame_time - record_config.event_pre_capture + ).astimezone(datetime.timezone.utc) + if end_time < retain_cutoff: + self.drop_segment(cache_path) + + def segment_stats( + self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime + ) -> SegmentInfo: + video_frame_count = 0 + active_count = 0 + region_count = 0 + motion_count = 0 + for frame in self.object_recordings_info[camera]: + # frame is after end time of segment + if frame[0] > end_time.timestamp(): + break + # frame is before start time of segment + if frame[0] < start_time.timestamp(): + continue + + video_frame_count += 1 + active_count += len( + [ + o + for o in frame[1] + if not o["false_positive"] and o["motionless_count"] == 0 + ] + ) + motion_count += len(frame[2]) + region_count += len(frame[3]) + + audio_values = [] + for frame in self.audio_recordings_info[camera]: + # frame is after end time of segment + if frame[0] > end_time.timestamp(): + break + + # frame is before start time of segment + if frame[0] < start_time.timestamp(): + continue + + # add active audio label count to count of active objects + active_count += len(frame[2]) + + # add sound level to audio values + audio_values.append(frame[1]) + + average_dBFS = 0 if not audio_values else np.average(audio_values) + + return SegmentInfo( + motion_count, active_count, region_count, round(average_dBFS) + ) + + async def move_segment( + self, + camera: str, + start_time: datetime.datetime, + end_time: datetime.datetime, + duration: float, + cache_path: str, + store_mode: RetainModeEnum, + ) -> Optional[Recordings]: + segment_info = self.segment_stats(camera, start_time, end_time) + + # check if the segment shouldn't be stored + if segment_info.should_discard_segment(store_mode): + self.drop_segment(cache_path) + return + + # directory will be in utc due to start_time being in utc + directory = os.path.join( + RECORD_DIR, + start_time.strftime("%Y-%m-%d/%H"), + camera, + ) + + if not os.path.exists(directory): + os.makedirs(directory) + + # file will be in utc due to start_time being in utc + file_name = f"{start_time.strftime('%M.%S.mp4')}" + file_path = os.path.join(directory, file_name) + + try: + if not os.path.exists(file_path): + start_frame = datetime.datetime.now().timestamp() + + # add faststart to kept segments to improve metadata reading + p = await asyncio.create_subprocess_exec( + self.config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-y", + "-i", + cache_path, + "-c", + "copy", + "-movflags", + "+faststart", + file_path, + stderr=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.DEVNULL, + ) + await p.wait() + + if p.returncode != 0: + logger.error(f"Unable to convert {cache_path} to {file_path}") + logger.error((await p.stderr.read()).decode("ascii")) + return None + else: + logger.debug( + f"Copied {file_path} in {datetime.datetime.now().timestamp() - start_frame} seconds." + ) + + try: + # get the segment size of the cache file + # file without faststart is same size + segment_size = round( + float(os.path.getsize(cache_path)) / pow(2, 20), 2 + ) + except OSError: + segment_size = 0 + + os.remove(cache_path) + + rand_id = "".join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + + return { + Recordings.id.name: f"{start_time.timestamp()}-{rand_id}", + Recordings.camera.name: camera, + Recordings.path.name: file_path, + Recordings.start_time.name: start_time.timestamp(), + Recordings.end_time.name: end_time.timestamp(), + Recordings.duration.name: duration, + Recordings.motion.name: segment_info.motion_count, + # TODO: update this to store list of active objects at some point + Recordings.objects.name: segment_info.active_object_count, + Recordings.regions.name: segment_info.region_count, + Recordings.dBFS.name: segment_info.average_dBFS, + Recordings.segment_size.name: segment_size, + } + except Exception as e: + logger.error(f"Unable to store recording segment {cache_path}") + Path(cache_path).unlink(missing_ok=True) + logger.error(e) + + # clear end_time cache + self.end_time_cache.pop(cache_path, None) + return None + + def run(self) -> None: + # Check for new files every 5 seconds + wait_time = 0.0 + while not self.stop_event.is_set(): + time.sleep(wait_time) + + if self.stop_event.is_set(): + break + + run_start = datetime.datetime.now().timestamp() + + # check if there is an updated config + self.config_subscriber.check_for_updates() + + stale_frame_count = 0 + stale_frame_count_threshold = 10 + # empty the object recordings info queue + while True: + (topic, data) = self.detection_subscriber.check_for_update( + timeout=FAST_QUEUE_TIMEOUT + ) + + if not topic: + break + + if topic == DetectionTypeEnum.video.value: + ( + camera, + _, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = data + + if self.config.cameras[camera].record.enabled: + self.object_recordings_info[camera].append( + ( + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) + ) + elif topic == DetectionTypeEnum.audio.value: + ( + camera, + frame_time, + dBFS, + audio_detections, + ) = data + + if self.config.cameras[camera].record.enabled: + self.audio_recordings_info[camera].append( + ( + frame_time, + dBFS, + audio_detections, + ) + ) + elif ( + topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value + ): + continue + + if frame_time < run_start - stale_frame_count_threshold: + stale_frame_count += 1 + + if stale_frame_count > 0: + logger.debug(f"Found {stale_frame_count} old frames.") + + try: + asyncio.run(self.move_files()) + except Exception as e: + logger.error( + "Error occurred when attempting to maintain recording cache" + ) + logger.error(e) + duration = datetime.datetime.now().timestamp() - run_start + wait_time = max(0, 5 - duration) + + self.requestor.stop() + self.config_subscriber.stop() + self.detection_subscriber.stop() + self.recordings_publisher.stop() + logger.info("Exiting recording maintenance...") diff --git a/sam2-cpu/frigate-dev/frigate/record/record.py b/sam2-cpu/frigate-dev/frigate/record/record.py new file mode 100644 index 0000000..624ed6e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/record/record.py @@ -0,0 +1,47 @@ +"""Run recording maintainer and cleanup.""" + +import logging +from multiprocessing.synchronize import Event as MpEvent + +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_HIGH +from frigate.models import Recordings, ReviewSegment +from frigate.record.maintainer import RecordingMaintainer +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +class RecordProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name="frigate.recording_manager", + daemon=True, + ) + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + db = SqliteQueueDatabase( + self.config.database.path, + pragmas={ + "auto_vacuum": "FULL", # Does not defragment database + "cache_size": -512 * 1000, # 512MB of cache + "synchronous": "NORMAL", # Safe when using WAL https://www.sqlite.org/pragma.html#pragma_synchronous + }, + timeout=max( + 60, 10 * len([c for c in self.config.cameras.values() if c.enabled]) + ), + ) + models = [ReviewSegment, Recordings] + db.bind(models) + + maintainer = RecordingMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/sam2-cpu/frigate-dev/frigate/record/util.py b/sam2-cpu/frigate-dev/frigate/record/util.py new file mode 100644 index 0000000..6a91c1a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/record/util.py @@ -0,0 +1,147 @@ +"""Recordings Utilities.""" + +import datetime +import logging +import os + +from peewee import DatabaseError, chunked + +from frigate.const import RECORD_DIR +from frigate.models import Recordings, RecordingsToDelete + +logger = logging.getLogger(__name__) + + +def remove_empty_directories(directory: str) -> None: + # list all directories recursively and sort them by path, + # longest first + paths = sorted( + [x[0] for x in os.walk(directory)], + key=lambda p: len(str(p)), + reverse=True, + ) + for path in paths: + # don't delete the parent + if path == directory: + continue + if len(os.listdir(path)) == 0: + os.rmdir(path) + + +def sync_recordings(limited: bool) -> None: + """Check the db for stale recordings entries that don't exist in the filesystem.""" + + def delete_db_entries_without_file(check_timestamp: float) -> bool: + """Delete db entries where file was deleted outside of frigate.""" + + if limited: + recordings = Recordings.select(Recordings.id, Recordings.path).where( + Recordings.start_time >= check_timestamp + ) + else: + # get all recordings in the db + recordings = Recordings.select(Recordings.id, Recordings.path) + + # Use pagination to process records in chunks + page_size = 1000 + num_pages = (recordings.count() + page_size - 1) // page_size + recordings_to_delete = set() + + for page in range(num_pages): + for recording in recordings.paginate(page, page_size): + if not os.path.exists(recording.path): + recordings_to_delete.add(recording.id) + + if len(recordings_to_delete) == 0: + return True + + logger.info( + f"Deleting {len(recordings_to_delete)} recording DB entries with missing files" + ) + + # convert back to list of dictionaries for insertion + recordings_to_delete = [ + {"id": recording_id} for recording_id in recordings_to_delete + ] + + if float(len(recordings_to_delete)) / max(1, recordings.count()) > 0.5: + logger.warning( + f"Deleting {(len(recordings_to_delete) / max(1, recordings.count()) * 100):.2f}% of recordings DB entries, could be due to configuration error. Aborting..." + ) + return False + + # create a temporary table for deletion + RecordingsToDelete.create_table(temporary=True) + + # insert ids to the temporary table + max_inserts = 1000 + for batch in chunked(recordings_to_delete, max_inserts): + RecordingsToDelete.insert_many(batch).execute() + + try: + # delete records in the main table that exist in the temporary table + query = Recordings.delete().where( + Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id)) + ) + query.execute() + except DatabaseError as e: + logger.error(f"Database error during recordings db cleanup: {e}") + + return True + + def delete_files_without_db_entry(files_on_disk: list[str]): + """Delete files where file is not inside frigate db.""" + files_to_delete = [] + + for file in files_on_disk: + if not Recordings.select().where(Recordings.path == file).exists(): + files_to_delete.append(file) + + if len(files_to_delete) == 0: + return True + + logger.info( + f"Deleting {len(files_to_delete)} recordings files with missing DB entries" + ) + + if float(len(files_to_delete)) / max(1, len(files_on_disk)) > 0.5: + logger.debug( + f"Deleting {(len(files_to_delete) / max(1, len(files_on_disk)) * 100):.2f}% of recordings DB entries, could be due to configuration error. Aborting..." + ) + return False + + for file in files_to_delete: + os.unlink(file) + + return True + + logger.debug("Start sync recordings.") + + # start checking on the hour 36 hours ago + check_point = datetime.datetime.now().replace( + minute=0, second=0, microsecond=0 + ).astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36) + db_success = delete_db_entries_without_file(check_point.timestamp()) + + # only try to cleanup files if db cleanup was successful + if db_success: + if limited: + # get recording files from last 36 hours + hour_check = f"{RECORD_DIR}/{check_point.strftime('%Y-%m-%d/%H')}" + files_on_disk = { + os.path.join(root, file) + for root, _, files in os.walk(RECORD_DIR) + for file in files + if root > hour_check + } + else: + # get all recordings files on disk and put them in a set + files_on_disk = { + os.path.join(root, file) + for root, _, files in os.walk(RECORD_DIR) + for file in files + } + + delete_files_without_db_entry(files_on_disk) + + logger.debug("End sync recordings.") diff --git a/sam2-cpu/frigate-dev/frigate/review/__init__.py b/sam2-cpu/frigate-dev/frigate/review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/review/maintainer.py b/sam2-cpu/frigate-dev/frigate/review/maintainer.py new file mode 100644 index 0000000..917c0c5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/review/maintainer.py @@ -0,0 +1,863 @@ +"""Maintain review segments in db.""" + +import copy +import datetime +import json +import logging +import os +import random +import string +import sys +import threading +from multiprocessing.synchronize import Event as MpEvent +from pathlib import Path +from typing import Any, Optional + +import cv2 +import numpy as np + +from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.review_updater import ReviewDataPublisher +from frigate.config import CameraConfig, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + CLEAR_ONGOING_REVIEW_SEGMENTS, + CLIPS_DIR, + UPSERT_REVIEW_SEGMENT, +) +from frigate.models import ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.track.object_processing import ManualEventState, TrackedObject +from frigate.util.image import SharedMemoryFrameManager, calculate_16_9_crop + +logger = logging.getLogger(__name__) + + +THUMB_HEIGHT = 180 +THUMB_WIDTH = 320 + + +class PendingReviewSegment: + def __init__( + self, + camera: str, + frame_time: float, + severity: SeverityEnum, + detections: dict[str, str], + sub_labels: dict[str, str], + zones: list[str], + audio: set[str], + ): + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + self.id = f"{frame_time}-{rand_id}" + self.camera = camera + self.start_time = frame_time + self.severity = severity + self.detections = detections + self.sub_labels = sub_labels + self.zones = zones + self.audio = audio + self.thumb_time: float | None = None + self.last_alert_time: float | None = None + self.last_detection_time: float = frame_time + + if severity == SeverityEnum.alert: + self.last_alert_time = frame_time + + # thumbnail + self._frame = np.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8) + self.has_frame = False + self.frame_active_count = 0 + self.frame_path = os.path.join( + CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp" + ) + + def update_frame( + self, camera_config: CameraConfig, frame, objects: list[TrackedObject] + ): + min_x = camera_config.frame_shape[1] + min_y = camera_config.frame_shape[0] + max_x = 0 + max_y = 0 + + # find bounds for all boxes + for o in objects: + min_x = min(o["box"][0], min_x) + min_y = min(o["box"][1], min_y) + max_x = max(o["box"][2], max_x) + max_y = max(o["box"][3], max_y) + + region = calculate_16_9_crop( + camera_config.frame_shape, min_x, min_y, max_x, max_y + ) + + # could not find suitable 16:9 region + if not region: + return + + self.frame_active_count = len(objects) + color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + color_frame = color_frame[region[1] : region[3], region[0] : region[2]] + width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) + self._frame = cv2.resize( + color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA + ) + + if self._frame is not None: + self.thumb_time = datetime.datetime.now().timestamp() + self.has_frame = True + cv2.imwrite( + self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + + def save_full_frame(self, camera_config: CameraConfig, frame): + color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) + width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) + self._frame = cv2.resize( + color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA + ) + + if self._frame is not None: + self.has_frame = True + cv2.imwrite( + self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + + def get_data(self, ended: bool) -> dict: + end_time = None + + if ended: + if self.severity == SeverityEnum.alert: + end_time = self.last_alert_time + else: + end_time = self.last_detection_time + + return copy.deepcopy( + { + ReviewSegment.id.name: self.id, + ReviewSegment.camera.name: self.camera, + ReviewSegment.start_time.name: self.start_time, + ReviewSegment.end_time.name: end_time, + ReviewSegment.severity.name: self.severity.value, + ReviewSegment.thumb_path.name: self.frame_path, + ReviewSegment.data.name: { + "detections": list(set(self.detections.keys())), + "objects": list(set(self.detections.values())), + "verified_objects": [ + o for o in self.detections.values() if "-verified" in o + ], + "sub_labels": list(self.sub_labels.values()), + "zones": self.zones, + "audio": list(self.audio), + "thumb_time": self.thumb_time, + "metadata": None, + }, + } + ) + + +class ActiveObjects: + def __init__( + self, + frame_time: float, + camera_config: CameraConfig, + all_objects: list[TrackedObject], + ): + self.camera_config = camera_config + + # get current categorization of objects to know if + # these objects are currently being categorized + self.categorized_objects = { + "alerts": [], + "detections": [], + } + + for o in all_objects: + if ( + o["motionless_count"] >= camera_config.detect.stationary.threshold + and not o["pending_loitering"] + ): + # no stationary objects unless loitering + continue + + if o["position_changes"] == 0: + # object must have moved at least once + continue + + if o["frame_time"] != frame_time: + # object must be detected in this frame + continue + + if o["false_positive"]: + # object must not be a false positive + continue + + if ( + o["label"] in camera_config.review.alerts.labels + and ( + not camera_config.review.alerts.required_zones + or ( + len(o["current_zones"]) > 0 + and set(o["current_zones"]) + & set(camera_config.review.alerts.required_zones) + ) + ) + and camera_config.review.alerts.enabled + ): + self.categorized_objects["alerts"].append(o) + continue + + if ( + ( + camera_config.review.detections.labels is None + or o["label"] in camera_config.review.detections.labels + ) + and ( + not camera_config.review.detections.required_zones + or ( + len(o["current_zones"]) > 0 + and set(o["current_zones"]) + & set(camera_config.review.detections.required_zones) + ) + ) + and camera_config.review.detections.enabled + ): + self.categorized_objects["detections"].append(o) + continue + + def has_active_objects(self) -> bool: + return ( + len(self.categorized_objects["alerts"]) > 0 + or len(self.categorized_objects["detections"]) > 0 + ) + + def has_activity_category(self, severity: SeverityEnum) -> bool: + if ( + severity == SeverityEnum.alert + and len(self.categorized_objects["alerts"]) > 0 + ): + return True + + if ( + severity == SeverityEnum.detection + and len(self.categorized_objects["detections"]) > 0 + ): + return True + + return False + + def get_all_objects(self) -> list[TrackedObject]: + return ( + self.categorized_objects["alerts"] + self.categorized_objects["detections"] + ) + + +class ReviewSegmentMaintainer(threading.Thread): + """Maintain review segments.""" + + def __init__(self, config: FrigateConfig, stop_event: MpEvent): + super().__init__(name="review_segment_maintainer") + self.config = config + self.active_review_segments: dict[str, Optional[PendingReviewSegment]] = {} + self.frame_manager = SharedMemoryFrameManager() + + # create communication for review segments + self.requestor = InterProcessRequestor() + self.config_subscriber = CameraConfigUpdateSubscriber( + config, + config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.record, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.review, + ], + ) + self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.all.value) + self.review_publisher = ReviewDataPublisher("") + + # manual events + self.indefinite_events: dict[str, dict[str, Any]] = {} + + # ensure dirs + Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True) + + self.stop_event = stop_event + + # clear ongoing review segments from last instance + self.requestor.send_data(CLEAR_ONGOING_REVIEW_SEGMENTS, "") + + def _publish_segment_start( + self, + segment: PendingReviewSegment, + ) -> None: + """New segment.""" + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + start_data = {k: v for k, v in new_data.items()} + review_update = { + "type": "new", + "before": start_data, + "after": start_data, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) + self.requestor.send_data( + f"{segment.camera}/review_status", segment.severity.value.upper() + ) + + def _publish_segment_update( + self, + segment: PendingReviewSegment, + camera_config: CameraConfig, + frame, + objects: list[TrackedObject], + prev_data: dict[str, Any], + ) -> None: + """Update segment.""" + if frame is not None: + segment.update_frame(camera_config, frame, objects) + + new_data = segment.get_data(ended=False) + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) + review_update = { + "type": "update", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in new_data.items()}, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) + self.requestor.send_data( + f"{segment.camera}/review_status", segment.severity.value.upper() + ) + + def _publish_segment_end( + self, + segment: PendingReviewSegment, + prev_data: dict[str, Any], + ) -> float: + """End segment.""" + final_data = segment.get_data(ended=True) + end_time = final_data[ReviewSegment.end_time.name] + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, final_data) + review_update = { + "type": "end", + "before": {k: v for k, v in prev_data.items()}, + "after": {k: v for k, v in final_data.items()}, + } + self.requestor.send_data( + "reviews", + json.dumps(review_update), + ) + self.review_publisher.publish(review_update, segment.camera) + self.requestor.send_data(f"{segment.camera}/review_status", "NONE") + self.active_review_segments[segment.camera] = None + return end_time + + def forcibly_end_segment(self, camera: str) -> float: + """Forcibly end the pending segment for a camera.""" + segment = self.active_review_segments.get(camera) + if segment: + prev_data = segment.get_data(False) + return self._publish_segment_end(segment, prev_data) + + def update_existing_segment( + self, + segment: PendingReviewSegment, + frame_name: str, + frame_time: float, + objects: list[TrackedObject], + ) -> None: + """Validate if existing review segment should continue.""" + camera_config = self.config.cameras[segment.camera] + + # get active objects + objects loitering in loitering zones + activity = ActiveObjects(frame_time, camera_config, objects) + prev_data = segment.get_data(False) + has_activity = False + + if activity.has_active_objects(): + has_activity = True + should_update_image = False + should_update_state = False + + if activity.has_activity_category(SeverityEnum.alert): + # update current time for last alert activity + segment.last_alert_time = frame_time + + if segment.severity != SeverityEnum.alert: + # if segment is not alert category but current activity is + # update this segment to be an alert + segment.severity = SeverityEnum.alert + should_update_state = True + should_update_image = True + + if activity.has_activity_category(SeverityEnum.detection): + segment.last_detection_time = frame_time + + for object in activity.get_all_objects(): + # Alert-level objects should always be added (they extend/upgrade the segment) + # Detection-level objects should only be added if: + # - The segment is a detection segment (matching severity), OR + # - The segment is an alert AND the object started before the alert ended + # (objects starting after will be in the new detection segment) + is_alert_object = object in activity.categorized_objects["alerts"] + + if not is_alert_object and segment.severity == SeverityEnum.alert: + # This is a detection-level object + # Only add if it started during the alert's active period + if object["start_time"] > segment.last_alert_time: + continue + + if not object["sub_label"]: + segment.detections[object["id"]] = object["label"] + elif object["sub_label"][0] in self.config.model.all_attributes: + segment.detections[object["id"]] = object["sub_label"][0] + else: + segment.detections[object["id"]] = f"{object['label']}-verified" + segment.sub_labels[object["id"]] = object["sub_label"][0] + + # keep zones up to date + if len(object["current_zones"]) > 0: + for zone in object["current_zones"]: + if zone not in segment.zones: + segment.zones.append(zone) + + if len(activity.get_all_objects()) > segment.frame_active_count: + should_update_state = True + should_update_image = True + + if prev_data["data"]["sub_labels"] != list(segment.sub_labels.values()): + should_update_state = True + + if should_update_state: + try: + if should_update_image: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + else: + yuv_frame = None + + self._publish_segment_update( + segment, + camera_config, + yuv_frame, + activity.get_all_objects(), + prev_data, + ) + self.frame_manager.close(frame_name) + except FileNotFoundError: + return + + if not has_activity: + if not segment.has_frame: + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + + segment.save_full_frame(camera_config, yuv_frame) + self.frame_manager.close(frame_name) + self._publish_segment_update( + segment, camera_config, None, [], prev_data + ) + except FileNotFoundError: + return + + if segment.severity == SeverityEnum.alert and frame_time > ( + segment.last_alert_time + camera_config.review.alerts.cutoff_time + ): + needs_new_detection = ( + segment.last_detection_time > segment.last_alert_time + and ( + segment.last_detection_time + + camera_config.review.detections.cutoff_time + ) + > frame_time + ) + last_detection_time = segment.last_detection_time + + end_time = self._publish_segment_end(segment, prev_data) + + if needs_new_detection: + new_detections: dict[str, str] = {} + new_zones = set() + + for o in activity.categorized_objects["detections"]: + new_detections[o["id"]] = o["label"] + new_zones.update(o["current_zones"]) + + if new_detections: + self.active_review_segments[activity.camera_config.name] = ( + PendingReviewSegment( + activity.camera_config.name, + end_time, + SeverityEnum.detection, + new_detections, + sub_labels={}, + audio=set(), + zones=list(new_zones), + ) + ) + self._publish_segment_start( + self.active_review_segments[activity.camera_config.name] + ) + self.active_review_segments[ + activity.camera_config.name + ].last_detection_time = last_detection_time + elif segment.severity == SeverityEnum.detection and frame_time > ( + segment.last_detection_time + + camera_config.review.detections.cutoff_time + ): + self._publish_segment_end(segment, prev_data) + + def check_if_new_segment( + self, + camera: str, + frame_name: str, + frame_time: float, + objects: list[TrackedObject], + ) -> None: + """Check if a new review segment should be created.""" + camera_config = self.config.cameras[camera] + activity = ActiveObjects(frame_time, camera_config, objects) + + if activity.has_active_objects(): + detections: dict[str, str] = {} + sub_labels: dict[str, str] = {} + zones: list[str] = [] + severity: SeverityEnum | None = None + + # if activity is alert category mark this review as alert + if severity != SeverityEnum.alert and activity.has_activity_category( + SeverityEnum.alert + ): + severity = SeverityEnum.alert + + # if object is detection label and not already higher severity + # mark this review as detection + if not severity and activity.has_activity_category(SeverityEnum.detection): + severity = SeverityEnum.detection + + for object in activity.get_all_objects(): + if not object["sub_label"]: + detections[object["id"]] = object["label"] + elif object["sub_label"][0] in self.config.model.all_attributes: + detections[object["id"]] = object["sub_label"][0] + else: + detections[object["id"]] = f"{object['label']}-verified" + sub_labels[object["id"]] = object["sub_label"][0] + + for zone in object["current_zones"]: + if zone not in zones: + zones.append(zone) + + if severity: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + severity, + detections, + sub_labels=sub_labels, + audio=set(), + zones=zones, + ) + + try: + yuv_frame = self.frame_manager.get( + frame_name, camera_config.frame_shape_yuv + ) + + if yuv_frame is None: + logger.debug(f"Failed to get frame {frame_name} from SHM") + return + + self.active_review_segments[camera].update_frame( + camera_config, yuv_frame, activity.get_all_objects() + ) + self.frame_manager.close(frame_name) + self._publish_segment_start(self.active_review_segments[camera]) + except FileNotFoundError: + return + + def run(self) -> None: + while not self.stop_event.is_set(): + # check if there is an updated config + updated_topics = self.config_subscriber.check_for_updates() + + if "record" in updated_topics: + for camera in updated_topics["record"]: + self.forcibly_end_segment(camera) + + if "enabled" in updated_topics: + for camera in updated_topics["enabled"]: + self.forcibly_end_segment(camera) + + (topic, data) = self.detection_subscriber.check_for_update(timeout=1) + + if not topic: + continue + + if topic == DetectionTypeEnum.video.value: + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + _, + _, + ) = data + elif topic == DetectionTypeEnum.audio.value: + ( + camera, + frame_time, + _, + audio_detections, + ) = data + elif topic == DetectionTypeEnum.api.value or DetectionTypeEnum.lpr.value: + ( + camera, + frame_time, + manual_info, + ) = data + + if camera not in self.indefinite_events: + self.indefinite_events[camera] = {} + + if ( + not self.config.cameras[camera].enabled + or not self.config.cameras[camera].record.enabled + ): + continue + + current_segment = self.active_review_segments.get(camera) + + # Check if the current segment should be processed based on enabled settings + if current_segment: + if ( + current_segment.severity == SeverityEnum.alert + and not self.config.cameras[camera].review.alerts.enabled + ) or ( + current_segment.severity == SeverityEnum.detection + and not self.config.cameras[camera].review.detections.enabled + ): + self.forcibly_end_segment(camera) + continue + + # If we reach here, the segment can be processed (if it exists) + if current_segment is not None: + if topic == DetectionTypeEnum.video: + self.update_existing_segment( + current_segment, + frame_name, + frame_time, + current_tracked_objects, + ) + elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: + camera_config = self.config.cameras[camera] + + for audio in audio_detections: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): + current_segment.audio.add(audio) + current_segment.severity = SeverityEnum.alert + current_segment.last_alert_time = frame_time + elif ( + camera_config.review.detections.labels is None + or audio in camera_config.review.detections.labels + ) and camera_config.review.detections.enabled: + current_segment.audio.add(audio) + current_segment.last_detection_time = frame_time + elif topic == DetectionTypeEnum.api or topic == DetectionTypeEnum.lpr: + if manual_info["state"] == ManualEventState.complete: + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + if ( + topic == DetectionTypeEnum.api + and self.config.cameras[camera].review.alerts.enabled + ): + current_segment.severity = SeverityEnum.alert + elif ( + topic == DetectionTypeEnum.lpr + and self.config.cameras[camera].review.detections.enabled + ): + current_segment.severity = SeverityEnum.detection + current_segment.last_alert_time = manual_info["end_time"] + elif manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + current_segment.detections[manual_info["event_id"]] = ( + manual_info["label"] + ) + if ( + topic == DetectionTypeEnum.api + and self.config.cameras[camera].review.alerts.enabled + ): + current_segment.severity = SeverityEnum.alert + elif ( + topic == DetectionTypeEnum.lpr + and self.config.cameras[camera].review.detections.enabled + ): + current_segment.severity = SeverityEnum.detection + + # temporarily make it so this event can not end + current_segment.last_alert_time = sys.maxsize + current_segment.last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.end: + event_id = manual_info["event_id"] + + if event_id in self.indefinite_events[camera]: + self.indefinite_events[camera].pop(event_id) + + if len(self.indefinite_events[camera]) == 0: + current_segment.last_alert_time = manual_info[ + "end_time" + ] + current_segment.last_detection_time = manual_info[ + "end_time" + ] + else: + logger.error( + f"Event with ID {event_id} has a set duration and can not be ended manually." + ) + else: + if topic == DetectionTypeEnum.video: + if ( + self.config.cameras[camera].review.alerts.enabled + or self.config.cameras[camera].review.detections.enabled + ): + self.check_if_new_segment( + camera, + frame_name, + frame_time, + current_tracked_objects, + ) + elif topic == DetectionTypeEnum.audio and len(audio_detections) > 0: + severity = None + + camera_config = self.config.cameras[camera] + detections = set() + + for audio in audio_detections: + if ( + audio in camera_config.review.alerts.labels + and camera_config.review.alerts.enabled + ): + detections.add(audio) + severity = SeverityEnum.alert + elif ( + camera_config.review.detections.labels is None + or audio in camera_config.review.detections.labels + ) and camera_config.review.detections.enabled: + detections.add(audio) + + if not severity: + severity = SeverityEnum.detection + + if severity: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + severity, + {}, + {}, + [], + detections, + ) + elif topic == DetectionTypeEnum.api: + if self.config.cameras[camera].review.alerts.enabled: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + SeverityEnum.alert, + {manual_info["event_id"]: manual_info["label"]}, + {}, + [], + set(), + ) + + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + self.active_review_segments[ + camera + ].last_alert_time = sys.maxsize + self.active_review_segments[ + camera + ].last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + self.active_review_segments[ + camera + ].last_alert_time = manual_info["end_time"] + self.active_review_segments[ + camera + ].last_detection_time = manual_info["end_time"] + else: + logger.warning( + f"Manual event API has been called for {camera}, but alerts are disabled. This manual event will not appear as an alert." + ) + elif topic == DetectionTypeEnum.lpr: + if self.config.cameras[camera].review.detections.enabled: + self.active_review_segments[camera] = PendingReviewSegment( + camera, + frame_time, + SeverityEnum.detection, + {manual_info["event_id"]: manual_info["label"]}, + {}, + [], + set(), + ) + + if manual_info["state"] == ManualEventState.start: + self.indefinite_events[camera][manual_info["event_id"]] = ( + manual_info["label"] + ) + # temporarily make it so this event can not end + self.active_review_segments[ + camera + ].last_alert_time = sys.maxsize + self.active_review_segments[ + camera + ].last_detection_time = sys.maxsize + elif manual_info["state"] == ManualEventState.complete: + self.active_review_segments[ + camera + ].last_alert_time = manual_info["end_time"] + self.active_review_segments[ + camera + ].last_detection_time = manual_info["end_time"] + else: + logger.warning( + f"Dedicated LPR camera API has been called for {camera}, but detections are disabled. LPR events will not appear as a detection." + ) + + self.config_subscriber.stop() + self.requestor.stop() + self.detection_subscriber.stop() + logger.info("Exiting review maintainer...") diff --git a/sam2-cpu/frigate-dev/frigate/review/review.py b/sam2-cpu/frigate-dev/frigate/review/review.py new file mode 100644 index 0000000..c00c302 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/review/review.py @@ -0,0 +1,30 @@ +"""Run recording maintainer and cleanup.""" + +import logging +from multiprocessing.synchronize import Event as MpEvent + +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_MED +from frigate.review.maintainer import ReviewSegmentMaintainer +from frigate.util.process import FrigateProcess + +logger = logging.getLogger(__name__) + + +class ReviewProcess(FrigateProcess): + def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_MED, + name="frigate.review_segment_manager", + daemon=True, + ) + self.config = config + + def run(self) -> None: + self.pre_run_setup(self.config.logger) + maintainer = ReviewSegmentMaintainer( + self.config, + self.stop_event, + ) + maintainer.start() diff --git a/sam2-cpu/frigate-dev/frigate/review/types.py b/sam2-cpu/frigate-dev/frigate/review/types.py new file mode 100644 index 0000000..0046f9b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/review/types.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class SeverityEnum(str, Enum): + alert = "alert" + detection = "detection" diff --git a/sam2-cpu/frigate-dev/frigate/service_manager/__init__.py b/sam2-cpu/frigate-dev/frigate/service_manager/__init__.py new file mode 100644 index 0000000..2da23b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/service_manager/__init__.py @@ -0,0 +1,4 @@ +from .multiprocessing import ServiceProcess +from .service import Service, ServiceManager + +__all__ = ["Service", "ServiceProcess", "ServiceManager"] diff --git a/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing.py b/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing.py new file mode 100644 index 0000000..87bb4ff --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing.py @@ -0,0 +1,163 @@ +import asyncio +import faulthandler +import logging +import multiprocessing as mp +import signal +import sys +import threading +from abc import ABC, abstractmethod +from asyncio.exceptions import TimeoutError +from logging.handlers import QueueHandler +from types import FrameType +from typing import Optional + +import frigate.log + +from .multiprocessing_waiter import wait as mp_wait +from .service import Service, ServiceManager + +DEFAULT_STOP_TIMEOUT = 10 # seconds + + +class BaseServiceProcess(Service, ABC): + """A Service the manages a multiprocessing.Process.""" + + _process: Optional[mp.Process] + + def __init__( + self, + *, + name: Optional[str] = None, + manager: Optional[ServiceManager] = None, + ) -> None: + super().__init__(name=name, manager=manager) + + self._process = None + + async def on_start(self) -> None: + if self._process is not None: + if self._process.is_alive(): + return # Already started. + else: + self._process.close() + + # At this point, the process is either stopped or dead, so we can recreate it. + self._process = mp.Process(target=self._run) + self._process.name = self.name + self._process.daemon = True + self.before_start() + self._process.start() + self.after_start() + + self.manager.logger.info(f"Started {self.name} (pid: {self._process.pid})") + + async def on_stop( + self, + *, + force: bool = False, + timeout: Optional[float] = None, + ) -> None: + if timeout is None: + timeout = DEFAULT_STOP_TIMEOUT + + if self._process is None: + return # Already stopped. + + running = True + + if not force: + self._process.terminate() + try: + await asyncio.wait_for(mp_wait(self._process), timeout) + running = False + except TimeoutError: + self.manager.logger.warning( + f"{self.name} is still running after {timeout} seconds. Killing." + ) + + if running: + self._process.kill() + await mp_wait(self._process) + + self._process.close() + self._process = None + + self.manager.logger.info(f"{self.name} stopped") + + @property + def pid(self) -> Optional[int]: + return self._process.pid if self._process else None + + def _run(self) -> None: + self.before_run() + self.run() + self.after_run() + + def before_start(self) -> None: + pass + + def after_start(self) -> None: + pass + + def before_run(self) -> None: + pass + + def after_run(self) -> None: + pass + + @abstractmethod + def run(self) -> None: + pass + + def __getstate__(self) -> dict: + return { + k: v + for k, v in self.__dict__.items() + if not (k.startswith("_Service__") or k == "_process") + } + + +class ServiceProcess(BaseServiceProcess): + logger: logging.Logger + + @property + def stop_event(self) -> threading.Event: + # Lazily create the stop_event. This allows the signal handler to tell if anyone is + # monitoring the stop event, and to raise a SystemExit if not. + if "stop_event" not in self.__dict__: + stop_event = threading.Event() + self.__dict__["stop_event"] = stop_event + else: + stop_event = self.__dict__["stop_event"] + assert isinstance(stop_event, threading.Event) + + return stop_event + + def before_start(self) -> None: + if frigate.log.log_listener is None: + raise RuntimeError("Logging has not yet been set up.") + self.__log_queue = frigate.log.log_listener.queue + + def before_run(self) -> None: + super().before_run() + + faulthandler.enable() + + def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: + # Get the stop_event through the dict to bypass lazy initialization. + stop_event = self.__dict__.get("stop_event") + if stop_event is not None: + # Someone is monitoring stop_event. We should set it. + stop_event.set() + else: + # Nobody is monitoring stop_event. We should raise SystemExit. + sys.exit() + + signal.signal(signal.SIGTERM, receiveSignal) + signal.signal(signal.SIGINT, receiveSignal) + + self.logger = logging.getLogger(self.name) + + logging.basicConfig(handlers=[], force=True) + logging.getLogger().addHandler(QueueHandler(self.__log_queue)) + del self.__log_queue diff --git a/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing_waiter.py b/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing_waiter.py new file mode 100644 index 0000000..8acdf58 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/service_manager/multiprocessing_waiter.py @@ -0,0 +1,150 @@ +import asyncio +import functools +import logging +import multiprocessing as mp +import queue +import threading +from multiprocessing.connection import Connection +from multiprocessing.connection import wait as mp_wait +from socket import socket +from typing import Any, Optional, Union + +logger = logging.getLogger(__name__) + + +class MultiprocessingWaiter(threading.Thread): + """A background thread that manages futures for the multiprocessing.connection.wait() method.""" + + def __init__(self) -> None: + super().__init__(daemon=True) + + # Queue of objects to wait for and futures to set results for. + self._queue: queue.Queue[tuple[Any, asyncio.Future[None]]] = queue.Queue() + + # This is required to get mp_wait() to wake up when new objects to wait for are received. + receive, send = mp.Pipe(duplex=False) + self._receive_connection = receive + self._send_connection = send + + def wait_for_sentinel(self, sentinel: Any) -> asyncio.Future[None]: + """Create an asyncio.Future tracking a sentinel for multiprocessing.connection.wait() + + Warning: This method is NOT thread-safe. + """ + # This would be incredibly stupid, but you never know. + assert sentinel != self._receive_connection + + # Send the future to the background thread for processing. + future = asyncio.get_running_loop().create_future() + self._queue.put((sentinel, future)) + + # Notify the background thread. + # + # This is the non-thread-safe part, but since this method is not really meant to be called + # by users, we can get away with not adding a lock at this point (to avoid adding 2 locks). + self._send_connection.send_bytes(b".") + + return future + + def run(self) -> None: + logger.debug("Started background thread") + + wait_dict: dict[Any, set[asyncio.Future[None]]] = { + self._receive_connection: set() + } + while True: + for ready_obj in mp_wait(wait_dict.keys()): + # Make sure we never remove the receive connection from the wait dict + if ready_obj is self._receive_connection: + continue + + logger.debug( + f"Sentinel {ready_obj!r} is ready. " + f"Notifying {len(wait_dict[ready_obj])} future(s)." + ) + + # Go over all the futures attached to this object and mark them as ready. + for fut in wait_dict.pop(ready_obj): + if fut.cancelled(): + logger.debug( + f"A future for sentinel {ready_obj!r} is ready, " + "but the future is cancelled. Skipping." + ) + else: + fut.get_loop().call_soon_threadsafe( + # Note: We need to check fut.cancelled() again, since it might + # have been set before the event loop's definition of "soon". + functools.partial( + lambda fut: fut.cancelled() or fut.set_result(None), fut + ) + ) + + # Check for cancellations in the remaining futures. + done_objects = [] + for obj, fut_set in wait_dict.items(): + if obj is self._receive_connection: + continue + + # Find any cancelled futures and remove them. + cancelled = [fut for fut in fut_set if fut.cancelled()] + fut_set.difference_update(cancelled) + logger.debug( + f"Removing {len(cancelled)} future(s) from sentinel: {obj!r}" + ) + + # Mark objects with no remaining futures for removal. + if len(fut_set) == 0: + done_objects.append(obj) + + # Remove any objects that are done after removing cancelled futures. + for obj in done_objects: + logger.debug( + f"Sentinel {obj!r} no longer has any futures waiting for it." + ) + del wait_dict[obj] + + # Get new objects to wait for from the queue. + while True: + try: + obj, fut = self._queue.get_nowait() + self._receive_connection.recv_bytes(maxlength=1) + self._queue.task_done() + + logger.debug(f"Received new sentinel: {obj!r}") + + wait_dict.setdefault(obj, set()).add(fut) + except queue.Empty: + break + + +waiter_lock = threading.Lock() +waiter_thread: Optional[MultiprocessingWaiter] = None + + +async def wait(object: Union[mp.Process, Connection, socket]) -> None: + """Wait for the supplied object to be ready. + + Under the hood, this uses multiprocessing.connection.wait() and a background thread manage the + returned futures. + """ + global waiter_thread, waiter_lock + + sentinel: Union[Connection, socket, int] + if isinstance(object, mp.Process): + sentinel = object.sentinel + elif isinstance(object, Connection) or isinstance(object, socket): + sentinel = object + else: + raise ValueError(f"Cannot wait for object of type {type(object).__qualname__}") + + with waiter_lock: + if waiter_thread is None: + # Start a new waiter thread. + waiter_thread = MultiprocessingWaiter() + waiter_thread.start() + + # Create the future while still holding the lock, + # since wait_for_sentinel() is not thread safe. + fut = waiter_thread.wait_for_sentinel(sentinel) + + await fut diff --git a/sam2-cpu/frigate-dev/frigate/service_manager/service.py b/sam2-cpu/frigate-dev/frigate/service_manager/service.py new file mode 100644 index 0000000..89d766e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/service_manager/service.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +import asyncio +import atexit +import logging +import threading +from abc import ABC, abstractmethod +from contextvars import ContextVar +from dataclasses import dataclass +from functools import partial +from typing import Coroutine, Optional, Union, cast + +from typing_extensions import Self + + +class Service(ABC): + """An abstract service instance.""" + + def __init__( + self, + *, + name: Optional[str] = None, + manager: Optional[ServiceManager] = None, + ): + if name: + self.__dict__["name"] = name + + self.__manager = manager or ServiceManager.current() + self.__lock = asyncio.Lock(loop=self.__manager._event_loop) # type: ignore[call-arg] + self.__manager._register(self) + + @property + def name(self) -> str: + try: + return cast(str, self.__dict__["name"]) + except KeyError: + return type(self).__qualname__ + + @property + def manager(self) -> ServiceManager: + """The service manager this service is registered with.""" + try: + return self.__manager + except AttributeError: + raise RuntimeError("Cannot access associated service manager") + + def start( + self, + *, + wait: bool = False, + wait_timeout: Optional[float] = None, + ) -> Self: + """Start this service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_start(), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + def stop( + self, + *, + force: bool = False, + timeout: Optional[float] = None, + wait: bool = False, + wait_timeout: Optional[float] = None, + ) -> Self: + """Stop this service. + + :param force: If set, the service will be killed immediately. + :param timeout: Maximum amount of time to wait before force-killing the service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_stop(force=force, timeout=timeout), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + def restart( + self, + *, + force: bool = False, + stop_timeout: Optional[float] = None, + wait: bool = False, + wait_timeout: Optional[float] = None, + ) -> Self: + """Restart this service. + + :param force: If set, the service will be killed immediately. + :param timeout: Maximum amount of time to wait before force-killing the service. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + self.manager.run_task( + self.on_restart(force=force, stop_timeout=stop_timeout), + wait=wait, + wait_timeout=wait_timeout, + lock=self.__lock, + ) + + return self + + @abstractmethod + async def on_start(self) -> None: + pass + + @abstractmethod + async def on_stop( + self, + *, + force: bool = False, + timeout: Optional[float] = None, + ) -> None: + pass + + async def on_restart( + self, + *, + force: bool = False, + stop_timeout: Optional[float] = None, + ) -> None: + await self.on_stop(force=force, timeout=stop_timeout) + await self.on_start() + + +default_service_manager_lock = threading.Lock() +default_service_manager: Optional[ServiceManager] = None + +current_service_manager: ContextVar[ServiceManager] = ContextVar( + "current_service_manager" +) + + +@dataclass +class Command: + """A coroutine to execute in the service manager thread. + + Attributes: + coro: The coroutine to execute. + lock: An async lock to acquire before calling the coroutine. + done: If specified, the service manager will set this event after the command completes. + """ + + coro: Coroutine + lock: Optional[asyncio.Lock] = None + done: Optional[threading.Event] = None + + +class ServiceManager: + """A set of services, along with the global state required to manage them efficiently. + + Typically users of the service infrastructure will not interact with a service manager directly, + but rather through individual Service subclasses that will automatically manage a service + manager instance. + + Each service manager instance has a background thread in which service lifecycle tasks are + executed in an async executor. This is done to avoid head-of-line blocking in the business logic + that spins up individual services. This thread is automatically started when the service manager + is created and stopped either manually, or on application exit. + + All (public) service manager methods are thread-safe. + """ + + _name: str + _logger: logging.Logger + + # The set of services this service manager knows about. + _services: dict[str, Service] + _services_lock: threading.Lock + + # Commands will be queued with associated event loop. Queueing `None` signals shutdown. + _command_queue: asyncio.Queue[Union[Command, None]] + _event_loop: asyncio.AbstractEventLoop + + # The pending command counter is used to ensure all commands have been queued before shutdown. + _pending_commands: AtomicCounter + + # The set of pending tasks after they have been received by the background thread and spawned. + _tasks: set + + # Fired after the async runtime starts. Object initialization completes after this is set. + _setup_event: threading.Event + + # Will be acquired to ensure the shutdown sentinel is sent only once. Never released. + _shutdown_lock: threading.Lock + + def __init__(self, *, name: Optional[str] = None): + self._name = name if name is not None else (__package__ or __name__) + self._logger = logging.getLogger(self.name) + + self._services = dict() + self._services_lock = threading.Lock() + + self._pending_commands = AtomicCounter() + self._tasks = set() + + self._shutdown_lock = threading.Lock() + + # --- Start the manager thread and wait for it to be ready. --- + + self._setup_event = threading.Event() + + async def start_manager() -> None: + self._event_loop = asyncio.get_running_loop() + self._command_queue = asyncio.Queue() + + self._setup_event.set() + await self._monitor_command_queue() + + self._manager_thread = threading.Thread( + name=self.name, + target=lambda: asyncio.run(start_manager()), + daemon=True, + ) + + self._manager_thread.start() + atexit.register(partial(self.shutdown, wait=True)) + + self._setup_event.wait() + + @property + def name(self) -> str: + """The name of this service manager. Primarily intended for logging purposes.""" + return self._name + + @property + def logger(self) -> logging.Logger: + """The logger used by this service manager.""" + return self._logger + + @classmethod + def current(cls) -> ServiceManager: + """The service manager set in the current context (async task or thread). + + A global default service manager will be automatically created on first access.""" + + global default_service_manager + + current = current_service_manager.get(None) + if current is None: + with default_service_manager_lock: + if default_service_manager is None: + default_service_manager = cls() + + current = default_service_manager + current_service_manager.set(current) + return current + + def make_current(self) -> None: + """Make this the current service manager.""" + + current_service_manager.set(self) + + def run_task( + self, + coro: Coroutine, + *, + wait: bool = False, + wait_timeout: Optional[float] = None, + lock: Optional[asyncio.Lock] = None, + ) -> None: + """Run an async task in the service manager thread. + + :param wait: If set, this function will block until the task is complete. + :param wait_timeout: If set, this function will not return until the task is complete or the + specified timeout has elapsed. + """ + + if not isinstance(coro, Coroutine): + raise TypeError(f"Cannot schedule task for object of type {type(coro)}") + + cmd = Command(coro=coro, lock=lock) + if wait or wait_timeout is not None: + cmd.done = threading.Event() + + self._send_command(cmd) + + if cmd.done is not None: + cmd.done.wait(timeout=wait_timeout) + + def shutdown( + self, *, wait: bool = False, wait_timeout: Optional[float] = None + ) -> None: + """Shutdown the service manager thread. + + After the shutdown process completes, any subsequent calls to the service manager will + produce an error. + + :param wait: If set, this function will block until the shutdown process is complete. + :param wait_timeout: If set, this function will not return until the shutdown process is + complete or the specified timeout has elapsed. + """ + + if self._shutdown_lock.acquire(blocking=False): + self._send_command(None) + if wait: + self._manager_thread.join(timeout=wait_timeout) + + def _ensure_running(self) -> None: + self._setup_event.wait() + if not self._manager_thread.is_alive(): + raise RuntimeError(f"ServiceManager {self.name} is not running") + + def _send_command(self, command: Union[Command, None]) -> None: + self._ensure_running() + + async def queue_command() -> None: + await self._command_queue.put(command) + self._pending_commands.sub() + + self._pending_commands.add() + asyncio.run_coroutine_threadsafe(queue_command(), self._event_loop) + + def _register(self, service: Service) -> None: + """Register a service with the service manager. This is done by the service constructor.""" + + self._ensure_running() + with self._services_lock: + name_conflict: Optional[Service] = next( + ( + existing + for name, existing in self._services.items() + if name == service.name + ), + None, + ) + + if name_conflict is service: + raise RuntimeError(f"Attempt to re-register service: {service.name}") + elif name_conflict is not None: + raise RuntimeError(f"Duplicate service name: {service.name}") + + self.logger.debug(f"Registering service: {service.name}") + self._services[service.name] = service + + def _run_command(self, command: Command) -> None: + """Execute a command and add it to the tasks set.""" + + def task_done(task: asyncio.Task) -> None: + exc = task.exception() + if exc: + self.logger.exception("Exception in service manager task", exc_info=exc) + self._tasks.discard(task) + if command.done is not None: + command.done.set() + + async def task_harness() -> None: + if command.lock is not None: + async with command.lock: + await command.coro + else: + await command.coro + + task = asyncio.create_task(task_harness()) + task.add_done_callback(task_done) + self._tasks.add(task) + + async def _monitor_command_queue(self) -> None: + """The main function of the background thread.""" + + self.logger.info("Started service manager") + + # Main command processing loop. + while (command := await self._command_queue.get()) is not None: + self._run_command(command) + + # Send a stop command to all services. We don't have a status command yet, so we can just + # stop everything and be done with it. + with self._services_lock: + self.logger.debug(f"Stopping {len(self._services)} services") + for service in self._services.values(): + service.stop() + + # Wait for all commands to finish executing. + await self._shutdown() + + self.logger.info("Exiting service manager") + + async def _shutdown(self) -> None: + """Ensure all commands have been queued & executed.""" + + while True: + command = None + try: + # Try and get a command from the queue. + command = self._command_queue.get_nowait() + except asyncio.QueueEmpty: + if self._pending_commands.value > 0: + # If there are pending commands to queue, await them. + command = await self._command_queue.get() + elif self._tasks: + # If there are still pending tasks, wait for them. These tasks might queue + # commands though, so we have to loop again. + await asyncio.wait(self._tasks) + else: + # Nothing is pending at this point, so we're done here. + break + + # If we got a command, run it. + if command is not None: + self._run_command(command) + + +class AtomicCounter: + """A lock-protected atomic counter.""" + + # Modern CPUs have atomics, but python doesn't seem to include them in the standard library. + # Besides, the performance penalty is negligible compared to, well, using python. + # So this will do just fine. + + def __init__(self, initial: int = 0): + self._lock = threading.Lock() + self._value = initial + + def add(self, value: int = 1) -> None: + with self._lock: + self._value += value + + def sub(self, value: int = 1) -> None: + with self._lock: + self._value -= value + + @property + def value(self) -> int: + with self._lock: + return self._value diff --git a/sam2-cpu/frigate-dev/frigate/stats/__init__.py b/sam2-cpu/frigate-dev/frigate/stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/stats/emitter.py b/sam2-cpu/frigate-dev/frigate/stats/emitter.py new file mode 100644 index 0000000..42d4c16 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/stats/emitter.py @@ -0,0 +1,101 @@ +"""Emit stats to listeners.""" + +import itertools +import json +import logging +import threading +import time +from multiprocessing.synchronize import Event as MpEvent +from typing import Any, Optional + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.const import FREQUENCY_STATS_POINTS +from frigate.stats.prometheus import update_metrics +from frigate.stats.util import stats_snapshot +from frigate.types import StatsTrackingTypes + +logger = logging.getLogger(__name__) + + +MAX_STATS_POINTS = 80 + + +class StatsEmitter(threading.Thread): + def __init__( + self, + config: FrigateConfig, + stats_tracking: StatsTrackingTypes, + stop_event: MpEvent, + ): + super().__init__(name="frigate_stats_emitter") + self.config = config + self.stats_tracking = stats_tracking + self.stop_event = stop_event + self.hwaccel_errors: list[str] = [] + self.stats_history: list[dict[str, Any]] = [] + + # create communication for stats + self.requestor = InterProcessRequestor() + + def get_latest_stats(self) -> dict[str, Any]: + """Get latest stats.""" + if len(self.stats_history) > 0: + return self.stats_history[-1] + else: + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + return stats + + def get_stats_history( + self, keys: Optional[list[str]] = None + ) -> list[dict[str, Any]]: + """Get stats history.""" + if not keys: + return self.stats_history + + selected_stats: list[dict[str, Any]] = [] + + for s in self.stats_history: + selected = {} + + for k in keys: + selected[k] = s.get(k) + + selected_stats.append(selected) + + return selected_stats + + def stats_init(config, camera_metrics, detectors, processes): + stats = { + "cameras": camera_metrics, + "detectors": detectors, + "processes": processes, + } + # Update Prometheus metrics with initial stats + update_metrics(stats) + return stats + + def run(self) -> None: + time.sleep(10) + for counter in itertools.cycle( + range(int(self.config.mqtt.stats_interval / FREQUENCY_STATS_POINTS)) + ): + if self.stop_event.wait(FREQUENCY_STATS_POINTS): + break + + logger.debug("Starting stats collection") + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + self.stats_history = self.stats_history[-MAX_STATS_POINTS:] + + if counter == 0: + self.requestor.send_data("stats", json.dumps(stats)) + + logger.debug("Finished stats collection") + + logger.info("Exiting stats emitter...") diff --git a/sam2-cpu/frigate-dev/frigate/stats/prometheus.py b/sam2-cpu/frigate-dev/frigate/stats/prometheus.py new file mode 100644 index 0000000..67d8d03 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/stats/prometheus.py @@ -0,0 +1,492 @@ +import logging +import re +from typing import Any, Dict, List + +from prometheus_client import CONTENT_TYPE_LATEST, generate_latest +from prometheus_client.core import ( + REGISTRY, + CounterMetricFamily, + GaugeMetricFamily, + InfoMetricFamily, +) + + +class CustomCollector(object): + def __init__(self, _url): + self.complete_stats = {} # Store complete stats data + self.process_stats = {} # Keep for CPU processing + self.previous_event_id = None + self.previous_event_start_time = None + self.all_events = {} + + def add_metric(self, metric, label, stats, key, multiplier=1.0): # Now a method + try: + string = str(stats[key]) + value = float(re.findall(r"-?\d*\.?\d*", string)[0]) + metric.add_metric(label, value * multiplier) + except (KeyError, TypeError, IndexError, ValueError): + pass + + def add_metric_process( + self, + metric, + camera_stats, + camera_name, + pid_name, + process_name, + cpu_or_memory, + process_type, + cpu_usages, + ): + try: + pid = str(camera_stats[pid_name]) + label_values = [pid, camera_name, process_name, process_type] + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label_values.append(cpu_usages[pid]["cmdline"]) + except KeyError: + pass + metric.add_metric(label_values, cpu_usages[pid][cpu_or_memory]) + # Don't modify the original data + except (KeyError, TypeError, IndexError): + pass + + def collect(self): + # Work with a copy of the complete stats + stats = self.complete_stats.copy() + + # Create a local copy of CPU usages to work with + cpu_usages = {} + try: + cpu_usages = stats.get("cpu_usages", {}).copy() + except (KeyError, AttributeError): + pass + + # process stats for cameras, detectors and other + cpu_usages_metric = GaugeMetricFamily( + "frigate_cpu_usage_percent", + "Process CPU usage %", + labels=["pid", "name", "process", "type", "cmdline"], + ) + mem_usages = GaugeMetricFamily( + "frigate_mem_usage_percent", + "Process memory usage %", + labels=["pid", "name", "process", "type", "cmdline"], + ) + + # camera stats + audio_dBFS = GaugeMetricFamily( + "frigate_audio_dBFS", "Audio dBFS for camera", labels=["camera_name"] + ) + audio_rms = GaugeMetricFamily( + "frigate_audio_rms", "Audio RMS for camera", labels=["camera_name"] + ) + camera_fps = GaugeMetricFamily( + "frigate_camera_fps", + "Frames per second being consumed from your camera.", + labels=["camera_name"], + ) + detection_enabled = GaugeMetricFamily( + "frigate_detection_enabled", + "Detection enabled for camera", + labels=["camera_name"], + ) + detection_fps = GaugeMetricFamily( + "frigate_detection_fps", + "Number of times detection is run per second.", + labels=["camera_name"], + ) + process_fps = GaugeMetricFamily( + "frigate_process_fps", + "Frames per second being processed by frigate.", + labels=["camera_name"], + ) + skipped_fps = GaugeMetricFamily( + "frigate_skipped_fps", + "Frames per second skip for processing by frigate.", + labels=["camera_name"], + ) + + # read camera stats assuming version < frigate:0.13.0-beta3 + cameras = stats + try: + # try to read camera stats in case >= frigate:0.13.0-beta3 + cameras = stats["cameras"] + except KeyError: + pass + + for camera_name, camera_stats in cameras.items(): + self.add_metric(audio_dBFS, [camera_name], camera_stats, "audio_dBFS") + self.add_metric(audio_rms, [camera_name], camera_stats, "audio_rms") + self.add_metric(camera_fps, [camera_name], camera_stats, "camera_fps") + self.add_metric( + detection_enabled, [camera_name], camera_stats, "detection_enabled" + ) + self.add_metric(detection_fps, [camera_name], camera_stats, "detection_fps") + self.add_metric(process_fps, [camera_name], camera_stats, "process_fps") + self.add_metric(skipped_fps, [camera_name], camera_stats, "skipped_fps") + + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "ffmpeg_pid", + "ffmpeg", + "cpu", + "Camera", + cpu_usages, + ) + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "capture_pid", + "capture", + "cpu", + "Camera", + cpu_usages, + ) + self.add_metric_process( + cpu_usages_metric, + camera_stats, + camera_name, + "pid", + "detect", + "cpu", + "Camera", + cpu_usages, + ) + + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "ffmpeg_pid", + "ffmpeg", + "mem", + "Camera", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "capture_pid", + "capture", + "mem", + "Camera", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + camera_stats, + camera_name, + "pid", + "detect", + "mem", + "Camera", + cpu_usages, + ) + + yield audio_dBFS + yield audio_rms + yield camera_fps + yield detection_enabled + yield detection_fps + yield process_fps + yield skipped_fps + + # bandwidth stats + bandwidth_usages = GaugeMetricFamily( + "frigate_bandwidth_usages_kBps", + "bandwidth usages kilobytes per second", + labels=["pid", "name", "process", "cmdline"], + ) + + try: + for b_pid, b_stats in stats["bandwidth_usages"].items(): + label = [b_pid] # pid label + try: + n = stats["cpu_usages"][b_pid]["cmdline"] + for p_name, p_stats in stats["processes"].items(): + if str(p_stats["pid"]) == b_pid: + n = p_name + break + + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(n) # name label + label.append(stats["cpu_usages"][b_pid]["cmdline"]) # process label + label.append(stats["cpu_usages"][b_pid]["cmdline"]) # cmdline label + self.add_metric(bandwidth_usages, label, b_stats, "bandwidth") + except KeyError: + pass + except KeyError: + pass + + yield bandwidth_usages + + # detector stats + try: + yield GaugeMetricFamily( + "frigate_detection_total_fps", + "Sum of detection_fps across all cameras and detectors.", + value=stats["detection_fps"], + ) + except KeyError: + pass + + detector_inference_speed = GaugeMetricFamily( + "frigate_detector_inference_speed_seconds", + "Time spent running object detection in seconds.", + labels=["name"], + ) + + detector_detection_start = GaugeMetricFamily( + "frigate_detection_start", + "Detector start time (unix timestamp)", + labels=["name"], + ) + + try: + for detector_name, detector_stats in stats["detectors"].items(): + self.add_metric( + detector_inference_speed, + [detector_name], + detector_stats, + "inference_speed", + 0.001, + ) # ms to seconds + self.add_metric( + detector_detection_start, + [detector_name], + detector_stats, + "detection_start", + ) + self.add_metric_process( + cpu_usages_metric, + stats["detectors"], + detector_name, + "pid", + "detect", + "cpu", + "Detector", + cpu_usages, + ) + self.add_metric_process( + mem_usages, + stats["detectors"], + detector_name, + "pid", + "detect", + "mem", + "Detector", + cpu_usages, + ) + except KeyError: + pass + + yield detector_inference_speed + yield detector_detection_start + + # detector process stats + try: + for detector_name, detector_stats in stats["detectors"].items(): + p_pid = str(detector_stats["pid"]) + label = [p_pid] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(detector_name) # name label + label.append(detector_name) # process label + label.append("detectors") # type label + label.append(cpu_usages[p_pid]["cmdline"]) # cmdline label + self.add_metric(cpu_usages_metric, label, cpu_usages[p_pid], "cpu") + self.add_metric(mem_usages, label, cpu_usages[p_pid], "mem") + # Don't modify the original data + except KeyError: + pass + + except KeyError: + pass + + # other named process stats + try: + for process_name, process_stats in stats["processes"].items(): + p_pid = str(process_stats["pid"]) + label = [p_pid] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(process_name) # name label + label.append(process_name) # process label + label.append(process_name) # type label + label.append(cpu_usages[p_pid]["cmdline"]) # cmdline label + self.add_metric(cpu_usages_metric, label, cpu_usages[p_pid], "cpu") + self.add_metric(mem_usages, label, cpu_usages[p_pid], "mem") + # Don't modify the original data + except KeyError: + pass + + except KeyError: + pass + + # remaining process stats + try: + for process_id, pid_stats in cpu_usages.items(): + label = [process_id] # pid label + try: + # new frigate:0.13.0-beta3 stat 'cmdline' + label.append(pid_stats["cmdline"]) # name label + label.append(pid_stats["cmdline"]) # process label + label.append("Other") # type label + label.append(pid_stats["cmdline"]) # cmdline label + except KeyError: + pass + self.add_metric(cpu_usages_metric, label, pid_stats, "cpu") + self.add_metric(mem_usages, label, pid_stats, "mem") + except KeyError: + pass + + yield cpu_usages_metric + yield mem_usages + + # gpu stats + gpu_usages = GaugeMetricFamily( + "frigate_gpu_usage_percent", "GPU utilisation %", labels=["gpu_name"] + ) + gpu_mem_usages = GaugeMetricFamily( + "frigate_gpu_mem_usage_percent", "GPU memory usage %", labels=["gpu_name"] + ) + + try: + for gpu_name, gpu_stats in stats["gpu_usages"].items(): + self.add_metric(gpu_usages, [gpu_name], gpu_stats, "gpu") + self.add_metric(gpu_mem_usages, [gpu_name], gpu_stats, "mem") + except KeyError: + pass + + yield gpu_usages + yield gpu_mem_usages + + # service stats + uptime_seconds = GaugeMetricFamily( + "frigate_service_uptime_seconds", "Uptime seconds" + ) + last_updated_timestamp = GaugeMetricFamily( + "frigate_service_last_updated_timestamp", + "Stats recorded time (unix timestamp)", + ) + + try: + service_stats = stats["service"] + self.add_metric(uptime_seconds, [""], service_stats, "uptime") + self.add_metric(last_updated_timestamp, [""], service_stats, "last_updated") + + info = { + "latest_version": stats["service"]["latest_version"], + "version": stats["service"]["version"], + } + yield InfoMetricFamily( + "frigate_service", "Frigate version info", value=info + ) + + except KeyError: + pass + + yield uptime_seconds + yield last_updated_timestamp + + temperatures = GaugeMetricFamily( + "frigate_device_temperature", "Device Temperature", labels=["device"] + ) + try: + for device_name in stats["service"]["temperatures"]: + self.add_metric( + temperatures, + [device_name], + stats["service"]["temperatures"], + device_name, + ) + except KeyError: + pass + + yield temperatures + + storage_free = GaugeMetricFamily( + "frigate_storage_free_bytes", "Storage free bytes", labels=["storage"] + ) + storage_mount_type = InfoMetricFamily( + "frigate_storage_mount_type", + "Storage mount type", + labels=["mount_type", "storage"], + ) + storage_total = GaugeMetricFamily( + "frigate_storage_total_bytes", "Storage total bytes", labels=["storage"] + ) + storage_used = GaugeMetricFamily( + "frigate_storage_used_bytes", "Storage used bytes", labels=["storage"] + ) + + try: + for storage_path, storage_stats in stats["service"]["storage"].items(): + self.add_metric( + storage_free, [storage_path], storage_stats, "free", 1e6 + ) # MB to bytes + self.add_metric( + storage_total, [storage_path], storage_stats, "total", 1e6 + ) # MB to bytes + self.add_metric( + storage_used, [storage_path], storage_stats, "used", 1e6 + ) # MB to bytes + storage_mount_type.add_metric( + storage_path, + { + "mount_type": storage_stats["mount_type"], + "storage": storage_path, + }, + ) + except KeyError: + pass + + yield storage_free + yield storage_mount_type + yield storage_total + yield storage_used + + camera_events = CounterMetricFamily( + "frigate_camera_events", + "Count of camera events since exporter started", + labels=["camera", "label"], + ) + + if len(self.all_events) > 0: + for event_count in self.all_events: + camera_events.add_metric( + [event_count["camera"], event_count["label"]], event_count["Count"] + ) + + yield camera_events + + +collector = CustomCollector(None) +REGISTRY.register(collector) + + +def update_metrics(stats: Dict[str, Any], event_counts: List[Dict[str, Any]]): + """Updates the Prometheus metrics with the given stats data.""" + try: + # Store the complete stats for later use by collect() + collector.complete_stats = stats.copy() + + # For backwards compatibility + collector.process_stats = stats.copy() + + collector.all_events = event_counts + + # No need to call collect() here - it will be called by get_metrics() + except Exception as e: + logging.error(f"Error updating metrics: {e}") + + +def get_metrics(): + """Returns the Prometheus metrics in text format.""" + content = generate_latest(REGISTRY) # Use generate_latest + return content, CONTENT_TYPE_LATEST diff --git a/sam2-cpu/frigate-dev/frigate/stats/util.py b/sam2-cpu/frigate-dev/frigate/stats/util.py new file mode 100644 index 0000000..17b45d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/stats/util.py @@ -0,0 +1,418 @@ +"""Utilities for stats.""" + +import asyncio +import os +import shutil +import time +from json import JSONDecodeError +from multiprocessing.managers import DictProxy +from typing import Any, Optional + +import requests +from requests.exceptions import RequestException + +from frigate.config import FrigateConfig +from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR +from frigate.data_processing.types import DataProcessorMetrics +from frigate.object_detection.base import ObjectDetectProcess +from frigate.types import StatsTrackingTypes +from frigate.util.services import ( + calculate_shm_requirements, + get_amd_gpu_stats, + get_bandwidth_stats, + get_cpu_stats, + get_fs_type, + get_intel_gpu_stats, + get_jetson_stats, + get_nvidia_gpu_stats, + get_openvino_npu_stats, + get_rockchip_gpu_stats, + get_rockchip_npu_stats, + is_vaapi_amd_driver, +) +from frigate.version import VERSION + + +def get_latest_version(config: FrigateConfig) -> str: + if not config.telemetry.version_check: + return "disabled" + + try: + request = requests.get( + "https://api.github.com/repos/blakeblackshear/frigate/releases/latest", + timeout=10, + ) + except (RequestException, JSONDecodeError): + return "unknown" + + response = request.json() + + if request.ok and response and "tag_name" in response: + return str(response.get("tag_name").replace("v", "")) + else: + return "unknown" + + +def stats_init( + config: FrigateConfig, + camera_metrics: DictProxy, + embeddings_metrics: DataProcessorMetrics | None, + detectors: dict[str, ObjectDetectProcess], + processes: dict[str, int], +) -> StatsTrackingTypes: + stats_tracking: StatsTrackingTypes = { + "camera_metrics": camera_metrics, + "embeddings_metrics": embeddings_metrics, + "detectors": detectors, + "started": int(time.time()), + "latest_frigate_version": get_latest_version(config), + "last_updated": int(time.time()), + "processes": processes, + } + return stats_tracking + + +def read_temperature(path: str) -> Optional[float]: + if os.path.isfile(path): + with open(path) as f: + line = f.readline().strip() + return int(line) / 1000 + return None + + +def get_temperatures() -> dict[str, float]: + temps = {} + + # Get temperatures for all attached Corals + base = "/sys/class/apex/" + if os.path.isdir(base): + for apex in os.listdir(base): + temp = read_temperature(os.path.join(base, apex, "temp")) + if temp is not None: + temps[apex] = temp + + return temps + + +def get_processing_stats( + config: FrigateConfig, stats: dict[str, str], hwaccel_errors: list[str] +) -> None: + """Get stats for cpu / gpu.""" + + async def run_tasks() -> None: + stats_tasks = [ + asyncio.create_task(set_gpu_stats(config, stats, hwaccel_errors)), + asyncio.create_task(set_cpu_stats(stats)), + asyncio.create_task(set_npu_usages(config, stats)), + ] + + if config.telemetry.stats.network_bandwidth: + stats_tasks.append(asyncio.create_task(set_bandwidth_stats(config, stats))) + + await asyncio.wait(stats_tasks) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(run_tasks()) + loop.close() + + +async def set_cpu_stats(all_stats: dict[str, Any]) -> None: + """Set cpu usage from top.""" + cpu_stats = get_cpu_stats() + + if cpu_stats: + all_stats["cpu_usages"] = cpu_stats + + +async def set_bandwidth_stats(config: FrigateConfig, all_stats: dict[str, Any]) -> None: + """Set bandwidth from nethogs.""" + bandwidth_stats = get_bandwidth_stats(config) + + if bandwidth_stats: + all_stats["bandwidth_usages"] = bandwidth_stats + + +async def set_gpu_stats( + config: FrigateConfig, all_stats: dict[str, Any], hwaccel_errors: list[str] +) -> None: + """Parse GPUs from hwaccel args and use for stats.""" + hwaccel_args = [] + + for camera in config.cameras.values(): + args = camera.ffmpeg.hwaccel_args + + if isinstance(args, list): + args = " ".join(args) + + if args and args not in hwaccel_args: + hwaccel_args.append(args) + + for stream_input in camera.ffmpeg.inputs: + args = stream_input.hwaccel_args + + if isinstance(args, list): + args = " ".join(args) + + if args and args not in hwaccel_args: + hwaccel_args.append(args) + + stats: dict[str, dict] = {} + + for args in hwaccel_args: + if args in hwaccel_errors: + # known erroring args should automatically return as error + stats["error-gpu"] = {"gpu": "", "mem": ""} + elif "cuvid" in args or "nvidia" in args: + # nvidia GPU + nvidia_usage = get_nvidia_gpu_stats() + + if nvidia_usage: + for i in range(len(nvidia_usage)): + stats[nvidia_usage[i]["name"]] = { + "gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%", + "mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%", + "enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%", + "dec": str(round(float(nvidia_usage[i]["dec"]), 2)) + "%", + } + + else: + stats["nvidia-gpu"] = {"gpu": "", "mem": ""} + hwaccel_errors.append(args) + elif "nvmpi" in args or "jetson" in args: + # nvidia Jetson + jetson_usage = get_jetson_stats() + + if jetson_usage: + stats["jetson-gpu"] = jetson_usage + else: + stats["jetson-gpu"] = {"gpu": "", "mem": ""} + hwaccel_errors.append(args) + elif "qsv" in args: + if not config.telemetry.stats.intel_gpu_stats: + continue + + # intel QSV GPU + intel_usage = get_intel_gpu_stats(config.telemetry.stats.intel_gpu_device) + + if intel_usage is not None: + stats["intel-qsv"] = intel_usage or {"gpu": "", "mem": ""} + else: + stats["intel-qsv"] = {"gpu": "", "mem": ""} + hwaccel_errors.append(args) + elif "vaapi" in args: + if is_vaapi_amd_driver(): + if not config.telemetry.stats.amd_gpu_stats: + continue + + # AMD VAAPI GPU + amd_usage = get_amd_gpu_stats() + + if amd_usage: + stats["amd-vaapi"] = amd_usage + else: + stats["amd-vaapi"] = {"gpu": "", "mem": ""} + hwaccel_errors.append(args) + else: + if not config.telemetry.stats.intel_gpu_stats: + continue + + # intel VAAPI GPU + intel_usage = get_intel_gpu_stats( + config.telemetry.stats.intel_gpu_device + ) + + if intel_usage is not None: + stats["intel-vaapi"] = intel_usage or {"gpu": "", "mem": ""} + else: + stats["intel-vaapi"] = {"gpu": "", "mem": ""} + hwaccel_errors.append(args) + elif "preset-rk" in args: + rga_usage = get_rockchip_gpu_stats() + + if rga_usage: + stats["rockchip"] = rga_usage + elif "v4l2m2m" in args or "rpi" in args: + # RPi v4l2m2m is currently not able to get usage stats + stats["rpi-v4l2m2m"] = {"gpu": "", "mem": ""} + + if stats: + all_stats["gpu_usages"] = stats + + +async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> None: + stats: dict[str, dict] = {} + + for detector in config.detectors.values(): + if detector.type == "rknn": + # Rockchip NPU usage + rk_usage = get_rockchip_npu_stats() + stats["rockchip"] = rk_usage + elif detector.type == "openvino" and detector.device == "NPU": + # OpenVINO NPU usage + ov_usage = get_openvino_npu_stats() + stats["openvino"] = ov_usage + + if stats: + all_stats["npu_usages"] = stats + + +def stats_snapshot( + config: FrigateConfig, stats_tracking: StatsTrackingTypes, hwaccel_errors: list[str] +) -> dict[str, Any]: + """Get a snapshot of the current stats that are being tracked.""" + camera_metrics = stats_tracking["camera_metrics"] + stats: dict[str, Any] = {} + + total_camera_fps = total_process_fps = total_skipped_fps = total_detection_fps = 0 + + stats["cameras"] = {} + for name, camera_stats in camera_metrics.items(): + total_camera_fps += camera_stats.camera_fps.value + total_process_fps += camera_stats.process_fps.value + total_skipped_fps += camera_stats.skipped_fps.value + total_detection_fps += camera_stats.detection_fps.value + pid = camera_stats.process_pid.value if camera_stats.process_pid.value else None + ffmpeg_pid = camera_stats.ffmpeg_pid.value if camera_stats.ffmpeg_pid else None + capture_pid = ( + camera_stats.capture_process_pid.value + if camera_stats.capture_process_pid.value + else None + ) + stats["cameras"][name] = { + "camera_fps": round(camera_stats.camera_fps.value, 2), + "process_fps": round(camera_stats.process_fps.value, 2), + "skipped_fps": round(camera_stats.skipped_fps.value, 2), + "detection_fps": round(camera_stats.detection_fps.value, 2), + "detection_enabled": config.cameras[name].detect.enabled, + "pid": pid, + "capture_pid": capture_pid, + "ffmpeg_pid": ffmpeg_pid, + "audio_rms": round(camera_stats.audio_rms.value, 4), + "audio_dBFS": round(camera_stats.audio_dBFS.value, 4), + } + + stats["detectors"] = {} + for name, detector in stats_tracking["detectors"].items(): + pid = detector.detect_process.pid if detector.detect_process else None + stats["detectors"][name] = { + "inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "detection_start": detector.detection_start.value, # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + "pid": pid, + } + stats["camera_fps"] = round(total_camera_fps, 2) + stats["process_fps"] = round(total_process_fps, 2) + stats["skipped_fps"] = round(total_skipped_fps, 2) + stats["detection_fps"] = round(total_detection_fps, 2) + + stats["embeddings"] = {} + + # Get metrics if available + embeddings_metrics = stats_tracking.get("embeddings_metrics") + + if embeddings_metrics: + # Add metrics based on what's enabled + if config.semantic_search.enabled: + stats["embeddings"].update( + { + "image_embedding_speed": round( + embeddings_metrics.image_embeddings_speed.value * 1000, 2 + ), + "image_embedding": round( + embeddings_metrics.image_embeddings_eps.value, 2 + ), + "text_embedding_speed": round( + embeddings_metrics.text_embeddings_speed.value * 1000, 2 + ), + "text_embedding": round( + embeddings_metrics.text_embeddings_eps.value, 2 + ), + } + ) + + if config.face_recognition.enabled: + stats["embeddings"]["face_recognition_speed"] = round( + embeddings_metrics.face_rec_speed.value * 1000, 2 + ) + stats["embeddings"]["face_recognition"] = round( + embeddings_metrics.face_rec_fps.value, 2 + ) + + if config.lpr.enabled: + stats["embeddings"]["plate_recognition_speed"] = round( + embeddings_metrics.alpr_speed.value * 1000, 2 + ) + stats["embeddings"]["plate_recognition"] = round( + embeddings_metrics.alpr_pps.value, 2 + ) + + if embeddings_metrics.yolov9_lpr_pps.value > 0.0: + stats["embeddings"]["yolov9_plate_detection_speed"] = round( + embeddings_metrics.yolov9_lpr_speed.value * 1000, 2 + ) + stats["embeddings"]["yolov9_plate_detection"] = round( + embeddings_metrics.yolov9_lpr_pps.value, 2 + ) + + if embeddings_metrics.review_desc_speed.value > 0.0: + stats["embeddings"]["review_description_speed"] = round( + embeddings_metrics.review_desc_speed.value * 1000, 2 + ) + stats["embeddings"]["review_description_events_per_second"] = round( + embeddings_metrics.review_desc_dps.value, 2 + ) + + if embeddings_metrics.object_desc_speed.value > 0.0: + stats["embeddings"]["object_description_speed"] = round( + embeddings_metrics.object_desc_speed.value * 1000, 2 + ) + stats["embeddings"]["object_description_events_per_second"] = round( + embeddings_metrics.object_desc_dps.value, 2 + ) + + for key in embeddings_metrics.classification_speeds.keys(): + stats["embeddings"][f"{key}_classification_speed"] = round( + embeddings_metrics.classification_speeds[key].value * 1000, 2 + ) + stats["embeddings"][f"{key}_classification_events_per_second"] = round( + embeddings_metrics.classification_cps[key].value, 2 + ) + + get_processing_stats(config, stats, hwaccel_errors) + + stats["service"] = { + "uptime": (int(time.time()) - stats_tracking["started"]), + "version": VERSION, + "latest_version": stats_tracking["latest_frigate_version"], + "storage": {}, + "temperatures": get_temperatures(), + "last_updated": int(time.time()), + } + + for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]: + try: + storage_stats = shutil.disk_usage(path) + except (FileNotFoundError, OSError): + stats["service"]["storage"][path] = {} + continue + + stats["service"]["storage"][path] = { + "total": round(storage_stats.total / pow(2, 20), 1), + "used": round(storage_stats.used / pow(2, 20), 1), + "free": round(storage_stats.free / pow(2, 20), 1), + "mount_type": get_fs_type(path), + } + + stats["service"]["storage"]["/dev/shm"] = calculate_shm_requirements(config) + + stats["processes"] = {} + for name, pid in stats_tracking["processes"].items(): + stats["processes"][name] = { + "pid": pid, + } + + return stats diff --git a/sam2-cpu/frigate-dev/frigate/storage.py b/sam2-cpu/frigate-dev/frigate/storage.py new file mode 100644 index 0000000..feabe06 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/storage.py @@ -0,0 +1,289 @@ +"""Handle storage retention and usage.""" + +import logging +import shutil +import threading +from pathlib import Path + +from peewee import SQL, fn + +from frigate.config import FrigateConfig +from frigate.const import RECORD_DIR +from frigate.models import Event, Recordings +from frigate.util.builtin import clear_and_unlink + +logger = logging.getLogger(__name__) +bandwidth_equation = Recordings.segment_size / ( + Recordings.end_time - Recordings.start_time +) + +MAX_CALCULATED_BANDWIDTH = 10000 # 10Gb/hr + + +class StorageMaintainer(threading.Thread): + """Maintain frigates recording storage.""" + + def __init__(self, config: FrigateConfig, stop_event) -> None: + super().__init__(name="storage_maintainer") + self.config = config + self.stop_event = stop_event + self.camera_storage_stats: dict[str, dict] = {} + + def calculate_camera_bandwidth(self) -> None: + """Calculate an average MB/hr for each camera.""" + for camera in self.config.cameras.keys(): + # cameras with < 50 segments should be refreshed to keep size accurate + # when few segments are available + if self.camera_storage_stats.get(camera, {}).get("needs_refresh", True): + self.camera_storage_stats[camera] = { + "needs_refresh": ( + Recordings.select(fn.COUNT("*")) + .where(Recordings.camera == camera, Recordings.segment_size > 0) + .scalar() + < 50 + ) + } + + # calculate MB/hr from last 100 segments + try: + # Subquery to get last 100 segments, then average their bandwidth + last_100 = ( + Recordings.select(bandwidth_equation.alias("bw")) + .where(Recordings.camera == camera, Recordings.segment_size > 0) + .order_by(Recordings.start_time.desc()) + .limit(100) + .alias("recent") + ) + + bandwidth = round( + Recordings.select(fn.AVG(SQL("bw"))).from_(last_100).scalar() + * 3600, + 2, + ) + + if bandwidth > MAX_CALCULATED_BANDWIDTH: + logger.warning( + f"{camera} has a bandwidth of {bandwidth} MB/hr which exceeds the expected maximum. This typically indicates an issue with the cameras recordings." + ) + bandwidth = MAX_CALCULATED_BANDWIDTH + except TypeError: + bandwidth = 0 + + self.camera_storage_stats[camera]["bandwidth"] = bandwidth + logger.debug(f"{camera} has a bandwidth of {bandwidth} MiB/hr.") + + def calculate_camera_usages(self) -> dict[str, dict]: + """Calculate the storage usage of each camera.""" + usages: dict[str, dict] = {} + + for camera in self.config.cameras.keys(): + camera_storage = ( + Recordings.select(fn.SUM(Recordings.segment_size)) + .where(Recordings.camera == camera, Recordings.segment_size != 0) + .scalar() + ) + + camera_key = ( + getattr(self.config.cameras[camera], "friendly_name", None) or camera + ) + usages[camera_key] = { + "usage": camera_storage, + "bandwidth": self.camera_storage_stats.get(camera, {}).get( + "bandwidth", 0 + ), + } + + return usages + + def check_storage_needs_cleanup(self) -> bool: + """Return if storage needs cleanup.""" + # currently runs cleanup if less than 1 hour of space is left + # disk_usage should not spin up disks + hourly_bandwidth = sum( + [b["bandwidth"] for b in self.camera_storage_stats.values()] + ) + remaining_storage = round(shutil.disk_usage(RECORD_DIR).free / pow(2, 20), 1) + logger.debug( + f"Storage cleanup check: {hourly_bandwidth} hourly with remaining storage: {remaining_storage}." + ) + return remaining_storage < hourly_bandwidth + + def reduce_storage_consumption(self) -> None: + """Remove oldest hour of recordings.""" + logger.debug("Starting storage cleanup.") + deleted_segments_size = 0 + hourly_bandwidth = sum( + [b["bandwidth"] for b in self.camera_storage_stats.values()] + ) + + recordings: Recordings = ( + Recordings.select( + Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, + Recordings.segment_size, + Recordings.path, + ) + .order_by(Recordings.start_time.asc()) + .namedtuples() + .iterator() + ) + + retained_events: Event = ( + Event.select( + Event.start_time, + Event.end_time, + ) + .where( + Event.retain_indefinitely == True, + Event.has_clip, + ) + .order_by(Event.start_time.asc()) + .namedtuples() + ) + + event_start = 0 + deleted_recordings = [] + for recording in recordings: + # check if 1 hour of storage has been reclaimed + if deleted_segments_size > hourly_bandwidth: + break + + keep = False + + # Now look for a reason to keep this recording segment + for idx in range(event_start, len(retained_events)): + event = retained_events[idx] + + # if the event starts in the future, stop checking events + # and let this recording segment expire + if event.start_time > recording.end_time: + keep = False + break + + # if the event is in progress or ends after the recording starts, keep it + # and stop looking at events + if event.end_time is None or event.end_time >= recording.start_time: + keep = True + break + + # if the event ends before this recording segment starts, skip + # this event and check the next event for an overlap. + # since the events and recordings are sorted, we can skip events + # that end before the previous recording segment started on future segments + if event.end_time < recording.start_time: + event_start = idx + + # Delete recordings not retained indefinitely + if not keep: + try: + clear_and_unlink(Path(recording.path), missing_ok=False) + deleted_recordings.append(recording) + deleted_segments_size += recording.segment_size + except FileNotFoundError: + # this file was not found so we must assume no space was cleaned up + pass + + # check if need to delete retained segments + if deleted_segments_size < hourly_bandwidth: + logger.error( + f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size} MB have been cleared. Retained recordings must be deleted." + ) + recordings = ( + Recordings.select( + Recordings.id, + Recordings.camera, + Recordings.start_time, + Recordings.end_time, + Recordings.path, + Recordings.segment_size, + ) + .order_by(Recordings.start_time.asc()) + .namedtuples() + .iterator() + ) + + for recording in recordings: + if deleted_segments_size > hourly_bandwidth: + break + + try: + clear_and_unlink(Path(recording.path), missing_ok=False) + deleted_segments_size += recording.segment_size + deleted_recordings.append(recording) + except FileNotFoundError: + # this file was not found so we must assume no space was cleaned up + pass + else: + logger.info(f"Cleaned up {deleted_segments_size} MB of recordings") + + logger.debug(f"Expiring {len(deleted_recordings)} recordings") + # delete up to 100,000 at a time + max_deletes = 100000 + + # Update has_clip for events that overlap with deleted recordings + if deleted_recordings: + # Group deleted recordings by camera + camera_recordings = {} + for recording in deleted_recordings: + if recording.camera not in camera_recordings: + camera_recordings[recording.camera] = { + "min_start": recording.start_time, + "max_end": recording.end_time, + } + else: + camera_recordings[recording.camera]["min_start"] = min( + camera_recordings[recording.camera]["min_start"], + recording.start_time, + ) + camera_recordings[recording.camera]["max_end"] = max( + camera_recordings[recording.camera]["max_end"], + recording.end_time, + ) + + # Find all events that overlap with deleted recordings time range per camera + events_to_update = [] + for camera, time_range in camera_recordings.items(): + overlapping_events = Event.select(Event.id).where( + Event.camera == camera, + Event.has_clip == True, + Event.start_time < time_range["max_end"], + Event.end_time > time_range["min_start"], + ) + + for event in overlapping_events: + events_to_update.append(event.id) + + # Update has_clip to False for overlapping events + if events_to_update: + for i in range(0, len(events_to_update), max_deletes): + batch = events_to_update[i : i + max_deletes] + Event.update(has_clip=False).where(Event.id << batch).execute() + logger.debug( + f"Updated has_clip to False for {len(events_to_update)} events" + ) + + deleted_recordings_list = [r.id for r in deleted_recordings] + for i in range(0, len(deleted_recordings_list), max_deletes): + Recordings.delete().where( + Recordings.id << deleted_recordings_list[i : i + max_deletes] + ).execute() + + def run(self): + """Check every 5 minutes if storage needs to be cleaned up.""" + self.calculate_camera_bandwidth() + while not self.stop_event.wait(300): + if not self.camera_storage_stats or True in [ + r["needs_refresh"] for r in self.camera_storage_stats.values() + ]: + self.calculate_camera_bandwidth() + logger.debug(f"Default camera bandwidths: {self.camera_storage_stats}.") + + if self.check_storage_needs_cleanup(): + logger.info( + "Less than 1 hour of recording space left, running storage maintenance..." + ) + self.reduce_storage_consumption() + + logger.info("Exiting storage maintainer...") diff --git a/sam2-cpu/frigate-dev/frigate/test/__init__.py b/sam2-cpu/frigate-dev/frigate/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/test/const.py b/sam2-cpu/frigate-dev/frigate/test/const.py new file mode 100644 index 0000000..3cbd377 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/const.py @@ -0,0 +1,4 @@ +"""Constants for testing.""" + +TEST_DB = "test.db" +TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"] diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/__init__.py b/sam2-cpu/frigate-dev/frigate/test/http_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/base_http_test.py b/sam2-cpu/frigate-dev/frigate/test/http_api/base_http_test.py new file mode 100644 index 0000000..8524909 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/base_http_test.py @@ -0,0 +1,245 @@ +import datetime +import logging +import os +import unittest + +from fastapi import Request +from fastapi.testclient import TestClient +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase +from playhouse.sqliteq import SqliteQueueDatabase +from pydantic import Json + +from frigate.api.fastapi_app import create_fastapi_app +from frigate.config import FrigateConfig +from frigate.const import BASE_DIR, CACHE_DIR +from frigate.models import Event, Recordings, ReviewSegment +from frigate.review.types import SeverityEnum +from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + + +class AuthTestClient(TestClient): + """TestClient that automatically adds auth headers to all requests.""" + + def request(self, *args, **kwargs): + # Add default auth headers if not already present + headers = kwargs.get("headers") or {} + if "remote-user" not in headers: + headers["remote-user"] = "admin" + if "remote-role" not in headers: + headers["remote-role"] = "admin" + kwargs["headers"] = headers + return super().request(*args, **kwargs) + + +class BaseTestHttp(unittest.TestCase): + def setUp(self, models): + # setup clean database for each test run + migrate_db = SqliteExtDatabase("test.db") + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + router.run() + migrate_db.close() + self.db = SqliteQueueDatabase(TEST_DB) + self.db.bind(models) + + self.minimal_config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.test_stats = { + "camera_fps": 5.0, + "process_fps": 5.0, + "skipped_fps": 0.0, + "detection_fps": 13.7, + "detectors": { + "cpu1": { + "detection_start": 0.0, + "inference_speed": 91.43, + "pid": 42, + }, + "cpu2": { + "detection_start": 0.0, + "inference_speed": 84.99, + "pid": 44, + }, + }, + "front_door": { + "camera_fps": 0.0, + "capture_pid": 53, + "detection_fps": 0.0, + "pid": 52, + "process_fps": 0.0, + "skipped_fps": 0.0, + }, + "service": { + "storage": { + "/dev/shm": { + "free": 50.5, + "mount_type": "tmpfs", + "total": 67.1, + "used": 16.6, + }, + os.path.join(BASE_DIR, "clips"): { + "free": 42429.9, + "mount_type": "ext4", + "total": 244529.7, + "used": 189607.0, + }, + os.path.join(BASE_DIR, "recordings"): { + "free": 0.2, + "mount_type": "ext4", + "total": 8.0, + "used": 7.8, + }, + CACHE_DIR: { + "free": 976.8, + "mount_type": "tmpfs", + "total": 1000.0, + "used": 23.2, + }, + }, + "uptime": 101113, + "version": "0.10.1", + "latest_version": "0.11", + }, + } + + def tearDown(self): + if not self.db.is_closed(): + self.db.close() + + try: + for file in TEST_DB_CLEANUPS: + os.remove(file) + except OSError: + pass + + def create_app(self, stats=None, event_metadata_publisher=None): + from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user + + app = create_fastapi_app( + FrigateConfig(**self.minimal_config), + self.db, + None, + None, + None, + None, + stats, + event_metadata_publisher, + None, + enforce_default_admin=False, + ) + + # Default test mocks for authentication + # Tests can override these in their setUp if needed + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + async def mock_get_allowed_cameras_for_filter(request: Request): + return list(self.minimal_config.get("cameras", {}).keys()) + + app.dependency_overrides[get_current_user] = mock_get_current_user + app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + return app + + def insert_mock_event( + self, + id: str, + start_time: float = datetime.datetime.now().timestamp(), + end_time: float = datetime.datetime.now().timestamp() + 20, + has_clip: bool = True, + top_score: int = 100, + score: int = 0, + data: Json = {}, + camera: str = "front_door", + ) -> Event: + """Inserts a basic event model with a given id.""" + return Event.insert( + id=id, + label="Mock", + camera=camera, + start_time=start_time, + end_time=end_time, + top_score=top_score, + score=score, + false_positive=False, + zones=list(), + thumbnail="", + region=[], + box=[], + area=0, + has_clip=has_clip, + has_snapshot=True, + data=data, + ).execute() + + def insert_mock_review_segment( + self, + id: str, + start_time: float | None = None, + end_time: float | None = None, + severity: SeverityEnum = SeverityEnum.alert, + data: dict | None = None, + camera: str = "front_door", + ) -> ReviewSegment: + """Inserts a review segment model with a given id.""" + if start_time is None: + start_time = datetime.datetime.now().timestamp() + if end_time is None: + end_time = start_time + 20 + if data is None: + data = {} + + return ReviewSegment.insert( + id=id, + camera=camera, + start_time=start_time, + end_time=end_time, + severity=severity, + thumb_path=False, + data=data, + ).execute() + + def insert_mock_recording( + self, + id: str, + start_time: float = datetime.datetime.now().timestamp(), + end_time: float = datetime.datetime.now().timestamp() + 20, + motion: int = 0, + ) -> Event: + """Inserts a recording model with a given id.""" + return Recordings.insert( + id=id, + path=id, + camera="front_door", + start_time=start_time, + end_time=end_time, + duration=end_time - start_time, + motion=motion, + ).execute() diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_app.py b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_app.py new file mode 100644 index 0000000..b04b1cf --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_app.py @@ -0,0 +1,24 @@ +from unittest.mock import Mock + +from frigate.models import Event, Recordings, ReviewSegment +from frigate.stats.emitter import StatsEmitter +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpApp(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment]) + self.app = super().create_app() + + #################################################################################################################### + ################################### GET /stats Endpoint ######################################################### + #################################################################################################################### + def test_stats_endpoint(self): + stats = Mock(spec=StatsEmitter) + stats.get_latest_stats.return_value = self.test_stats + app = super().create_app(stats) + + with AuthTestClient(app) as client: + response = client.get("/stats") + response_json = response.json() + assert response_json == self.test_stats diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_camera_access.py b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_camera_access.py new file mode 100644 index 0000000..5cd1154 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_camera_access.py @@ -0,0 +1,192 @@ +from unittest.mock import patch + +from fastapi import HTTPException, Request + +from frigate.api.auth import ( + get_allowed_cameras_for_filter, + get_current_user, +) +from frigate.models import Event, Recordings, ReviewSegment +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestCameraAccessEventReview(BaseTestHttp): + def setUp(self): + super().setUp([Event, ReviewSegment, Recordings]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + def test_event_camera_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events") + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids + assert "event2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events") + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids and "event2" in ids + + def test_review_camera_access(self): + super().insert_mock_review_segment("rev1", camera="front_door") + super().insert_mock_review_segment("rev2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/review") + assert resp.status_code == 200 + ids = [r["id"] for r in resp.json()] + assert "rev1" in ids + assert "rev2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/review") + assert resp.status_code == 200 + ids = [r["id"] for r in resp.json()] + assert "rev1" in ids and "rev2" in ids + + def test_event_single_access(self): + super().insert_mock_event("event1", camera="front_door") + + # Allowed + async def mock_require_allowed(camera: str, request: Request = None): + if camera == "front_door": + return + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.event.require_camera_access", mock_require_allowed): + with AuthTestClient(self.app) as client: + resp = client.get("/events/event1") + assert resp.status_code == 200 + assert resp.json()["id"] == "event1" + + # Disallowed + async def mock_require_disallowed(camera: str, request: Request = None): + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.event.require_camera_access", mock_require_disallowed): + with AuthTestClient(self.app) as client: + resp = client.get("/events/event1") + assert resp.status_code == 403 + + def test_review_single_access(self): + super().insert_mock_review_segment("rev1", camera="front_door") + + # Allowed + async def mock_require_allowed(camera: str, request: Request = None): + if camera == "front_door": + return + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.review.require_camera_access", mock_require_allowed): + with AuthTestClient(self.app) as client: + resp = client.get("/review/rev1") + assert resp.status_code == 200 + assert resp.json()["id"] == "rev1" + + # Disallowed + async def mock_require_disallowed(camera: str, request: Request = None): + raise HTTPException(status_code=403, detail="Access denied") + + with patch("frigate.api.review.require_camera_access", mock_require_disallowed): + with AuthTestClient(self.app) as client: + resp = client.get("/review/rev1") + assert resp.status_code == 403 + + def test_event_search_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events", params={"cameras": "all"}) + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids + assert "event2" not in ids + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events", params={"cameras": "all"}) + assert resp.status_code == 200 + ids = [e["id"] for e in resp.json()] + assert "event1" in ids and "event2" in ids + + def test_event_summary_access(self): + super().insert_mock_event("event1", camera="front_door") + super().insert_mock_event("event2", camera="back_door") + + async def mock_cameras(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events/summary") + assert resp.status_code == 200 + summary_list = resp.json() + assert len(summary_list) == 1 + + async def mock_cameras(request: Request): + return [ + "front_door", + "back_door", + ] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = mock_cameras + with AuthTestClient(self.app) as client: + resp = client.get("/events/summary") + summary_list = resp.json() + assert len(summary_list) == 2 diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_event.py b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_event.py new file mode 100644 index 0000000..44c4fd3 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_event.py @@ -0,0 +1,412 @@ +from datetime import datetime +from typing import Any +from unittest.mock import Mock + +from playhouse.shortcuts import model_to_dict + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.comms.event_metadata_updater import EventMetadataPublisher +from frigate.models import Event, Recordings, ReviewSegment, Timeline +from frigate.stats.emitter import StatsEmitter +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp, Request +from frigate.test.test_storage import _insert_mock_event + + +class TestHttpApp(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment, Timeline]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + #################################################################################################################### + ################################### GET /events Endpoint ######################################################### + #################################################################################################################### + def test_get_event_list_no_events(self): + with AuthTestClient(self.app) as client: + events = client.get("/events").json() + assert len(events) == 0 + + def test_get_event_list_no_match_event_id(self): + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + events = client.get("/events", params={"event_id": "abc"}).json() + assert len(events) == 0 + + def test_get_event_list_match_event_id(self): + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + events = client.get("/events", params={"event_id": id}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_match_length(self): + now = int(datetime.now().timestamp()) + + id = "123456.random" + with AuthTestClient(self.app) as client: + super().insert_mock_event(id, now, now + 1) + events = client.get( + "/events", params={"max_length": 1, "min_length": 1} + ).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_no_match_max_length(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"max_length": 1}).json() + assert len(events) == 0 + + def test_get_event_list_no_match_min_length(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"min_length": 3}).json() + assert len(events) == 0 + + def test_get_event_list_limit(self): + id = "123456.random" + id2 = "54321.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + events = client.get("/events").json() + assert len(events) == 1 + assert events[0]["id"] == id + + super().insert_mock_event(id2) + events = client.get("/events").json() + assert len(events) == 2 + + events = client.get("/events", params={"limit": 1}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + events = client.get("/events", params={"limit": 3}).json() + assert len(events) == 2 + + def test_get_event_list_no_match_has_clip(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, now, now + 2) + events = client.get("/events", params={"has_clip": 0}).json() + assert len(events) == 0 + + def test_get_event_list_has_clip(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_event(id, has_clip=True) + events = client.get("/events", params={"has_clip": 1}).json() + assert len(events) == 1 + assert events[0]["id"] == id + + def test_get_event_list_sort_score(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "54321.random" + super().insert_mock_event(id, top_score=37, score=37, data={"score": 50}) + super().insert_mock_event(id2, top_score=47, score=47, data={"score": 20}) + events = client.get("/events", params={"sort": "score_asc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id2 + assert events[1]["id"] == id + + events = client.get("/events", params={"sort": "score_des"}).json() + assert len(events) == 2 + assert events[0]["id"] == id + assert events[1]["id"] == id2 + + def test_get_event_list_sort_start_time(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "54321.random" + super().insert_mock_event(id, start_time=now + 3) + super().insert_mock_event(id2, start_time=now) + events = client.get("/events", params={"sort": "date_asc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id2 + assert events[1]["id"] == id + + events = client.get("/events", params={"sort": "date_desc"}).json() + assert len(events) == 2 + assert events[0]["id"] == id + assert events[1]["id"] == id2 + + def test_get_good_event(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event = client.get(f"/events/{id}").json() + + assert event + assert event["id"] == id + assert event["id"] == model_to_dict(Event.get(Event.id == id))["id"] + + def test_get_bad_event(self): + id = "123456.random" + bad_id = "654321.other" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event_response = client.get(f"/events/{bad_id}") + assert event_response.status_code == 404 + assert event_response.json() == "Event not found" + + def test_delete_event(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + response = client.delete(f"/events/{id}", headers={"remote-role": "admin"}) + assert response.status_code == 200 + event_after_delete = client.get(f"/events/{id}") + assert event_after_delete.status_code == 404 + + def test_event_retention(self): + id = "123456.random" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(id) + client.post(f"/events/{id}/retain", headers={"remote-role": "admin"}) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["retain_indefinitely"] is True + client.delete(f"/events/{id}/retain", headers={"remote-role": "admin"}) + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["retain_indefinitely"] is False + + def test_event_time_filtering(self): + morning_id = "123456.random" + evening_id = "654321.random" + morning = 1656590400 # 06/30/2022 6 am (GMT) + evening = 1656633600 # 06/30/2022 6 pm (GMT) + + with AuthTestClient(self.app) as client: + super().insert_mock_event(morning_id, morning) + super().insert_mock_event(evening_id, evening) + # both events come back + events = client.get("/events").json() + assert events + assert len(events) == 2 + # morning event is excluded + events = client.get( + "/events", + params={"time_range": "07:00,24:00"}, + ).json() + assert events + assert len(events) == 1 + # evening event is excluded + events = client.get( + "/events", + params={"time_range": "00:00,18:00"}, + ).json() + assert events + assert len(events) == 1 + + def test_set_delete_sub_label(self): + mock_event_updater = Mock(spec=EventMetadataPublisher) + app = super().create_app(event_metadata_publisher=mock_event_updater) + id = "123456.random" + sub_label = "sub" + + def update_event(payload: Any, topic: str): + event = Event.get(id=id) + event.sub_label = payload[1] + event.save() + + mock_event_updater.publish.side_effect = update_event + + with AuthTestClient(app) as client: + super().insert_mock_event(id) + new_sub_label_response = client.post( + f"/events/{id}/sub_label", + json={"subLabel": sub_label}, + headers={"remote-role": "admin"}, + ) + assert new_sub_label_response.status_code == 200 + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["sub_label"] == sub_label + empty_sub_label_response = client.post( + f"/events/{id}/sub_label", + json={"subLabel": ""}, + headers={"remote-role": "admin"}, + ) + assert empty_sub_label_response.status_code == 200 + event = client.get(f"/events/{id}").json() + assert event + assert event["id"] == id + assert event["sub_label"] == None + + def test_sub_label_list(self): + mock_event_updater = Mock(spec=EventMetadataPublisher) + app = super().create_app(event_metadata_publisher=mock_event_updater) + app.event_metadata_publisher = mock_event_updater + id = "123456.random" + sub_label = "sub" + + def update_event(payload: Any, _: str): + event = Event.get(id=id) + event.sub_label = payload[1] + event.save() + + mock_event_updater.publish.side_effect = update_event + + with AuthTestClient(app) as client: + super().insert_mock_event(id) + client.post( + f"/events/{id}/sub_label", + json={"subLabel": sub_label}, + headers={"remote-role": "admin"}, + ) + sub_labels = client.get("/sub_labels").json() + assert sub_labels + assert sub_labels == [sub_label] + + #################################################################################################################### + ################################### GET /metrics Endpoint ######################################################### + #################################################################################################################### + def test_get_metrics(self): + """ensure correct prometheus metrics api response""" + with AuthTestClient(self.app) as client: + ts_start = datetime.now().timestamp() + ts_end = ts_start + 30 + _insert_mock_event( + id="abcde.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="01234.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="56789.random", start=ts_start, end=ts_end, retain=True + ) + _insert_mock_event( + id="101112.random", + label="outside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="131415.random", + label="outside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="161718.random", + camera="porch", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="192021.random", + camera="porch", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="222324.random", + camera="porch", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="252627.random", + camera="porch", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="282930.random", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + _insert_mock_event( + id="313233.random", + label="inside", + start=ts_start, + end=ts_end, + retain=True, + ) + + stats_emitter = Mock(spec=StatsEmitter) + stats_emitter.get_latest_stats.return_value = self.test_stats + self.app.stats_emitter = stats_emitter + event = client.get("/metrics") + + assert "# TYPE frigate_detection_total_fps gauge" in event.text + assert "frigate_detection_total_fps 13.7" in event.text + assert ( + "# HELP frigate_camera_events_total Count of camera events since exporter started" + in event.text + ) + assert "# TYPE frigate_camera_events_total counter" in event.text + assert ( + 'frigate_camera_events_total{camera="front_door",label="Mock"} 3.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="front_door",label="inside"} 2.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="front_door",label="outside"} 2.0' + in event.text + ) + assert ( + 'frigate_camera_events_total{camera="porch",label="Mock"} 2.0' in event.text + ) + assert 'frigate_camera_events_total{camera="porch",label="inside"} 2.0' diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_media.py b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_media.py new file mode 100644 index 0000000..6af3dd9 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_media.py @@ -0,0 +1,405 @@ +"""Unit tests for recordings/media API endpoints.""" + +from datetime import datetime, timezone + +import pytz +from fastapi import Request + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.models import Recordings +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpMedia(BaseTestHttp): + """Test media API endpoints, particularly recordings with DST handling.""" + + def setUp(self): + """Set up test fixtures.""" + super().setUp([Recordings]) + self.app = super().create_app() + + # Mock get_current_user for all tests + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + """Clean up after tests.""" + self.app.dependency_overrides.clear() + super().tearDown() + + def test_recordings_summary_across_dst_spring_forward(self): + """ + Test recordings summary across spring DST transition (spring forward). + + In 2024, DST in America/New_York transitions on March 10, 2024 at 2:00 AM + Clocks spring forward from 2:00 AM to 3:00 AM (EST to EDT) + """ + tz = pytz.timezone("America/New_York") + + # March 9, 2024 at 12:00 PM EST (before DST) + march_9_noon = tz.localize(datetime(2024, 3, 9, 12, 0, 0)).timestamp() + + # March 10, 2024 at 12:00 PM EDT (after DST transition) + march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() + + # March 11, 2024 at 12:00 PM EDT (after DST) + march_11_noon = tz.localize(datetime(2024, 3, 11, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day + Recordings.insert( + id="recording_march_9", + path="/media/recordings/march_9.mp4", + camera="front_door", + start_time=march_9_noon, + end_time=march_9_noon + 3600, # 1 hour recording + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_march_10", + path="/media/recordings/march_10.mp4", + camera="front_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + Recordings.insert( + id="recording_march_11", + path="/media/recordings/march_11.mp4", + camera="front_door", + start_time=march_11_noon, + end_time=march_11_noon + 3600, + duration=3600, + motion=200, + objects=10, + ).execute() + + # Test recordings summary with America/New_York timezone + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get exactly 3 days + assert len(summary) == 3, f"Expected 3 days, got {len(summary)}" + + # Verify the correct dates are returned (API returns dict with True values) + assert "2024-03-09" in summary, f"Expected 2024-03-09 in {summary}" + assert "2024-03-10" in summary, f"Expected 2024-03-10 in {summary}" + assert "2024-03-11" in summary, f"Expected 2024-03-11 in {summary}" + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + assert summary["2024-03-11"] is True + + def test_recordings_summary_across_dst_fall_back(self): + """ + Test recordings summary across fall DST transition (fall back). + + In 2024, DST in America/New_York transitions on November 3, 2024 at 2:00 AM + Clocks fall back from 2:00 AM to 1:00 AM (EDT to EST) + """ + tz = pytz.timezone("America/New_York") + + # November 2, 2024 at 12:00 PM EDT (before DST transition) + nov_2_noon = tz.localize(datetime(2024, 11, 2, 12, 0, 0)).timestamp() + + # November 3, 2024 at 12:00 PM EST (after DST transition) + # Need to specify is_dst=False to get the time after fall back + nov_3_noon = tz.localize( + datetime(2024, 11, 3, 12, 0, 0), is_dst=False + ).timestamp() + + # November 4, 2024 at 12:00 PM EST (after DST) + nov_4_noon = tz.localize(datetime(2024, 11, 4, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for each day + Recordings.insert( + id="recording_nov_2", + path="/media/recordings/nov_2.mp4", + camera="front_door", + start_time=nov_2_noon, + end_time=nov_2_noon + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_nov_3", + path="/media/recordings/nov_3.mp4", + camera="front_door", + start_time=nov_3_noon, + end_time=nov_3_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + Recordings.insert( + id="recording_nov_4", + path="/media/recordings/nov_4.mp4", + camera="front_door", + start_time=nov_4_noon, + end_time=nov_4_noon + 3600, + duration=3600, + motion=200, + objects=10, + ).execute() + + # Test recordings summary with America/New_York timezone + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get exactly 3 days + assert len(summary) == 3, f"Expected 3 days, got {len(summary)}" + + # Verify the correct dates are returned (API returns dict with True values) + assert "2024-11-02" in summary, f"Expected 2024-11-02 in {summary}" + assert "2024-11-03" in summary, f"Expected 2024-11-03 in {summary}" + assert "2024-11-04" in summary, f"Expected 2024-11-04 in {summary}" + assert summary["2024-11-02"] is True + assert summary["2024-11-03"] is True + assert summary["2024-11-04"] is True + + def test_recordings_summary_multiple_cameras_across_dst(self): + """ + Test recordings summary with multiple cameras across DST boundary. + """ + tz = pytz.timezone("America/New_York") + + # March 9, 2024 at 10:00 AM EST (before DST) + march_9_morning = tz.localize(datetime(2024, 3, 9, 10, 0, 0)).timestamp() + + # March 10, 2024 at 3:00 PM EDT (after DST transition) + march_10_afternoon = tz.localize(datetime(2024, 3, 10, 15, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Override allowed cameras for this test to include both + async def mock_get_allowed_cameras_for_filter(_request: Request): + return ["front_door", "back_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + # Insert recordings for front_door on March 9 + Recordings.insert( + id="front_march_9", + path="/media/recordings/front_march_9.mp4", + camera="front_door", + start_time=march_9_morning, + end_time=march_9_morning + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + # Insert recordings for back_door on March 10 + Recordings.insert( + id="back_march_10", + path="/media/recordings/back_march_10.mp4", + camera="back_door", + start_time=march_10_afternoon, + end_time=march_10_afternoon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with all cameras + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get both days + assert len(summary) == 2, f"Expected 2 days, got {len(summary)}" + assert "2024-03-09" in summary + assert "2024-03-10" in summary + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + + # Reset dependency override back to default single camera for other tests + async def reset_allowed_cameras(_request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + reset_allowed_cameras + ) + + def test_recordings_summary_at_dst_transition_time(self): + """ + Test recordings that span the exact DST transition time. + """ + tz = pytz.timezone("America/New_York") + + # March 10, 2024 at 1:00 AM EST (1 hour before DST transition) + # At 2:00 AM, clocks jump to 3:00 AM + before_transition = tz.localize(datetime(2024, 3, 10, 1, 0, 0)).timestamp() + + # Recording that spans the transition (1:00 AM to 3:30 AM EDT) + # This is 1.5 hours of actual time but spans the "missing" hour + after_transition = tz.localize(datetime(2024, 3, 10, 3, 30, 0)).timestamp() + + with AuthTestClient(self.app) as client: + Recordings.insert( + id="recording_during_transition", + path="/media/recordings/transition.mp4", + camera="front_door", + start_time=before_transition, + end_time=after_transition, + duration=after_transition - before_transition, + motion=100, + objects=5, + ).execute() + + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + + # The recording should appear on March 10 + assert len(summary) == 1 + assert "2024-03-10" in summary + assert summary["2024-03-10"] is True + + def test_recordings_summary_utc_timezone(self): + """ + Test recordings summary with UTC timezone (no DST). + """ + # Use UTC timestamps directly + march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp() + march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp() + + with AuthTestClient(self.app) as client: + Recordings.insert( + id="recording_march_9_utc", + path="/media/recordings/march_9_utc.mp4", + camera="front_door", + start_time=march_9_utc, + end_time=march_9_utc + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="recording_march_10_utc", + path="/media/recordings/march_10_utc.mp4", + camera="front_door", + start_time=march_10_utc, + end_time=march_10_utc + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with UTC timezone + response = client.get( + "/recordings/summary", params={"timezone": "utc", "cameras": "all"} + ) + + assert response.status_code == 200 + summary = response.json() + + # Verify we get both days + assert len(summary) == 2 + assert "2024-03-09" in summary + assert "2024-03-10" in summary + assert summary["2024-03-09"] is True + assert summary["2024-03-10"] is True + + def test_recordings_summary_no_recordings(self): + """ + Test recordings summary when no recordings exist. + """ + with AuthTestClient(self.app) as client: + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "all"}, + ) + + assert response.status_code == 200 + summary = response.json() + assert len(summary) == 0 + + def test_recordings_summary_single_camera_filter(self): + """ + Test recordings summary filtered to a single camera. + """ + tz = pytz.timezone("America/New_York") + march_10_noon = tz.localize(datetime(2024, 3, 10, 12, 0, 0)).timestamp() + + with AuthTestClient(self.app) as client: + # Insert recordings for both cameras + Recordings.insert( + id="front_recording", + path="/media/recordings/front.mp4", + camera="front_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=100, + objects=5, + ).execute() + + Recordings.insert( + id="back_recording", + path="/media/recordings/back.mp4", + camera="back_door", + start_time=march_10_noon, + end_time=march_10_noon + 3600, + duration=3600, + motion=150, + objects=8, + ).execute() + + # Test with only front_door camera + response = client.get( + "/recordings/summary", + params={"timezone": "America/New_York", "cameras": "front_door"}, + ) + + assert response.status_code == 200 + summary = response.json() + assert len(summary) == 1 + assert "2024-03-10" in summary + assert summary["2024-03-10"] is True diff --git a/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_review.py b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_review.py new file mode 100644 index 0000000..7c6615b --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/http_api/test_http_review.py @@ -0,0 +1,710 @@ +from datetime import datetime, timedelta + +from fastapi import Request +from peewee import DoesNotExist + +from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user +from frigate.models import Event, Recordings, ReviewSegment, UserReviewStatus +from frigate.review.types import SeverityEnum +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpReview(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment, UserReviewStatus]) + self.app = super().create_app() + self.user_id = "admin" + + # Mock get_current_user for all tests + # This mock uses headers set by AuthTestClient + async def mock_get_current_user(request: Request): + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + if not username or not role: + from fastapi.responses import JSONResponse + + return JSONResponse( + content={"message": "No authorization headers."}, status_code=401 + ) + return {"username": username, "role": role} + + self.app.dependency_overrides[get_current_user] = mock_get_current_user + + async def mock_get_allowed_cameras_for_filter(request: Request): + return ["front_door"] + + self.app.dependency_overrides[get_allowed_cameras_for_filter] = ( + mock_get_allowed_cameras_for_filter + ) + + def tearDown(self): + self.app.dependency_overrides.clear() + super().tearDown() + + def _get_reviews(self, ids: list[str]): + return list( + ReviewSegment.select(ReviewSegment.id) + .where(ReviewSegment.id.in_(ids)) + .execute() + ) + + def _get_recordings(self, ids: list[str]): + return list( + Recordings.select(Recordings.id).where(Recordings.id.in_(ids)).execute() + ) + + def _insert_user_review_status(self, review_id: str, reviewed: bool = True): + UserReviewStatus.create( + user_id=self.user_id, + review_segment=ReviewSegment.get(ReviewSegment.id == review_id), + has_been_reviewed=reviewed, + ) + + #################################################################################################################### + ################################### GET /review Endpoint ######################################################## + #################################################################################################################### + + def test_get_review_that_overlaps_default_period(self): + """Test that a review item that starts during the default period + but ends after is included in the results.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random", now, now + 2) + response = client.get("/review") + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + + def test_get_review_no_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 2, now - 1) + response = client.get("/review") + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + assert response_json[0]["has_been_reviewed"] == False + + def test_get_review_with_time_filter_no_matches(self): + """Test that review items outside the range are not returned.""" + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now - 2, now - 1) + super().insert_mock_review_segment(f"{id}2", now + 4, now + 5) + params = { + "after": now, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_get_review_with_time_filter(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2) + params = { + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + def test_get_review_with_limit_filter(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + id2 = "654321.random" + super().insert_mock_review_segment(id, now, now + 2) + super().insert_mock_review_segment(id2, now + 1, now + 2) + params = { + "limit": 1, + "after": now, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id2 + + def test_get_review_with_severity_filters_no_matches(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) + params = { + "severity": "detection", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + def test_get_review_with_severity_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection) + params = { + "severity": "alert", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_get_review_with_all_filters(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now, now + 2) + params = { + "cameras": "front_door", + "labels": "all", + "zones": "all", + "reviewed": 0, + "limit": 1, + "severity": "alert", + "after": now - 1, + "before": now + 3, + } + response = client.get("/review", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 1 + assert response_json[0]["id"] == id + + #################################################################################################################### + ################################### GET /review/summary Endpoint ################################################# + #################################################################################################################### + def test_get_review_summary_all_filters(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + params = { + "cameras": "front_door", + "labels": "all", + "zones": "all", + "timezone": "utc", + } + response = client.get("/review/summary", params=params) + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = datetime.today().strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_no_filters(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = datetime.today().strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_days(self): + now = datetime.now() + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment( + "123456.random", now.timestamp() - 2, now.timestamp() - 1 + ) + super().insert_mock_review_segment( + "654321.random", + five_days_ago.timestamp(), + five_days_ago.timestamp() + 1, + ) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = now.strftime("%Y-%m-%d") + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_in_same_day(self): + now = datetime.now() + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random", now.timestamp()) + five_days_ago_ts = five_days_ago.timestamp() + for i in range(20): + super().insert_mock_review_segment( + f"123456_{i}.random_alert", + five_days_ago_ts, + five_days_ago_ts, + SeverityEnum.alert, + ) + for i in range(15): + super().insert_mock_review_segment( + f"123456_{i}.random_detection", + five_days_ago_ts, + five_days_ago_ts, + SeverityEnum.detection, + ) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-24' + today_formatted = now.strftime("%Y-%m-%d") + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + today_formatted: { + "day": today_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 1, + "total_detection": 0, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 0, + "reviewed_detection": 0, + "total_alert": 20, + "total_detection": 15, + }, + } + self.assertEqual(response_json, expected_response) + + def test_get_review_summary_multiple_in_same_day_with_reviewed(self): + five_days_ago = datetime.today() - timedelta(days=5) + + with AuthTestClient(self.app) as client: + five_days_ago_ts = five_days_ago.timestamp() + for i in range(10): + id = f"123456_{i}.random_alert_not_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.alert + ) + for i in range(10): + id = f"123456_{i}.random_alert_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.alert + ) + self._insert_user_review_status(id, reviewed=True) + for i in range(10): + id = f"123456_{i}.random_detection_not_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.detection + ) + for i in range(5): + id = f"123456_{i}.random_detection_reviewed" + super().insert_mock_review_segment( + id, five_days_ago_ts, five_days_ago_ts, SeverityEnum.detection + ) + self._insert_user_review_status(id, reviewed=True) + response = client.get("/review/summary") + assert response.status_code == 200 + response_json = response.json() + # e.g. '2024-11-19' + five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d") + expected_response = { + "last24Hours": { + "reviewed_alert": None, + "reviewed_detection": None, + "total_alert": None, + "total_detection": None, + }, + five_days_ago_formatted: { + "day": five_days_ago_formatted, + "reviewed_alert": 10, + "reviewed_detection": 5, + "total_alert": 20, + "total_detection": 15, + }, + } + self.assertEqual(response_json, expected_response) + + #################################################################################################################### + ################################### POST reviews/viewed Endpoint ################################################ + #################################################################################################################### + + def test_post_reviews_viewed_no_body(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.post("/reviews/viewed") + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_viewed_no_body_ids(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + body = {"ids": [""]} + response = client.post("/reviews/viewed", json=body) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_viewed_non_existent_id(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": ["1"]} + response = client.post("/reviews/viewed", json=body) + assert response.status_code == 200 + response = response.json() + assert response["success"] == True + assert response["message"] == "Marked multiple items as reviewed" + # Verify that in DB the review segment was not changed + with self.assertRaises(DoesNotExist): + UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == "1", + ) + + def test_post_reviews_viewed(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": [id]} + response = client.post("/reviews/viewed", json=body) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Marked multiple items as reviewed" + # Verify UserReviewStatus was created + user_review = UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == id, + ) + assert user_review.has_been_reviewed == True + + #################################################################################################################### + ################################### POST reviews/delete Endpoint ################################################ + #################################################################################################################### + def test_post_reviews_delete_no_body(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + response = client.post("/reviews/delete", headers={"remote-role": "admin"}) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_delete_no_body_ids(self): + with AuthTestClient(self.app) as client: + super().insert_mock_review_segment("123456.random") + body = {"ids": [""]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + # Missing ids + assert response.status_code == 422 + + def test_post_reviews_delete_non_existent_id(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": ["1"]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + # Verify that in DB the review segment was not deleted + review_ids_in_db_after = self._get_reviews([id]) + assert len(review_ids_in_db_after) == 1 + assert review_ids_in_db_after[0].id == id + + def test_post_reviews_delete(self): + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id) + body = {"ids": [id]} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + # Verify that in DB the review segment was deleted + review_ids_in_db_after = self._get_reviews([id]) + assert len(review_ids_in_db_after) == 0 + + def test_post_reviews_delete_many(self): + with AuthTestClient(self.app) as client: + ids = ["123456.random", "654321.random"] + for id in ids: + super().insert_mock_review_segment(id) + super().insert_mock_recording(id) + + review_ids_in_db_before = self._get_reviews(ids) + recordings_ids_in_db_before = self._get_recordings(ids) + assert len(review_ids_in_db_before) == 2 + assert len(recordings_ids_in_db_before) == 2 + + body = {"ids": ids} + response = client.post( + "/reviews/delete", json=body, headers={"remote-role": "admin"} + ) + assert response.status_code == 200 + response_json = response.json() + assert response_json["success"] == True + assert response_json["message"] == "Deleted review items." + + # Verify that in DB all review segments and recordings that were passed were deleted + review_ids_in_db_after = self._get_reviews(ids) + recording_ids_in_db_after = self._get_recordings(ids) + assert len(review_ids_in_db_after) == 0 + assert len(recording_ids_in_db_after) == 0 + + #################################################################################################################### + ################################### GET /review/activity/motion Endpoint ######################################## + #################################################################################################################### + def test_review_activity_motion_no_data_for_time_range(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + params = { + "after": now, + "before": now + 3, + } + response = client.get("/review/activity/motion", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 0 + + def test_review_activity_motion(self): + now = int(datetime.now().timestamp()) + + with AuthTestClient(self.app) as client: + one_m = int((datetime.now() + timedelta(minutes=1)).timestamp()) + id = "123456.random" + id2 = "123451.random" + super().insert_mock_recording(id, now + 1, now + 2, motion=101) + super().insert_mock_recording(id2, one_m + 1, one_m + 2, motion=200) + params = { + "after": now, + "before": one_m + 3, + "scale": 1, + } + response = client.get("/review/activity/motion", params=params) + assert response.status_code == 200 + response_json = response.json() + assert len(response_json) == 61 + self.assertDictEqual( + {"motion": 50.5, "camera": "front_door", "start_time": now + 1}, + response_json[0], + ) + for item in response_json[1:-1]: + self.assertDictEqual( + {"motion": 0.0, "camera": "", "start_time": item["start_time"]}, + item, + ) + self.assertDictEqual( + {"motion": 100.0, "camera": "front_door", "start_time": one_m + 1}, + response_json[len(response_json) - 1], + ) + + #################################################################################################################### + ################################### GET /review/event/{event_id} Endpoint ####################################### + #################################################################################################################### + def test_review_event_not_found(self): + with AuthTestClient(self.app) as client: + response = client.get("/review/event/123456.random") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_review_event_not_found_in_data(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + id = "123456.random" + super().insert_mock_review_segment(id, now + 1, now + 2) + response = client.get(f"/review/event/{id}") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_review_get_specific_event(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + event_id = "123456.event.random" + super().insert_mock_event(event_id) + review_id = "123456.review.random" + super().insert_mock_review_segment( + review_id, now + 1, now + 2, data={"detections": {"event_id": event_id}} + ) + response = client.get(f"/review/event/{event_id}") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + { + "id": review_id, + "camera": "front_door", + "start_time": now + 1, + "end_time": now + 2, + "severity": "alert", + "thumb_path": "False", + "data": {"detections": {"event_id": event_id}}, + }, + response_json, + ) + + #################################################################################################################### + ################################### GET /review/{review_id} Endpoint ####################################### + #################################################################################################################### + def test_review_not_found(self): + with AuthTestClient(self.app) as client: + response = client.get("/review/123456.random") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": "Review item not found"}, + response_json, + ) + + def test_get_review(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + review_id = "123456.review.random" + super().insert_mock_review_segment(review_id, now + 1, now + 2) + response = client.get(f"/review/{review_id}") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + { + "id": review_id, + "camera": "front_door", + "start_time": now + 1, + "end_time": now + 2, + "severity": "alert", + "thumb_path": "False", + "data": {}, + }, + response_json, + ) + + #################################################################################################################### + ################################### DELETE /review/{review_id}/viewed Endpoint ################################## + #################################################################################################################### + + def test_delete_review_viewed_review_not_found(self): + with AuthTestClient(self.app) as client: + review_id = "123456.random" + response = client.delete(f"/review/{review_id}/viewed") + assert response.status_code == 404 + response_json = response.json() + self.assertDictEqual( + {"success": False, "message": f"Review {review_id} not found"}, + response_json, + ) + + def test_delete_review_viewed(self): + now = datetime.now().timestamp() + + with AuthTestClient(self.app) as client: + review_id = "123456.review.random" + super().insert_mock_review_segment(review_id, now + 1, now + 2) + self._insert_user_review_status(review_id, reviewed=True) + # Verify it’s reviewed before + response = client.get(f"/review/{review_id}") + + response = client.delete(f"/review/{review_id}/viewed") + assert response.status_code == 200 + response_json = response.json() + self.assertDictEqual( + {"success": True, "message": f"Set Review {review_id} as not viewed"}, + response_json, + ) + + # Verify it’s unreviewed after + with self.assertRaises(DoesNotExist): + UserReviewStatus.get( + UserReviewStatus.user_id == self.user_id, + UserReviewStatus.review_segment == review_id, + ) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_birdseye.py b/sam2-cpu/frigate-dev/frigate/test/test_birdseye.py new file mode 100644 index 0000000..33683f5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_birdseye.py @@ -0,0 +1,47 @@ +"""Test camera user and password cleanup.""" + +import unittest + +from frigate.output.birdseye import get_canvas_shape + + +class TestBirdseye(unittest.TestCase): + def test_16x9(self): + """Test 16x9 aspect ratio works as expected for birdseye.""" + width = 1280 + height = 720 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_4x3(self): + """Test 4x3 aspect ratio works as expected for birdseye.""" + width = 1280 + height = 960 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_32x9(self): + """Test 32x9 aspect ratio works as expected for birdseye.""" + width = 2560 + height = 720 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_9x16(self): + """Test 9x16 aspect ratio works as expected for birdseye.""" + width = 720 + height = 1280 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width + assert canvas_height == height + + def test_non_16x9(self): + """Test non 16x9 aspect ratio fails for birdseye.""" + width = 1280 + height = 840 + canvas_width, canvas_height = get_canvas_shape(width, height) + assert canvas_width == width # width will be the same + assert canvas_height != height diff --git a/sam2-cpu/frigate-dev/frigate/test/test_camera_pw.py b/sam2-cpu/frigate-dev/frigate/test/test_camera_pw.py new file mode 100644 index 0000000..0964f38 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_camera_pw.py @@ -0,0 +1,49 @@ +"""Test camera user and password cleanup.""" + +import unittest + +from frigate.util.builtin import clean_camera_user_pass, escape_special_characters + + +class TestUserPassCleanup(unittest.TestCase): + def setUp(self) -> None: + self.rtsp_with_pass = "rtsp://user:password@192.168.0.2:554/live" + self.rtsp_with_special_pass = ( + "rtsp://user:password`~!@#$%^&*()-_;',.<>:\"\{\}\[\]@@192.168.0.2:554/live" + ) + self.rtsp_no_pass = "rtsp://192.168.0.3:554/live" + + def test_cleanup(self): + """Test that user / pass are cleaned up.""" + clean = clean_camera_user_pass(self.rtsp_with_pass) + assert clean != self.rtsp_with_pass + assert "user:password" not in clean + + def test_no_cleanup(self): + """Test that nothing changes when no user / pass are defined.""" + clean = clean_camera_user_pass(self.rtsp_no_pass) + assert clean == self.rtsp_no_pass + + def test_special_char_password(self): + """Test that special characters in pw are escaped, but not others.""" + escaped = escape_special_characters(self.rtsp_with_special_pass) + assert ( + escaped + == "rtsp://user:password%60~%21%40%23%24%25%5E%26%2A%28%29-_%3B%27%2C.%3C%3E%3A%22%5C%7B%5C%7D%5C%5B%5C%5D%40@192.168.0.2:554/live" + ) + + def test_no_special_char_password(self): + """Test that no change is made to path with no special characters.""" + escaped = escape_special_characters(self.rtsp_with_pass) + assert escaped == self.rtsp_with_pass + + +class TestUserPassMasking(unittest.TestCase): + def setUp(self) -> None: + self.rtsp_log_message = "Did you mean file:rtsp://user:password@192.168.1.3:554" + + def test_rtsp_in_log_message(self): + """Test that the rtsp url in a log message is escaped.""" + escaped = clean_camera_user_pass(self.rtsp_log_message) + print(f"The escaped is {escaped}") + assert escaped == "Did you mean file:rtsp://*:*@192.168.1.3:554" diff --git a/sam2-cpu/frigate-dev/frigate/test/test_config.py b/sam2-cpu/frigate-dev/frigate/test/test_config.py new file mode 100644 index 0000000..4bafe73 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_config.py @@ -0,0 +1,1536 @@ +import json +import os +import unittest +from unittest.mock import patch + +import numpy as np +from pydantic import ValidationError +from ruamel.yaml.constructor import DuplicateKeyError + +from frigate.config import BirdseyeModeEnum, FrigateConfig +from frigate.const import MODEL_CACHE_DIR +from frigate.detectors import DetectorTypeEnum +from frigate.util.builtin import deep_merge + + +class TestConfig(unittest.TestCase): + def setUp(self): + self.minimal = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.plus_model_info = { + "id": "e63b7345cc83a84ed79dedfc99c16616", + "name": "SSDLite Mobiledet", + "description": "Fine tuned model", + "trainDate": "2023-04-28T23:22:01.262Z", + "type": "ssd", + "supportedDetectors": ["cpu", "edgetpu"], + "width": 320, + "height": 320, + "inputShape": "nhwc", + "pixelFormat": "rgb", + "labelMap": { + "0": "amazon", + "1": "car", + "2": "cat", + "3": "deer", + "4": "dog", + "5": "face", + "6": "fedex", + "7": "license_plate", + "8": "package", + "9": "person", + "10": "ups", + }, + } + + if not os.path.exists(MODEL_CACHE_DIR) and not os.path.islink(MODEL_CACHE_DIR): + os.makedirs(MODEL_CACHE_DIR) + + def test_config_class(self): + frigate_config = FrigateConfig(**self.minimal) + assert "cpu" in frigate_config.detectors.keys() + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["cpu"].model.width == 320 + + @patch("frigate.detectors.detector_config.load_labels") + def test_detector_custom_model_path(self, mock_labels): + mock_labels.return_value = {} + config = { + "detectors": { + "cpu": { + "type": "cpu", + "model_path": "/cpu_model.tflite", + }, + "edgetpu": { + "type": "edgetpu", + "model_path": "/edgetpu_model.tflite", + }, + "openvino": { + "type": "openvino", + }, + }, + # needs to be a file that will exist, doesn't matter what + "model": {"path": "/etc/hosts", "width": 512}, + } + + frigate_config = FrigateConfig(**(deep_merge(config, self.minimal))) + + assert "cpu" in frigate_config.detectors.keys() + assert "edgetpu" in frigate_config.detectors.keys() + assert "openvino" in frigate_config.detectors.keys() + + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["edgetpu"].type == DetectorTypeEnum.edgetpu + assert frigate_config.detectors["openvino"].type == DetectorTypeEnum.openvino + + assert frigate_config.detectors["cpu"].num_threads == 3 + assert frigate_config.detectors["edgetpu"].device is None + assert frigate_config.detectors["openvino"].device is None + + assert frigate_config.model.path == "/etc/hosts" + assert frigate_config.detectors["cpu"].model.path == "/cpu_model.tflite" + assert frigate_config.detectors["edgetpu"].model.path == "/edgetpu_model.tflite" + assert frigate_config.detectors["openvino"].model.path == "/etc/hosts" + + def test_invalid_mqtt_config(self): + config = { + "mqtt": {"host": "mqtt", "user": "test"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_inherit_tracked_objects(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.track + + def test_override_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "birdseye": {"enabled": False, "mode": "motion"}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert not frigate_config.cameras["back"].birdseye.enabled + assert frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion + + def test_override_birdseye_non_inheritable(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous", "height": 1920}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].birdseye.enabled + + def test_inherit_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": {"enabled": True, "mode": "continuous"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].birdseye.enabled + assert ( + frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous + ) + + def test_override_tracked_objects(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": {"track": ["cat"]}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "cat" in frigate_config.cameras["back"].objects.track + + def test_default_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + + def test_inherit_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7 + + def test_override_object_filters(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7 + + def test_global_object_mask(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person", "dog"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "mask": "0,0,1,1,0,1", + "filters": {"dog": {"mask": "1,1,1,1,1,1"}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + back_camera = frigate_config.cameras["back"] + assert "dog" in back_camera.objects.filters + assert len(back_camera.objects.filters["dog"].raw_mask) == 2 + assert len(back_camera.objects.filters["person"].raw_mask) == 1 + + def test_motion_mask_relative_matches_explicit(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"alerts": {"retain": {"days": 20}}}, + "cameras": { + "explicit": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "motion": { + "mask": [ + "0,0,200,100,600,300,800,400", + ] + }, + }, + "relative": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "motion": { + "mask": [ + "0.0,0.0,0.25,0.25,0.75,0.75,1.0,1.0", + ] + }, + }, + }, + } + + frigate_config = FrigateConfig(**config) + assert np.array_equal( + frigate_config.cameras["explicit"].motion.mask, + frigate_config.cameras["relative"].motion.mask, + ) + + def test_default_input_args(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-rtsp_transport" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_global(self): + config = { + "ffmpeg": {"input_args": "-re"}, + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_camera(self): + config = { + "mqtt": {"host": "mqtt"}, + "ffmpeg": {"input_args": ["test"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ], + "input_args": ["-re"], + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_ffmpeg_params_input(self): + config = { + "mqtt": {"host": "mqtt"}, + "ffmpeg": {"input_args": ["test2"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + "input_args": "-re test", + } + ], + "input_args": "test3", + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"threshold": 0.7}}, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "-re" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test" in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test2" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + assert "test3" not in frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + + def test_inherit_clips_retention(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"alerts": {"retain": {"days": 20}}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].record.alerts.retain.days == 20 + + def test_roles_listed_twice_throws_error(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}, + {"path": "rtsp://10.0.0.1:554/video2", "roles": ["detect"]}, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_zone_matching_camera_name_throws_error(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": {"back": {"coordinates": "1,1,1,1,1,1"}}, + } + }, + } + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_zone_assigns_color_and_contour(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": {"test": {"coordinates": "1,1,1,1,1,1"}}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert isinstance( + frigate_config.cameras["back"].zones["test"].contour, np.ndarray + ) + assert frigate_config.cameras["back"].zones["test"].color != (0, 0, 0) + + def test_zone_relative_matches_explicit(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": { + "alerts": { + "retain": { + "days": 20, + } + } + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 400, + "width": 800, + "fps": 5, + }, + "zones": { + "explicit": { + "coordinates": "0,0,200,100,600,300,800,400", + }, + "relative": { + "coordinates": "0.0,0.0,0.25,0.25,0.75,0.75,1.0,1.0", + }, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert np.array_equal( + frigate_config.cameras["back"].zones["explicit"].contour, + frigate_config.cameras["back"].zones["relative"].contour, + ) + + def test_role_assigned_but_not_enabled(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + {"path": "rtsp://10.0.0.1:554/record", "roles": ["record"]}, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + ffmpeg_cmds = frigate_config.cameras["back"].ffmpeg_cmds + assert len(ffmpeg_cmds) == 1 + assert "clips" not in ffmpeg_cmds[0]["roles"] + + def test_max_disappeared_default(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "enabled": True, + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 5 * 5 + + def test_motion_frame_height_wont_go_below_120(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].motion.frame_height == 100 + + def test_motion_contour_area_dynamic(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert round(frigate_config.cameras["back"].motion.contour_area) == 10 + + def test_merge_labelmap(self): + config = { + "mqtt": {"host": "mqtt"}, + "model": {"labelmap": {7: "truck"}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[7] == "truck" + + def test_default_labelmap_empty(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "person" + + def test_default_labelmap(self): + config = { + "mqtt": {"host": "mqtt"}, + "model": {"width": 320, "height": 320}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "person" + + def test_plus_labelmap(self): + with open(os.path.join(MODEL_CACHE_DIR, "test"), "w") as f: + json.dump(self.plus_model_info, f) + with open(os.path.join(MODEL_CACHE_DIR, "test.json"), "w") as f: + json.dump(self.plus_model_info, f) + + config = { + "mqtt": {"host": "mqtt"}, + "model": {"path": "plus://test"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.model.merged_labelmap[0] == "amazon" + + def test_fails_on_invalid_role(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["clips"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) + + def test_fails_on_missing_role(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "audio": {"enabled": True}, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config)) + + def test_works_on_missing_role_multiple_cams(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + "cam2": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + { + "path": "rtsp://10.0.0.1:554/video2", + "roles": ["record"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + }, + } + + FrigateConfig(**config) + + def test_global_detect(self): + config = { + "mqtt": {"host": "mqtt"}, + "detect": {"max_disappeared": 1}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 1 + assert frigate_config.cameras["back"].detect.height == 1080 + + def test_default_detect(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 720, + "width": 1280, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 25 + assert frigate_config.cameras["back"].detect.height == 720 + + def test_global_detect_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "detect": {"max_disappeared": 1, "height": 720}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].detect.max_disappeared == 1 + assert frigate_config.cameras["back"].detect.height == 1080 + assert frigate_config.cameras["back"].detect.width == 1920 + + def test_global_snapshots(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"enabled": True}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "snapshots": { + "height": 100, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.enabled + assert frigate_config.cameras["back"].snapshots.height == 100 + + def test_default_snapshots(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.bounding_box + assert frigate_config.cameras["back"].snapshots.quality == 70 + + def test_global_snapshots_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"bounding_box": False, "height": 300}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "snapshots": { + "height": 150, + "enabled": True, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.bounding_box is False + assert frigate_config.cameras["back"].snapshots.height == 150 + assert frigate_config.cameras["back"].snapshots.enabled + + def test_global_jsmpeg(self): + config = { + "mqtt": {"host": "mqtt"}, + "live": {"quality": 4}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 4 + + def test_default_live(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 8 + + def test_global_live_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "live": {"quality": 4, "height": 480}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "live": { + "quality": 7, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].live.quality == 7 + assert frigate_config.cameras["back"].live.height == 480 + + def test_global_timestamp_style(self): + config = { + "mqtt": {"host": "mqtt"}, + "timestamp_style": {"position": "bl"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "bl" + + def test_default_timestamp_style(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "tl" + + def test_global_timestamp_style_merge(self): + config = { + "mqtt": {"host": "mqtt"}, + "timestamp_style": {"position": "br", "thickness": 2}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "timestamp_style": {"position": "bl", "thickness": 4}, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].timestamp_style.position == "bl" + assert frigate_config.cameras["back"].timestamp_style.thickness == 4 + + def test_allow_retain_to_be_a_decimal(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"retain": {"default": 1.5}}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].snapshots.retain.default == 1.5 + + def test_fails_on_bad_camera_name(self): + config = { + "mqtt": {"host": "mqtt"}, + "snapshots": {"retain": {"default": 1.5}}, + "cameras": { + "back camer#": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises(ValidationError, lambda: FrigateConfig(**config).cameras) + + def test_fails_on_bad_segment_time(self): + config = { + "mqtt": {"host": "mqtt"}, + "record": {"enabled": True}, + "cameras": { + "back": { + "ffmpeg": { + "output_args": { + "record": "-f segment -segment_time 70 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an" + }, + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ], + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + self.assertRaises( + ValueError, + lambda: FrigateConfig(**config).ffmpeg.output_args.record, + ) + + def test_fails_zone_defines_untracked_object(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": {"track": ["person"]}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + }, + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "zones": { + "steps": { + "coordinates": "0,0,0,0", + "objects": ["car", "person"], + }, + }, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config).cameras) + + def test_fails_duplicate_keys(self): + raw_config = """ + cameras: + test: + ffmpeg: + inputs: + - one + - two + inputs: + - three + - four + """ + + self.assertRaises( + DuplicateKeyError, lambda: FrigateConfig.parse_yaml(raw_config) + ) + + def test_object_filter_ratios_work(self): + config = { + "mqtt": {"host": "mqtt"}, + "objects": { + "track": ["person", "dog"], + "filters": {"dog": {"min_ratio": 0.2, "max_ratio": 10.1}}, + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert "dog" in frigate_config.cameras["back"].objects.filters + assert frigate_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2 + assert frigate_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1 + + def test_valid_movement_weights(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "onvif": { + "autotracking": { + "movement_weights": "0, 1, 1.23, 2.34, 0.50, 1" + } + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + assert frigate_config.cameras["back"].onvif.autotracking.movement_weights == [ + "0.0", + "1.0", + "1.23", + "2.34", + "0.5", + "1.0", + ] + + def test_fails_invalid_movement_weights(self): + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "onvif": {"autotracking": {"movement_weights": "1.234, 2.345a"}}, + } + }, + } + + self.assertRaises(ValueError, lambda: FrigateConfig(**config)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_copy_yuv_to_position.py b/sam2-cpu/frigate-dev/frigate/test/test_copy_yuv_to_position.py new file mode 100644 index 0000000..4a31928 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_copy_yuv_to_position.py @@ -0,0 +1,68 @@ +from unittest import TestCase, main + +import cv2 +import numpy as np + +from frigate.util.image import copy_yuv_to_position, get_yuv_crop + + +class TestCopyYuvToPosition(TestCase): + def setUp(self): + self.source_frame_bgr = np.zeros((400, 800, 3), np.uint8) + self.source_frame_bgr[:] = (0, 0, 255) + self.source_yuv_frame = cv2.cvtColor( + self.source_frame_bgr, cv2.COLOR_BGR2YUV_I420 + ) + y, u1, u2, v1, v2 = get_yuv_crop( + self.source_yuv_frame.shape, + ( + 0, + 0, + self.source_frame_bgr.shape[1], + self.source_frame_bgr.shape[0], + ), + ) + self.source_channel_dims = { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + } + + self.dest_frame_bgr = np.zeros((400, 800, 3), np.uint8) + self.dest_frame_bgr[:] = (112, 202, 50) + self.dest_frame_bgr[100:300, 200:600] = (255, 0, 0) + self.dest_yuv_frame = cv2.cvtColor(self.dest_frame_bgr, cv2.COLOR_BGR2YUV_I420) + + def test_clear_position(self): + copy_yuv_to_position(self.dest_yuv_frame, (100, 100), (100, 100)) + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + def test_copy_position(self): + copy_yuv_to_position( + self.dest_yuv_frame, + (100, 100), + (100, 200), + self.source_yuv_frame, + self.source_channel_dims, + ) + + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + def test_copy_position_full_screen(self): + copy_yuv_to_position( + self.dest_yuv_frame, + (0, 0), + (400, 800), + self.source_yuv_frame, + self.source_channel_dims, + ) + # cv2.imwrite(f"source_frame_yuv.jpg", self.source_yuv_frame) + # cv2.imwrite(f"dest_frame_yuv.jpg", self.dest_yuv_frame) + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_ffmpeg_presets.py b/sam2-cpu/frigate-dev/frigate/test/test_ffmpeg_presets.py new file mode 100644 index 0000000..92df057 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_ffmpeg_presets.py @@ -0,0 +1,147 @@ +import unittest + +from frigate.config import FrigateConfig +from frigate.config.camera.ffmpeg import FFMPEG_INPUT_ARGS_DEFAULT +from frigate.ffmpeg_presets import parse_preset_input + + +class TestFfmpegPresets(unittest.TestCase): + def setUp(self): + self.default_ffmpeg = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ], + "output_args": { + "detect": "-f rawvideo -pix_fmt yuv420p", + "record": "-f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an", + }, + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "record": { + "enabled": True, + }, + "name": "back", + } + }, + } + + def test_default_ffmpeg(self): + FrigateConfig(**self.default_ffmpeg) + + def test_ffmpeg_hwaccel_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "preset-rpi-64-h264" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-rpi-64-h264" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v:1 h264_v4l2m2m" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_hwaccel_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "-other-hwaccel args" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-other-hwaccel args" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_hwaccel_scale_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["hwaccel_args"] = ( + "preset-nvidia-h264" + ) + self.default_ffmpeg["cameras"]["back"]["detect"] = { + "height": 1920, + "width": 2560, + "fps": 10, + } + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-nvidia-h264" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert ( + "fps=10,scale_cuda=w=2560:h=1920,hwdownload,format=nv12,eq=gamma=1.4:gamma_weight=0.5" + in (" ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"])) + ) + + def test_default_ffmpeg_input_arg_preset(self): + frigate_config = FrigateConfig(**self.default_ffmpeg) + + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = ( + "preset-rtsp-generic" + ) + frigate_preset_config = FrigateConfig(**self.default_ffmpeg) + assert ( + # Ignore global and user_agent args in comparison + frigate_preset_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + == frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + ) + + def test_ffmpeg_input_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = ( + "preset-rtmp-generic" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-rtmp-generic" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert (" ".join(parse_preset_input("preset-rtmp-generic", 5))) in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_input_args_as_string(self): + # Strip user_agent args here to avoid handling quoting issues + defaultArgsList = parse_preset_input(FFMPEG_INPUT_ARGS_DEFAULT, 5)[2::] + argsString = " ".join(defaultArgsList) + ' -some "arg with space"' + argsList = defaultArgsList + ["-some", "arg with space"] + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = argsString + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert set(argsList).issubset( + frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"] + ) + + def test_ffmpeg_input_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["input_args"] = "-some inputs" + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-some inputs" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = ( + "preset-record-generic-audio-aac" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "preset-record-generic-audio-aac" not in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + assert "-c:v copy -c:a aac" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + def test_ffmpeg_output_record_not_preset(self): + self.default_ffmpeg["cameras"]["back"]["ffmpeg"]["output_args"]["record"] = ( + "-some output -segment_time 10" + ) + frigate_config = FrigateConfig(**self.default_ffmpeg) + assert "-some output" in ( + " ".join(frigate_config.cameras["back"].ffmpeg_cmds[0]["cmd"]) + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_gpu_stats.py b/sam2-cpu/frigate-dev/frigate/test/test_gpu_stats.py new file mode 100644 index 0000000..fd0df94 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_gpu_stats.py @@ -0,0 +1,46 @@ +import unittest +from unittest.mock import MagicMock, patch + +from frigate.util.services import get_amd_gpu_stats, get_intel_gpu_stats + + +class TestGpuStats(unittest.TestCase): + def setUp(self): + self.amd_results = "Unknown Radeon card. <= R500 won't work, new cards might.\nDumping to -, line limit 1.\n1664070990.607556: bus 10, gpu 4.17%, ee 0.00%, vgt 0.00%, ta 0.00%, tc 0.00%, sx 0.00%, sh 0.00%, spi 0.83%, smx 0.00%, cr 0.00%, sc 0.00%, pa 0.00%, db 0.00%, cb 0.00%, vram 60.37% 294.04mb, gtt 0.33% 52.21mb, mclk 100.00% 1.800ghz, sclk 26.65% 0.533ghz\n" + self.intel_results = """{"period":{"duration":1.194033,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":3349.991164,"unit":"irq/s"},"rc6":{"value":47.844741,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":4.533124,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":6.194385,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}},{"period":{"duration":1.189291,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":0.000000,"unit":"irq/s"},"rc6":{"value":100.000000,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}}""" + self.nvidia_results = "name, utilization.gpu [%], memory.used [MiB], memory.total [MiB]\nNVIDIA GeForce RTX 3050, 42 %, 5036 MiB, 8192 MiB\n" + + @patch("subprocess.run") + def test_amd_gpu_stats(self, sp): + process = MagicMock() + process.returncode = 0 + process.stdout = self.amd_results + sp.return_value = process + amd_stats = get_amd_gpu_stats() + assert amd_stats == {"gpu": "4.17%", "mem": "60.37%"} + + # @patch("subprocess.run") + # def test_nvidia_gpu_stats(self, sp): + # process = MagicMock() + # process.returncode = 0 + # process.stdout = self.nvidia_results + # sp.return_value = process + # nvidia_stats = get_nvidia_gpu_stats() + # assert nvidia_stats == { + # "name": "NVIDIA GeForce RTX 3050", + # "gpu": "42 %", + # "mem": "61.5 %", + # } + + @patch("subprocess.run") + def test_intel_gpu_stats(self, sp): + process = MagicMock() + process.returncode = 124 + process.stdout = self.intel_results + sp.return_value = process + intel_stats = get_intel_gpu_stats(False) + print(f"the intel stats are {intel_stats}") + assert intel_stats == { + "gpu": "1.13%", + "mem": "-%", + } diff --git a/sam2-cpu/frigate-dev/frigate/test/test_obects.py b/sam2-cpu/frigate-dev/frigate/test/test_obects.py new file mode 100644 index 0000000..8fe8319 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_obects.py @@ -0,0 +1,50 @@ +import unittest + +from frigate.track.tracked_object import TrackedObjectAttribute + + +class TestAttribute(unittest.TestCase): + def test_overlapping_object_selection(self) -> None: + attribute = TrackedObjectAttribute( + ( + "amazon", + 0.80078125, + (847, 242, 883, 255), + 468, + 2.769230769230769, + (702, 134, 1050, 482), + ) + ) + objects = [ + { + "label": "car", + "score": 0.98828125, + "box": (728, 223, 1266, 719), + "area": 266848, + "ratio": 1.0846774193548387, + "region": (349, 0, 1397, 1048), + "frame_time": 1727785394.498972, + "centroid": (997, 471), + "id": "1727785349.150633-408hal", + "start_time": 1727785349.150633, + "motionless_count": 362, + "position_changes": 0, + "score_history": [0.98828125, 0.95703125, 0.98828125, 0.98828125], + }, + { + "label": "person", + "score": 0.76953125, + "box": (826, 172, 939, 417), + "area": 27685, + "ratio": 0.46122448979591835, + "region": (702, 134, 1050, 482), + "frame_time": 1727785394.498972, + "centroid": (882, 294), + "id": "1727785390.499768-9fbhem", + "start_time": 1727785390.499768, + "motionless_count": 2, + "position_changes": 1, + "score_history": [0.8828125, 0.83984375, 0.91796875, 0.94140625], + }, + ] + assert attribute.find_best_object(objects) == "1727785390.499768-9fbhem" diff --git a/sam2-cpu/frigate-dev/frigate/test/test_object_detector.py b/sam2-cpu/frigate-dev/frigate/test/test_object_detector.py new file mode 100644 index 0000000..dc15b23 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_object_detector.py @@ -0,0 +1,138 @@ +import unittest +from unittest.mock import Mock, patch + +import numpy as np +from pydantic import parse_obj_as + +import frigate.detectors as detectors +import frigate.object_detection.base +from frigate.config import DetectorConfig, ModelConfig +from frigate.detectors import DetectorTypeEnum +from frigate.detectors.detector_config import InputTensorEnum + + +class TestLocalObjectDetector(unittest.TestCase): + def test_localdetectorprocess_should_only_create_specified_detector_type(self): + for det_type in detectors.api_types: + with self.subTest(det_type=det_type): + with patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ): + test_cfg = parse_obj_as( + DetectorConfig, ({"type": det_type, "model": {}}) + ) + test_cfg.model.path = "/test/modelpath" + test_obj = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg + ) + + assert test_obj is not None + for api_key, mock_detector in detectors.api_types.items(): + if test_cfg.type == api_key: + mock_detector.assert_called_once_with(test_cfg) + else: + mock_detector.assert_not_called() + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + def test_detect_raw_given_tensor_input_should_return_api_detect_raw_result(self): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + ) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RESULT + + test_result = test_obj_detect.detect_raw(TEST_DATA) + + mock_det_api.detect_raw.assert_called_once_with(tensor_input=TEST_DATA) + assert test_result is mock_det_api.detect_raw.return_value + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + def test_detect_raw_given_tensor_input_should_call_api_detect_raw_with_transposed_tensor( + self, + ): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = np.zeros((1, 32, 32, 3), np.uint8) + TEST_DETECT_RESULT = np.ndarray([1, 2, 4, 8, 16, 32]) + + test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + test_cfg.model.input_tensor = InputTensorEnum.nchw + + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg + ) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RESULT + + test_result = test_obj_detect.detect_raw(TEST_DATA) + + mock_det_api.detect_raw.assert_called_once() + assert ( + mock_det_api.detect_raw.call_args.kwargs["tensor_input"].shape + == np.zeros((1, 3, 32, 32)).shape + ) + + assert test_result is mock_det_api.detect_raw.return_value + + @patch.dict( + "frigate.detectors.api_types", + {det_type: Mock() for det_type in DetectorTypeEnum}, + ) + @patch("frigate.object_detection.base.load_labels") + def test_detect_given_tensor_input_should_return_lfiltered_detections( + self, mock_load_labels + ): + mock_cputfl = detectors.api_types[DetectorTypeEnum.cpu] + + TEST_DATA = np.zeros((1, 32, 32, 3), np.uint8) + TEST_DETECT_RAW = [ + [2, 0.9, 5, 4, 3, 2], + [1, 0.5, 8, 7, 6, 5], + [0, 0.4, 2, 4, 8, 16], + ] + TEST_DETECT_RESULT = [ + ("label-3", 0.9, (5, 4, 3, 2)), + ("label-2", 0.5, (8, 7, 6, 5)), + ] + TEST_LABEL_FILE = "/test_labels.txt" + mock_load_labels.return_value = [ + "label-1", + "label-2", + "label-3", + "label-4", + "label-5", + ] + + test_cfg = parse_obj_as(DetectorConfig, {"type": "cpu", "model": {}}) + test_cfg.model = ModelConfig() + test_obj_detect = frigate.object_detection.base.LocalObjectDetector( + detector_config=test_cfg, + labels=TEST_LABEL_FILE, + ) + + mock_load_labels.assert_called_once_with(TEST_LABEL_FILE) + + mock_det_api = mock_cputfl.return_value + mock_det_api.detect_raw.return_value = TEST_DETECT_RAW + + test_result = test_obj_detect.detect(tensor_input=TEST_DATA, threshold=0.5) + + mock_det_api.detect_raw.assert_called_once() + assert ( + mock_det_api.detect_raw.call_args.kwargs["tensor_input"].shape + == np.zeros((1, 32, 32, 3)).shape + ) + assert test_result == TEST_DETECT_RESULT diff --git a/sam2-cpu/frigate-dev/frigate/test/test_proxy_auth.py b/sam2-cpu/frigate-dev/frigate/test/test_proxy_auth.py new file mode 100644 index 0000000..6195548 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_proxy_auth.py @@ -0,0 +1,78 @@ +import unittest + +from frigate.api.auth import resolve_role +from frigate.config import HeaderMappingConfig, ProxyConfig + + +class TestProxyRoleResolution(unittest.TestCase): + def setUp(self): + self.proxy_config = ProxyConfig( + auth_secret=None, + default_role="viewer", + separator="|", + header_map=HeaderMappingConfig( + user="x-remote-user", + role="x-remote-role", + role_map={ + "admin": ["group_admin"], + "viewer": ["group_viewer"], + }, + ), + ) + self.config_roles = list(["admin", "viewer"]) + + def test_role_map_single_group_match(self): + headers = {"x-remote-role": "group_admin"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_role_map_multiple_groups(self): + headers = {"x-remote-role": "group_admin|group_viewer"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_direct_role_header_with_separator(self): + config = self.proxy_config + config.header_map.role_map = None # disable role_map + headers = {"x-remote-role": "admin|viewer"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + def test_invalid_role_header(self): + config = self.proxy_config + config.header_map.role_map = None + headers = {"x-remote-role": "notarole"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, config.default_role) + + def test_missing_role_header(self): + headers = {} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) + + def test_empty_role_header(self): + headers = {"x-remote-role": ""} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) + + def test_whitespace_groups(self): + headers = {"x-remote-role": " | group_admin | "} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "admin") + + def test_mixed_valid_and_invalid_groups(self): + headers = {"x-remote-role": "bogus|group_viewer"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, "viewer") + + def test_case_insensitive_role_direct(self): + config = self.proxy_config + config.header_map.role_map = None + headers = {"x-remote-role": "AdMiN"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + def test_role_map_no_match_falls_back(self): + headers = {"x-remote-role": "group_unknown"} + role = resolve_role(headers, self.proxy_config, self.config_roles) + self.assertEqual(role, self.proxy_config.default_role) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_record_retention.py b/sam2-cpu/frigate-dev/frigate/test/test_record_retention.py new file mode 100644 index 0000000..b826c3a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_record_retention.py @@ -0,0 +1,33 @@ +import unittest + +from frigate.config import RetainModeEnum +from frigate.record.maintainer import SegmentInfo + + +class TestRecordRetention(unittest.TestCase): + def test_motion_should_keep_motion_not_object(self): + segment_info = SegmentInfo( + motion_count=1, active_object_count=0, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert segment_info.should_discard_segment(RetainModeEnum.active_objects) + + def test_object_should_keep_object_when_motion(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=1, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert not segment_info.should_discard_segment(RetainModeEnum.active_objects) + + def test_all_should_keep_all(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=0, region_count=0, average_dBFS=0 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.all) + + def test_should_keep_audio_in_motion_mode(self): + segment_info = SegmentInfo( + motion_count=0, active_object_count=0, region_count=0, average_dBFS=1 + ) + assert not segment_info.should_discard_segment(RetainModeEnum.motion) + assert segment_info.should_discard_segment(RetainModeEnum.active_objects) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_reduce_boxes.py b/sam2-cpu/frigate-dev/frigate/test/test_reduce_boxes.py new file mode 100644 index 0000000..5ac913d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_reduce_boxes.py @@ -0,0 +1,26 @@ +from unittest import TestCase, main + +from frigate.util.object import box_overlaps, reduce_boxes + + +class TestBoxOverlaps(TestCase): + def test_overlap(self): + assert box_overlaps((100, 100, 200, 200), (50, 50, 150, 150)) + + def test_overlap_2(self): + assert box_overlaps((50, 50, 150, 150), (100, 100, 200, 200)) + + def test_no_overlap(self): + assert not box_overlaps((100, 100, 200, 200), (250, 250, 350, 350)) + + +class TestReduceBoxes(TestCase): + def test_cluster(self): + clusters = reduce_boxes( + [(144, 290, 221, 459), (225, 178, 426, 341), (343, 105, 584, 250)] + ) + assert len(clusters) == 2 + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/sam2-cpu/frigate-dev/frigate/test/test_storage.py b/sam2-cpu/frigate-dev/frigate/test/test_storage.py new file mode 100644 index 0000000..4ae5715 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_storage.py @@ -0,0 +1,316 @@ +import datetime +import logging +import os +import tempfile +import unittest +from unittest.mock import MagicMock + +from peewee import DoesNotExist +from peewee_migrate import Router +from playhouse.sqlite_ext import SqliteExtDatabase +from playhouse.sqliteq import SqliteQueueDatabase + +from frigate.config import FrigateConfig +from frigate.models import Event, Recordings +from frigate.storage import StorageMaintainer +from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS + + +class TestHttp(unittest.TestCase): + def setUp(self): + # setup clean database for each test run + migrate_db = SqliteExtDatabase("test.db") + del logging.getLogger("peewee_migrate").handlers[:] + router = Router(migrate_db) + router.run() + migrate_db.close() + self.db = SqliteQueueDatabase(TEST_DB) + models = [Event, Recordings] + self.db.bind(models) + self.test_dir = tempfile.mkdtemp() + + self.minimal_config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + self.double_cam_config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + "back_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + }, + }, + } + + def tearDown(self): + if not self.db.is_closed(): + self.db.close() + + try: + for file in TEST_DB_CLEANUPS: + os.remove(file) + except OSError: + pass + + def test_segment_calculations(self): + """Test that the segment calculations are correct.""" + config = FrigateConfig(**self.double_cam_config) + storage = StorageMaintainer(config, MagicMock()) + + time_keep = datetime.datetime.now().timestamp() + rec_fd_id = "1234567.frontdoor" + rec_bd_id = "1234568.backdoor" + _insert_mock_recording( + rec_fd_id, + os.path.join(self.test_dir, f"{rec_fd_id}.tmp"), + time_keep, + time_keep + 10, + camera="front_door", + seg_size=4, + seg_dur=10, + ) + _insert_mock_recording( + rec_bd_id, + os.path.join(self.test_dir, f"{rec_bd_id}.tmp"), + time_keep + 10, + time_keep + 20, + camera="back_door", + seg_size=8, + seg_dur=20, + ) + storage.calculate_camera_bandwidth() + assert storage.camera_storage_stats == { + "front_door": {"bandwidth": 1440, "needs_refresh": True}, + "back_door": {"bandwidth": 2880, "needs_refresh": True}, + } + + def test_segment_calculations_with_zero_segments(self): + """Ensure segment calculation does not fail when migrating from previous version.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + time_keep = datetime.datetime.now().timestamp() + rec_fd_id = "1234567.frontdoor" + _insert_mock_recording( + rec_fd_id, + os.path.join(self.test_dir, f"{rec_fd_id}.tmp"), + time_keep, + time_keep + 10, + camera="front_door", + seg_size=0, + seg_dur=10, + ) + storage.calculate_camera_bandwidth() + assert storage.camera_storage_stats == { + "front_door": {"bandwidth": 0, "needs_refresh": True}, + } + + def test_storage_cleanup(self): + """Ensure that all recordings are cleaned up when necessary.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + id = "123456.keep" + time_keep = datetime.datetime.now().timestamp() + _insert_mock_event( + id, + time_keep, + time_keep + 30, + True, + ) + rec_k_id = "1234567.keep" + rec_k2_id = "1234568.keep" + rec_k3_id = "1234569.keep" + _insert_mock_recording( + rec_k_id, + os.path.join(self.test_dir, f"{rec_k_id}.tmp"), + time_keep, + time_keep + 10, + ) + _insert_mock_recording( + rec_k2_id, + os.path.join(self.test_dir, f"{rec_k2_id}.tmp"), + time_keep + 10, + time_keep + 20, + ) + _insert_mock_recording( + rec_k3_id, + os.path.join(self.test_dir, f"{rec_k3_id}.tmp"), + time_keep + 20, + time_keep + 30, + ) + + id2 = "7890.delete" + time_delete = datetime.datetime.now().timestamp() - 360 + _insert_mock_event(id2, time_delete, time_delete + 30, False) + rec_d_id = "78901.delete" + rec_d2_id = "78902.delete" + rec_d3_id = "78903.delete" + _insert_mock_recording( + rec_d_id, + os.path.join(self.test_dir, f"{rec_d_id}.tmp"), + time_delete, + time_delete + 10, + ) + _insert_mock_recording( + rec_d2_id, + os.path.join(self.test_dir, f"{rec_d2_id}.tmp"), + time_delete + 10, + time_delete + 20, + ) + _insert_mock_recording( + rec_d3_id, + os.path.join(self.test_dir, f"{rec_d3_id}.tmp"), + time_delete + 20, + time_delete + 30, + ) + + storage.calculate_camera_bandwidth() + storage.reduce_storage_consumption() + with self.assertRaises(DoesNotExist): + assert Recordings.get(Recordings.id == rec_k_id) + assert Recordings.get(Recordings.id == rec_k2_id) + assert Recordings.get(Recordings.id == rec_k3_id) + Recordings.get(Recordings.id == rec_d_id) + Recordings.get(Recordings.id == rec_d2_id) + Recordings.get(Recordings.id == rec_d3_id) + + def test_storage_cleanup_keeps_retained(self): + """Ensure that all recordings are cleaned up when necessary.""" + config = FrigateConfig(**self.minimal_config) + storage = StorageMaintainer(config, MagicMock()) + + id = "123456.keep" + time_keep = datetime.datetime.now().timestamp() + _insert_mock_event( + id, + time_keep, + time_keep + 30, + True, + ) + rec_k_id = "1234567.keep" + rec_k2_id = "1234568.keep" + rec_k3_id = "1234569.keep" + _insert_mock_recording( + rec_k_id, + os.path.join(self.test_dir, f"{rec_k_id}.tmp"), + time_keep, + time_keep + 10, + ) + _insert_mock_recording( + rec_k2_id, + os.path.join(self.test_dir, f"{rec_k2_id}.tmp"), + time_keep + 10, + time_keep + 20, + ) + _insert_mock_recording( + rec_k3_id, + os.path.join(self.test_dir, f"{rec_k3_id}.tmp"), + time_keep + 20, + time_keep + 30, + ) + + time_delete = datetime.datetime.now().timestamp() - 7200 + for i in range(0, 59): + id = f"{123456 + i}.delete" + _insert_mock_recording( + id, + os.path.join(self.test_dir, f"{id}.tmp"), + time_delete, + time_delete + 600, + ) + + storage.calculate_camera_bandwidth() + storage.reduce_storage_consumption() + assert Recordings.get(Recordings.id == rec_k_id) + assert Recordings.get(Recordings.id == rec_k2_id) + assert Recordings.get(Recordings.id == rec_k3_id) + + +def _insert_mock_event( + id: str, + start: int, + end: int, + retain: bool, + camera: str = "front_door", + label: str = "Mock", +) -> Event: + """Inserts a basic event model with a given id.""" + return Event.insert( + id=id, + label=label, + camera=camera, + start_time=start, + end_time=end, + top_score=100, + false_positive=False, + zones=list(), + thumbnail="", + region=[], + box=[], + area=0, + has_clip=True, + has_snapshot=True, + retain_indefinitely=retain, + ).execute() + + +def _insert_mock_recording( + id: str, + file: str, + start: int, + end: int, + camera="front_door", + seg_size=8, + seg_dur=10, +) -> Event: + """Inserts a basic recording model with a given id.""" + # we must open the file so storage maintainer will delete it + with open(file, "w"): + pass + + return Recordings.insert( + id=id, + camera=camera, + path=file, + start_time=start, + end_time=end, + duration=seg_dur, + motion=True, + objects=True, + segment_size=seg_size, + ).execute() diff --git a/sam2-cpu/frigate-dev/frigate/test/test_video.py b/sam2-cpu/frigate-dev/frigate/test/test_video.py new file mode 100644 index 0000000..8612990 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_video.py @@ -0,0 +1,354 @@ +import unittest + +import cv2 +import numpy as np +from norfair.drawing.color import Palette +from norfair.drawing.drawer import Drawer + +from frigate.util.image import intersection, transliterate_to_latin +from frigate.util.object import ( + get_cluster_boundary, + get_cluster_candidates, + get_cluster_region, + get_region_from_grid, + reduce_detections, +) + + +def draw_box(frame, box, color=(255, 0, 0), thickness=2): + cv2.rectangle( + frame, + (box[0], box[1]), + (box[2], box[3]), + color, + thickness, + ) + + +def save_clusters_image(name, boxes, candidates, regions=[]): + canvas = np.zeros((1000, 2000, 3), np.uint8) + for cluster in candidates: + color = Palette.choose_color(np.random.rand()) + for b in cluster: + box = boxes[b] + draw_box(canvas, box, color, 2) + # bottom right + text_anchor = ( + box[2], + box[3], + ) + canvas = Drawer.text( + canvas, + str(b), + position=text_anchor, + size=None, + color=(255, 255, 255), + thickness=None, + ) + for r in regions: + draw_box(canvas, r, (0, 255, 0), 2) + cv2.imwrite( + f"debug/frames/{name}.jpg", + canvas, + ) + + +def save_cluster_boundary_image(name, boxes, bounding_boxes): + canvas = np.zeros((1000, 2000, 3), np.uint8) + color = Palette.choose_color(np.random.rand()) + for box in boxes: + draw_box(canvas, box, color, 2) + for bound in bounding_boxes: + draw_box(canvas, bound, (0, 255, 0), 2) + cv2.imwrite( + f"debug/frames/{name}.jpg", + canvas, + ) + + +class TestRegion(unittest.TestCase): + def setUp(self): + self.frame_shape = (1000, 2000) + self.min_region_size = 160 + + def test_cluster_candidates(self): + boxes = [(100, 100, 200, 200), (202, 150, 252, 200), (900, 900, 950, 950)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + # save_clusters_image("cluster_candidates", boxes, cluster_candidates) + + assert len(cluster_candidates) == 2 + + def test_transliterate_to_latin(self): + self.assertEqual(transliterate_to_latin("frégate"), "fregate") + self.assertEqual(transliterate_to_latin("utilité"), "utilite") + self.assertEqual(transliterate_to_latin("imágé"), "image") + + def test_cluster_boundary(self): + boxes = [(100, 100, 200, 200), (215, 215, 325, 325)] + boundary_boxes = [ + get_cluster_boundary(box, self.min_region_size) for box in boxes + ] + + # save_cluster_boundary_image("bound", boxes, boundary_boxes) + assert len(boundary_boxes) == 2 + + def test_cluster_regions(self): + boxes = [(100, 100, 200, 200), (202, 150, 252, 200), (900, 900, 950, 950)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("regions", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + def test_box_too_small_for_cluster(self): + boxes = [(100, 100, 600, 600), (655, 100, 700, 145)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + save_clusters_image("too_small", boxes, cluster_candidates, regions) + + assert len(cluster_candidates) == 2 + assert len(regions) == 2 + + def test_redundant_clusters(self): + boxes = [(100, 100, 200, 200), (305, 305, 415, 415)] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("redundant", boxes, cluster_candidates, regions) + + assert len(cluster_candidates) == 2 + assert all([len(c) == 1 for c in cluster_candidates]) + assert len(regions) == 2 + + def test_combine_boxes(self): + boxes = [ + (480, 0, 540, 128), + (536, 0, 558, 99), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("combine", boxes, cluster_candidates, regions) + assert len(regions) == 1 + + def test_dont_combine_smaller_boxes(self): + boxes = [ + (460, 0, 561, 144), + (565, 0, 586, 71), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("combine", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + def test_dont_combine_boxes(self): + boxes = [ + (460, 0, 532, 129), + (586, 0, 606, 46), + ] + + # boundary_boxes = [get_cluster_boundary(box) for box in boxes] + # save_cluster_boundary_image("dont_combine_bound", boxes, boundary_boxes) + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + regions = [ + get_cluster_region(self.frame_shape, self.min_region_size, candidate, boxes) + for candidate in cluster_candidates + ] + + # save_clusters_image("dont_combine", boxes, cluster_candidates, regions) + assert len(regions) == 2 + + +class TestObjectBoundingBoxes(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_box_intersection(self): + box_a = [2012, 191, 2031, 205] + box_b = [887, 92, 985, 151] + box_c = [899, 128, 1080, 175] + + assert intersection(box_a, box_b) == None + assert intersection(box_b, box_c) == (899, 128, 985, 151) + + def test_overlapping_objects_reduced(self): + """Test that object not on edge of region is used when a higher scoring object at the edge of region is provided.""" + detections = [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ), + ( + "car", + 0.88, + (1238, 73, 1401, 171), + 15974, + 1.663265306122449, + (1242, 0, 1602, 360), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert consolidated_detections == [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ) + ] + + def test_non_overlapping_objects_not_reduced(self): + """Test that non overlapping objects are not reduced.""" + detections = [ + ( + "car", + 0.81, + (1209, 73, 1437, 163), + 20520, + 2.53333333, + (1150, 0, 1500, 200), + ), + ( + "car", + 0.83203125, + (1121, 55, 1214, 100), + 4185, + 2.066666666666667, + (922, 0, 1242, 320), + ), + ( + "car", + 0.85546875, + (1414, 97, 1571, 186), + 13973, + 1.7640449438202248, + (1248, 0, 1568, 320), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + def test_overlapping_different_size_objects_not_reduced(self): + """Test that overlapping objects that are significantly different in size are not reduced.""" + detections = [ + ( + "car", + 0.81, + (164, 279, 816, 719), + 286880, + 1.48, + (90, 0, 910, 820), + ), + ( + "car", + 0.83203125, + (248, 340, 328, 385), + 3600, + 1.777, + (0, 0, 460, 460), + ), + ] + frame_shape = (720, 2560) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + def test_vert_stacked_cars_not_reduced(self): + detections = [ + ("car", 0.8, (954, 312, 1247, 475), 498512, 1.48, (800, 200, 1400, 600)), + ("car", 0.85, (970, 380, 1273, 610), 698752, 1.56, (800, 200, 1400, 700)), + ] + frame_shape = (720, 1280) + consolidated_detections = reduce_detections(frame_shape, detections) + assert len(consolidated_detections) == len(detections) + + +class TestRegionGrid(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_region_in_range(self): + """Test that region is kept at minimal size when within std dev.""" + frame_shape = (720, 1280) + box = [450, 450, 550, 550] + region_grid = [ + [], + [], + [], + [{}, {}, {}, {}, {}, {"sizes": [0.25], "mean": 0.26, "std_dev": 0.01}], + ] + + region = get_region_from_grid(frame_shape, box, 320, region_grid) + assert region[2] - region[0] == 320 + + def test_region_out_of_range(self): + """Test that region is upsized when outside of std dev.""" + frame_shape = (720, 1280) + box = [450, 450, 550, 550] + region_grid = [ + [], + [], + [], + [{}, {}, {}, {}, {}, {"sizes": [0.5], "mean": 0.5, "std_dev": 0.1}], + ] + + region = get_region_from_grid(frame_shape, box, 320, region_grid) + assert region[2] - region[0] > 320 diff --git a/sam2-cpu/frigate-dev/frigate/test/test_yuv_region_2_rgb.py b/sam2-cpu/frigate-dev/frigate/test/test_yuv_region_2_rgb.py new file mode 100644 index 0000000..10144e6 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/test/test_yuv_region_2_rgb.py @@ -0,0 +1,43 @@ +from unittest import TestCase, main + +import cv2 +import numpy as np + +from frigate.util.image import yuv_region_2_rgb + + +class TestYuvRegion2RGB(TestCase): + def setUp(self): + self.bgr_frame = np.zeros((100, 200, 3), np.uint8) + self.bgr_frame[:] = (0, 0, 255) + self.bgr_frame[5:55, 5:55] = (255, 0, 0) + # cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame) + self.yuv_frame = cv2.cvtColor(self.bgr_frame, cv2.COLOR_BGR2YUV_I420) + + def test_crop_yuv(self): + cropped = yuv_region_2_rgb(self.yuv_frame, (10, 10, 50, 50)) + # ensure the upper left pixel is blue + assert np.all(cropped[0, 0] == [0, 0, 255]) + + def test_crop_yuv_out_of_bounds(self): + cropped = yuv_region_2_rgb(self.yuv_frame, (0, 0, 200, 200)) + # cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) + # ensure the upper left pixel is red + # the yuv conversion has some noise + assert np.all(cropped[0, 0] == [255, 1, 0]) + # ensure the bottom right is black + assert np.all(cropped[199, 199] == [0, 0, 0]) + + def test_crop_yuv_portrait(self): + bgr_frame = np.zeros((1920, 1080, 3), np.uint8) + bgr_frame[:] = (0, 0, 255) + bgr_frame[5:55, 5:55] = (255, 0, 0) + # cv2.imwrite(f"bgr_frame.jpg", self.bgr_frame) + yuv_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2YUV_I420) + + yuv_region_2_rgb(yuv_frame, (0, 852, 648, 1500)) + # cv2.imwrite(f"cropped.jpg", cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) + + +if __name__ == "__main__": + main(verbosity=2) diff --git a/sam2-cpu/frigate-dev/frigate/timeline.py b/sam2-cpu/frigate-dev/frigate/timeline.py new file mode 100644 index 0000000..a2d59b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/timeline.py @@ -0,0 +1,197 @@ +"""Record events for object, audio, etc. detections.""" + +import logging +import queue +import threading +from multiprocessing import Queue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +from frigate.config import FrigateConfig +from frigate.events.maintainer import EventStateEnum, EventTypeEnum +from frigate.models import Timeline +from frigate.util.builtin import to_relative_box + +logger = logging.getLogger(__name__) + + +class TimelineProcessor(threading.Thread): + """Handle timeline queue and update DB.""" + + def __init__( + self, + config: FrigateConfig, + queue: Queue, + stop_event: MpEvent, + ) -> None: + super().__init__(name="timeline_processor") + self.config = config + self.queue = queue + self.stop_event = stop_event + self.pre_event_cache: dict[str, list[dict[str, Any]]] = {} + + def run(self) -> None: + while not self.stop_event.is_set(): + try: + ( + camera, + input_type, + event_type, + prev_event_data, + event_data, + ) = self.queue.get(timeout=1) + except queue.Empty: + continue + + if input_type == EventTypeEnum.tracked_object: + # None prev_event_data is only allowed for the start of an event + if event_type != EventStateEnum.start and prev_event_data is None: + continue + + self.handle_object_detection( + camera, event_type, prev_event_data, event_data + ) + elif input_type == EventTypeEnum.api: + self.handle_api_entry(camera, event_type, event_data) + + def insert_or_save( + self, + entry: dict[str, Any], + prev_event_data: dict[Any, Any], + event_data: dict[Any, Any], + ) -> None: + """Insert into db or cache.""" + id = entry[Timeline.source_id] + if not event_data["has_clip"] and not event_data["has_snapshot"]: + # the related event has not been saved yet, should be added to cache + if id in self.pre_event_cache.keys(): + self.pre_event_cache[id].append(entry) + else: + self.pre_event_cache[id] = [entry] + else: + # the event is saved, insert to db and insert cached into db + if id in self.pre_event_cache.keys(): + for e in self.pre_event_cache[id]: + Timeline.insert(e).execute() + + self.pre_event_cache.pop(id) + + Timeline.insert(entry).execute() + + def handle_object_detection( + self, + camera: str, + event_type: str, + prev_event_data: dict[Any, Any], + event_data: dict[Any, Any], + ) -> bool: + """Handle object detection.""" + save = False + camera_config = self.config.cameras[camera] + event_id = event_data["id"] + + timeline_entry = { + Timeline.timestamp: event_data["frame_time"], + Timeline.camera: camera, + Timeline.source: "tracked_object", + Timeline.source_id: event_id, + Timeline.data: { + "box": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["box"], + ), + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + "region": to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["region"], + ), + "attribute": "", + "score": event_data["score"], + }, + } + + # update sub labels for existing entries that haven't been added yet + if ( + prev_event_data != None + and prev_event_data["sub_label"] != event_data["sub_label"] + and event_id in self.pre_event_cache.keys() + ): + for e in self.pre_event_cache[event_id]: + e[Timeline.data]["sub_label"] = event_data["sub_label"] + + if event_type == EventStateEnum.start: + timeline_entry[Timeline.class_type] = "visible" + save = True + elif event_type == EventStateEnum.update: + if ( + len(prev_event_data["current_zones"]) < len(event_data["current_zones"]) + and not event_data["stationary"] + ): + timeline_entry[Timeline.class_type] = "entered_zone" + timeline_entry[Timeline.data]["zones"] = event_data["current_zones"] + save = True + elif prev_event_data["stationary"] != event_data["stationary"]: + timeline_entry[Timeline.class_type] = ( + "stationary" if event_data["stationary"] else "active" + ) + save = True + elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}: + timeline_entry[Timeline.class_type] = "attribute" + timeline_entry[Timeline.data]["attribute"] = list( + event_data["attributes"].keys() + )[0] + + if len(event_data["current_attributes"]) > 0: + timeline_entry[Timeline.data]["attribute_box"] = to_relative_box( + camera_config.detect.width, + camera_config.detect.height, + event_data["current_attributes"][0]["box"], + ) + + save = True + elif event_type == EventStateEnum.end: + timeline_entry[Timeline.class_type] = "gone" + save = True + + if save: + self.insert_or_save(timeline_entry, prev_event_data, event_data) + + def handle_api_entry( + self, + camera: str, + event_type: str, + event_data: dict[Any, Any], + ) -> bool: + if event_type != "start": + return False + + if event_data.get("type", "api") == "audio": + timeline_entry = { + Timeline.class_type: "heard", + Timeline.timestamp: event_data["start_time"], + Timeline.camera: camera, + Timeline.source: "audio", + Timeline.source_id: event_data["id"], + Timeline.data: { + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + }, + } + else: + timeline_entry = { + Timeline.class_type: "external", + Timeline.timestamp: event_data["start_time"], + Timeline.camera: camera, + Timeline.source: "api", + Timeline.source_id: event_data["id"], + Timeline.data: { + "label": event_data["label"], + "sub_label": event_data.get("sub_label"), + }, + } + + Timeline.insert(timeline_entry).execute() + return True diff --git a/sam2-cpu/frigate-dev/frigate/track/__init__.py b/sam2-cpu/frigate-dev/frigate/track/__init__.py new file mode 100644 index 0000000..b5453aa --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/__init__.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import Any + +from frigate.config import DetectConfig + + +class ObjectTracker(ABC): + @abstractmethod + def __init__(self, config: DetectConfig) -> None: + pass + + @abstractmethod + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + pass diff --git a/sam2-cpu/frigate-dev/frigate/track/centroid_tracker.py b/sam2-cpu/frigate-dev/frigate/track/centroid_tracker.py new file mode 100644 index 0000000..56f2062 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/centroid_tracker.py @@ -0,0 +1,247 @@ +import random +import string +from collections import defaultdict +from typing import Any + +import numpy as np +from scipy.spatial import distance as dist + +from frigate.config import DetectConfig +from frigate.track import ObjectTracker +from frigate.util.image import intersection_over_union + + +class CentroidTracker(ObjectTracker): + def __init__(self, config: DetectConfig): + self.tracked_objects: dict[str, dict[str, Any]] = {} + self.untracked_object_boxes: list[tuple[int, int, int, int]] = [] + self.disappeared: dict[str, Any] = {} + self.positions: dict[str, Any] = {} + self.max_disappeared = config.max_disappeared + self.detect_config = config + + def register(self, obj: dict[str, Any]) -> None: + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + id = f"{obj['frame_time']}-{rand_id}" + obj["id"] = id + obj["start_time"] = obj["frame_time"] + obj["motionless_count"] = 0 + obj["position_changes"] = 0 + self.tracked_objects[id] = obj + self.disappeared[id] = 0 + self.positions[id] = { + "xmins": [], + "ymins": [], + "xmaxs": [], + "ymaxs": [], + "xmin": 0, + "ymin": 0, + "xmax": self.detect_config.width, + "ymax": self.detect_config.height, + } + + def deregister(self, id: str) -> None: + del self.tracked_objects[id] + del self.disappeared[id] + + # tracks the current position of the object based on the last N bounding boxes + # returns False if the object has moved outside its previous position + def update_position(self, id: str, box: tuple[int, int, int, int]) -> bool: + position = self.positions[id] + position_box = ( + position["xmin"], + position["ymin"], + position["xmax"], + position["ymax"], + ) + + xmin, ymin, xmax, ymax = box + + iou = intersection_over_union(position_box, box) + + # if the iou drops below the threshold + # assume the object has moved to a new position and reset the computed box + if iou < 0.6: + self.positions[id] = { + "xmins": [xmin], + "ymins": [ymin], + "xmaxs": [xmax], + "ymaxs": [ymax], + "xmin": xmin, + "ymin": ymin, + "xmax": xmax, + "ymax": ymax, + } + return False + + # if there are less than 10 entries for the position, add the bounding box + # and recompute the position box + if len(position["xmins"]) < 10: + position["xmins"].append(xmin) + position["ymins"].append(ymin) + position["xmaxs"].append(xmax) + position["ymaxs"].append(ymax) + # by using percentiles here, we hopefully remove outliers + position["xmin"] = np.percentile(position["xmins"], 15) + position["ymin"] = np.percentile(position["ymins"], 15) + position["xmax"] = np.percentile(position["xmaxs"], 85) + position["ymax"] = np.percentile(position["ymaxs"], 85) + + return True + + def is_expired(self, id: str) -> bool: + obj = self.tracked_objects[id] + # get the max frames for this label type or the default + max_frames = self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + + # if there is no max_frames for this label type, continue + if max_frames is None: + return False + + # if the object has exceeded the max_frames setting, deregister + if ( + obj["motionless_count"] - self.detect_config.stationary.threshold + > max_frames + ): + return True + + return False + + def update(self, id: str, new_obj: dict[str, Any]) -> None: + self.disappeared[id] = 0 + # update the motionless count if the object has not moved to a new position + if self.update_position(id, new_obj["box"]): + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id) + return + else: + # register the first position change and then only increment if + # the object was previously stationary + if ( + self.tracked_objects[id]["position_changes"] == 0 + or self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ): + self.tracked_objects[id]["position_changes"] += 1 + self.tracked_objects[id]["motionless_count"] = 0 + + self.tracked_objects[id].update(new_obj) + + def update_frame_times(self, frame_name: str, frame_time: float) -> None: + for id in list(self.tracked_objects.keys()): + self.tracked_objects[id]["frame_time"] = frame_time + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id) + + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + # group by name + detection_groups = defaultdict(lambda: []) + for det in detections: + detection_groups[det[0]].append( + { + "label": det[0], + "score": det[1], + "box": det[2], + "area": det[3], + "ratio": det[4], + "region": det[5], + "frame_time": frame_time, + } + ) + + # update any tracked objects with labels that are not + # seen in the current objects and deregister if needed + for obj in list(self.tracked_objects.values()): + if obj["label"] not in detection_groups: + if self.disappeared[obj["id"]] >= self.max_disappeared: + self.deregister(obj["id"]) + else: + self.disappeared[obj["id"]] += 1 + + if len(detections) == 0: + return + + # track objects for each label type + for label, group in detection_groups.items(): + current_objects = [ + o for o in self.tracked_objects.values() if o["label"] == label + ] + current_ids = [o["id"] for o in current_objects] + current_centroids = np.array([o["centroid"] for o in current_objects]) + + # compute centroids of new objects + for obj in group: + centroid_x = int((obj["box"][0] + obj["box"][2]) / 2.0) + centroid_y = int((obj["box"][1] + obj["box"][3]) / 2.0) + obj["centroid"] = (centroid_x, centroid_y) + + if len(current_objects) == 0: + for index, obj in enumerate(group): + self.register(obj) + continue + + new_centroids = np.array([o["centroid"] for o in group]) + + # compute the distance between each pair of tracked + # centroids and new centroids, respectively -- our + # goal will be to match each current centroid to a new + # object centroid + D = dist.cdist(current_centroids, new_centroids) + + # in order to perform this matching we must (1) find the smallest + # value in each row (i.e. the distance from each current object to + # the closest new object) and then (2) sort the row indexes based + # on their minimum values so that the row with the smallest + # distance (the best match) is at the *front* of the index list + rows = D.min(axis=1).argsort() + + # next, we determine which new object each existing object matched + # against, and apply the same sorting as was applied previously + cols = D.argmin(axis=1)[rows] + + # many current objects may register with each new object, so only + # match the closest ones. unique returns the indices of the first + # occurrences of each value, and because the rows are sorted by + # distance, this will be index of the closest match + _, index = np.unique(cols, return_index=True) + rows = rows[index] + cols = cols[index] + + # loop over the combination of the (row, column) index tuples + for row, col in zip(rows, cols): + # grab the object ID for the current row, set its new centroid, + # and reset the disappeared counter + objectID = current_ids[row] + self.update(objectID, group[col]) + + # compute the row and column indices we have NOT yet examined + unusedRows = set(range(D.shape[0])).difference(rows) + unusedCols = set(range(D.shape[1])).difference(cols) + + # in the event that the number of object centroids is + # equal or greater than the number of input centroids + # we need to check and see if some of these objects have + # potentially disappeared + if D.shape[0] >= D.shape[1]: + for row in unusedRows: + id = current_ids[row] + + if self.disappeared[id] >= self.max_disappeared: + self.deregister(id) + else: + self.disappeared[id] += 1 + # if the number of input centroids is greater + # than the number of existing object centroids we need to + # register each new input centroid as a trackable object + else: + for col in unusedCols: + self.register(group[col]) diff --git a/sam2-cpu/frigate-dev/frigate/track/norfair_tracker.py b/sam2-cpu/frigate-dev/frigate/track/norfair_tracker.py new file mode 100644 index 0000000..84a0f39 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/norfair_tracker.py @@ -0,0 +1,728 @@ +import logging +import random +import string +from typing import Any, Sequence, cast + +import cv2 +import numpy as np +from norfair.drawing.draw_boxes import draw_boxes +from norfair.drawing.drawer import Drawable, Drawer +from norfair.filter import OptimizedKalmanFilterFactory +from norfair.tracker import Detection, TrackedObject, Tracker +from rich import print +from rich.console import Console +from rich.table import Table + +from frigate.camera import PTZMetrics +from frigate.config import CameraConfig +from frigate.ptz.autotrack import PtzMotionEstimator +from frigate.track import ObjectTracker +from frigate.track.stationary_classifier import ( + StationaryMotionClassifier, + StationaryThresholds, + get_stationary_threshold, +) +from frigate.util.image import ( + SharedMemoryFrameManager, + get_histogram, + intersection_over_union, +) +from frigate.util.object import average_boxes, median_of_boxes + +logger = logging.getLogger(__name__) + + +# Normalizes distance from estimate relative to object size +# Other ideas: +# - if estimates are inaccurate for first N detections, compare with last_detection (may be fine) +# - could be variable based on time since last_detection +# - include estimated velocity in the distance (car driving by of a parked car) +# - include some visual similarity factor in the distance for occlusions +def distance(detection: np.ndarray, estimate: np.ndarray) -> float: + # ultimately, this should try and estimate distance in 3-dimensional space + # consider change in location, width, and height + + estimate_dim = np.diff(estimate, axis=0).flatten() + detection_dim = np.diff(detection, axis=0).flatten() + + # get bottom center positions + detection_position = np.array( + [np.average(detection[:, 0]), np.max(detection[:, 1])] + ) + estimate_position = np.array([np.average(estimate[:, 0]), np.max(estimate[:, 1])]) + + distance = (detection_position - estimate_position).astype(float) + # change in x relative to w + distance[0] /= estimate_dim[0] + # change in y relative to h + distance[1] /= estimate_dim[1] + + # get ratio of widths and heights + # normalize to 1 + widths = np.sort([estimate_dim[0], detection_dim[0]]) + heights = np.sort([estimate_dim[1], detection_dim[1]]) + width_ratio = widths[1] / widths[0] - 1.0 + height_ratio = heights[1] / heights[0] - 1.0 + + # change vector is relative x,y change and w,h ratio + change = np.append(distance, np.array([width_ratio, height_ratio])) + + # calculate euclidean distance of the change vector + return float(np.linalg.norm(change)) + + +def frigate_distance(detection: Detection, tracked_object: TrackedObject) -> float: + return distance(detection.points, tracked_object.estimate) + + +def histogram_distance( + matched_not_init_trackers: TrackedObject, unmatched_trackers: TrackedObject +) -> float: + snd_embedding = unmatched_trackers.last_detection.embedding + + if snd_embedding is None: + for detection in reversed(unmatched_trackers.past_detections): + if detection.embedding is not None: + snd_embedding = detection.embedding + break + else: + return 1 + + for detection_fst in matched_not_init_trackers.past_detections: + if detection_fst.embedding is None: + continue + + distance = 1 - cv2.compareHist( + snd_embedding, detection_fst.embedding, cv2.HISTCMP_CORREL + ) + if distance < 0.5: + return distance + return 1 + + +class NorfairTracker(ObjectTracker): + def __init__( + self, + config: CameraConfig, + ptz_metrics: PTZMetrics, + ): + self.frame_manager = SharedMemoryFrameManager() + self.tracked_objects: dict[str, dict[str, Any]] = {} + self.untracked_object_boxes: list[list[int]] = [] + self.disappeared: dict[str, int] = {} + self.positions: dict[str, dict[str, Any]] = {} + self.stationary_box_history: dict[str, list[list[int]]] = {} + self.camera_config = config + self.detect_config = config.detect + self.ptz_metrics = ptz_metrics + self.ptz_motion_estimator: PtzMotionEstimator | None = None + self.camera_name = config.name + self.track_id_map: dict[str, str] = {} + self.stationary_classifier = StationaryMotionClassifier() + + # Define tracker configurations for static camera + self.object_type_configs = { + "car": { + "filter_factory": OptimizedKalmanFilterFactory(R=3.4, Q=0.03), + "distance_function": frigate_distance, + "distance_threshold": 2.5, + }, + "license_plate": { + "filter_factory": OptimizedKalmanFilterFactory(R=2.5, Q=0.05), + "distance_function": frigate_distance, + "distance_threshold": 3.75, + }, + } + + # Define autotracking PTZ-specific configurations + self.ptz_object_type_configs = { + "person": { + "filter_factory": OptimizedKalmanFilterFactory( + R=4.5, + Q=0.25, + ), + "distance_function": frigate_distance, + "distance_threshold": 2, + "past_detections_length": 5, + "reid_distance_function": histogram_distance, + "reid_distance_threshold": 0.5, + "reid_hit_counter_max": 10, + }, + } + + # Default tracker configuration + # use default filter factory with custom values + # R is the multiplier for the sensor measurement noise matrix, default of 4.0 + # lowering R means that we trust the position of the bounding boxes more + # testing shows that the prediction was being relied on a bit too much + self.default_tracker_config = { + "filter_factory": OptimizedKalmanFilterFactory(R=3.4), + "distance_function": frigate_distance, + "distance_threshold": 2.5, + } + + self.default_ptz_tracker_config = { + "filter_factory": OptimizedKalmanFilterFactory(R=4, Q=0.2), + "distance_function": frigate_distance, + "distance_threshold": 3, + } + + self.trackers: dict[str, dict[str, Tracker]] = {} + # Handle static trackers + for obj_type, tracker_config in self.object_type_configs.items(): + if obj_type in self.camera_config.objects.track: + if obj_type not in self.trackers: + self.trackers[obj_type] = {} + self.trackers[obj_type]["static"] = self._create_tracker( + obj_type, tracker_config + ) + + # Handle PTZ trackers + for obj_type, tracker_config in self.ptz_object_type_configs.items(): + if ( + obj_type in self.camera_config.onvif.autotracking.track + and self.camera_config.onvif.autotracking.enabled_in_config + ): + if obj_type not in self.trackers: + self.trackers[obj_type] = {} + self.trackers[obj_type]["ptz"] = self._create_tracker( + obj_type, tracker_config + ) + + # Initialize default trackers + self.default_tracker = { + "static": Tracker( + distance_function=frigate_distance, + distance_threshold=self.default_tracker_config[ # type: ignore[arg-type] + "distance_threshold" + ], + initialization_delay=self.detect_config.min_initialized, + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_tracker_config["filter_factory"], # type: ignore[arg-type] + ), + "ptz": Tracker( + distance_function=frigate_distance, + distance_threshold=self.default_ptz_tracker_config[ + "distance_threshold" + ], # type: ignore[arg-type] + initialization_delay=self.detect_config.min_initialized, + hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type] + filter_factory=self.default_ptz_tracker_config["filter_factory"], # type: ignore[arg-type] + ), + } + + if self.ptz_metrics.autotracker_enabled.value: + self.ptz_motion_estimator = PtzMotionEstimator( + self.camera_config, self.ptz_metrics + ) + + def _create_tracker(self, obj_type: str, tracker_config: dict[str, Any]) -> Tracker: + """Helper function to create a tracker with given configuration.""" + tracker_params = { + "distance_function": tracker_config["distance_function"], + "distance_threshold": tracker_config["distance_threshold"], + "initialization_delay": self.detect_config.min_initialized, + "hit_counter_max": self.detect_config.max_disappeared, + "filter_factory": tracker_config["filter_factory"], + } + + # Add reid parameters if max_frames is None + if ( + self.detect_config.stationary.max_frames.objects.get( + obj_type, self.detect_config.stationary.max_frames.default + ) + is None + ): + reid_keys = [ + "past_detections_length", + "reid_distance_function", + "reid_distance_threshold", + "reid_hit_counter_max", + ] + tracker_params.update( + {key: tracker_config[key] for key in reid_keys if key in tracker_config} + ) + + return Tracker(**tracker_params) + + def get_tracker(self, object_type: str) -> Tracker: + """Get the appropriate tracker based on object type and camera mode.""" + mode = ( + "ptz" + if self.camera_config.onvif.autotracking.enabled_in_config + and object_type in self.camera_config.onvif.autotracking.track + and object_type in self.ptz_object_type_configs.keys() + else "static" + ) + if object_type in self.trackers: + return self.trackers[object_type][mode] + return self.default_tracker[mode] + + def register(self, track_id: str, obj: dict[str, Any]) -> None: + rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) + id = f"{obj['frame_time']}-{rand_id}" + self.track_id_map[track_id] = id + obj["id"] = id + obj["start_time"] = obj["frame_time"] + obj["motionless_count"] = 0 + obj["position_changes"] = 0 + + # Get the correct tracker for this object's label + tracker = self.get_tracker(obj["label"]) + obj_match = next( + (o for o in tracker.tracked_objects if str(o.global_id) == track_id), None + ) + # if we don't have a match, we have a new object + obj["score_history"] = ( + [p.data["score"] for p in obj_match.past_detections] if obj_match else [] + ) + self.tracked_objects[id] = obj + self.disappeared[id] = 0 + if obj_match: + boxes = [p.data["box"] for p in obj_match.past_detections] + else: + boxes = [obj["box"]] + + xmins, ymins, xmaxs, ymaxs = zip(*boxes) + + self.positions[id] = { + "xmins": list(xmins), + "ymins": list(ymins), + "xmaxs": list(xmaxs), + "ymaxs": list(ymaxs), + "xmin": 0, + "ymin": 0, + "xmax": self.detect_config.width, + "ymax": self.detect_config.height, + } + self.stationary_box_history[id] = boxes + + def deregister(self, id: str, track_id: str) -> None: + obj = self.tracked_objects[id] + + del self.tracked_objects[id] + del self.disappeared[id] + + # only manually deregister objects from norfair's list if max_frames is defined + if ( + self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + is not None + ): + tracker = self.get_tracker(obj["label"]) + tracker.tracked_objects = [ + o + for o in tracker.tracked_objects + if str(o.global_id) != track_id and o.hit_counter < 0 + ] + + del self.track_id_map[track_id] + + # tracks the current position of the object based on the last N bounding boxes + # returns False if the object has moved outside its previous position + def update_position( + self, + id: str, + box: list[int], + stationary: bool, + thresholds: StationaryThresholds, + yuv_frame: np.ndarray | None, + ) -> bool: + def reset_position(xmin: int, ymin: int, xmax: int, ymax: int) -> None: + self.positions[id] = { + "xmins": [xmin], + "ymins": [ymin], + "xmaxs": [xmax], + "ymaxs": [ymax], + "xmin": xmin, + "ymin": ymin, + "xmax": xmax, + "ymax": ymax, + } + + xmin, ymin, xmax, ymax = box + position = self.positions[id] + self.stationary_box_history[id].append(box) + + if len(self.stationary_box_history[id]) > thresholds.max_stationary_history: + self.stationary_box_history[id] = self.stationary_box_history[id][ + -thresholds.max_stationary_history : + ] + + avg_box = average_boxes(self.stationary_box_history[id]) + avg_iou = intersection_over_union(box, avg_box) + median_box = median_of_boxes(self.stationary_box_history[id]) + + # Establish anchor early when stationary and stable + if stationary and yuv_frame is not None: + history = self.stationary_box_history[id] + if id not in self.stationary_classifier.anchor_crops and len(history) >= 5: + stability_iou = intersection_over_union(avg_box, median_box) + if stability_iou >= 0.7: + self.stationary_classifier.ensure_anchor( + id, yuv_frame, cast(tuple[int, int, int, int], median_box) + ) + + # object has minimal or zero iou + # assume object is active + if avg_iou < thresholds.known_active_iou: + if stationary and yuv_frame is not None: + if not self.stationary_classifier.evaluate( + id, yuv_frame, cast(tuple[int, int, int, int], tuple(box)) + ): + reset_position(xmin, ymin, xmax, ymax) + return False + else: + reset_position(xmin, ymin, xmax, ymax) + return False + + threshold = ( + thresholds.stationary_check_iou + if stationary + else thresholds.active_check_iou + ) + + # object has iou below threshold, check median and optionally crop similarity + if avg_iou < threshold: + median_iou = intersection_over_union( + ( + position["xmin"], + position["ymin"], + position["xmax"], + position["ymax"], + ), + median_box, + ) + + # if the median iou drops below the threshold + # assume object is no longer stationary + if median_iou < threshold: + # If we have a yuv_frame to check before flipping to active, check with classifier if we have YUV frame + if stationary and yuv_frame is not None: + if not self.stationary_classifier.evaluate( + id, yuv_frame, cast(tuple[int, int, int, int], tuple(box)) + ): + reset_position(xmin, ymin, xmax, ymax) + return False + else: + reset_position(xmin, ymin, xmax, ymax) + return False + + # if there are more than 5 and less than 10 entries for the position, add the bounding box + # and recompute the position box + if len(position["xmins"]) < 10: + position["xmins"].append(xmin) + position["ymins"].append(ymin) + position["xmaxs"].append(xmax) + position["ymaxs"].append(ymax) + # by using percentiles here, we hopefully remove outliers + position["xmin"] = np.percentile(position["xmins"], 15) + position["ymin"] = np.percentile(position["ymins"], 15) + position["xmax"] = np.percentile(position["xmaxs"], 85) + position["ymax"] = np.percentile(position["ymaxs"], 85) + + return True + + def is_expired(self, id: str) -> bool: + obj = self.tracked_objects[id] + # get the max frames for this label type or the default + max_frames = self.detect_config.stationary.max_frames.objects.get( + obj["label"], self.detect_config.stationary.max_frames.default + ) + + # if there is no max_frames for this label type, continue + if max_frames is None: + return False + + # if the object has exceeded the max_frames setting, deregister + if ( + obj["motionless_count"] - self.detect_config.stationary.threshold + > max_frames + ): + return True + + return False + + def update( + self, + track_id: str, + obj: dict[str, Any], + thresholds: StationaryThresholds, + yuv_frame: np.ndarray | None, + ) -> None: + id = self.track_id_map[track_id] + self.disappeared[id] = 0 + stationary = ( + self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ) + # update the motionless count if the object has not moved to a new position + if self.update_position(id, obj["box"], stationary, thresholds, yuv_frame): + self.tracked_objects[id]["motionless_count"] += 1 + if self.is_expired(id): + self.deregister(id, track_id) + return + else: + # register the first position change and then only increment if + # the object was previously stationary + if ( + self.tracked_objects[id]["position_changes"] == 0 + or self.tracked_objects[id]["motionless_count"] + >= self.detect_config.stationary.threshold + ): + self.tracked_objects[id]["position_changes"] += 1 + self.tracked_objects[id]["motionless_count"] = 0 + self.stationary_box_history[id] = [] + self.stationary_classifier.on_active(id) + + self.tracked_objects[id].update(obj) + + def update_frame_times(self, frame_name: str, frame_time: float) -> None: + # if the object was there in the last frame, assume it's still there + detections = [ + ( + obj["label"], + obj["score"], + obj["box"], + obj["area"], + obj["ratio"], + obj["region"], + ) + for id, obj in self.tracked_objects.items() + if self.disappeared[id] == 0 + ] + self.match_and_update(frame_name, frame_time, detections=detections) + + def match_and_update( + self, + frame_name: str, + frame_time: float, + detections: list[tuple[Any, Any, Any, Any, Any, Any]], + ) -> None: + # Group detections by object type + detections_by_type: dict[str, list[Detection]] = {} + yuv_frame: np.ndarray | None = None + + if ( + self.ptz_metrics.autotracker_enabled.value + or self.detect_config.stationary.classifier + ): + yuv_frame = self.frame_manager.get( + frame_name, self.camera_config.frame_shape_yuv + ) + for obj in detections: + label = obj[0] + if label not in detections_by_type: + detections_by_type[label] = [] + + # centroid is used for other things downstream + centroid_x = int((obj[2][0] + obj[2][2]) / 2.0) + centroid_y = int((obj[2][1] + obj[2][3]) / 2.0) + + # track based on top,left and bottom,right corners instead of centroid + points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]]) + + embedding = None + if self.ptz_metrics.autotracker_enabled.value: + embedding = get_histogram( + yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3] + ) + + detection = Detection( + points=points, + label=label, + # TODO: stationary objects won't have embeddings + embedding=embedding, + data={ + "label": label, + "score": obj[1], + "box": obj[2], + "area": obj[3], + "ratio": obj[4], + "region": obj[5], + "frame_time": frame_time, + "centroid": (centroid_x, centroid_y), + }, + ) + detections_by_type[label].append(detection) + + coord_transformations = None + + if self.ptz_metrics.autotracker_enabled.value: + # we must have been enabled by mqtt, so set up the estimator + if not self.ptz_motion_estimator: + self.ptz_motion_estimator = PtzMotionEstimator( + self.camera_config, self.ptz_metrics + ) + + coord_transformations = self.ptz_motion_estimator.motion_estimator( + detections, frame_name, frame_time, self.camera_name + ) + + # Update all configured trackers + all_tracked_objects = [] + for label in self.trackers: + tracker = self.get_tracker(label) + tracked_objects = tracker.update( + detections=detections_by_type.get(label, []), + coord_transformations=coord_transformations, + ) + all_tracked_objects.extend(tracked_objects) + + # Collect detections for objects without specific trackers + default_detections = [] + for label, dets in detections_by_type.items(): + if label not in self.trackers: + default_detections.extend(dets) + + # Update default tracker with untracked detections + mode = ( + "ptz" + if self.camera_config.onvif.autotracking.enabled_in_config + else "static" + ) + tracked_objects = self.default_tracker[mode].update( + detections=default_detections, coord_transformations=coord_transformations + ) + all_tracked_objects.extend(tracked_objects) + + # update or create new tracks + active_ids = [] + for t in all_tracked_objects: + estimate = tuple(t.estimate.flatten().astype(int)) + # keep the estimate within the bounds of the image + estimate = ( + max(0, estimate[0]), + max(0, estimate[1]), + min(self.detect_config.width - 1, estimate[2]), # type: ignore[operator] + min(self.detect_config.height - 1, estimate[3]), # type: ignore[operator] + ) + new_obj = { + **t.last_detection.data, + "estimate": estimate, + "estimate_velocity": t.estimate_velocity, + } + active_ids.append(str(t.global_id)) + if str(t.global_id) not in self.track_id_map: + self.register(str(t.global_id), new_obj) + # if there wasn't a detection in this frame, increment disappeared + elif t.last_detection.data["frame_time"] != frame_time: + id = self.track_id_map[str(t.global_id)] + self.disappeared[id] += 1 + # sometimes the estimate gets way off + # only update if the upper left corner is actually upper left + if estimate[0] < estimate[2] and estimate[1] < estimate[3]: + self.tracked_objects[id]["estimate"] = new_obj["estimate"] + # else update it + else: + thresholds = get_stationary_threshold(new_obj["label"]) + self.update( + str(t.global_id), + new_obj, + thresholds, + yuv_frame if thresholds.motion_classifier_enabled else None, + ) + + # clear expired tracks + expired_ids = [k for k in self.track_id_map.keys() if k not in active_ids] + for e_id in expired_ids: + self.deregister(self.track_id_map[e_id], e_id) + + # update list of object boxes that don't have a tracked object yet + tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()] + self.untracked_object_boxes = [ + o[2] for o in detections if o[2] not in tracked_object_boxes + ] + + def print_objects_as_table(self, tracked_objects: Sequence) -> None: + """Used for helping in debugging""" + print() + console = Console() + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Id", style="yellow", justify="center") + table.add_column("Age", justify="right") + table.add_column("Hit Counter", justify="right") + table.add_column("Last distance", justify="right") + table.add_column("Init Id", justify="center") + for obj in tracked_objects: + table.add_row( + str(obj.id), + str(obj.age), + str(obj.hit_counter), + f"{obj.last_distance:.4f}" if obj.last_distance is not None else "N/A", + str(obj.initializing_id), + ) + console.print(table) + + def debug_draw(self, frame: np.ndarray, frame_time: float) -> None: + # Collect all tracked objects from each tracker + all_tracked_objects: list[TrackedObject] = [] + + # print a table to the console with norfair tracked object info + if False: + if len(self.trackers["license_plate"]["static"].tracked_objects) > 0: # type: ignore[unreachable] + self.print_objects_as_table( + self.trackers["license_plate"]["static"].tracked_objects + ) + + # Get tracked objects from type-specific trackers + for object_trackers in self.trackers.values(): + for tracker in object_trackers.values(): + all_tracked_objects.extend(tracker.tracked_objects) + + # Get tracked objects from default trackers + for tracker in self.default_tracker.values(): + all_tracked_objects.extend(tracker.tracked_objects) + + active_detections = [ + Drawable(id=obj.id, points=obj.last_detection.points, label=obj.label) + for obj in all_tracked_objects + if obj.last_detection.data["frame_time"] == frame_time + ] + missing_detections = [ + Drawable(id=obj.id, points=obj.last_detection.points, label=obj.label) + for obj in all_tracked_objects + if obj.last_detection.data["frame_time"] != frame_time + ] + # draw the estimated bounding box + draw_boxes(frame, all_tracked_objects, color="green", draw_ids=True) + # draw the detections that were detected in the current frame + draw_boxes(frame, active_detections, color="blue", draw_ids=True) # type: ignore[arg-type] + # draw the detections that are missing in the current frame + draw_boxes(frame, missing_detections, color="red", draw_ids=True) # type: ignore[arg-type] + + # draw the distance calculation for the last detection + # estimate vs detection + for obj in all_tracked_objects: + ld = obj.last_detection + # bottom right + text_anchor = ( + ld.points[1, 0], # type: ignore[index] + ld.points[1, 1], # type: ignore[index] + ) + frame = Drawer.text( + frame, + f"{obj.id}: {str(obj.last_distance)}", + position=text_anchor, + size=None, + color=(255, 0, 0), + thickness=None, + ) + + if False: + # draw the current formatted time on the frame + from datetime import datetime # type: ignore[unreachable] + + formatted_time = datetime.fromtimestamp(frame_time).strftime( + "%m/%d/%Y %I:%M:%S %p" + ) + + frame = Drawer.text( + frame, + formatted_time, + position=(10, 50), + size=1.5, + color=(255, 255, 255), + thickness=None, + ) diff --git a/sam2-cpu/frigate-dev/frigate/track/object_processing.py b/sam2-cpu/frigate-dev/frigate/track/object_processing.py new file mode 100644 index 0000000..e0ee742 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/object_processing.py @@ -0,0 +1,802 @@ +import base64 +import datetime +import json +import logging +import queue +import threading +from collections import defaultdict +from enum import Enum +from multiprocessing import Queue as MpQueue +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 +import numpy as np +from peewee import SQL, DoesNotExist + +from frigate.camera.state import CameraState +from frigate.comms.detections_updater import DetectionPublisher, DetectionTypeEnum +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.event_metadata_updater import ( + EventMetadataSubscriber, + EventMetadataTypeEnum, +) +from frigate.comms.events_updater import EventEndSubscriber, EventUpdatePublisher +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import ( + CameraMqttConfig, + FrigateConfig, + RecordConfig, + SnapshotsConfig, +) +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + FAST_QUEUE_TIMEOUT, + UPDATE_CAMERA_ACTIVITY, + UPSERT_REVIEW_SEGMENT, +) +from frigate.events.types import EventStateEnum, EventTypeEnum +from frigate.models import Event, ReviewSegment, Timeline +from frigate.ptz.autotrack import PtzAutoTrackerThread +from frigate.track.tracked_object import TrackedObject +from frigate.util.image import SharedMemoryFrameManager + +logger = logging.getLogger(__name__) + + +class ManualEventState(str, Enum): + complete = "complete" + start = "start" + end = "end" + + +class TrackedObjectProcessor(threading.Thread): + def __init__( + self, + config: FrigateConfig, + dispatcher: Dispatcher, + tracked_objects_queue: MpQueue, + ptz_autotracker_thread: PtzAutoTrackerThread, + stop_event: MpEvent, + ) -> None: + super().__init__(name="detected_frames_processor") + self.config = config + self.dispatcher = dispatcher + self.tracked_objects_queue = tracked_objects_queue + self.stop_event: MpEvent = stop_event + self.camera_states: dict[str, CameraState] = {} + self.frame_manager = SharedMemoryFrameManager() + self.last_motion_detected: dict[str, float] = {} + self.ptz_autotracker_thread = ptz_autotracker_thread + + self.camera_config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.zones, + ], + ) + + self.requestor = InterProcessRequestor() + self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value) + self.event_sender = EventUpdatePublisher() + self.event_end_subscriber = EventEndSubscriber() + self.sub_label_subscriber = EventMetadataSubscriber(EventMetadataTypeEnum.all) + + self.camera_activity: dict[str, dict[str, Any]] = {} + self.ongoing_manual_events: dict[str, str] = {} + + # { + # 'zone_name': { + # 'person': { + # 'camera_1': 2, + # 'camera_2': 1 + # } + # } + # } + self.zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) + self.active_zone_data: dict[str, dict[str, Any]] = defaultdict( + lambda: defaultdict(dict) + ) + + for camera in self.config.cameras.keys(): + self.create_camera_state(camera) + + def create_camera_state(self, camera: str) -> None: + """Creates a new camera state.""" + + def start(camera: str, obj: TrackedObject, frame_name: str) -> None: + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.start, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def update(camera: str, obj: TrackedObject, frame_name: str) -> None: + obj.has_snapshot = self.should_save_snapshot(camera, obj) + obj.has_clip = self.should_retain_recording(camera, obj) + after = obj.to_dict() + message = { + "before": obj.previous, + "after": after, + "type": "new" if obj.previous["false_positive"] else "update", + } + self.dispatcher.publish("events", json.dumps(message), retain=False) + obj.previous = after + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.update, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def autotrack(camera: str, obj: TrackedObject, frame_name: str) -> None: + self.ptz_autotracker_thread.ptz_autotracker.autotrack_object(camera, obj) + + def end(camera: str, obj: TrackedObject, frame_name: str) -> None: + # populate has_snapshot + obj.has_snapshot = self.should_save_snapshot(camera, obj) + obj.has_clip = self.should_retain_recording(camera, obj) + + # write thumbnail to disk if it will be saved as an event + if obj.has_snapshot or obj.has_clip: + obj.write_thumbnail_to_disk() + + # write the snapshot to disk + if obj.has_snapshot: + obj.write_snapshot_to_disk() + + if not obj.false_positive: + message = { + "before": obj.previous, + "after": obj.to_dict(), + "type": "end", + } + self.dispatcher.publish("events", json.dumps(message), retain=False) + self.ptz_autotracker_thread.ptz_autotracker.end_object(camera, obj) + + self.event_sender.publish( + ( + EventTypeEnum.tracked_object, + EventStateEnum.end, + camera, + frame_name, + obj.to_dict(), + ) + ) + + def snapshot(camera: str, obj: TrackedObject) -> bool: + mqtt_config: CameraMqttConfig = self.config.cameras[camera].mqtt + if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj): + jpg_bytes = obj.get_img_bytes( + ext="jpg", + timestamp=mqtt_config.timestamp, + bounding_box=mqtt_config.bounding_box, + crop=mqtt_config.crop, + height=mqtt_config.height, + quality=mqtt_config.quality, + ) + + if jpg_bytes is None: + logger.warning( + f"Unable to send mqtt snapshot for {obj.obj_data['id']}." + ) + else: + self.dispatcher.publish( + f"{camera}/{obj.obj_data['label']}/snapshot", + jpg_bytes, + retain=True, + ) + + if obj.obj_data.get("sub_label"): + sub_label = obj.obj_data["sub_label"][0] + + if sub_label in self.config.model.all_attribute_logos: + self.dispatcher.publish( + f"{camera}/{sub_label}/snapshot", + jpg_bytes, + retain=True, + ) + + return True + + return False + + def camera_activity(camera: str, activity: dict[str, Any]) -> None: + last_activity = self.camera_activity.get(camera) + + if not last_activity or activity != last_activity: + self.camera_activity[camera] = activity + self.requestor.send_data(UPDATE_CAMERA_ACTIVITY, self.camera_activity) + + camera_state = CameraState( + camera, self.config, self.frame_manager, self.ptz_autotracker_thread + ) + camera_state.on("start", start) + camera_state.on("autotrack", autotrack) + camera_state.on("update", update) + camera_state.on("end", end) + camera_state.on("snapshot", snapshot) + camera_state.on("camera_activity", camera_activity) + self.camera_states[camera] = camera_state + + def should_save_snapshot(self, camera: str, obj: TrackedObject) -> bool: + if obj.false_positive: + return False + + snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots + + if not snapshot_config.enabled: + return False + + # object never changed position + if obj.obj_data["position_changes"] == 0: + return False + + # if there are required zones and there is no overlap + required_zones = snapshot_config.required_zones + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): + logger.debug( + f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones" + ) + return False + + return True + + def should_retain_recording(self, camera: str, obj: TrackedObject) -> bool: + if obj.false_positive: + return False + + record_config: RecordConfig = self.config.cameras[camera].record + + # Recording is disabled + if not record_config.enabled: + return False + + # object never changed position + if obj.obj_data["position_changes"] == 0: + return False + + # If the object is not considered an alert or detection + if obj.max_severity is None: + return False + + return True + + def should_mqtt_snapshot(self, camera: str, obj: TrackedObject) -> bool: + # object never changed position + if obj.is_stationary(): + return False + + # if there are required zones and there is no overlap + required_zones = self.config.cameras[camera].mqtt.required_zones + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): + logger.debug( + f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones" + ) + return False + + return True + + def update_mqtt_motion( + self, camera: str, frame_time: float, motion_boxes: list + ) -> None: + # publish if motion is currently being detected + if motion_boxes: + # only send ON if motion isn't already active + if self.last_motion_detected.get(camera, 0) == 0: + self.dispatcher.publish( + f"{camera}/motion", + "ON", + retain=False, + ) + + # always updated latest motion + self.last_motion_detected[camera] = frame_time + elif self.last_motion_detected.get(camera, 0) > 0: + mqtt_delay = self.config.cameras[camera].motion.mqtt_off_delay + + # If no motion, make sure the off_delay has passed + if frame_time - self.last_motion_detected.get(camera, 0) >= mqtt_delay: + self.dispatcher.publish( + f"{camera}/motion", + "OFF", + retain=False, + ) + # reset the last_motion so redundant `off` commands aren't sent + self.last_motion_detected[camera] = 0 + + def get_best(self, camera: str, label: str) -> dict[str, Any]: + # TODO: need a lock here + camera_state = self.camera_states[camera] + if label in camera_state.best_objects: + best_obj = camera_state.best_objects[label] + + if not best_obj.thumbnail_data: + return {} + + best = best_obj.thumbnail_data.copy() + best["frame"] = camera_state.frame_cache.get( + best_obj.thumbnail_data["frame_time"] + ) + return best + else: + return {} + + def get_current_frame( + self, camera: str, draw_options: dict[str, Any] = {} + ) -> np.ndarray | None: + if camera == "birdseye": + return self.frame_manager.get( + "birdseye", + (self.config.birdseye.height * 3 // 2, self.config.birdseye.width), + ) + + if camera not in self.camera_states: + return None + + return self.camera_states[camera].get_current_frame(draw_options) + + def get_current_frame_time(self, camera: str) -> float: + """Returns the latest frame time for a given camera.""" + return self.camera_states[camera].current_frame_time + + def set_sub_label( + self, event_id: str, sub_label: str | None, score: float | None + ) -> None: + """Update sub label for given event id.""" + tracked_obj: TrackedObject | None = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event | None = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data["sub_label"] = (sub_label, score) + + if event: + event.sub_label = sub_label # type: ignore[assignment] + data = event.data + if sub_label is None: + data["sub_label_score"] = None # type: ignore[index] + elif score is not None: + data["sub_label_score"] = score # type: ignore[index] + event.data = data + event.save() + + # update timeline items + Timeline.update( + data=Timeline.data.update({"sub_label": (sub_label, score)}) + ).where(Timeline.source_id == event_id).execute() + + # only update ended review segments + # manually updating a sub_label from the UI is only possible for ended tracked objects + try: + review_segment = ReviewSegment.get( + ( + SQL( + "json_extract(data, '$.detections') LIKE ?", + [f'%"{event_id}"%'], + ) + ) + & (ReviewSegment.end_time.is_null(False)) + ) + + segment_data = review_segment.data + detection_ids = segment_data.get("detections", []) + + # Rebuild objects list and sync sub_labels + objects_list = [] + sub_labels = set() + events = Event.select(Event.id, Event.label, Event.sub_label).where( + Event.id.in_(detection_ids) # type: ignore[call-arg, misc] + ) + for det_event in events: + if det_event.sub_label: + sub_labels.add(det_event.sub_label) + objects_list.append( + f"{det_event.label}-verified" + ) # eg, "bird-verified" + else: + objects_list.append(det_event.label) # eg, "bird" + + segment_data["sub_labels"] = list(sub_labels) + segment_data["objects"] = objects_list + + updated_data = { + ReviewSegment.id.name: review_segment.id, + ReviewSegment.camera.name: review_segment.camera, + ReviewSegment.start_time.name: review_segment.start_time, + ReviewSegment.end_time.name: review_segment.end_time, + ReviewSegment.severity.name: review_segment.severity, + ReviewSegment.thumb_path.name: review_segment.thumb_path, + ReviewSegment.data.name: segment_data, + } + + self.requestor.send_data(UPSERT_REVIEW_SEGMENT, updated_data) + logger.debug( + f"Updated sub_label for event {event_id} in review segment {review_segment.id}" + ) + + except DoesNotExist: + logger.debug( + f"No review segment found with event ID {event_id} when updating sub_label" + ) + + def set_object_attribute( + self, + event_id: str, + field_name: str, + field_value: str | None, + score: float | None, + ) -> None: + """Update attribute for given event id.""" + tracked_obj: TrackedObject | None = None + + for state in self.camera_states.values(): + tracked_obj = state.tracked_objects.get(event_id) + + if tracked_obj is not None: + break + + try: + event: Event | None = Event.get(Event.id == event_id) + except DoesNotExist: + event = None + + if not tracked_obj and not event: + return + + if tracked_obj: + tracked_obj.obj_data[field_name] = ( + field_value, + score, + ) + + if event: + data = event.data + data[field_name] = field_value # type: ignore[index] + if field_value is None: + data[f"{field_name}_score"] = None # type: ignore[index] + elif score is not None: + data[f"{field_name}_score"] = score # type: ignore[index] + event.data = data + event.save() + + def save_lpr_snapshot(self, payload: tuple) -> None: + # save the snapshot image + (frame, event_id, camera) = payload + + img = cv2.imdecode( + np.frombuffer(base64.b64decode(frame), dtype=np.uint8), + cv2.IMREAD_COLOR, + ) + + self.camera_states[camera].save_manual_event_image( + img, event_id, "license_plate", {} + ) + + def create_manual_event(self, payload: tuple) -> None: + ( + frame_time, + camera_name, + label, + event_id, + include_recording, + score, + sub_label, + duration, + source_type, + draw, + ) = payload + + # save the snapshot image + self.camera_states[camera_name].save_manual_event_image( + None, event_id, label, draw + ) + end_time = frame_time + duration if duration is not None else None + + # send event to event maintainer + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.start, + camera_name, + "", + { + "id": event_id, + "label": label, + "sub_label": sub_label, + "score": score, + "camera": camera_name, + "start_time": frame_time + - self.config.cameras[camera_name].record.event_pre_capture, + "end_time": end_time, + "has_clip": self.config.cameras[camera_name].record.enabled + and include_recording, + "has_snapshot": True, + "type": source_type, + }, + ) + ) + + if source_type == "api": + self.ongoing_manual_events[event_id] = camera_name + self.detection_publisher.publish( + ( + camera_name, + frame_time, + { + "state": ( + ManualEventState.complete + if end_time + else ManualEventState.start + ), + "label": f"{label}: {sub_label}" if sub_label else label, + "event_id": event_id, + "end_time": end_time, + }, + ), + DetectionTypeEnum.api.value, + ) + + def create_lpr_event(self, payload: tuple) -> None: + ( + frame_time, + camera_name, + label, + event_id, + include_recording, + score, + sub_label, + plate, + ) = payload + + # send event to event maintainer + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.start, + camera_name, + "", + { + "id": event_id, + "label": label, + "sub_label": sub_label, + "score": score, + "camera": camera_name, + "start_time": frame_time + - self.config.cameras[camera_name].record.event_pre_capture, + "end_time": None, + "has_clip": self.config.cameras[camera_name].record.enabled + and include_recording, + "has_snapshot": True, + "type": "api", + "recognized_license_plate": plate, + "recognized_license_plate_score": score, + }, + ) + ) + + self.ongoing_manual_events[event_id] = camera_name + self.detection_publisher.publish( + ( + camera_name, + frame_time, + { + "state": ManualEventState.start, + "label": f"{label}: {sub_label}" if sub_label else label, + "event_id": event_id, + "end_time": None, + }, + ), + DetectionTypeEnum.lpr.value, + ) + + def end_manual_event(self, payload: tuple) -> None: + (event_id, end_time) = payload + + self.event_sender.publish( + ( + EventTypeEnum.api, + EventStateEnum.end, + None, + "", + {"id": event_id, "end_time": end_time}, + ) + ) + + if event_id in self.ongoing_manual_events: + self.detection_publisher.publish( + ( + self.ongoing_manual_events[event_id], + end_time, + { + "state": ManualEventState.end, + "event_id": event_id, + "end_time": end_time, + }, + ), + DetectionTypeEnum.api.value, + ) + self.ongoing_manual_events.pop(event_id) + + def force_end_all_events(self, camera: str, camera_state: CameraState) -> None: + """Ends all active events on camera when disabling.""" + last_frame_name = camera_state.previous_frame_id + for obj_id, obj in list(camera_state.tracked_objects.items()): + if "end_time" not in obj.obj_data: + logger.debug(f"Camera {camera} disabled, ending active event {obj_id}") + obj.obj_data["end_time"] = datetime.datetime.now().timestamp() + # end callbacks + for callback in camera_state.callbacks["end"]: + callback(camera, obj, last_frame_name) + + # camera activity callbacks + for callback in camera_state.callbacks["camera_activity"]: + callback( + camera, + {"enabled": False, "motion": 0, "objects": []}, + ) + + def run(self) -> None: + while not self.stop_event.is_set(): + # check for config updates + updated_topics = self.camera_config_subscriber.check_for_updates() + + if "enabled" in updated_topics: + for camera in updated_topics["enabled"]: + if self.camera_states[camera].prev_enabled is None: + self.camera_states[camera].prev_enabled = self.config.cameras[ + camera + ].enabled + elif "add" in updated_topics: + for camera in updated_topics["add"]: + self.config.cameras[camera] = ( + self.camera_config_subscriber.camera_configs[camera] + ) + self.create_camera_state(camera) + elif "remove" in updated_topics: + for camera in updated_topics["remove"]: + camera_state = self.camera_states[camera] + camera_state.shutdown() + self.camera_states.pop(camera) + + # manage camera disabled state + for camera, config in self.config.cameras.items(): + if not config.enabled_in_config: + continue + + current_enabled = config.enabled + camera_state = self.camera_states[camera] + + if camera_state.prev_enabled and not current_enabled: + logger.debug(f"Not processing objects for disabled camera {camera}") + self.force_end_all_events(camera, camera_state) + + camera_state.prev_enabled = current_enabled + + if not current_enabled: + continue + + # check for sub label updates + while True: + update = self.sub_label_subscriber.check_for_update(timeout=0) + + if not update: + break + + (raw_topic, payload) = update + + if not raw_topic or not payload: + break + + topic = str(raw_topic) + + if topic.endswith(EventMetadataTypeEnum.sub_label.value): + (event_id, sub_label, score) = payload + self.set_sub_label(event_id, sub_label, score) + if topic.endswith(EventMetadataTypeEnum.attribute.value): + (event_id, field_name, field_value, score) = payload + self.set_object_attribute(event_id, field_name, field_value, score) + elif topic.endswith(EventMetadataTypeEnum.lpr_event_create.value): + self.create_lpr_event(payload) + elif topic.endswith(EventMetadataTypeEnum.save_lpr_snapshot.value): + self.save_lpr_snapshot(payload) + elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): + self.create_manual_event(payload) + elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): + self.end_manual_event(payload) + + try: + ( + camera, + frame_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = self.tracked_objects_queue.get(True, 1) + except queue.Empty: + continue + + if not self.config.cameras[camera].enabled: + logger.debug(f"Camera {camera} disabled, skipping update") + continue + + camera_state = self.camera_states[camera] + + camera_state.update( + frame_name, frame_time, current_tracked_objects, motion_boxes, regions + ) + + self.update_mqtt_motion(camera, frame_time, motion_boxes) + + tracked_objects = [ + o.to_dict() for o in camera_state.tracked_objects.values() + ] + + # publish info on this frame + self.detection_publisher.publish( + ( + camera, + frame_name, + frame_time, + tracked_objects, + motion_boxes, + regions, + ), + DetectionTypeEnum.video.value, + ) + + # cleanup event finished queue + while not self.stop_event.is_set(): + update = self.event_end_subscriber.check_for_update( + timeout=FAST_QUEUE_TIMEOUT + ) + + if not update: + break + + event_id, camera, _ = update + self.camera_states[camera].finished(event_id) + + # shut down camera states + for state in self.camera_states.values(): + state.shutdown() + + self.requestor.stop() + self.detection_publisher.stop() + self.event_sender.stop() + self.event_end_subscriber.stop() + self.sub_label_subscriber.stop() + self.camera_config_subscriber.stop() + + logger.info("Exiting object processor...") diff --git a/sam2-cpu/frigate-dev/frigate/track/stationary_classifier.py b/sam2-cpu/frigate-dev/frigate/track/stationary_classifier.py new file mode 100644 index 0000000..832df5d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/stationary_classifier.py @@ -0,0 +1,254 @@ +"""Tools for determining if an object is stationary.""" + +import logging +from dataclasses import dataclass, field +from typing import Any, cast + +import cv2 +import numpy as np +from scipy.ndimage import gaussian_filter + +logger = logging.getLogger(__name__) + + +@dataclass +class StationaryThresholds: + """IOU thresholds and history parameters for stationary object classification. + + This allows different sensitivity settings for different object types. + """ + + # Objects to apply these thresholds to + # If None, apply to all objects + objects: list[str] = field(default_factory=list) + + # Threshold of IoU that causes the object to immediately be considered active + # Below this threshold, assume object is active + known_active_iou: float = 0.2 + + # IOU threshold for checking if stationary object has moved + # If mean and median IOU drops below this, assume object is no longer stationary + stationary_check_iou: float = 0.6 + + # IOU threshold for checking if active object has changed position + # Higher threshold makes it more difficult for the object to be considered stationary + active_check_iou: float = 0.9 + + # Maximum number of bounding boxes to keep in stationary history + max_stationary_history: int = 10 + + # Whether to use the motion classifier + motion_classifier_enabled: bool = False + + +# Thresholds for objects that are expected to be stationary +STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds( + objects=["bbq_grill", "package", "waste_bin"], + known_active_iou=0.0, + motion_classifier_enabled=True, +) + +# Thresholds for objects that are active but can be stationary for longer periods of time +DYNAMIC_OBJECT_THRESHOLDS = StationaryThresholds( + objects=["bicycle", "boat", "car", "motorcycle", "tractor", "truck"], + active_check_iou=0.75, + motion_classifier_enabled=True, +) + + +def get_stationary_threshold(label: str) -> StationaryThresholds: + """Get the stationary thresholds for a given object label.""" + + if label in STATIONARY_OBJECT_THRESHOLDS.objects: + return STATIONARY_OBJECT_THRESHOLDS + + if label in DYNAMIC_OBJECT_THRESHOLDS.objects: + return DYNAMIC_OBJECT_THRESHOLDS + + return StationaryThresholds() + + +class StationaryMotionClassifier: + """Fallback classifier to prevent false flips from stationary to active. + + Uses appearance consistency on a fixed spatial region (historical median box) + to detect actual movement, ignoring bounding box detection variations. + """ + + CROP_SIZE = 96 + NCC_KEEP_THRESHOLD = 0.90 # High correlation = keep stationary + NCC_ACTIVE_THRESHOLD = 0.85 # Low correlation = consider active + SHIFT_KEEP_THRESHOLD = 0.02 # Small shift = keep stationary + SHIFT_ACTIVE_THRESHOLD = 0.04 # Large shift = consider active + DRIFT_ACTIVE_THRESHOLD = 0.12 # Cumulative drift over 5 frames + CHANGED_FRAMES_TO_FLIP = 2 + + def __init__(self) -> None: + self.anchor_crops: dict[str, np.ndarray] = {} + self.anchor_boxes: dict[str, tuple[int, int, int, int]] = {} + self.changed_counts: dict[str, int] = {} + self.shift_histories: dict[str, list[float]] = {} + + # Pre-compute Hanning window for phase correlation + hann = np.hanning(self.CROP_SIZE).astype(np.float64) + self._hann2d = np.outer(hann, hann) + + def reset(self, id: str) -> None: + logger.debug("StationaryMotionClassifier.reset: id=%s", id) + if id in self.anchor_crops: + del self.anchor_crops[id] + if id in self.anchor_boxes: + del self.anchor_boxes[id] + self.changed_counts[id] = 0 + self.shift_histories[id] = [] + + def _extract_y_crop( + self, yuv_frame: np.ndarray, box: tuple[int, int, int, int] + ) -> np.ndarray: + """Extract and normalize Y-plane crop from bounding box.""" + y_height = yuv_frame.shape[0] // 3 * 2 + width = yuv_frame.shape[1] + x1 = max(0, min(width - 1, box[0])) + y1 = max(0, min(y_height - 1, box[1])) + x2 = max(0, min(width - 1, box[2])) + y2 = max(0, min(y_height - 1, box[3])) + + if x2 <= x1: + x2 = min(width - 1, x1 + 1) + if y2 <= y1: + y2 = min(y_height - 1, y1 + 1) + + # Extract Y-plane crop, resize, and blur + y_plane = yuv_frame[0:y_height, 0:width] + crop = y_plane[y1:y2, x1:x2] + crop_resized = cv2.resize( + crop, (self.CROP_SIZE, self.CROP_SIZE), interpolation=cv2.INTER_AREA + ) + result = cast(np.ndarray[Any, Any], gaussian_filter(crop_resized, sigma=0.5)) + logger.debug( + "_extract_y_crop: box=%s clamped=(%d,%d,%d,%d) crop_shape=%s", + box, + x1, + y1, + x2, + y2, + crop.shape if "crop" in locals() else None, + ) + return result + + def ensure_anchor( + self, id: str, yuv_frame: np.ndarray, median_box: tuple[int, int, int, int] + ) -> None: + """Initialize anchor crop from stable median box when object becomes stationary.""" + if id not in self.anchor_crops: + self.anchor_boxes[id] = median_box + self.anchor_crops[id] = self._extract_y_crop(yuv_frame, median_box) + self.changed_counts[id] = 0 + self.shift_histories[id] = [] + logger.debug( + "ensure_anchor: initialized id=%s median_box=%s crop_shape=%s", + id, + median_box, + self.anchor_crops[id].shape, + ) + + def on_active(self, id: str) -> None: + """Reset state when object becomes active to allow re-anchoring.""" + logger.debug("on_active: id=%s became active; resetting state", id) + self.reset(id) + + def evaluate( + self, id: str, yuv_frame: np.ndarray, current_box: tuple[int, int, int, int] + ) -> bool: + """Return True to keep stationary, False to flip to active. + + Compares the same spatial region (historical median box) across frames + to detect actual movement, ignoring bounding box variations. + """ + + if id not in self.anchor_crops or id not in self.anchor_boxes: + logger.debug("evaluate: id=%s has no anchor; default keep stationary", id) + return True + + # Compare same spatial region across frames + anchor_box = self.anchor_boxes[id] + anchor_crop = self.anchor_crops[id] + curr_crop = self._extract_y_crop(yuv_frame, anchor_box) + + # Compute appearance and motion metrics + ncc = cv2.matchTemplate(curr_crop, anchor_crop, cv2.TM_CCOEFF_NORMED)[0, 0] + a64 = anchor_crop.astype(np.float64) * self._hann2d + c64 = curr_crop.astype(np.float64) * self._hann2d + (shift_x, shift_y), _ = cv2.phaseCorrelate(a64, c64) + shift_norm = float(np.hypot(shift_x, shift_y)) / float(self.CROP_SIZE) + + logger.debug( + "evaluate: id=%s metrics ncc=%.4f shift_norm=%.4f (shift_x=%.3f, shift_y=%.3f)", + id, + float(ncc), + shift_norm, + float(shift_x), + float(shift_y), + ) + + # Update rolling shift history + history = self.shift_histories.get(id, []) + history.append(shift_norm) + if len(history) > 5: + history = history[-5:] + self.shift_histories[id] = history + drift_sum = float(sum(history)) + + logger.debug( + "evaluate: id=%s history_len=%d last_shift=%.4f drift_sum=%.4f", + id, + len(history), + history[-1] if history else -1.0, + drift_sum, + ) + + # Early exit for clear stationary case + if ncc >= self.NCC_KEEP_THRESHOLD and shift_norm < self.SHIFT_KEEP_THRESHOLD: + self.changed_counts[id] = 0 + logger.debug( + "evaluate: id=%s early-stationary keep=True (ncc>=%.2f and shift<%.2f)", + id, + self.NCC_KEEP_THRESHOLD, + self.SHIFT_KEEP_THRESHOLD, + ) + return True + + # Check for movement indicators + movement_detected = ( + ncc < self.NCC_ACTIVE_THRESHOLD + or shift_norm >= self.SHIFT_ACTIVE_THRESHOLD + or drift_sum >= self.DRIFT_ACTIVE_THRESHOLD + ) + + if movement_detected: + cnt = self.changed_counts.get(id, 0) + 1 + self.changed_counts[id] = cnt + if ( + cnt >= self.CHANGED_FRAMES_TO_FLIP + or drift_sum >= self.DRIFT_ACTIVE_THRESHOLD + ): + logger.debug( + "evaluate: id=%s flip_to_active=True cnt=%d drift_sum=%.4f thresholds(changed>=%d drift>=%.2f)", + id, + cnt, + drift_sum, + self.CHANGED_FRAMES_TO_FLIP, + self.DRIFT_ACTIVE_THRESHOLD, + ) + return False + logger.debug( + "evaluate: id=%s movement_detected cnt=%d keep_until_cnt>=%d", + id, + cnt, + self.CHANGED_FRAMES_TO_FLIP, + ) + else: + self.changed_counts[id] = 0 + logger.debug("evaluate: id=%s no_movement keep=True", id) + + return True diff --git a/sam2-cpu/frigate-dev/frigate/track/tracked_object.py b/sam2-cpu/frigate-dev/frigate/track/tracked_object.py new file mode 100644 index 0000000..4537986 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/track/tracked_object.py @@ -0,0 +1,699 @@ +"""Object attribute.""" + +import logging +import math +import os +from collections import defaultdict +from statistics import median +from typing import Any, Optional, cast + +import cv2 +import numpy as np + +from frigate.config import ( + CameraConfig, + FilterConfig, + SnapshotsConfig, + UIConfig, +) +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.detectors.detector_config import ModelConfig +from frigate.review.types import SeverityEnum +from frigate.util.builtin import sanitize_float +from frigate.util.image import ( + area, + calculate_region, + draw_box_with_label, + draw_timestamp, + is_better_thumbnail, +) +from frigate.util.object import box_inside +from frigate.util.velocity import calculate_real_world_speed + +logger = logging.getLogger(__name__) + + +# In most cases objects that loiter in a loitering zone should alert, +# but can still be expected to stay stationary for extended periods of time +# (ex: car loitering on the street vs when a known person parks on the street) +# person is the main object that should keep alerts going as long as they loiter +# even if they are stationary. +EXTENDED_LOITERING_OBJECTS = ["person"] + + +class TrackedObject: + def __init__( + self, + model_config: ModelConfig, + camera_config: CameraConfig, + ui_config: UIConfig, + frame_cache: dict[float, dict[str, Any]], + obj_data: dict[str, Any], + ) -> None: + # set the score history then remove as it is not part of object state + self.score_history: list[float] = obj_data["score_history"] + del obj_data["score_history"] + + self.obj_data = obj_data + self.colormap = model_config.colormap + self.logos = model_config.all_attribute_logos + self.camera_config = camera_config + self.ui_config = ui_config + self.frame_cache = frame_cache + self.zone_presence: dict[str, int] = {} + self.zone_loitering: dict[str, int] = {} + self.current_zones: list[str] = [] + self.entered_zones: list[str] = [] + self.attributes: dict[str, float] = defaultdict(float) + self.false_positive = True + self.has_clip = False + self.has_snapshot = False + self.top_score = self.computed_score = 0.0 + self.thumbnail_data: dict[str, Any] | None = None + self.last_updated = 0 + self.last_published = 0 + self.frame = None + self.active = True + self.pending_loitering = False + self.speed_history: list[float] = [] + self.current_estimated_speed: float = 0 + self.average_estimated_speed: float = 0 + self.velocity_angle = 0 + self.path_data: list[tuple[Any, float]] = [] + self.previous = self.to_dict() + + @property + def max_severity(self) -> Optional[str]: + review_config = self.camera_config.review + + if ( + self.camera_config.review.alerts.enabled + and self.obj_data["label"] in review_config.alerts.labels + and ( + not review_config.alerts.required_zones + or set(self.entered_zones) & set(review_config.alerts.required_zones) + ) + ): + return SeverityEnum.alert + + if ( + self.camera_config.review.detections.enabled + and ( + not review_config.detections.labels + or self.obj_data["label"] in review_config.detections.labels + ) + and ( + not review_config.detections.required_zones + or set(self.entered_zones) + & set(review_config.detections.required_zones) + ) + ): + return SeverityEnum.detection + + return None + + def _is_false_positive(self) -> bool: + # once a true positive, always a true positive + if not self.false_positive: + return False + + threshold = self.camera_config.objects.filters[self.obj_data["label"]].threshold + return self.computed_score < threshold + + def compute_score(self) -> float: + """get median of scores for object.""" + return median(self.score_history) + + def update( + self, current_frame_time: float, obj_data: dict[str, Any], has_valid_frame: bool + ) -> tuple[bool, bool, bool, bool]: + thumb_update = False + significant_change = False + path_update = False + autotracker_update = False + # if the object is not in the current frame, add a 0.0 to the score history + if obj_data["frame_time"] != current_frame_time: + self.score_history.append(0.0) + else: + self.score_history.append(obj_data["score"]) + + # only keep the last 10 scores + if len(self.score_history) > 10: + self.score_history = self.score_history[-10:] + + # calculate if this is a false positive + self.computed_score = self.compute_score() + if self.computed_score > self.top_score: + self.top_score = self.computed_score + self.false_positive = self._is_false_positive() + self.active = self.is_active() + + if not self.false_positive and has_valid_frame: + # determine if this frame is a better thumbnail + if self.thumbnail_data is None or is_better_thumbnail( + self.obj_data["label"], + self.thumbnail_data, + obj_data, + self.camera_config.frame_shape, + ): + if obj_data["frame_time"] == current_frame_time: + self.thumbnail_data = { + "frame_time": obj_data["frame_time"], + "box": obj_data["box"], + "area": obj_data["area"], + "region": obj_data["region"], + "score": obj_data["score"], + "attributes": obj_data["attributes"], + "current_estimated_speed": self.current_estimated_speed, + "velocity_angle": self.velocity_angle, + "path_data": self.path_data.copy(), + "recognized_license_plate": obj_data.get( + "recognized_license_plate" + ), + "recognized_license_plate_score": obj_data.get( + "recognized_license_plate_score" + ), + } + thumb_update = True + else: + logger.debug( + f"{self.camera_config.name}: Object frame time {obj_data['frame_time']} is not equal to the current frame time {current_frame_time}, not updating thumbnail" + ) + + # check zones + current_zones = [] + bottom_center = (obj_data["centroid"][0], obj_data["box"][3]) + in_loitering_zone = False + in_speed_zone = False + + # check each zone + for name, zone in self.camera_config.zones.items(): + # if the zone is not for this object type, skip + if len(zone.objects) > 0 and obj_data["label"] not in zone.objects: + continue + contour = zone.contour + zone_score = self.zone_presence.get(name, 0) + 1 + + # check if the object is in the zone + if cv2.pointPolygonTest(contour, bottom_center, False) >= 0: + # if the object passed the filters once, dont apply again + if name in self.current_zones or not zone_filtered(self, zone.filters): + # Calculate speed first if this is a speed zone + if ( + zone.distances + and obj_data["frame_time"] == current_frame_time + and self.active + ): + speed_magnitude, self.velocity_angle = ( + calculate_real_world_speed( + zone.contour, + zone.distances, + self.obj_data["estimate_velocity"], + bottom_center, + self.camera_config.detect.fps, + ) + ) + + # users can configure speed zones incorrectly, so sanitize speed_magnitude + # and velocity_angle in case the values come back as inf or NaN + speed_magnitude = sanitize_float(speed_magnitude) + self.velocity_angle = sanitize_float(self.velocity_angle) + + if self.ui_config.unit_system == "metric": + self.current_estimated_speed = ( + speed_magnitude * 3.6 + ) # m/s to km/h + else: + self.current_estimated_speed = ( + speed_magnitude * 0.681818 + ) # ft/s to mph + + self.speed_history.append(self.current_estimated_speed) + if len(self.speed_history) > 10: + self.speed_history = self.speed_history[-10:] + + self.average_estimated_speed = sum(self.speed_history) / len( + self.speed_history + ) + + # we've exceeded the speed threshold on the zone + # or we don't have a speed threshold set + if ( + zone.speed_threshold is None + or self.average_estimated_speed > zone.speed_threshold + ): + in_speed_zone = True + + logger.debug( + f"Camera: {self.camera_config.name}, tracked object ID: {self.obj_data['id']}, " + f"zone: {name}, pixel velocity: {str(tuple(np.round(self.obj_data['estimate_velocity']).flatten().astype(int)))}, " + f"speed magnitude: {speed_magnitude}, velocity angle: {self.velocity_angle}, " + f"estimated speed: {self.current_estimated_speed:.1f}, " + f"average speed: {self.average_estimated_speed:.1f}, " + f"length: {len(self.speed_history)}" + ) + + # Check zone entry conditions - for speed zones, require both inertia and speed + if zone_score >= zone.inertia: + if zone.distances and not in_speed_zone: + continue # Skip zone entry for speed zones until speed threshold met + + # if the zone has loitering time, and the object is an extended loiter object + # always mark it as loitering actively + if ( + self.obj_data["label"] in EXTENDED_LOITERING_OBJECTS + and zone.loitering_time > 0 + ): + in_loitering_zone = True + + loitering_score = self.zone_loitering.get(name, 0) + 1 + + # loitering time is configured as seconds, convert to count of frames + if loitering_score >= ( + self.camera_config.zones[name].loitering_time + * self.camera_config.detect.fps + ): + current_zones.append(name) + + if name not in self.entered_zones: + self.entered_zones.append(name) + else: + self.zone_loitering[name] = loitering_score + + # this object is pending loitering but has not entered the zone yet + if zone.loitering_time > 0: + in_loitering_zone = True + else: + self.zone_presence[name] = zone_score + else: + # once an object has a zone inertia of 3+ it is not checked anymore + if 0 < zone_score < zone.inertia: + self.zone_presence[name] = zone_score - 1 + + # Reset speed if not in speed zone + if zone.distances and name not in current_zones: + self.current_estimated_speed = 0 + + # update loitering status + self.pending_loitering = in_loitering_zone + + # maintain attributes + for attr in obj_data["attributes"]: + if self.attributes[attr["label"]] < attr["score"]: + self.attributes[attr["label"]] = attr["score"] + + # populate the sub_label for object with highest scoring logo + if self.obj_data["label"] in ["car", "motorcycle", "package", "person"]: + recognized_logos = { + k: self.attributes[k] for k in self.logos if k in self.attributes + } + if len(recognized_logos) > 0: + max_logo = max(recognized_logos, key=recognized_logos.get) # type: ignore[arg-type] + + # don't overwrite sub label if it is already set + if ( + self.obj_data.get("sub_label") is None + or self.obj_data["sub_label"][0] == max_logo + ): + self.obj_data["sub_label"] = (max_logo, recognized_logos[max_logo]) + + # check for significant change + if not self.false_positive: + # if the zones changed, signal an update + if set(self.current_zones) != set(current_zones): + significant_change = True + + # if the position changed, signal an update + if self.obj_data["position_changes"] != obj_data["position_changes"]: + significant_change = True + + if self.obj_data["attributes"] != obj_data["attributes"]: + significant_change = True + + # if the state changed between stationary and active + if self.previous["active"] != self.active: + significant_change = True + + # update at least once per minute + if self.obj_data["frame_time"] - self.previous["frame_time"] > 60: + significant_change = True + + # update autotrack at most 3 objects per second + if self.obj_data["frame_time"] - self.previous["frame_time"] >= (1 / 3): + autotracker_update = True + + # update path + width = self.camera_config.detect.width + height = self.camera_config.detect.height + + if width is not None and height is not None: + bottom_center = ( + round(obj_data["centroid"][0] / width, 4), + round(obj_data["box"][3] / height, 4), + ) + + # calculate a reasonable movement threshold (e.g., 5% of the frame diagonal) + threshold = 0.05 * math.sqrt(width**2 + height**2) / max(width, height) + + if not self.path_data: + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + elif ( + math.dist(self.path_data[-1][0], bottom_center) >= threshold + or len(self.path_data) == 1 + ): + # check Euclidean distance before appending + self.path_data.append((bottom_center, obj_data["frame_time"])) + path_update = True + logger.debug( + f"Point tracking: {obj_data['id']}, {bottom_center}, {obj_data['frame_time']}" + ) + + self.obj_data.update(obj_data) + self.current_zones = current_zones + logger.debug( + f"{self.camera_config.name}: Updating {obj_data['id']}: thumb update? {thumb_update}, significant change? {significant_change}, path update? {path_update}, autotracker update? {autotracker_update} " + ) + return (thumb_update, significant_change, path_update, autotracker_update) + + def to_dict(self) -> dict[str, Any]: + event = { + "id": self.obj_data["id"], + "camera": self.camera_config.name, + "frame_time": self.obj_data["frame_time"], + "snapshot": self.thumbnail_data, + "label": self.obj_data["label"], + "sub_label": self.obj_data.get("sub_label"), + "top_score": self.top_score, + "false_positive": self.false_positive, + "start_time": self.obj_data["start_time"], + "end_time": self.obj_data.get("end_time", None), + "score": self.obj_data["score"], + "box": self.obj_data["box"], + "area": self.obj_data["area"], + "ratio": self.obj_data["ratio"], + "region": self.obj_data["region"], + "active": self.active, + "stationary": not self.active, + "motionless_count": self.obj_data["motionless_count"], + "position_changes": self.obj_data["position_changes"], + "current_zones": self.current_zones.copy(), + "entered_zones": self.entered_zones.copy(), + "has_clip": self.has_clip, + "has_snapshot": self.has_snapshot, + "attributes": self.attributes, + "current_attributes": self.obj_data["attributes"], + "pending_loitering": self.pending_loitering, + "max_severity": self.max_severity, + "current_estimated_speed": self.current_estimated_speed, + "average_estimated_speed": self.average_estimated_speed, + "velocity_angle": self.velocity_angle, + "path_data": self.path_data.copy(), + "recognized_license_plate": self.obj_data.get("recognized_license_plate"), + } + + return event + + def is_active(self) -> bool: + return not self.is_stationary() + + def is_stationary(self) -> bool: + count = cast(int | float, self.obj_data["motionless_count"]) + return count > (self.camera_config.detect.stationary.threshold or 50) + + def get_thumbnail(self, ext: str) -> bytes | None: + img_bytes = self.get_img_bytes( + ext, timestamp=False, bounding_box=False, crop=True, height=175 + ) + + if img_bytes: + return img_bytes + else: + _, img = cv2.imencode(f".{ext}", np.zeros((175, 175, 3), np.uint8)) + return img.tobytes() + + def get_clean_webp(self) -> bytes | None: + if self.thumbnail_data is None: + return None + + try: + best_frame = cv2.cvtColor( + self.frame_cache[self.thumbnail_data["frame_time"]]["frame"], + cv2.COLOR_YUV2BGR_I420, + ) + except KeyError: + logger.warning( + f"Unable to create clean webp because frame {self.thumbnail_data['frame_time']} is not in the cache" + ) + return None + + ret, webp = cv2.imencode( + ".webp", best_frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] + ) + if ret: + return webp.tobytes() + else: + return None + + def get_img_bytes( + self, + ext: str, + timestamp: bool = False, + bounding_box: bool = False, + crop: bool = False, + height: int | None = None, + quality: int | None = None, + ) -> bytes | None: + if self.thumbnail_data is None: + return None + + try: + best_frame = cv2.cvtColor( + self.frame_cache[self.thumbnail_data["frame_time"]]["frame"], + cv2.COLOR_YUV2BGR_I420, + ) + except KeyError: + logger.warning( + f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache" + ) + return None + + if bounding_box: + thickness = 2 + color = self.colormap.get(self.obj_data["label"], (255, 255, 255)) + + # draw the bounding boxes on the frame + box = self.thumbnail_data["box"] + draw_box_with_label( + best_frame, + box[0], + box[1], + box[2], + box[3], + self.obj_data["label"], + f"{int(self.thumbnail_data['score'] * 100)}% {int(self.thumbnail_data['area'])}" + + ( + f" {self.thumbnail_data['current_estimated_speed']:.1f}" + if self.thumbnail_data["current_estimated_speed"] != 0 + else "" + ), + thickness=thickness, + color=color, + ) + + # draw any attributes + for attribute in self.thumbnail_data["attributes"]: + box = attribute["box"] + box_area = int((box[2] - box[0]) * (box[3] - box[1])) + draw_box_with_label( + best_frame, + box[0], + box[1], + box[2], + box[3], + attribute["label"], + f"{attribute['score']:.0%} {str(box_area)}", + thickness=thickness, + color=color, + ) + + if crop: + box = self.thumbnail_data["box"] + box_size = 300 + region = calculate_region( + best_frame.shape, + box[0], + box[1], + box[2], + box[3], + box_size, + multiplier=1.1, + ) + best_frame = best_frame[region[1] : region[3], region[0] : region[2]] + + if height: + width = int(height * best_frame.shape[1] / best_frame.shape[0]) + best_frame = cv2.resize( + best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA + ) + if timestamp: + colors = self.camera_config.timestamp_style.color + draw_timestamp( + best_frame, + self.thumbnail_data["frame_time"], + self.camera_config.timestamp_style.format, + font_effect=self.camera_config.timestamp_style.effect, + font_thickness=self.camera_config.timestamp_style.thickness, + font_color=(colors.blue, colors.green, colors.red), + position=self.camera_config.timestamp_style.position, + ) + + quality_params = [] + + if ext == "jpg": + quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality or 70] + elif ext == "webp": + quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality or 60] + + ret, jpg = cv2.imencode(f".{ext}", best_frame, quality_params) + + if ret: + return jpg.tobytes() + else: + return None + + def write_snapshot_to_disk(self) -> None: + snapshot_config: SnapshotsConfig = self.camera_config.snapshots + jpg_bytes = self.get_img_bytes( + ext="jpg", + timestamp=snapshot_config.timestamp, + bounding_box=snapshot_config.bounding_box, + crop=snapshot_config.crop, + height=snapshot_config.height, + quality=snapshot_config.quality, + ) + if jpg_bytes is None: + logger.warning(f"Unable to save snapshot for {self.obj_data['id']}.") + else: + with open( + os.path.join( + CLIPS_DIR, f"{self.camera_config.name}-{self.obj_data['id']}.jpg" + ), + "wb", + ) as j: + j.write(jpg_bytes) + + # write clean snapshot if enabled + if snapshot_config.clean_copy: + webp_bytes = self.get_clean_webp() + if webp_bytes is None: + logger.warning( + f"Unable to save clean snapshot for {self.obj_data['id']}." + ) + else: + with open( + os.path.join( + CLIPS_DIR, + f"{self.camera_config.name}-{self.obj_data['id']}-clean.webp", + ), + "wb", + ) as p: + p.write(webp_bytes) + + def write_thumbnail_to_disk(self) -> None: + if not self.camera_config.name: + return + + directory = os.path.join(THUMB_DIR, self.camera_config.name) + + if not os.path.exists(directory): + os.makedirs(directory) + + thumb_bytes = self.get_thumbnail("webp") + + if thumb_bytes: + with open( + os.path.join(directory, f"{self.obj_data['id']}.webp"), "wb" + ) as f: + f.write(thumb_bytes) + + +def zone_filtered(obj: TrackedObject, object_config: dict[str, FilterConfig]) -> bool: + object_name = obj.obj_data["label"] + + if object_name in object_config: + obj_settings = object_config[object_name] + + # if the min area is larger than the + # detected object, don't add it to detected objects + if obj_settings.min_area > obj.obj_data["area"]: + return True + + # if the detected object is larger than the + # max area, don't add it to detected objects + if obj_settings.max_area < obj.obj_data["area"]: + return True + + # if the score is lower than the threshold, skip + if obj_settings.threshold > obj.computed_score: + return True + + # if the object is not proportionally wide enough + if obj_settings.min_ratio > obj.obj_data["ratio"]: + return True + + # if the object is proportionally too wide + if obj_settings.max_ratio < obj.obj_data["ratio"]: + return True + + return False + + +class TrackedObjectAttribute: + def __init__(self, raw_data: tuple) -> None: + self.label = raw_data[0] + self.score = raw_data[1] + self.box = raw_data[2] + self.area = raw_data[3] + self.ratio = raw_data[4] + self.region = raw_data[5] + + def get_tracking_data(self) -> dict[str, Any]: + """Return data saved to the object.""" + return { + "label": self.label, + "score": self.score, + "box": self.box, + } + + def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]: + """Find the best attribute for each object and return its ID.""" + best_object_area: float | None = None + best_object_id: str | None = None + best_object_label: str | None = None + + for obj in objects: + if not box_inside(obj["box"], self.box): + continue + + object_area = area(obj["box"]) + + # if multiple objects have the same attribute then they + # are overlapping, it is most likely that the smaller object + # is the one with the attribute + if best_object_area is None: + best_object_area = object_area + best_object_id = obj["id"] + best_object_label = obj["label"] + else: + if best_object_label == obj["label"]: + # if multiple objects of the same type are overlapping + # then the attribute will not be assigned + return None + elif object_area < best_object_area: + # if a car and person are overlapping then assign the label to the smaller object (which should be the person) + best_object_area = object_area + best_object_id = obj["id"] + best_object_label = obj["label"] + + return best_object_id diff --git a/sam2-cpu/frigate-dev/frigate/types.py b/sam2-cpu/frigate-dev/frigate/types.py new file mode 100644 index 0000000..6c51356 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/types.py @@ -0,0 +1,33 @@ +from enum import Enum +from typing import TypedDict + +from frigate.camera import CameraMetrics +from frigate.data_processing.types import DataProcessorMetrics +from frigate.object_detection.base import ObjectDetectProcess + + +class StatsTrackingTypes(TypedDict): + camera_metrics: dict[str, CameraMetrics] + embeddings_metrics: DataProcessorMetrics | None + detectors: dict[str, ObjectDetectProcess] + started: int + latest_frigate_version: str + last_updated: int + processes: dict[str, int] + + +class ModelStatusTypesEnum(str, Enum): + not_downloaded = "not_downloaded" + downloading = "downloading" + downloaded = "downloaded" + error = "error" + training = "training" + complete = "complete" + failed = "failed" + + +class TrackedObjectUpdateTypesEnum(str, Enum): + description = "description" + face = "face" + lpr = "lpr" + classification = "classification" diff --git a/sam2-cpu/frigate-dev/frigate/util/__init__.py b/sam2-cpu/frigate-dev/frigate/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sam2-cpu/frigate-dev/frigate/util/audio.py b/sam2-cpu/frigate-dev/frigate/util/audio.py new file mode 100644 index 0000000..eede9c0 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/audio.py @@ -0,0 +1,116 @@ +"""Utilities for creating and manipulating audio.""" + +import logging +import os +import subprocess as sp +from typing import Optional + +from pathvalidate import sanitize_filename + +from frigate.const import CACHE_DIR +from frigate.models import Recordings + +logger = logging.getLogger(__name__) + + +def get_audio_from_recording( + ffmpeg, + camera_name: str, + start_ts: float, + end_ts: float, + sample_rate: int = 16000, +) -> Optional[bytes]: + """Extract audio from recording files between start_ts and end_ts in WAV format suitable for sherpa-onnx. + + Args: + ffmpeg: FFmpeg configuration object + camera_name: Name of the camera + start_ts: Start timestamp in seconds + end_ts: End timestamp in seconds + sample_rate: Sample rate for output audio (default 16kHz for sherpa-onnx) + + Returns: + Bytes of WAV audio data or None if extraction failed + """ + # Fetch all relevant recording segments + recordings = ( + Recordings.select( + Recordings.path, + Recordings.start_time, + Recordings.end_time, + ) + .where( + (Recordings.start_time.between(start_ts, end_ts)) + | (Recordings.end_time.between(start_ts, end_ts)) + | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) + ) + .where(Recordings.camera == camera_name) + .order_by(Recordings.start_time.asc()) + ) + + if not recordings: + logger.debug( + f"No recordings found for {camera_name} between {start_ts} and {end_ts}" + ) + return None + + # Generate concat playlist file + file_name = sanitize_filename( + f"audio_playlist_{camera_name}_{start_ts}-{end_ts}.txt" + ) + file_path = os.path.join(CACHE_DIR, file_name) + try: + with open(file_path, "w") as file: + for clip in recordings: + file.write(f"file '{clip.path}'\n") + if clip.start_time < start_ts: + file.write(f"inpoint {int(start_ts - clip.start_time)}\n") + if clip.end_time > end_ts: + file.write(f"outpoint {int(end_ts - clip.start_time)}\n") + + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-protocol_whitelist", + "pipe,file", + "-f", + "concat", + "-safe", + "0", + "-i", + file_path, + "-vn", # No video + "-acodec", + "pcm_s16le", # 16-bit PCM encoding + "-ar", + str(sample_rate), + "-ac", + "1", # Mono audio + "-f", + "wav", + "-", + ] + + process = sp.run( + ffmpeg_cmd, + capture_output=True, + ) + + if process.returncode == 0: + logger.debug( + f"Successfully extracted audio for {camera_name} from {start_ts} to {end_ts}" + ) + return process.stdout + else: + logger.error(f"Failed to extract audio: {process.stderr.decode()}") + return None + except Exception as e: + logger.error(f"Error extracting audio from recordings: {e}") + return None + finally: + try: + os.unlink(file_path) + except OSError: + pass diff --git a/sam2-cpu/frigate-dev/frigate/util/builtin.py b/sam2-cpu/frigate-dev/frigate/util/builtin.py new file mode 100644 index 0000000..b1a7621 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/builtin.py @@ -0,0 +1,404 @@ +"""Utilities for builtin types manipulation.""" + +import ast +import copy +import datetime +import logging +import math +import multiprocessing.queues +import queue +import re +import shlex +import struct +import urllib.parse +from collections.abc import Mapping +from multiprocessing.sharedctypes import Synchronized +from pathlib import Path +from typing import Any, Dict, Optional, Tuple, Union + +import numpy as np +from ruamel.yaml import YAML + +from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS + +logger = logging.getLogger(__name__) + + +class EventsPerSecond: + def __init__(self, max_events=1000, last_n_seconds=10) -> None: + self._start = None + self._max_events = max_events + self._last_n_seconds = last_n_seconds + self._timestamps = [] + + def start(self) -> None: + self._start = datetime.datetime.now().timestamp() + + def update(self) -> None: + now = datetime.datetime.now().timestamp() + if self._start is None: + self._start = now + self._timestamps.append(now) + # truncate the list when it goes 100 over the max_size + if len(self._timestamps) > self._max_events + 100: + self._timestamps = self._timestamps[(1 - self._max_events) :] + self.expire_timestamps(now) + + def eps(self) -> float: + now = datetime.datetime.now().timestamp() + if self._start is None: + self._start = now + # compute the (approximate) events in the last n seconds + self.expire_timestamps(now) + seconds = min(now - self._start, self._last_n_seconds) + # avoid divide by zero + if seconds == 0: + seconds = 1 + return len(self._timestamps) / seconds + + # remove aged out timestamps + def expire_timestamps(self, now: float) -> None: + threshold = now - self._last_n_seconds + while self._timestamps and self._timestamps[0] < threshold: + del self._timestamps[0] + + +class InferenceSpeed: + def __init__(self, metric: Synchronized) -> None: + self.__metric = metric + self.__initialized = False + + def update(self, inference_time: float) -> None: + if not self.__initialized: + self.__metric.value = inference_time + self.__initialized = True + return + + self.__metric.value = (self.__metric.value * 9 + inference_time) / 10 + + def current(self) -> float: + return self.__metric.value + + +def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dict: + """ + :param dct1: First dict to merge + :param dct2: Second dict to merge + :param override: if same key exists in both dictionaries, should override? otherwise ignore. (default=True) + :return: The merge dictionary + """ + merged = copy.deepcopy(dct1) + for k, v2 in dct2.items(): + if k in merged: + v1 = merged[k] + if isinstance(v1, dict) and isinstance(v2, Mapping): + merged[k] = deep_merge(v1, v2, override) + elif isinstance(v1, list) and isinstance(v2, list): + if merge_lists: + merged[k] = v1 + v2 + else: + if override: + merged[k] = copy.deepcopy(v2) + else: + merged[k] = copy.deepcopy(v2) + return merged + + +def clean_camera_user_pass(line: str) -> str: + """Removes user and password from line.""" + rtsp_cleaned = re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line) + return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", rtsp_cleaned) + + +def escape_special_characters(path: str) -> str: + """Cleans reserved characters to encodings for ffmpeg.""" + if len(path) > 1000: + return ValueError("Input too long to check") + + try: + found = re.search(REGEX_RTSP_CAMERA_USER_PASS, path).group(0)[3:-1] + pw = found[(found.index(":") + 1) :] + return path.replace(pw, urllib.parse.quote_plus(pw)) + except AttributeError: + # path does not have user:pass + return path + + +def get_ffmpeg_arg_list(arg: Any) -> list: + """Use arg if list or convert to list format.""" + return arg if isinstance(arg, list) else shlex.split(arg) + + +def load_labels(path: Optional[str], encoding="utf-8", prefill=91): + """Loads labels from file (with or without index numbers). + Args: + path: path to label file. + encoding: label file encoding. + Returns: + Dictionary mapping indices to labels. + """ + if path is None: + return {} + + with open(path, "r", encoding=encoding) as f: + labels = {index: "unknown" for index in range(prefill)} + lines = f.readlines() + if not lines: + return {} + + if lines[0].split(" ", maxsplit=1)[0].isdigit(): + pairs = [line.split(" ", maxsplit=1) for line in lines] + labels.update({int(index): label.strip() for index, label in pairs}) + else: + labels.update({index: line.strip() for index, line in enumerate(lines)}) + return labels + + +def to_relative_box( + width: int, height: int, box: Tuple[int, int, int, int] +) -> Tuple[int | float, int | float, int | float, int | float]: + return ( + box[0] / width, # x + box[1] / height, # y + (box[2] - box[0]) / width, # w + (box[3] - box[1]) / height, # h + ) + + +def create_mask(frame_shape, mask): + mask_img = np.zeros(frame_shape, np.uint8) + mask_img[:] = 255 + + +def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any]: + updates = {} + for key_path_str, new_value_list in query_string.items(): + # use the string key as-is for updates dictionary + if len(new_value_list) > 1: + updates[key_path_str] = new_value_list + else: + value = new_value_list[0] + try: + # no need to convert if we have a mask/zone string + value = ast.literal_eval(value) if "," not in value else value + except (ValueError, SyntaxError): + pass + updates[key_path_str] = value + return updates + + +def flatten_config_data( + config_data: Dict[str, Any], parent_key: str = "" +) -> Dict[str, Any]: + items = [] + for key, value in config_data.items(): + new_key = f"{parent_key}.{key}" if parent_key else key + if isinstance(value, dict): + items.extend(flatten_config_data(value, new_key).items()) + else: + items.append((new_key, value)) + return dict(items) + + +def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]): + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + + try: + with open(file_path, "r") as f: + data = yaml.load(f) + except FileNotFoundError: + logger.error( + f"Unable to read from Frigate config file {file_path}. Make sure it exists and is readable." + ) + return + + # Apply all updates + for key_path_str, new_value in updates.items(): + key_path = key_path_str.split(".") + for i in range(len(key_path)): + try: + index = int(key_path[i]) + key_path[i] = (key_path[i - 1], index) + key_path.pop(i - 1) + except ValueError: + pass + data = update_yaml(data, key_path, new_value) + + try: + with open(file_path, "w") as f: + yaml.dump(data, f) + except Exception as e: + logger.error(f"Unable to write to Frigate config file {file_path}: {e}") + + +def update_yaml(data, key_path, new_value): + temp = data + for key in key_path[:-1]: + if isinstance(key, tuple): + if key[0] not in temp: + temp[key[0]] = [{}] * max(1, key[1] + 1) + elif len(temp[key[0]]) <= key[1]: + temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1) + temp = temp[key[0]][key[1]] + else: + if key not in temp or temp[key] is None: + temp[key] = {} + temp = temp[key] + + last_key = key_path[-1] + if new_value == "": + if isinstance(last_key, tuple): + del temp[last_key[0]][last_key[1]] + else: + del temp[last_key] + else: + if isinstance(last_key, tuple): + if last_key[0] not in temp: + temp[last_key[0]] = [{}] * max(1, last_key[1] + 1) + elif len(temp[last_key[0]]) <= last_key[1]: + temp[last_key[0]] += [{}] * (last_key[1] - len(temp[last_key[0]]) + 1) + temp[last_key[0]][last_key[1]] = new_value + else: + if ( + last_key in temp + and isinstance(temp[last_key], dict) + and isinstance(new_value, dict) + ): + temp[last_key].update(new_value) + else: + temp[last_key] = new_value + + return data + + +def find_by_key(dictionary, target_key): + if target_key in dictionary: + return dictionary[target_key] + else: + for value in dictionary.values(): + if isinstance(value, dict): + result = find_by_key(value, target_key) + if result is not None: + return result + return None + + +def clear_and_unlink(file: Path, missing_ok: bool = True) -> None: + """clear file then unlink to avoid space retained by file descriptors.""" + if not missing_ok and not file.exists(): + raise FileNotFoundError() + + # empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769 + with open(file, "w"): + pass + + file.unlink(missing_ok=missing_ok) + + +def empty_and_close_queue(q): + while True: + try: + q.get(block=True, timeout=0.5) + except (queue.Empty, EOFError): + break + except Exception as e: + logger.debug(f"Error while emptying queue: {e}") + break + + # close the queue if it is a multiprocessing queue + # manager proxy queues do not have close or join_thread method + if isinstance(q, multiprocessing.queues.Queue): + try: + q.close() + q.join_thread() + except Exception: + pass + + +def generate_color_palette(n): + # mimic matplotlib's color scheme + base_colors = [ + (31, 119, 180), # blue + (255, 127, 14), # orange + (44, 160, 44), # green + (214, 39, 40), # red + (148, 103, 189), # purple + (140, 86, 75), # brown + (227, 119, 194), # pink + (127, 127, 127), # gray + (188, 189, 34), # olive + (23, 190, 207), # cyan + ] + + def interpolate(color1, color2, factor): + return tuple(int(c1 + (c2 - c1) * factor) for c1, c2 in zip(color1, color2)) + + if n <= len(base_colors): + return base_colors[:n] + + colors = base_colors.copy() + step = 1 / (n - len(base_colors) + 1) + extra_colors_needed = n - len(base_colors) + + # interpolate between the base colors to generate more if needed + for i in range(extra_colors_needed): + index = i % (len(base_colors) - 1) + factor = (i + 1) * step + color1 = base_colors[index] + color2 = base_colors[index + 1] + colors.append(interpolate(color1, color2, factor)) + + return colors + + +def serialize( + vector: Union[list[float], np.ndarray, float], pack: bool = True +) -> bytes: + """Serializes a list of floats, numpy array, or single float into a compact "raw bytes" format""" + if isinstance(vector, np.ndarray): + # Convert numpy array to list of floats + vector = vector.flatten().tolist() + elif isinstance(vector, (float, np.float32, np.float64)): + # Handle single float values + vector = [vector] + elif not isinstance(vector, list): + raise TypeError( + f"Input must be a list of floats, a numpy array, or a single float. Got {type(vector)}" + ) + + try: + if pack: + return struct.pack("%sf" % len(vector), *vector) + else: + return vector + except struct.error as e: + raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") + + +def deserialize(bytes_data: bytes) -> list[float]: + """Deserializes a compact "raw bytes" format into a list of floats""" + return list(struct.unpack("%sf" % (len(bytes_data) // 4), bytes_data)) + + +def sanitize_float(value): + """Replace NaN or inf with 0.0.""" + if isinstance(value, (int, float)) and not math.isfinite(value): + return 0.0 + return value + + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return 1 - cosine_distance(a, b) + + +def cosine_distance(a: np.ndarray, b: np.ndarray) -> float: + """Returns cosine distance to match sqlite-vec's calculation.""" + dot = np.dot(a, b) + a_mag = np.dot(a, a) # ||a||^2 + b_mag = np.dot(b, b) # ||b||^2 + + if a_mag == 0 or b_mag == 0: + return 1.0 + + return 1.0 - (dot / (np.sqrt(a_mag) * np.sqrt(b_mag))) diff --git a/sam2-cpu/frigate-dev/frigate/util/classification.py b/sam2-cpu/frigate-dev/frigate/util/classification.py new file mode 100644 index 0000000..7777af5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/classification.py @@ -0,0 +1,881 @@ +"""Util for classification models.""" + +import datetime +import json +import logging +import os +import random +from collections import defaultdict + +import cv2 +import numpy as np + +from frigate.comms.embeddings_updater import EmbeddingsRequestEnum, EmbeddingsRequestor +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FfmpegConfig +from frigate.const import ( + CLIPS_DIR, + MODEL_CACHE_DIR, + PROCESS_PRIORITY_LOW, + UPDATE_MODEL_STATE, +) +from frigate.log import redirect_output_to_logger +from frigate.models import Event, Recordings, ReviewSegment +from frigate.types import ModelStatusTypesEnum +from frigate.util.downloader import ModelDownloader +from frigate.util.file import get_event_thumbnail_bytes +from frigate.util.image import get_image_from_recording +from frigate.util.process import FrigateProcess + +BATCH_SIZE = 16 +EPOCHS = 50 +LEARNING_RATE = 0.001 +TRAINING_METADATA_FILE = ".training_metadata.json" + +logger = logging.getLogger(__name__) + + +def write_training_metadata(model_name: str, image_count: int) -> None: + """ + Write training metadata to a hidden file in the model's clips directory. + + Args: + model_name: Name of the classification model + image_count: Number of images used in training + """ + clips_model_dir = os.path.join(CLIPS_DIR, model_name) + os.makedirs(clips_model_dir, exist_ok=True) + + metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE) + metadata = { + "last_training_date": datetime.datetime.now().isoformat(), + "last_training_image_count": image_count, + } + + try: + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=2) + logger.info(f"Wrote training metadata for {model_name}: {image_count} images") + except Exception as e: + logger.error(f"Failed to write training metadata for {model_name}: {e}") + + +def read_training_metadata(model_name: str) -> dict[str, any] | None: + """ + Read training metadata from the hidden file in the model's clips directory. + + Args: + model_name: Name of the classification model + + Returns: + Dictionary with last_training_date and last_training_image_count, or None if not found + """ + clips_model_dir = os.path.join(CLIPS_DIR, model_name) + metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE) + + if not os.path.exists(metadata_path): + return None + + try: + with open(metadata_path, "r") as f: + metadata = json.load(f) + return metadata + except Exception as e: + logger.error(f"Failed to read training metadata for {model_name}: {e}") + return None + + +def get_dataset_image_count(model_name: str) -> int: + """ + Count the total number of images in the model's dataset directory. + + Args: + model_name: Name of the classification model + + Returns: + Total count of images across all categories + """ + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + + if not os.path.exists(dataset_dir): + return 0 + + total_count = 0 + try: + for category in os.listdir(dataset_dir): + category_dir = os.path.join(dataset_dir, category) + if not os.path.isdir(category_dir): + continue + + image_files = [ + f + for f in os.listdir(category_dir) + if f.lower().endswith((".webp", ".png", ".jpg", ".jpeg")) + ] + total_count += len(image_files) + except Exception as e: + logger.error(f"Failed to count dataset images for {model_name}: {e}") + return 0 + + return total_count + + +class ClassificationTrainingProcess(FrigateProcess): + def __init__(self, model_name: str) -> None: + self.BASE_WEIGHT_URL = os.environ.get( + "TF_KERAS_MOBILENET_V2_WEIGHTS_URL", + "", + ) + super().__init__( + stop_event=None, + priority=PROCESS_PRIORITY_LOW, + name=f"model_training:{model_name}", + ) + self.model_name = model_name + + def run(self) -> None: + self.pre_run_setup() + success = self.__train_classification_model() + exit(0 if success else 1) + + def __generate_representative_dataset_factory(self, dataset_dir: str): + def generate_representative_dataset(): + image_paths = [] + for root, dirs, files in os.walk(dataset_dir): + for file in files: + if file.lower().endswith((".jpg", ".jpeg", ".png")): + image_paths.append(os.path.join(root, file)) + + for path in image_paths[:300]: + img = cv2.imread(path) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = cv2.resize(img, (224, 224)) + img_array = np.array(img, dtype=np.float32) / 255.0 + img_array = img_array[None, ...] + yield [img_array] + + return generate_representative_dataset + + @redirect_output_to_logger(logger, logging.DEBUG) + def __train_classification_model(self) -> bool: + """Train a classification model.""" + try: + # import in the function so that tensorflow is not initialized multiple times + import tensorflow as tf + from tensorflow.keras import layers, models, optimizers + from tensorflow.keras.applications import MobileNetV2 + from tensorflow.keras.preprocessing.image import ImageDataGenerator + + dataset_dir = os.path.join(CLIPS_DIR, self.model_name, "dataset") + model_dir = os.path.join(MODEL_CACHE_DIR, self.model_name) + os.makedirs(model_dir, exist_ok=True) + + num_classes = len( + [ + d + for d in os.listdir(dataset_dir) + if os.path.isdir(os.path.join(dataset_dir, d)) + ] + ) + + if num_classes < 2: + logger.error( + f"Training failed for {self.model_name}: Need at least 2 classes, found {num_classes}" + ) + return False + + weights_path = "imagenet" + # Download MobileNetV2 weights if not present + if self.BASE_WEIGHT_URL: + weights_path = os.path.join( + MODEL_CACHE_DIR, "MobileNet", "mobilenet_v2_weights.h5" + ) + if not os.path.exists(weights_path): + logger.info("Downloading MobileNet V2 weights file") + ModelDownloader.download_from_url( + self.BASE_WEIGHT_URL, weights_path + ) + + # Start with imagenet base model with 35% of channels in each layer + base_model = MobileNetV2( + input_shape=(224, 224, 3), + include_top=False, + weights=weights_path, + alpha=0.35, + ) + base_model.trainable = False # Freeze pre-trained layers + + model = models.Sequential( + [ + base_model, + layers.GlobalAveragePooling2D(), + layers.Dense(128, activation="relu"), + layers.Dropout(0.3), + layers.Dense(num_classes, activation="softmax"), + ] + ) + + model.compile( + optimizer=optimizers.Adam(learning_rate=LEARNING_RATE), + loss="categorical_crossentropy", + metrics=["accuracy"], + ) + + # create training set + datagen = ImageDataGenerator(rescale=1.0 / 255, validation_split=0.2) + train_gen = datagen.flow_from_directory( + dataset_dir, + target_size=(224, 224), + batch_size=BATCH_SIZE, + class_mode="categorical", + subset="training", + ) + + total_images = train_gen.samples + logger.debug( + f"Training {self.model_name}: {total_images} images across {num_classes} classes" + ) + + # write labelmap + class_indices = train_gen.class_indices + index_to_class = {v: k for k, v in class_indices.items()} + sorted_classes = [index_to_class[i] for i in range(len(index_to_class))] + with open(os.path.join(model_dir, "labelmap.txt"), "w") as f: + for class_name in sorted_classes: + f.write(f"{class_name}\n") + + # train the model + logger.debug(f"Training {self.model_name} for {EPOCHS} epochs...") + model.fit(train_gen, epochs=EPOCHS, verbose=0) + logger.debug(f"Converting {self.model_name} to TFLite...") + + # convert model to tflite + converter = tf.lite.TFLiteConverter.from_keras_model(model) + converter.optimizations = [tf.lite.Optimize.DEFAULT] + converter.representative_dataset = ( + self.__generate_representative_dataset_factory(dataset_dir) + ) + converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] + converter.inference_input_type = tf.uint8 + converter.inference_output_type = tf.uint8 + tflite_model = converter.convert() + + # write model + model_path = os.path.join(model_dir, "model.tflite") + with open(model_path, "wb") as f: + f.write(tflite_model) + + # verify model file was written successfully + if not os.path.exists(model_path) or os.path.getsize(model_path) == 0: + logger.error( + f"Training failed for {self.model_name}: Model file was not created or is empty" + ) + return False + + # write training metadata with image count + dataset_image_count = get_dataset_image_count(self.model_name) + write_training_metadata(self.model_name, dataset_image_count) + + logger.info(f"Finished training {self.model_name}") + return True + + except Exception as e: + logger.error(f"Training failed for {self.model_name}: {e}", exc_info=True) + return False + + +def kickoff_model_training( + embeddingRequestor: EmbeddingsRequestor, model_name: str +) -> None: + requestor = InterProcessRequestor() + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.training, + }, + ) + + # run training in sub process so that + # tensorflow will free CPU / GPU memory + # upon training completion + training_process = ClassificationTrainingProcess(model_name) + training_process.start() + training_process.join() + + # check if training succeeded by examining the exit code + training_success = training_process.exitcode == 0 + + if training_success: + # reload model and mark training as complete + embeddingRequestor.send_data( + EmbeddingsRequestEnum.reload_classification_model.value, + {"model_name": model_name}, + ) + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.complete, + }, + ) + else: + logger.error( + f"Training subprocess failed for {model_name} (exit code: {training_process.exitcode})" + ) + # mark training as failed so UI shows error state + # don't reload the model since it failed + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": model_name, + "state": ModelStatusTypesEnum.failed, + }, + ) + + requestor.stop() + + +@staticmethod +def collect_state_classification_examples( + model_name: str, cameras: dict[str, tuple[float, float, float, float]] +) -> None: + """ + Collect representative state classification examples from review items. + + This function: + 1. Queries review items from specified cameras + 2. Selects 100 balanced timestamps across the data + 3. Extracts keyframes from recordings (cropped to specified regions) + 4. Selects 24 most visually distinct images + 5. Saves them to the dataset directory + + Args: + model_name: Name of the classification model + cameras: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1) + """ + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + temp_dir = os.path.join(dataset_dir, "temp") + os.makedirs(temp_dir, exist_ok=True) + + # Step 1: Get review items for the cameras + camera_names = list(cameras.keys()) + review_items = list( + ReviewSegment.select() + .where(ReviewSegment.camera.in_(camera_names)) + .where(ReviewSegment.end_time.is_null(False)) + .order_by(ReviewSegment.start_time.asc()) + ) + + if not review_items: + logger.warning(f"No review items found for cameras: {camera_names}") + return + + # Step 2: Create balanced timestamp selection (100 samples) + timestamps = _select_balanced_timestamps(review_items, target_count=100) + + # Step 3: Extract keyframes from recordings with crops applied + keyframes = _extract_keyframes( + "/usr/lib/ffmpeg/7.0/bin/ffmpeg", timestamps, temp_dir, cameras + ) + + # Step 4: Select 24 most visually distinct images (they're already cropped) + distinct_images = _select_distinct_images(keyframes, target_count=24) + + # Step 5: Save to train directory for later classification + train_dir = os.path.join(CLIPS_DIR, model_name, "train") + os.makedirs(train_dir, exist_ok=True) + + saved_count = 0 + for idx, image_path in enumerate(distinct_images): + dest_path = os.path.join(train_dir, f"example_{idx:03d}.jpg") + try: + img = cv2.imread(image_path) + + if img is not None: + cv2.imwrite(dest_path, img) + saved_count += 1 + except Exception as e: + logger.error(f"Failed to save image {image_path}: {e}") + + import shutil + + try: + shutil.rmtree(temp_dir) + except Exception as e: + logger.warning(f"Failed to clean up temp directory: {e}") + + +def _select_balanced_timestamps( + review_items: list[ReviewSegment], target_count: int = 100 +) -> list[dict]: + """ + Select balanced timestamps from review items. + + Strategy: + - Group review items by camera and time of day + - Sample evenly across groups to ensure diversity + - For each selected review item, pick a random timestamp within its duration + + Returns: + List of dicts with keys: camera, timestamp, review_item + """ + # Group by camera and hour of day for temporal diversity + grouped = defaultdict(list) + + for item in review_items: + camera = item.camera + # Group by 6-hour blocks for temporal diversity + hour_block = int(item.start_time // (6 * 3600)) + key = f"{camera}_{hour_block}" + grouped[key].append(item) + + # Calculate how many samples per group + num_groups = len(grouped) + if num_groups == 0: + return [] + + samples_per_group = max(1, target_count // num_groups) + timestamps = [] + + # Sample from each group + for group_items in grouped.values(): + # Take samples_per_group items from this group + sample_size = min(samples_per_group, len(group_items)) + sampled_items = random.sample(group_items, sample_size) + + for item in sampled_items: + # Pick a random timestamp within the review item's duration + duration = item.end_time - item.start_time + if duration <= 0: + continue + + # Sample from middle 80% to avoid edge artifacts + offset = random.uniform(duration * 0.1, duration * 0.9) + timestamp = item.start_time + offset + + timestamps.append( + { + "camera": item.camera, + "timestamp": timestamp, + "review_item": item, + } + ) + + # If we don't have enough, sample more from larger groups + while len(timestamps) < target_count and len(timestamps) < len(review_items): + for group_items in grouped.values(): + if len(timestamps) >= target_count: + break + + # Pick a random item not already sampled + item = random.choice(group_items) + duration = item.end_time - item.start_time + if duration <= 0: + continue + + offset = random.uniform(duration * 0.1, duration * 0.9) + timestamp = item.start_time + offset + + # Check if we already have a timestamp near this one + if not any(abs(t["timestamp"] - timestamp) < 1.0 for t in timestamps): + timestamps.append( + { + "camera": item.camera, + "timestamp": timestamp, + "review_item": item, + } + ) + + return timestamps[:target_count] + + +def _extract_keyframes( + ffmpeg_path: str, + timestamps: list[dict], + output_dir: str, + camera_crops: dict[str, tuple[float, float, float, float]], +) -> list[str]: + """ + Extract keyframes from recordings at specified timestamps and crop to specified regions. + + This implementation batches work by running multiple ffmpeg snapshot commands + concurrently, which significantly reduces total runtime compared to + processing each timestamp serially. + + Args: + ffmpeg_path: Path to ffmpeg binary + timestamps: List of timestamp dicts from _select_balanced_timestamps + output_dir: Directory to save extracted frames + camera_crops: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1) + + Returns: + List of paths to successfully extracted and cropped keyframe images + """ + from concurrent.futures import ThreadPoolExecutor, as_completed + + if not timestamps: + return [] + + # Limit the number of concurrent ffmpeg processes so we don't overload the host. + max_workers = min(5, len(timestamps)) + + def _process_timestamp(idx: int, ts_info: dict) -> tuple[int, str | None]: + camera = ts_info["camera"] + timestamp = ts_info["timestamp"] + + if camera not in camera_crops: + logger.warning(f"No crop coordinates for camera {camera}") + return idx, None + + norm_x1, norm_y1, norm_x2, norm_y2 = camera_crops[camera] + + try: + recording = ( + Recordings.select() + .where( + (timestamp >= Recordings.start_time) + & (timestamp <= Recordings.end_time) + & (Recordings.camera == camera) + ) + .order_by(Recordings.start_time.desc()) + .limit(1) + .get() + ) + except Exception: + return idx, None + + relative_time = timestamp - recording.start_time + + try: + config = FfmpegConfig(path="/usr/lib/ffmpeg/7.0") + image_data = get_image_from_recording( + config, + recording.path, + relative_time, + codec="mjpeg", + height=None, + ) + + if not image_data: + return idx, None + + nparr = np.frombuffer(image_data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is None: + return idx, None + + height, width = img.shape[:2] + + x1 = int(norm_x1 * width) + y1 = int(norm_y1 * height) + x2 = int(norm_x2 * width) + y2 = int(norm_y2 * height) + + x1_clipped = max(0, min(x1, width)) + y1_clipped = max(0, min(y1, height)) + x2_clipped = max(0, min(x2, width)) + y2_clipped = max(0, min(y2, height)) + + if x2_clipped <= x1_clipped or y2_clipped <= y1_clipped: + return idx, None + + cropped = img[y1_clipped:y2_clipped, x1_clipped:x2_clipped] + resized = cv2.resize(cropped, (224, 224)) + + output_path = os.path.join(output_dir, f"frame_{idx:04d}.jpg") + cv2.imwrite(output_path, resized) + return idx, output_path + except Exception as e: + logger.debug( + f"Failed to extract frame from {recording.path} at {relative_time}s: {e}" + ) + return idx, None + + keyframes_with_index: list[tuple[int, str]] = [] + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_idx = { + executor.submit(_process_timestamp, idx, ts_info): idx + for idx, ts_info in enumerate(timestamps) + } + + for future in as_completed(future_to_idx): + _, path = future.result() + if path: + keyframes_with_index.append((future_to_idx[future], path)) + + keyframes_with_index.sort(key=lambda item: item[0]) + return [path for _, path in keyframes_with_index] + + +def _select_distinct_images( + image_paths: list[str], target_count: int = 20 +) -> list[str]: + """ + Select the most visually distinct images from a set of keyframes. + + Uses a greedy algorithm based on image histograms: + 1. Start with a random image + 2. Iteratively add the image that is most different from already selected images + 3. Difference is measured using histogram comparison + + Args: + image_paths: List of paths to candidate images + target_count: Number of distinct images to select + + Returns: + List of paths to selected images + """ + if len(image_paths) <= target_count: + return image_paths + + histograms = {} + valid_paths = [] + + for path in image_paths: + try: + img = cv2.imread(path) + + if img is None: + continue + + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + hist = cv2.calcHist( + [hsv], [0, 1, 2], None, [8, 8, 8], [0, 180, 0, 256, 0, 256] + ) + hist = cv2.normalize(hist, hist).flatten() + histograms[path] = hist + valid_paths.append(path) + except Exception as e: + logger.debug(f"Failed to process image {path}: {e}") + continue + + if len(valid_paths) <= target_count: + return valid_paths + + selected = [] + first_image = random.choice(valid_paths) + selected.append(first_image) + remaining = [p for p in valid_paths if p != first_image] + + while len(selected) < target_count and remaining: + max_min_distance = -1 + best_candidate = None + + for candidate in remaining: + min_distance = float("inf") + + for selected_img in selected: + distance = cv2.compareHist( + histograms[candidate], + histograms[selected_img], + cv2.HISTCMP_BHATTACHARYYA, + ) + min_distance = min(min_distance, distance) + + if min_distance > max_min_distance: + max_min_distance = min_distance + best_candidate = candidate + + if best_candidate: + selected.append(best_candidate) + remaining.remove(best_candidate) + else: + break + + return selected + + +@staticmethod +def collect_object_classification_examples( + model_name: str, + label: str, +) -> None: + """ + Collect representative object classification examples from event thumbnails. + + This function: + 1. Queries events for the specified label + 2. Selects 100 balanced events across different cameras and times + 3. Retrieves thumbnails for selected events (with 33% center crop applied) + 4. Selects 24 most visually distinct thumbnails + 5. Saves to dataset directory + + Args: + model_name: Name of the classification model + label: Object label to collect (e.g., "person", "car") + """ + dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") + temp_dir = os.path.join(dataset_dir, "temp") + os.makedirs(temp_dir, exist_ok=True) + + # Step 1: Query events for the specified label and cameras + events = list( + Event.select().where((Event.label == label)).order_by(Event.start_time.asc()) + ) + + if not events: + logger.warning(f"No events found for label '{label}'") + return + + logger.debug(f"Found {len(events)} events") + + # Step 2: Select balanced events (100 samples) + selected_events = _select_balanced_events(events, target_count=100) + logger.debug(f"Selected {len(selected_events)} events") + + # Step 3: Extract thumbnails from events + thumbnails = _extract_event_thumbnails(selected_events, temp_dir) + logger.debug(f"Successfully extracted {len(thumbnails)} thumbnails") + + # Step 4: Select 24 most visually distinct thumbnails + distinct_images = _select_distinct_images(thumbnails, target_count=24) + logger.debug(f"Selected {len(distinct_images)} distinct images") + + # Step 5: Save to train directory for later classification + train_dir = os.path.join(CLIPS_DIR, model_name, "train") + os.makedirs(train_dir, exist_ok=True) + + saved_count = 0 + for idx, image_path in enumerate(distinct_images): + dest_path = os.path.join(train_dir, f"example_{idx:03d}.jpg") + try: + img = cv2.imread(image_path) + + if img is not None: + cv2.imwrite(dest_path, img) + saved_count += 1 + except Exception as e: + logger.error(f"Failed to save image {image_path}: {e}") + + import shutil + + try: + shutil.rmtree(temp_dir) + except Exception as e: + logger.warning(f"Failed to clean up temp directory: {e}") + + logger.debug( + f"Successfully collected {saved_count} classification examples in {train_dir}" + ) + + +def _select_balanced_events( + events: list[Event], target_count: int = 100 +) -> list[Event]: + """ + Select balanced events from the event list. + + Strategy: + - Group events by camera and time of day + - Sample evenly across groups to ensure diversity + - Prioritize events with higher scores + + Returns: + List of selected events + """ + grouped = defaultdict(list) + + for event in events: + camera = event.camera + hour_block = int(event.start_time // (6 * 3600)) + key = f"{camera}_{hour_block}" + grouped[key].append(event) + + num_groups = len(grouped) + if num_groups == 0: + return [] + + samples_per_group = max(1, target_count // num_groups) + selected = [] + + for group_events in grouped.values(): + sorted_events = sorted( + group_events, + key=lambda e: e.data.get("score", 0) if e.data else 0, + reverse=True, + ) + + sample_size = min(samples_per_group, len(sorted_events)) + selected.extend(sorted_events[:sample_size]) + + if len(selected) < target_count: + remaining = [e for e in events if e not in selected] + remaining_sorted = sorted( + remaining, + key=lambda e: e.data.get("score", 0) if e.data else 0, + reverse=True, + ) + needed = target_count - len(selected) + selected.extend(remaining_sorted[:needed]) + + return selected[:target_count] + + +def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]: + """ + Extract thumbnails from events and save to disk. + + Args: + events: List of Event objects + output_dir: Directory to save thumbnails + + Returns: + List of paths to successfully extracted thumbnail images + """ + thumbnail_paths = [] + + for idx, event in enumerate(events): + try: + thumbnail_bytes = get_event_thumbnail_bytes(event) + + if thumbnail_bytes: + nparr = np.frombuffer(thumbnail_bytes, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is not None: + height, width = img.shape[:2] + + crop_size = 1.0 + if event.data and "box" in event.data and "region" in event.data: + box = event.data["box"] + region = event.data["region"] + + if len(box) == 4 and len(region) == 4: + box_w, box_h = box[2], box[3] + region_w, region_h = region[2], region[3] + + box_area = (box_w * box_h) / (region_w * region_h) + + if box_area < 0.05: + crop_size = 0.4 + elif box_area < 0.10: + crop_size = 0.5 + elif box_area < 0.20: + crop_size = 0.65 + elif box_area < 0.35: + crop_size = 0.80 + else: + crop_size = 0.95 + + crop_width = int(width * crop_size) + crop_height = int(height * crop_size) + + x1 = (width - crop_width) // 2 + y1 = (height - crop_height) // 2 + x2 = x1 + crop_width + y2 = y1 + crop_height + + cropped = img[y1:y2, x1:x2] + resized = cv2.resize(cropped, (224, 224)) + output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg") + cv2.imwrite(output_path, resized) + thumbnail_paths.append(output_path) + + except Exception as e: + logger.debug(f"Failed to extract thumbnail for event {event.id}: {e}") + continue + + return thumbnail_paths diff --git a/sam2-cpu/frigate-dev/frigate/util/config.py b/sam2-cpu/frigate-dev/frigate/util/config.py new file mode 100644 index 0000000..c3d7963 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/config.py @@ -0,0 +1,528 @@ +"""configuration utils.""" + +import asyncio +import logging +import os +import shutil +from typing import Any, Optional, Union + +from ruamel.yaml import YAML + +from frigate.const import CONFIG_DIR, EXPORT_DIR +from frigate.util.services import get_video_properties + +logger = logging.getLogger(__name__) + +CURRENT_CONFIG_VERSION = "0.17-0" +DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") + + +def find_config_file() -> str: + config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE) + + if not os.path.isfile(config_path): + config_path = config_path.replace("yml", "yaml") + + return config_path + + +def migrate_frigate_config(config_file: str): + """handle migrating the frigate config.""" + logger.info("Checking if frigate config needs migration...") + + if not os.access(config_file, mode=os.W_OK): + logger.error("Config file is read-only, unable to migrate config file.") + return + + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + with open(config_file, "r") as f: + config: dict[str, dict[str, Any]] = yaml.load(f) + + if config is None: + logger.error(f"Failed to load config at {config_file}") + return + + previous_version = str(config.get("version", "0.13")) + + if previous_version == CURRENT_CONFIG_VERSION: + logger.info("frigate config does not need migration...") + return + + logger.info("copying config as backup...") + shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml")) + + if previous_version < "0.14": + logger.info(f"Migrating frigate config from {previous_version} to 0.14...") + new_config = migrate_014(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.14" + + logger.info("Migrating export file names...") + if os.path.isdir(EXPORT_DIR): + for file in os.listdir(EXPORT_DIR): + if "@" not in file: + continue + + new_name = file.replace("@", "_") + os.rename( + os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name) + ) + + if previous_version < "0.15-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...") + new_config = migrate_015_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.15-0" + + if previous_version < "0.15-1": + logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...") + new_config = migrate_015_1(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.15-1" + + if previous_version < "0.16-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...") + new_config = migrate_016_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.16-0" + + if previous_version < "0.17-0": + logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...") + new_config = migrate_017_0(config) + with open(config_file, "w") as f: + yaml.dump(new_config, f) + previous_version = "0.17-0" + + logger.info("Finished frigate config migration...") + + +def migrate_014(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.14""" + # migrate record.events.required_zones to review.alerts.required_zones + new_config = config.copy() + global_required_zones = ( + config.get("record", {}).get("events", {}).get("required_zones", []) + ) + + if global_required_zones: + # migrate to new review config + if not new_config.get("review"): + new_config["review"] = {} + + if not new_config["review"].get("alerts"): + new_config["review"]["alerts"] = {} + + if not new_config["review"]["alerts"].get("required_zones"): + new_config["review"]["alerts"]["required_zones"] = global_required_zones + + # remove record required zones config + del new_config["record"]["events"]["required_zones"] + + # remove record altogether if there is not other config + if not new_config["record"]["events"]: + del new_config["record"]["events"] + + if not new_config["record"]: + del new_config["record"] + + # Remove UI fields + if new_config.get("ui"): + if new_config["ui"].get("use_experimental"): + del new_config["ui"]["use_experimental"] + + if new_config["ui"].get("live_mode"): + del new_config["ui"]["live_mode"] + + if not new_config["ui"]: + del new_config["ui"] + + # remove rtmp + if new_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"): + del new_config["ffmpeg"]["output_args"]["rtmp"] + + if new_config.get("rtmp"): + del new_config["rtmp"] + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + required_zones = ( + camera_config.get("record", {}).get("events", {}).get("required_zones", []) + ) + + if required_zones: + # migrate to new review config + if not camera_config.get("review"): + camera_config["review"] = {} + + if not camera_config["review"].get("alerts"): + camera_config["review"]["alerts"] = {} + + if not camera_config["review"]["alerts"].get("required_zones"): + camera_config["review"]["alerts"]["required_zones"] = required_zones + + # remove record required zones config + del camera_config["record"]["events"]["required_zones"] + + # remove record altogether if there is not other config + if not camera_config["record"]["events"]: + del camera_config["record"]["events"] + + if not camera_config["record"]: + del camera_config["record"] + + # remove rtmp + if camera_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"): + del camera_config["ffmpeg"]["output_args"]["rtmp"] + + if camera_config.get("rtmp"): + del camera_config["rtmp"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.14" + return new_config + + +def migrate_015_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.15-0""" + new_config = config.copy() + + # migrate record.events to record.alerts and record.detections + global_record_events = config.get("record", {}).get("events") + if global_record_events: + alerts_retention = {"retain": {}} + detections_retention = {"retain": {}} + + if global_record_events.get("pre_capture"): + alerts_retention["pre_capture"] = global_record_events["pre_capture"] + + if global_record_events.get("post_capture"): + alerts_retention["post_capture"] = global_record_events["post_capture"] + + if global_record_events.get("retain", {}).get("default"): + alerts_retention["retain"]["days"] = global_record_events["retain"][ + "default" + ] + + # decide logical detections retention based on current detections config + if not config.get("review", {}).get("alerts", {}).get( + "required_zones" + ) or config.get("review", {}).get("detections"): + if global_record_events.get("pre_capture"): + detections_retention["pre_capture"] = global_record_events[ + "pre_capture" + ] + + if global_record_events.get("post_capture"): + detections_retention["post_capture"] = global_record_events[ + "post_capture" + ] + + if global_record_events.get("retain", {}).get("default"): + detections_retention["retain"]["days"] = global_record_events["retain"][ + "default" + ] + else: + continuous_days = config.get("record", {}).get("retain", {}).get("days") + detections_retention["retain"]["days"] = ( + continuous_days if continuous_days else 1 + ) + + new_config["record"]["alerts"] = alerts_retention + new_config["record"]["detections"] = detections_retention + + del new_config["record"]["events"] + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + record_events: dict[str, Any] = camera_config.get("record", {}).get("events") + + if record_events: + alerts_retention = {"retain": {}} + detections_retention = {"retain": {}} + + if record_events.get("pre_capture"): + alerts_retention["pre_capture"] = record_events["pre_capture"] + + if record_events.get("post_capture"): + alerts_retention["post_capture"] = record_events["post_capture"] + + if record_events.get("retain", {}).get("default"): + alerts_retention["retain"]["days"] = record_events["retain"]["default"] + + # decide logical detections retention based on current detections config + if not camera_config.get("review", {}).get("alerts", {}).get( + "required_zones" + ) or camera_config.get("review", {}).get("detections"): + if record_events.get("pre_capture"): + detections_retention["pre_capture"] = record_events["pre_capture"] + + if record_events.get("post_capture"): + detections_retention["post_capture"] = record_events["post_capture"] + + if record_events.get("retain", {}).get("default"): + detections_retention["retain"]["days"] = record_events["retain"][ + "default" + ] + else: + continuous_days = ( + camera_config.get("record", {}).get("retain", {}).get("days") + ) + detections_retention["retain"]["days"] = ( + continuous_days if continuous_days else 1 + ) + + camera_config["record"]["alerts"] = alerts_retention + camera_config["record"]["detections"] = detections_retention + del camera_config["record"]["events"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.15-0" + return new_config + + +def migrate_015_1(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.15-1""" + new_config = config.copy() + + for detector, detector_config in config.get("detectors", {}).items(): + path = detector_config.get("model", {}).get("path") + + if path: + new_config["detectors"][detector]["model_path"] = path + del new_config["detectors"][detector]["model"] + + new_config["version"] = "0.15-1" + return new_config + + +def migrate_016_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.16-0""" + new_config = config.copy() + + # migrate config that does not have detect -> enabled explicitly set to have it enabled + if new_config.get("detect", {}).get("enabled") is None: + detect_config = new_config.get("detect", {}) + detect_config["enabled"] = True + new_config["detect"] = detect_config + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + + live_config = camera_config.get("live", {}) + if "stream_name" in live_config: + # Migrate from live -> stream_name to live -> streams -> dict + stream_name = live_config["stream_name"] + live_config["streams"] = {stream_name: stream_name} + + del live_config["stream_name"] + + camera_config["live"] = live_config + + # add another value to movement_weights for autotracking cams + onvif_config = camera_config.get("onvif", {}) + if "autotracking" in onvif_config: + movement_weights = ( + camera_config.get("onvif", {}) + .get("autotracking") + .get("movement_weights", {}) + ) + + if movement_weights and len(movement_weights.split(",")) == 5: + onvif_config["autotracking"]["movement_weights"] = ( + movement_weights + ", 0" + ) + camera_config["onvif"] = onvif_config + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.16-0" + return new_config + + +def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Handle migrating frigate config to 0.17-0""" + new_config = config.copy() + + # migrate global to new recording configuration + global_record_retain = config.get("record", {}).get("retain") + + if global_record_retain: + continuous = {"days": 0} + motion = {"days": 0} + days = global_record_retain.get("days") + mode = global_record_retain.get("mode", "all") + + if days: + if mode == "all": + continuous["days"] = days + + # if a user was keeping all for number of days + # we need to keep motion and all for that number of days + motion["days"] = days + else: + motion["days"] = days + + new_config["record"]["continuous"] = continuous + new_config["record"]["motion"] = motion + + del new_config["record"]["retain"] + + # migrate global genai to new objects config + global_genai = config.get("genai", {}) + + if global_genai: + new_genai_config = {} + new_object_config = new_config.get("objects", {}) + new_object_config["genai"] = {} + + for key in global_genai.keys(): + if key in ["model", "provider", "base_url", "api_key"]: + new_genai_config[key] = global_genai[key] + else: + new_object_config["genai"][key] = global_genai[key] + + new_config["genai"] = new_genai_config + new_config["objects"] = new_object_config + + for name, camera in config.get("cameras", {}).items(): + camera_config: dict[str, dict[str, Any]] = camera.copy() + camera_record_retain = camera_config.get("record", {}).get("retain") + + if camera_record_retain: + continuous = {"days": 0} + motion = {"days": 0} + days = camera_record_retain.get("days") + mode = camera_record_retain.get("mode", "all") + + if days: + if mode == "all": + continuous["days"] = days + else: + motion["days"] = days + + camera_config["record"]["continuous"] = continuous + camera_config["record"]["motion"] = motion + + del camera_config["record"]["retain"] + + camera_genai = camera_config.get("genai", {}) + + if camera_genai: + camera_object_config = camera_config.get("objects", {}) + camera_object_config["genai"] = camera_genai + camera_config["objects"] = camera_object_config + del camera_config["genai"] + + new_config["cameras"][name] = camera_config + + new_config["version"] = "0.17-0" + return new_config + + +def get_relative_coordinates( + mask: Optional[Union[str, list]], frame_shape: tuple[int, int] +) -> Union[str, list]: + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + if mask: + if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")): + relative_masks = [] + for m in mask: + points = m.split(",") + + if any(x > "1.0" for x in points): + rel_points = [] + for i in range(0, len(points), 2): + x = int(points[i]) + y = int(points[i + 1]) + + if x > frame_shape[1] or y > frame_shape[0]: + logger.error( + f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask." + ) + continue + + rel_points.append( + f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}" + ) + + relative_masks.append(",".join(rel_points)) + else: + relative_masks.append(m) + + mask = relative_masks + elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")): + points = mask.split(",") + rel_points = [] + + for i in range(0, len(points), 2): + x = int(points[i]) + y = int(points[i + 1]) + + if x > frame_shape[1] or y > frame_shape[0]: + logger.error( + f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask." + ) + return [] + + rel_points.append( + f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}" + ) + + mask = ",".join(rel_points) + + return mask + + return mask + + +def convert_area_to_pixels( + area_value: Union[int, float], frame_shape: tuple[int, int] +) -> int: + """ + Convert area specification to pixels. + + Args: + area_value: Area value (pixels or percentage) + frame_shape: Tuple of (height, width) for the frame + + Returns: + Area in pixels + """ + # If already an integer, assume it's in pixels + if isinstance(area_value, int): + return area_value + + # Check if it's a percentage + if isinstance(area_value, float): + if 0.000001 <= area_value <= 0.99: + frame_area = frame_shape[0] * frame_shape[1] + return max(1, int(frame_area * area_value)) + else: + raise ValueError( + f"Percentage must be between 0.000001 and 0.99, got {area_value}" + ) + + raise TypeError(f"Unexpected type for area: {type(area_value)}") + + +class StreamInfoRetriever: + def __init__(self) -> None: + self.stream_cache: dict[str, tuple[int, int]] = {} + + def get_stream_info(self, ffmpeg, path: str) -> str: + if path in self.stream_cache: + return self.stream_cache[path] + + info = asyncio.run(get_video_properties(ffmpeg, path)) + self.stream_cache[path] = info + return info diff --git a/sam2-cpu/frigate-dev/frigate/util/downloader.py b/sam2-cpu/frigate-dev/frigate/util/downloader.py new file mode 100644 index 0000000..ee80b38 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/downloader.py @@ -0,0 +1,120 @@ +import logging +import os +import threading +from pathlib import Path +from typing import Callable, List + +import requests + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.const import UPDATE_MODEL_STATE +from frigate.types import ModelStatusTypesEnum +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + + +class ModelDownloader: + def __init__( + self, + model_name: str, + download_path: str, + file_names: List[str], + download_func: Callable[[str], None], + complete_func: Callable[[], None] | None = None, + silent: bool = False, + ): + self.model_name = model_name + self.download_path = download_path + self.file_names = file_names + self.download_func = download_func + self.complete_func = complete_func + self.silent = silent + self.requestor = InterProcessRequestor() + self.download_thread = None + self.download_complete = threading.Event() + + def ensure_model_files(self): + self.mark_files_state( + self.requestor, + self.model_name, + self.file_names, + ModelStatusTypesEnum.downloading, + ) + self.download_thread = threading.Thread( + target=self._download_models, + name=f"_download_model_{self.model_name}", + daemon=True, + ) + self.download_thread.start() + + def _download_models(self): + for file_name in self.file_names: + path = os.path.join(self.download_path, file_name) + lock_path = f"{path}.lock" + lock = FileLock(lock_path, cleanup_stale_on_init=True) + + if not os.path.exists(path): + with lock: + if not os.path.exists(path): + self.download_func(path) + + self.requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{self.model_name}-{file_name}", + "state": ModelStatusTypesEnum.downloaded, + }, + ) + + if self.complete_func: + self.complete_func() + + self.requestor.stop() + self.download_complete.set() + + @staticmethod + def download_from_url(url: str, save_path: str, silent: bool = False) -> Path: + temporary_filename = Path(save_path).with_name( + os.path.basename(save_path) + ".part" + ) + temporary_filename.parent.mkdir(parents=True, exist_ok=True) + + if not silent: + logger.info(f"Downloading model file from: {url}") + + try: + with requests.get(url, stream=True, allow_redirects=True) as r: + r.raise_for_status() + with open(temporary_filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + temporary_filename.rename(save_path) + except Exception as e: + logger.error(f"Error downloading model: {str(e)}") + raise + + if not silent: + logger.info(f"Downloading complete: {url}") + + return Path(save_path) + + @staticmethod + def mark_files_state( + requestor: InterProcessRequestor, + model_name: str, + files: list[str], + state: ModelStatusTypesEnum, + ) -> None: + for file_name in files: + requestor.send_data( + UPDATE_MODEL_STATE, + { + "model": f"{model_name}-{file_name}", + "state": state, + }, + ) + + def wait_for_download(self): + self.download_complete.wait() diff --git a/sam2-cpu/frigate-dev/frigate/util/file.py b/sam2-cpu/frigate-dev/frigate/util/file.py new file mode 100644 index 0000000..22be3e5 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/file.py @@ -0,0 +1,276 @@ +"""Path and file utilities.""" + +import base64 +import fcntl +import logging +import os +import time +from pathlib import Path +from typing import Optional + +import cv2 +from numpy import ndarray + +from frigate.const import CLIPS_DIR, THUMB_DIR +from frigate.models import Event + +logger = logging.getLogger(__name__) + + +def get_event_thumbnail_bytes(event: Event) -> bytes | None: + if event.thumbnail: + return base64.b64decode(event.thumbnail) + else: + try: + with open( + os.path.join(THUMB_DIR, event.camera, f"{event.id}.webp"), "rb" + ) as f: + return f.read() + except Exception: + return None + + +def get_event_snapshot(event: Event) -> ndarray: + media_name = f"{event.camera}-{event.id}" + return cv2.imread(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") + + +### Deletion + + +def delete_event_images(event: Event) -> bool: + return delete_event_snapshot(event) and delete_event_thumbnail(event) + + +def delete_event_snapshot(event: Event) -> bool: + media_name = f"{event.camera}-{event.id}" + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") + + try: + media_path.unlink(missing_ok=True) + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.webp") + media_path.unlink(missing_ok=True) + # also delete clean.png (legacy) for backward compatibility + media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png") + media_path.unlink(missing_ok=True) + return True + except OSError: + return False + + +def delete_event_thumbnail(event: Event) -> bool: + if event.thumbnail: + return True + else: + Path(os.path.join(THUMB_DIR, event.camera, f"{event.id}.webp")).unlink( + missing_ok=True + ) + return True + + +### File Locking + + +class FileLock: + """ + A file-based lock for coordinating access to resources across processes. + + Uses fcntl.flock() for proper POSIX file locking on Linux. Supports timeouts, + stale lock detection, and can be used as a context manager. + + Example: + ```python + # Using as a context manager (recommended) + with FileLock("/path/to/resource.lock", timeout=60): + # Critical section + do_something() + + # Manual acquisition and release + lock = FileLock("/path/to/resource.lock") + if lock.acquire(timeout=60): + try: + do_something() + finally: + lock.release() + ``` + + Attributes: + lock_path: Path to the lock file + timeout: Maximum time to wait for lock acquisition (seconds) + poll_interval: Time to wait between lock acquisition attempts (seconds) + stale_timeout: Time after which a lock is considered stale (seconds) + """ + + def __init__( + self, + lock_path: str | Path, + timeout: int = 300, + poll_interval: float = 1.0, + stale_timeout: int = 600, + cleanup_stale_on_init: bool = False, + ): + """ + Initialize a FileLock. + + Args: + lock_path: Path to the lock file + timeout: Maximum time to wait for lock acquisition in seconds (default: 300) + poll_interval: Time to wait between lock attempts in seconds (default: 1.0) + stale_timeout: Time after which a lock is considered stale in seconds (default: 600) + cleanup_stale_on_init: Whether to clean up stale locks on initialization (default: False) + """ + self.lock_path = Path(lock_path) + self.timeout = timeout + self.poll_interval = poll_interval + self.stale_timeout = stale_timeout + self._fd: Optional[int] = None + self._acquired = False + + if cleanup_stale_on_init: + self._cleanup_stale_lock() + + def _cleanup_stale_lock(self) -> bool: + """ + Clean up a stale lock file if it exists and is old. + + Returns: + True if lock was cleaned up, False otherwise + """ + try: + if self.lock_path.exists(): + # Check if lock file is older than stale_timeout + lock_age = time.time() - self.lock_path.stat().st_mtime + if lock_age > self.stale_timeout: + logger.warning( + f"Removing stale lock file: {self.lock_path} (age: {lock_age:.1f}s)" + ) + self.lock_path.unlink() + return True + except Exception as e: + logger.error(f"Error cleaning up stale lock: {e}") + + return False + + def is_stale(self) -> bool: + """ + Check if the lock file is stale (older than stale_timeout). + + Returns: + True if lock is stale, False otherwise + """ + try: + if self.lock_path.exists(): + lock_age = time.time() - self.lock_path.stat().st_mtime + return lock_age > self.stale_timeout + except Exception: + pass + + return False + + def acquire(self, timeout: Optional[int] = None) -> bool: + """ + Acquire the file lock using fcntl.flock(). + + Args: + timeout: Maximum time to wait for lock in seconds (uses instance timeout if None) + + Returns: + True if lock acquired, False if timeout or error + """ + if self._acquired: + logger.warning(f"Lock already acquired: {self.lock_path}") + return True + + if timeout is None: + timeout = self.timeout + + # Ensure parent directory exists + self.lock_path.parent.mkdir(parents=True, exist_ok=True) + + # Clean up stale lock before attempting to acquire + self._cleanup_stale_lock() + + try: + self._fd = os.open(self.lock_path, os.O_CREAT | os.O_RDWR) + + start_time = time.time() + while time.time() - start_time < timeout: + try: + fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + self._acquired = True + logger.debug(f"Acquired lock: {self.lock_path}") + return True + except (OSError, IOError): + # Lock is held by another process + if time.time() - start_time >= timeout: + logger.warning(f"Timeout waiting for lock: {self.lock_path}") + os.close(self._fd) + self._fd = None + return False + + time.sleep(self.poll_interval) + + # Timeout reached + if self._fd is not None: + os.close(self._fd) + self._fd = None + return False + + except Exception as e: + logger.error(f"Error acquiring lock: {e}") + if self._fd is not None: + try: + os.close(self._fd) + except Exception: + pass + self._fd = None + return False + + def release(self) -> None: + """ + Release the file lock. + + This closes the file descriptor and removes the lock file. + """ + if not self._acquired: + return + + try: + # Close file descriptor and release fcntl lock + if self._fd is not None: + try: + fcntl.flock(self._fd, fcntl.LOCK_UN) + os.close(self._fd) + except Exception as e: + logger.warning(f"Error closing lock file descriptor: {e}") + finally: + self._fd = None + + # Remove lock file + if self.lock_path.exists(): + self.lock_path.unlink() + logger.debug(f"Released lock: {self.lock_path}") + + except FileNotFoundError: + # Lock file already removed, that's fine + pass + except Exception as e: + logger.error(f"Error releasing lock: {e}") + finally: + self._acquired = False + + def __enter__(self): + """Context manager entry - acquire the lock.""" + if not self.acquire(): + raise TimeoutError(f"Failed to acquire lock: {self.lock_path}") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - release the lock.""" + self.release() + return False + + def __del__(self): + """Destructor - ensure lock is released.""" + if self._acquired: + self.release() diff --git a/sam2-cpu/frigate-dev/frigate/util/image.py b/sam2-cpu/frigate-dev/frigate/util/image.py new file mode 100644 index 0000000..ea9fb0a --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/image.py @@ -0,0 +1,1059 @@ +"""Utilities for creating and manipulating image frames.""" + +import datetime +import logging +import subprocess as sp +import threading +from abc import ABC, abstractmethod +from multiprocessing import resource_tracker as _mprt +from multiprocessing import shared_memory as _mpshm +from string import printable +from typing import Any, AnyStr, Optional + +import cv2 +import numpy as np +from unidecode import unidecode + +logger = logging.getLogger(__name__) + + +def transliterate_to_latin(text: str) -> str: + """ + Transliterate a given text to Latin. + + This function uses the unidecode library to transliterate the input text to Latin. + It is useful for converting texts with diacritics or non-Latin characters to a + Latin equivalent. + + Args: + text (str): The text to be transliterated. + + Returns: + str: The transliterated text. + + Example: + >>> transliterate_to_latin('frégate') + 'fregate' + """ + return unidecode(text) + + +def on_edge(box, frame_shape): + if ( + box[0] == 0 + or box[1] == 0 + or box[2] == frame_shape[1] - 1 + or box[3] == frame_shape[0] - 1 + ): + return True + + +def has_better_attr(current_thumb, new_obj, attr_label) -> bool: + max_new_attr = max( + [0] + + [area(a["box"]) for a in new_obj["attributes"] if a["label"] == attr_label] + ) + max_current_attr = max( + [0] + + [ + area(a["box"]) + for a in current_thumb["attributes"] + if a["label"] == attr_label + ] + ) + + # if the thumb has a higher scoring attr + return max_new_attr > max_current_attr + + +def is_better_thumbnail( + label: str, + current_thumb: dict[str, Any], + new_obj: dict[str, Any], + frame_shape: tuple[int, int], +) -> bool: + # larger is better + # cutoff images are less ideal, but they should also be smaller? + # better scores are obviously better too + + # check face on person + if label == "person": + if has_better_attr(current_thumb, new_obj, "face"): + return True + # if the current thumb has a face attr, dont update unless it gets better + if any([a["label"] == "face" for a in current_thumb["attributes"]]): + return False + + # check license_plate on car + if label in ["car", "motorcycle"]: + if has_better_attr(current_thumb, new_obj, "license_plate"): + return True + # if the current thumb has a license_plate attr, dont update unless it gets better + if any([a["label"] == "license_plate" for a in current_thumb["attributes"]]): + return False + + # if the new_thumb is on an edge, and the current thumb is not + if on_edge(new_obj["box"], frame_shape) and not on_edge( + current_thumb["box"], frame_shape + ): + return False + + # if the score is better by more than 5% + if new_obj["score"] > current_thumb["score"] + 0.05: + return True + + # if the area is 10% larger + if new_obj["area"] > current_thumb["area"] * 1.1: + return True + + return False + + +def draw_timestamp( + frame, + timestamp, + timestamp_format, + font_effect=None, + font_thickness=2, + font_color=(255, 255, 255), + position="tl", +): + time_to_show = datetime.datetime.fromtimestamp(timestamp).strftime(timestamp_format) + + # calculate a dynamic font size + size = cv2.getTextSize( + time_to_show, + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=1.0, + thickness=font_thickness, + ) + + text_width = size[0][0] + desired_size = max(150, 0.33 * frame.shape[1]) + font_scale = desired_size / text_width + + # calculate the actual size with the dynamic scale + size = cv2.getTextSize( + time_to_show, + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + thickness=font_thickness, + ) + + image_width = frame.shape[1] + image_height = frame.shape[0] + text_width = size[0][0] + text_height = size[0][1] + line_height = text_height + size[1] + + if position == "tl": + text_offset_x = 0 + text_offset_y = 0 if 0 < line_height else 0 - (line_height + 8) + elif position == "tr": + text_offset_x = image_width - text_width + text_offset_y = 0 if 0 < line_height else 0 - (line_height + 8) + elif position == "bl": + text_offset_x = 0 + text_offset_y = image_height - (line_height + 8) + elif position == "br": + text_offset_x = image_width - text_width + text_offset_y = image_height - (line_height + 8) + + if font_effect == "solid": + # make the coords of the box with a small padding of two pixels + timestamp_box_coords = np.array( + [ + [text_offset_x, text_offset_y], + [text_offset_x + text_width, text_offset_y], + [text_offset_x + text_width, text_offset_y + line_height + 8], + [text_offset_x, text_offset_y + line_height + 8], + ] + ) + + cv2.fillPoly( + frame, + [timestamp_box_coords], + # inverse color of text for background for max. contrast + (255 - font_color[0], 255 - font_color[1], 255 - font_color[2]), + ) + elif font_effect == "shadow": + cv2.putText( + frame, + time_to_show, + (text_offset_x + 3, text_offset_y + line_height), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=(255 - font_color[0], 255 - font_color[1], 255 - font_color[2]), + thickness=font_thickness, + ) + + cv2.putText( + frame, + time_to_show, + (text_offset_x, text_offset_y + line_height - 3), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_scale, + color=font_color, + thickness=font_thickness, + ) + + +def draw_box_with_label( + frame, + x_min, + y_min, + x_max, + y_max, + label, + info, + thickness=2, + color=None, + position="ul", +): + if color is None: + color = (0, 0, 255) + try: + display_text = transliterate_to_latin("{}: {}".format(label, info)) + except Exception: + display_text = "{}: {}".format(label, info) + cv2.rectangle(frame, (x_min, y_min), (x_max, y_max), color, thickness) + font_scale = 0.5 + font = cv2.FONT_HERSHEY_SIMPLEX + # get the width and height of the text box + size = cv2.getTextSize(display_text, font, fontScale=font_scale, thickness=2) + text_width = size[0][0] + text_height = size[0][1] + line_height = text_height + size[1] + # get frame height + frame_height = frame.shape[0] + # set the text start position + if position == "ul": + text_offset_x = x_min + text_offset_y = max(0, y_min - (line_height + 8)) + elif position == "ur": + text_offset_x = max(0, x_max - (text_width + 8)) + text_offset_y = max(0, y_min - (line_height + 8)) + elif position == "bl": + text_offset_x = x_min + text_offset_y = min(frame_height - line_height, y_max) + elif position == "br": + text_offset_x = max(0, x_max - (text_width + 8)) + text_offset_y = min(frame_height - line_height, y_max) + # Adjust position if it overlaps with the box or goes out of frame + if position in {"ul", "ur"}: + if text_offset_y < y_min + thickness: # Label overlaps with the box + if y_min - (line_height + 8) < 0 and y_max + line_height <= frame_height: + # Not enough space above, and there is space below + text_offset_y = y_max + elif y_min - (line_height + 8) >= 0: + # Enough space above, keep the label at the top + text_offset_y = max(0, y_min - (line_height + 8)) + elif position in {"bl", "br"}: + if text_offset_y + line_height > frame_height: + # If there's not enough space below, try above the box + text_offset_y = max(0, y_min - (line_height + 8)) + + # make the coords of the box with a small padding of two pixels + textbox_coords = ( + (text_offset_x, text_offset_y), + (text_offset_x + text_width + 2, text_offset_y + line_height), + ) + cv2.rectangle(frame, textbox_coords[0], textbox_coords[1], color, cv2.FILLED) + cv2.putText( + frame, + display_text, + (text_offset_x, text_offset_y + line_height - 3), + font, + fontScale=font_scale, + color=(0, 0, 0), + thickness=2, + ) + + +def grab_cv2_contours(cnts): + # if the length the contours tuple returned by cv2.findContours + # is '2' then we are using either OpenCV v2.4, v4-beta, or + # v4-official + if len(cnts) == 2: + return cnts[0] + + # if the length of the contours tuple is '3' then we are using + # either OpenCV v3, v4-pre, or v4-alpha + elif len(cnts) == 3: + return cnts[1] + + +def is_label_printable(label) -> bool: + """Check if label is printable.""" + return not bool(set(label) - set(printable)) + + +def calculate_region(frame_shape, xmin, ymin, xmax, ymax, model_size, multiplier=2): + # size is the longest edge and divisible by 4 + size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4) + # dont go any smaller than the model_size + if size < model_size: + size = model_size + + # x_offset is midpoint of bounding box minus half the size + x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0) + # if outside the image + if x_offset < 0: + x_offset = 0 + elif x_offset > (frame_shape[1] - size): + x_offset = max(0, (frame_shape[1] - size)) + + # y_offset is midpoint of bounding box minus half the size + y_offset = int((ymax - ymin) / 2.0 + ymin - size / 2.0) + # # if outside the image + if y_offset < 0: + y_offset = 0 + elif y_offset > (frame_shape[0] - size): + y_offset = max(0, (frame_shape[0] - size)) + + return (x_offset, y_offset, x_offset + size, y_offset + size) + + +def calculate_16_9_crop(frame_shape, xmin, ymin, xmax, ymax, multiplier=1.25): + min_size = 200 + + # size is the longest edge and divisible by 4 + x_size = int((xmax - xmin) * multiplier) + + if x_size < min_size: + x_size = min_size + + y_size = int((ymax - ymin) * multiplier) + + if y_size < min_size: + y_size = min_size + + if frame_shape[1] / frame_shape[0] > 16 / 9 and x_size / y_size > 4: + return None + + # calculate 16x9 using height + aspect_y_size = int(9 / 16 * x_size) + + # if 16:9 by height is too small + if aspect_y_size < y_size or aspect_y_size > frame_shape[0]: + x_size = int((16 / 9) * y_size) // 4 * 4 + + if x_size / y_size > 1.8: + return None + else: + y_size = aspect_y_size // 4 * 4 + + # x_offset is midpoint of bounding box minus half the size + x_offset = int((xmax - xmin) / 2.0 + xmin - x_size / 2.0) + # if outside the image + if x_offset < 0: + x_offset = 0 + elif x_offset > (frame_shape[1] - x_size): + x_offset = max(0, (frame_shape[1] - x_size)) + + # y_offset is midpoint of bounding box minus half the size + y_offset = int((ymax - ymin) / 2.0 + ymin - y_size / 2.0) + # # if outside the image + if y_offset < 0: + y_offset = 0 + elif y_offset > (frame_shape[0] - y_size): + y_offset = max(0, (frame_shape[0] - y_size)) + + return (x_offset, y_offset, x_offset + x_size, y_offset + y_size) + + +def get_yuv_crop(frame_shape, crop): + # crop should be (x1,y1,x2,y2) + frame_height = frame_shape[0] // 3 * 2 + frame_width = frame_shape[1] + + # compute the width/height of the uv channels + uv_width = frame_width // 2 # width of the uv channels + uv_height = frame_height // 4 # height of the uv channels + + # compute the offset for upper left corner of the uv channels + uv_x_offset = crop[0] // 2 # x offset of the uv channels + uv_y_offset = crop[1] // 4 # y offset of the uv channels + + # compute the width/height of the uv crops + uv_crop_width = (crop[2] - crop[0]) // 2 # width of the cropped uv channels + uv_crop_height = (crop[3] - crop[1]) // 4 # height of the cropped uv channels + + # ensure crop dimensions are multiples of 2 and 4 + y = (crop[0], crop[1], crop[0] + uv_crop_width * 2, crop[1] + uv_crop_height * 4) + + u1 = ( + 0 + uv_x_offset, + frame_height + uv_y_offset, + 0 + uv_x_offset + uv_crop_width, + frame_height + uv_y_offset + uv_crop_height, + ) + + u2 = ( + uv_width + uv_x_offset, + frame_height + uv_y_offset, + uv_width + uv_x_offset + uv_crop_width, + frame_height + uv_y_offset + uv_crop_height, + ) + + v1 = ( + 0 + uv_x_offset, + frame_height + uv_height + uv_y_offset, + 0 + uv_x_offset + uv_crop_width, + frame_height + uv_height + uv_y_offset + uv_crop_height, + ) + + v2 = ( + uv_width + uv_x_offset, + frame_height + uv_height + uv_y_offset, + uv_width + uv_x_offset + uv_crop_width, + frame_height + uv_height + uv_y_offset + uv_crop_height, + ) + + return y, u1, u2, v1, v2 + + +def yuv_crop_and_resize(frame, region, height=None): + # Crops and resizes a YUV frame while maintaining aspect ratio + # https://stackoverflow.com/a/57022634 + height = frame.shape[0] // 3 * 2 + width = frame.shape[1] + + # get the crop box if the region extends beyond the frame + crop_x1 = max(0, region[0]) + crop_y1 = max(0, region[1]) + # ensure these are a multiple of 4 + crop_x2 = min(width, region[2]) + crop_y2 = min(height, region[3]) + crop_box = (crop_x1, crop_y1, crop_x2, crop_y2) + + y, u1, u2, v1, v2 = get_yuv_crop(frame.shape, crop_box) + + # if the region starts outside the frame, indent the start point in the cropped frame + y_channel_x_offset = abs(min(0, region[0])) + y_channel_y_offset = abs(min(0, region[1])) + + uv_channel_x_offset = y_channel_x_offset // 2 + uv_channel_y_offset = y_channel_y_offset // 4 + + # create the yuv region frame + # make sure the size is a multiple of 4 + # TODO: this should be based on the size after resize now + size = (region[3] - region[1]) // 4 * 4 + yuv_cropped_frame = np.zeros((size + size // 2, size), np.uint8) + # fill in black + yuv_cropped_frame[:] = 128 + yuv_cropped_frame[0:size, 0:size] = 16 + + # copy the y channel + yuv_cropped_frame[ + y_channel_y_offset : y_channel_y_offset + y[3] - y[1], + y_channel_x_offset : y_channel_x_offset + y[2] - y[0], + ] = frame[y[1] : y[3], y[0] : y[2]] + + uv_crop_width = u1[2] - u1[0] + uv_crop_height = u1[3] - u1[1] + + # copy u1 + yuv_cropped_frame[ + size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, + 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width, + ] = frame[u1[1] : u1[3], u1[0] : u1[2]] + + # copy u2 + yuv_cropped_frame[ + size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, + size // 2 + uv_channel_x_offset : size // 2 + + uv_channel_x_offset + + uv_crop_width, + ] = frame[u2[1] : u2[3], u2[0] : u2[2]] + + # copy v1 + yuv_cropped_frame[ + size + size // 4 + uv_channel_y_offset : size + + size // 4 + + uv_channel_y_offset + + uv_crop_height, + 0 + uv_channel_x_offset : 0 + uv_channel_x_offset + uv_crop_width, + ] = frame[v1[1] : v1[3], v1[0] : v1[2]] + + # copy v2 + yuv_cropped_frame[ + size + size // 4 + uv_channel_y_offset : size + + size // 4 + + uv_channel_y_offset + + uv_crop_height, + size // 2 + uv_channel_x_offset : size // 2 + + uv_channel_x_offset + + uv_crop_width, + ] = frame[v2[1] : v2[3], v2[0] : v2[2]] + + return yuv_cropped_frame + + +def yuv_to_3_channel_yuv(yuv_frame): + height = yuv_frame.shape[0] // 3 * 2 + width = yuv_frame.shape[1] + + # flatten the image into array + yuv_data = yuv_frame.ravel() + + # create a numpy array to hold all the 3 channel yuv data + all_yuv_data = np.empty((height, width, 3), dtype=np.uint8) + + y_count = height * width + uv_count = y_count // 4 + + # copy the y_channel + all_yuv_data[:, :, 0] = yuv_data[0:y_count].reshape((height, width)) + # copy the u channel doubling each dimension + all_yuv_data[:, :, 1] = np.repeat( + np.reshape( + np.repeat(yuv_data[y_count : y_count + uv_count], repeats=2, axis=0), + (height // 2, width), + ), + repeats=2, + axis=0, + ) + # copy the v channel doubling each dimension + all_yuv_data[:, :, 2] = np.repeat( + np.reshape( + np.repeat( + yuv_data[y_count + uv_count : y_count + uv_count + uv_count], + repeats=2, + axis=0, + ), + (height // 2, width), + ), + repeats=2, + axis=0, + ) + + return all_yuv_data + + +def copy_yuv_to_position( + destination_frame, + destination_offset, + destination_shape, + source_frame=None, + source_channel_dim=None, + interpolation=cv2.INTER_LINEAR, +): + # get the coordinates of the channels for this position in the layout + y, u1, u2, v1, v2 = get_yuv_crop( + destination_frame.shape, + ( + destination_offset[1], + destination_offset[0], + destination_offset[1] + destination_shape[1], + destination_offset[0] + destination_shape[0], + ), + ) + + # clear y + destination_frame[ + y[1] : y[3], + y[0] : y[2], + ] = 16 + + # clear u1 + destination_frame[u1[1] : u1[3], u1[0] : u1[2]] = 128 + # clear u2 + destination_frame[u2[1] : u2[3], u2[0] : u2[2]] = 128 + # clear v1 + destination_frame[v1[1] : v1[3], v1[0] : v1[2]] = 128 + # clear v2 + destination_frame[v2[1] : v2[3], v2[0] : v2[2]] = 128 + + if source_frame is not None: + # calculate the resized frame, maintaining the aspect ratio + source_aspect_ratio = source_frame.shape[1] / (source_frame.shape[0] // 3 * 2) + dest_aspect_ratio = destination_shape[1] / destination_shape[0] + + if source_aspect_ratio <= dest_aspect_ratio: + y_resize_height = int(destination_shape[0] // 4 * 4) + y_resize_width = int((y_resize_height * source_aspect_ratio) // 4 * 4) + else: + y_resize_width = int(destination_shape[1] // 4 * 4) + y_resize_height = int((y_resize_width / source_aspect_ratio) // 4 * 4) + + uv_resize_width = int(y_resize_width // 2) + uv_resize_height = int(y_resize_height // 4) + + y_y_offset = int((destination_shape[0] - y_resize_height) / 4 // 4 * 4) + y_x_offset = int((destination_shape[1] - y_resize_width) / 2 // 4 * 4) + + uv_y_offset = y_y_offset // 4 + uv_x_offset = y_x_offset // 2 + + # resize/copy y channel + destination_frame[ + y[1] + y_y_offset : y[1] + y_y_offset + y_resize_height, + y[0] + y_x_offset : y[0] + y_x_offset + y_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["y"][1] : source_channel_dim["y"][3], + source_channel_dim["y"][0] : source_channel_dim["y"][2], + ], + dsize=(y_resize_width, y_resize_height), + interpolation=interpolation, + ) + + # resize/copy u1 + destination_frame[ + u1[1] + uv_y_offset : u1[1] + uv_y_offset + uv_resize_height, + u1[0] + uv_x_offset : u1[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["u1"][1] : source_channel_dim["u1"][3], + source_channel_dim["u1"][0] : source_channel_dim["u1"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy u2 + destination_frame[ + u2[1] + uv_y_offset : u2[1] + uv_y_offset + uv_resize_height, + u2[0] + uv_x_offset : u2[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["u2"][1] : source_channel_dim["u2"][3], + source_channel_dim["u2"][0] : source_channel_dim["u2"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy v1 + destination_frame[ + v1[1] + uv_y_offset : v1[1] + uv_y_offset + uv_resize_height, + v1[0] + uv_x_offset : v1[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["v1"][1] : source_channel_dim["v1"][3], + source_channel_dim["v1"][0] : source_channel_dim["v1"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + # resize/copy v2 + destination_frame[ + v2[1] + uv_y_offset : v2[1] + uv_y_offset + uv_resize_height, + v2[0] + uv_x_offset : v2[0] + uv_x_offset + uv_resize_width, + ] = cv2.resize( + source_frame[ + source_channel_dim["v2"][1] : source_channel_dim["v2"][3], + source_channel_dim["v2"][0] : source_channel_dim["v2"][2], + ], + dsize=(uv_resize_width, uv_resize_height), + interpolation=interpolation, + ) + + +def get_blank_yuv_frame(width: int, height: int) -> np.ndarray: + """Creates a black YUV 4:2:0 frame.""" + yuv_height = height * 3 // 2 + yuv_frame = np.zeros((yuv_height, width), dtype=np.uint8) + + uv_height = height // 2 + + # The U and V planes are stored after the Y plane. + u_start = height # U plane starts right after Y plane + v_start = u_start + uv_height // 2 # V plane starts after U plane + yuv_frame[u_start : u_start + uv_height, :width] = 128 + yuv_frame[v_start : v_start + uv_height, :width] = 128 + + return yuv_frame + + +def yuv_region_2_yuv(frame, region): + try: + # TODO: does this copy the numpy array? + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return yuv_to_3_channel_yuv(yuv_cropped_frame) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def yuv_region_2_rgb(frame, region): + try: + # TODO: does this copy the numpy array? + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2RGB_I420) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def yuv_region_2_bgr(frame, region): + try: + yuv_cropped_frame = yuv_crop_and_resize(frame, region) + return cv2.cvtColor(yuv_cropped_frame, cv2.COLOR_YUV2BGR_I420) + except: + print(f"frame.shape: {frame.shape}") + print(f"region: {region}") + raise + + +def intersection(box_a, box_b) -> Optional[list[int]]: + """Return intersection box or None if boxes do not intersect.""" + if ( + box_a[2] < box_b[0] + or box_a[0] > box_b[2] + or box_a[1] > box_b[3] + or box_a[3] < box_b[1] + ): + return None + + return ( + max(box_a[0], box_b[0]), + max(box_a[1], box_b[1]), + min(box_a[2], box_b[2]), + min(box_a[3], box_b[3]), + ) + + +def area(box): + return (box[2] - box[0] + 1) * (box[3] - box[1] + 1) + + +def intersection_over_union(box_a, box_b): + # determine the (x, y)-coordinates of the intersection rectangle + intersect = intersection(box_a, box_b) + + if intersect is None: + return 0.0 + + # compute the area of intersection rectangle + inter_area = max(0, intersect[2] - intersect[0] + 1) * max( + 0, intersect[3] - intersect[1] + 1 + ) + + if inter_area == 0: + return 0.0 + + # compute the area of both the prediction and ground-truth + # rectangles + box_a_area = (box_a[2] - box_a[0] + 1) * (box_a[3] - box_a[1] + 1) + box_b_area = (box_b[2] - box_b[0] + 1) * (box_b[3] - box_b[1] + 1) + + # compute the intersection over union by taking the intersection + # area and dividing it by the sum of prediction + ground-truth + # areas - the intersection area + iou = inter_area / float(box_a_area + box_b_area - inter_area) + + # return the intersection over union value + return iou + + +def clipped(obj, frame_shape): + # if the object is within 5 pixels of the region border, and the region is not on the edge + # consider the object to be clipped + box = obj[2] + region = obj[5] + if ( + (region[0] > 5 and box[0] - region[0] <= 5) + or (region[1] > 5 and box[1] - region[1] <= 5) + or (frame_shape[1] - region[2] > 5 and region[2] - box[2] <= 5) + or (frame_shape[0] - region[3] > 5 and region[3] - box[3] <= 5) + ): + return True + else: + return False + + +class FrameManager(ABC): + @abstractmethod + def create(self, name: str, size: int) -> AnyStr: + pass + + @abstractmethod + def write(self, name: str) -> Optional[memoryview]: + pass + + @abstractmethod + def get(self, name: str, timeout_ms: int = 0): + pass + + @abstractmethod + def close(self, name: str): + pass + + @abstractmethod + def delete(self, name: str): + pass + + @abstractmethod + def cleanup(self): + pass + + +class UntrackedSharedMemory(_mpshm.SharedMemory): + # https://github.com/python/cpython/issues/82300#issuecomment-2169035092 + + __lock = threading.Lock() + + def __init__( + self, + name: Optional[str] = None, + create: bool = False, + size: int = 0, + *, + track: bool = False, + ) -> None: + self._track = track + + # if tracking, normal init will suffice + if track: + return super().__init__(name=name, create=create, size=size) + + # lock so that other threads don't attempt to use the + # register function during this time + with self.__lock: + # temporarily disable registration during initialization + orig_register = _mprt.register + _mprt.register = self.__tmp_register + + # initialize; ensure original register function is + # re-instated + try: + super().__init__(name=name, create=create, size=size) + finally: + _mprt.register = orig_register + + @staticmethod + def __tmp_register(*args, **kwargs) -> None: + return + + def unlink(self) -> None: + if _mpshm._USE_POSIX and self._name: + _mpshm._posixshmem.shm_unlink(self._name) + if self._track: + _mprt.unregister(self._name, "shared_memory") + + +class SharedMemoryFrameManager(FrameManager): + def __init__(self): + self.shm_store: dict[str, UntrackedSharedMemory] = {} + + def create(self, name: str, size) -> AnyStr: + try: + shm = UntrackedSharedMemory( + name=name, + create=True, + size=size, + ) + except FileExistsError: + shm = UntrackedSharedMemory(name=name) + + self.shm_store[name] = shm + return shm.buf + + def write(self, name: str) -> Optional[memoryview]: + try: + if name in self.shm_store: + shm = self.shm_store[name] + else: + shm = UntrackedSharedMemory(name=name) + self.shm_store[name] = shm + return shm.buf + except FileNotFoundError: + logger.info(f"the file {name} not found") + return None + + def get(self, name: str, shape) -> Optional[np.ndarray]: + try: + if name in self.shm_store: + shm = self.shm_store[name] + else: + shm = UntrackedSharedMemory(name=name) + self.shm_store[name] = shm + return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) + except FileNotFoundError: + return None + + def close(self, name: str): + if name in self.shm_store: + self.shm_store[name].close() + del self.shm_store[name] + + def delete(self, name: str): + if name in self.shm_store: + self.shm_store[name].close() + + try: + self.shm_store[name].unlink() + except FileNotFoundError: + pass + + del self.shm_store[name] + else: + try: + shm = UntrackedSharedMemory(name=name) + shm.close() + shm.unlink() + except FileNotFoundError: + pass + + def cleanup(self) -> None: + for shm in self.shm_store.values(): + shm.close() + + try: + shm.unlink() + except FileNotFoundError: + pass + + +def create_mask(frame_shape, mask): + mask_img = np.zeros(frame_shape, np.uint8) + mask_img[:] = 255 + + if isinstance(mask, list): + for m in mask: + add_mask(m, mask_img) + + elif isinstance(mask, str): + add_mask(mask, mask_img) + + return mask_img + + +def add_mask(mask: str, mask_img: np.ndarray): + points = mask.split(",") + + # masks and zones are saved as relative coordinates + # we know if any points are > 1 then it is using the + # old native resolution coordinates + if any(x > "1.0" for x in points): + raise Exception("add mask expects relative coordinates only") + + contour = np.array( + [ + [ + int(float(points[i]) * mask_img.shape[1]), + int(float(points[i + 1]) * mask_img.shape[0]), + ] + for i in range(0, len(points), 2) + ] + ) + cv2.fillPoly(mask_img, pts=[contour], color=(0)) + + +def run_ffmpeg_snapshot( + ffmpeg, + input_path: str, + codec: str, + seek_time: Optional[float] = None, + height: Optional[int] = None, + timeout: Optional[int] = None, +) -> tuple[Optional[bytes], str]: + """Run ffmpeg to extract a snapshot/image from a video source.""" + ffmpeg_cmd = [ + ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + ] + + if seek_time is not None: + ffmpeg_cmd.extend(["-ss", f"00:00:{seek_time}"]) + + ffmpeg_cmd.extend( + [ + "-i", + input_path, + "-frames:v", + "1", + "-c:v", + codec, + "-f", + "image2pipe", + "-", + ] + ) + + if height is not None: + ffmpeg_cmd.insert(-3, "-vf") + ffmpeg_cmd.insert(-3, f"scale=-1:{height}") + + try: + process = sp.run( + ffmpeg_cmd, + capture_output=True, + timeout=timeout, + ) + + if process.returncode == 0 and process.stdout: + return process.stdout, "" + else: + return None, process.stderr.decode() if process.stderr else "ffmpeg failed" + except sp.TimeoutExpired: + return None, "timeout" + + +def get_image_from_recording( + ffmpeg, # Ffmpeg Config + file_path: str, + relative_frame_time: float, + codec: str, + height: Optional[int] = None, +) -> Optional[Any]: + """retrieve a frame from given time in recording file.""" + + image_data, _ = run_ffmpeg_snapshot( + ffmpeg, file_path, codec, seek_time=relative_frame_time, height=height + ) + + return image_data + + +def get_histogram(image, x_min, y_min, x_max, y_max): + image_bgr = cv2.cvtColor(image, cv2.COLOR_YUV2BGR_I420) + image_bgr = image_bgr[y_min:y_max, x_min:x_max] + + hist = cv2.calcHist( + [image_bgr], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256] + ) + return cv2.normalize(hist, hist).flatten() + + +def create_thumbnail( + yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500 +) -> Optional[bytes]: + """Return jpg thumbnail of a region of the frame.""" + frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420) + region = calculate_region( + frame.shape, box[0], box[1], box[2], box[3], height, multiplier=1.4 + ) + frame = frame[region[1] : region[3], region[0] : region[2]] + width = int(height * frame.shape[1] / frame.shape[0]) + frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 100]) + + if ret: + return jpg.tobytes() + + return None + + +def ensure_jpeg_bytes(image_data: bytes) -> bytes: + """Ensure image data is jpeg bytes for genai""" + try: + img_array = np.frombuffer(image_data, dtype=np.uint8) + img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + + if img is None: + return image_data + + success, encoded_img = cv2.imencode(".jpg", img) + + if success: + return encoded_img.tobytes() + except Exception as e: + logger.warning(f"Error when converting thumbnail to jpeg for genai: {e}") + + return image_data diff --git a/sam2-cpu/frigate-dev/frigate/util/model.py b/sam2-cpu/frigate-dev/frigate/util/model.py new file mode 100644 index 0000000..338303e --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/model.py @@ -0,0 +1,380 @@ +"""Model Utils""" + +import logging +import os +from typing import Any + +import cv2 +import numpy as np +import onnxruntime as ort + +from frigate.const import MODEL_CACHE_DIR + +logger = logging.getLogger(__name__) + + +### Post Processing + + +def post_process_dfine( + tensor_output: np.ndarray, width: int, height: int +) -> np.ndarray: + class_ids = tensor_output[0][tensor_output[2] > 0.4] + boxes = tensor_output[1][tensor_output[2] > 0.4] + scores = tensor_output[2][tensor_output[2] > 0.4] + + input_shape = np.array([height, width, height, width]) + boxes = np.divide(boxes, input_shape, dtype=np.float32) + indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4) + detections = np.zeros((20, 6), np.float32) + + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes[indices], scores[indices], class_ids[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1], + bbox[0], + bbox[3], + bbox[2], + ] + + return detections + + +def post_process_rfdetr(tensor_output: list[np.ndarray, np.ndarray]) -> np.ndarray: + boxes = tensor_output[0] + raw_scores = tensor_output[1] + + # apply soft max to scores + exp = np.exp(raw_scores - np.max(raw_scores, axis=-1, keepdims=True)) + all_scores = exp / np.sum(exp, axis=-1, keepdims=True) + + # get highest scoring class from every detection + scores = np.max(all_scores[0, :, 1:], axis=-1) + labels = np.argmax(all_scores[0, :, 1:], axis=-1) + + idxs = scores > 0.4 + filtered_boxes = boxes[0, idxs] + filtered_scores = scores[idxs] + filtered_labels = labels[idxs] + + # convert boxes from [x_center, y_center, width, height] + x_center, y_center, w, h = ( + filtered_boxes[:, 0], + filtered_boxes[:, 1], + filtered_boxes[:, 2], + filtered_boxes[:, 3], + ) + x_min = x_center - w / 2 + y_min = y_center - h / 2 + x_max = x_center + w / 2 + y_max = y_center + h / 2 + filtered_boxes = np.stack([x_min, y_min, x_max, y_max], axis=-1) + + # apply nms + indices = cv2.dnn.NMSBoxes( + filtered_boxes, filtered_scores, score_threshold=0.4, nms_threshold=0.4 + ) + detections = np.zeros((20, 6), np.float32) + + for i, (bbox, confidence, class_id) in enumerate( + zip(filtered_boxes[indices], filtered_scores[indices], filtered_labels[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1], + bbox[0], + bbox[3], + bbox[2], + ] + + return detections + + +def __post_process_multipart_yolo( + output_list, + width, + height, +): + anchors = [ + [(12, 16), (19, 36), (40, 28)], + [(36, 75), (76, 55), (72, 146)], + [(142, 110), (192, 243), (459, 401)], + ] + + stride_map = {0: 8, 1: 16, 2: 32} + + all_boxes = [] + all_scores = [] + all_class_ids = [] + + for i, output in enumerate(output_list): + bs, _, ny, nx = output.shape + stride = stride_map[i] + anchor_set = anchors[i] + + num_anchors = len(anchor_set) + output = output.reshape(bs, num_anchors, 85, ny, nx) + output = output.transpose(0, 1, 3, 4, 2) + output = output[0] + + for a_idx, (anchor_w, anchor_h) in enumerate(anchor_set): + for y in range(ny): + for x in range(nx): + pred = output[a_idx, y, x] + class_probs = pred[5:] + class_id = np.argmax(class_probs) + class_conf = class_probs[class_id] + conf = class_conf * pred[4] + + if conf < 0.4: + continue + + dx = pred[0] + dy = pred[1] + dw = pred[2] + dh = pred[3] + + bx = ((dx * 2.0 - 0.5) + x) * stride + by = ((dy * 2.0 - 0.5) + y) * stride + bw = ((dw * 2.0) ** 2) * anchor_w + bh = ((dh * 2.0) ** 2) * anchor_h + + x1 = max(0, bx - bw / 2) + y1 = max(0, by - bh / 2) + x2 = min(width, bx + bw / 2) + y2 = min(height, by + bh / 2) + + all_boxes.append([x1, y1, x2, y2]) + all_scores.append(conf) + all_class_ids.append(class_id) + + indices = cv2.dnn.NMSBoxes( + bboxes=all_boxes, + scores=all_scores, + score_threshold=0.4, + nms_threshold=0.4, + ) + + results = np.zeros((20, 6), np.float32) + + if len(indices) > 0: + for i, idx in enumerate(indices.flatten()[:20]): + class_id = all_class_ids[idx] + conf = all_scores[idx] + x1, y1, x2, y2 = all_boxes[idx] + results[i] = [ + class_id, + conf, + y1 / height, + x1 / width, + y2 / height, + x2 / width, + ] + + return results + + +def __post_process_nms_yolo(predictions: np.ndarray, width, height) -> np.ndarray: + predictions = np.squeeze(predictions) + + # transpose the output so it has order (inferences, class_ids) + if predictions.shape[0] < predictions.shape[1]: + predictions = predictions.T + + scores = np.max(predictions[:, 4:], axis=1) + predictions = predictions[scores > 0.4, :] + scores = scores[scores > 0.4] + class_ids = np.argmax(predictions[:, 4:], axis=1) + + # Rescale box + boxes = predictions[:, :4] + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 + boxes = boxes_xyxy + + # run NMS + indices = cv2.dnn.NMSBoxes(boxes, scores, score_threshold=0.4, nms_threshold=0.4) + detections = np.zeros((20, 6), np.float32) + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes[indices], scores[indices], class_ids[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1] / height, + bbox[0] / width, + bbox[3] / height, + bbox[2] / width, + ] + + return detections + + +def post_process_yolo(output: list[np.ndarray], width: int, height: int) -> np.ndarray: + if len(output) > 1: + return __post_process_multipart_yolo(output, width, height) + else: + return __post_process_nms_yolo(output[0], width, height) + + +def post_process_yolox( + predictions: np.ndarray, + width: int, + height: int, + grids: np.ndarray, + expanded_strides: np.ndarray, +) -> np.ndarray: + predictions[..., :2] = (predictions[..., :2] + grids) * expanded_strides + predictions[..., 2:4] = np.exp(predictions[..., 2:4]) * expanded_strides + + # process organized predictions + predictions = predictions[0] + boxes = predictions[:, :4] + scores = predictions[:, 4:5] * predictions[:, 5:] + + boxes_xyxy = np.ones_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 + + cls_inds = scores.argmax(1) + scores = scores[np.arange(len(cls_inds)), cls_inds] + + indices = cv2.dnn.NMSBoxes( + boxes_xyxy, scores, score_threshold=0.4, nms_threshold=0.4 + ) + + detections = np.zeros((20, 6), np.float32) + for i, (bbox, confidence, class_id) in enumerate( + zip(boxes_xyxy[indices], scores[indices], cls_inds[indices]) + ): + if i == 20: + break + + detections[i] = [ + class_id, + confidence, + bbox[1] / height, + bbox[0] / width, + bbox[3] / height, + bbox[2] / width, + ] + + return detections + + +### ONNX Utilities + + +def get_ort_providers( + force_cpu: bool = False, + device: str | None = "AUTO", + requires_fp16: bool = False, +) -> tuple[list[str], list[dict[str, Any]]]: + if force_cpu: + return ( + ["CPUExecutionProvider"], + [ + { + "enable_cpu_mem_arena": False, + } + ], + ) + + providers = [] + options = [] + + for provider in ort.get_available_providers(): + if provider == "CUDAExecutionProvider": + device_id = 0 if (not device or not device.isdigit()) else int(device) + providers.append(provider) + options.append( + { + "arena_extend_strategy": "kSameAsRequested", + "use_ep_level_unified_stream": True, + "device_id": device_id, + } + ) + elif provider == "TensorrtExecutionProvider": + # TensorrtExecutionProvider uses too much memory without options to control it + # so it is not enabled by default + if device == "Tensorrt": + os.makedirs( + os.path.join(MODEL_CACHE_DIR, "tensorrt/ort/trt-engines"), + exist_ok=True, + ) + device_id = 0 if not device.isdigit() else int(device) + providers.append(provider) + options.append( + { + "device_id": device_id, + "trt_fp16_enable": requires_fp16 + and os.environ.get("USE_FP_16", "True") != "False", + "trt_timing_cache_enable": True, + "trt_engine_cache_enable": True, + "trt_timing_cache_path": os.path.join( + MODEL_CACHE_DIR, "tensorrt/ort" + ), + "trt_engine_cache_path": os.path.join( + MODEL_CACHE_DIR, "tensorrt/ort/trt-engines" + ), + } + ) + else: + continue + elif provider == "OpenVINOExecutionProvider": + # OpenVINO is used directly + if device == "OpenVINO": + os.makedirs( + os.path.join(MODEL_CACHE_DIR, "openvino/ort"), exist_ok=True + ) + providers.append(provider) + options.append( + { + "cache_dir": os.path.join(MODEL_CACHE_DIR, "openvino/ort"), + "device_type": device, + } + ) + elif provider == "MIGraphXExecutionProvider": + migraphx_cache_dir = os.path.join(MODEL_CACHE_DIR, "migraphx") + os.makedirs(migraphx_cache_dir, exist_ok=True) + + providers.append(provider) + options.append( + { + "migraphx_model_cache_dir": migraphx_cache_dir, + } + ) + elif provider == "CPUExecutionProvider": + providers.append(provider) + options.append( + { + "enable_cpu_mem_arena": False, + } + ) + elif provider == "AzureExecutionProvider": + # Skip Azure provider - not typically available on local hardware + # and prevents fallback to OpenVINO when it's the first provider + continue + else: + providers.append(provider) + options.append({}) + + return (providers, options) diff --git a/sam2-cpu/frigate-dev/frigate/util/object.py b/sam2-cpu/frigate-dev/frigate/util/object.py new file mode 100644 index 0000000..905745d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/object.py @@ -0,0 +1,592 @@ +"""Utils for reading and writing object detection data.""" + +import datetime +import logging +import math +from collections import defaultdict +from typing import Any + +import cv2 +import numpy as np +from peewee import DoesNotExist + +from frigate.config import DetectConfig, ModelConfig +from frigate.const import ( + LABEL_CONSOLIDATION_DEFAULT, + LABEL_CONSOLIDATION_MAP, + LABEL_NMS_DEFAULT, + LABEL_NMS_MAP, +) +from frigate.detectors.detector_config import PixelFormatEnum +from frigate.models import Event, Regions, Timeline +from frigate.util.image import ( + area, + calculate_region, + clipped, + intersection, + intersection_over_union, + yuv_region_2_bgr, + yuv_region_2_rgb, + yuv_region_2_yuv, +) + +logger = logging.getLogger(__name__) + +GRID_SIZE = 8 + + +def get_camera_regions_grid( + name: str, + detect: DetectConfig, + min_region_size: int, +) -> list[list[dict[str, Any]]]: + """Build a grid of expected region sizes for a camera.""" + # get grid from db if available + try: + regions: Regions = Regions.select().where(Regions.camera == name).get() + grid = regions.grid + last_update = regions.last_update + except DoesNotExist: + grid = [] + for x in range(GRID_SIZE): + row = [] + for y in range(GRID_SIZE): + row.append({"sizes": []}) + grid.append(row) + last_update = 0 + + # get events for timeline entries + events = ( + Event.select(Event.id) + .where(Event.camera == name) + .where((Event.false_positive == None) | (Event.false_positive == False)) + .where(Event.start_time > last_update) + ) + valid_event_ids = [e["id"] for e in events.dicts()] + logger.debug(f"Found {len(valid_event_ids)} new events for {name}") + + # no new events, return as is + if not valid_event_ids: + return grid + + new_update = datetime.datetime.now().timestamp() + timeline = ( + Timeline.select( + *[ + Timeline.camera, + Timeline.source, + Timeline.data, + ] + ) + .where(Timeline.source_id << valid_event_ids) + .limit(10000) + .dicts() + ) + + logger.debug(f"Found {len(timeline)} new entries for {name}") + + width = detect.width + height = detect.height + + for t in timeline: + if t.get("source") != "tracked_object": + continue + + box = t["data"]["box"] + + # calculate centroid position + x = box[0] + (box[2] / 2) + y = box[1] + (box[3] / 2) + + x_pos = int(x * GRID_SIZE) + y_pos = int(y * GRID_SIZE) + + calculated_region = calculate_region( + (height, width), + box[0] * width, + box[1] * height, + (box[0] + box[2]) * width, + (box[1] + box[3]) * height, + min_region_size, + 1.35, + ) + # save width of region to grid as relative + grid[x_pos][y_pos]["sizes"].append( + (calculated_region[2] - calculated_region[0]) / width + ) + + for x in range(GRID_SIZE): + for y in range(GRID_SIZE): + cell = grid[x][y] + + if len(cell["sizes"]) == 0: + continue + + std_dev = np.std(cell["sizes"]) + mean = np.mean(cell["sizes"]) + logger.debug(f"std dev: {std_dev} mean: {mean}") + cell["x"] = x + cell["y"] = y + cell["std_dev"] = std_dev + cell["mean"] = mean + + # update db with new grid + region = { + Regions.camera: name, + Regions.grid: grid, + Regions.last_update: new_update, + } + ( + Regions.insert(region) + .on_conflict( + conflict_target=[Regions.camera], + update=region, + ) + .execute() + ) + + return grid + + +def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region_grid): + min_x = frame_shape[1] + min_y = frame_shape[0] + max_x = 0 + max_y = 0 + for b in cluster: + min_x = min(boxes[b][0], min_x) + min_y = min(boxes[b][1], min_y) + max_x = max(boxes[b][2], max_x) + max_y = max(boxes[b][3], max_y) + return get_region_from_grid( + frame_shape, [min_x, min_y, max_x, max_y], min_region, region_grid + ) + + +def get_region_from_grid( + frame_shape: tuple[int, int], + cluster: list[int], + min_region: int, + region_grid: list[list[dict[str, Any]]], +) -> list[int]: + """Get a region for a box based on the region grid.""" + box = calculate_region( + frame_shape, cluster[0], cluster[1], cluster[2], cluster[3], min_region + ) + centroid = ( + box[0] + (min(frame_shape[1], box[2]) - box[0]) / 2, + box[1] + (min(frame_shape[0], box[3]) - box[1]) / 2, + ) + grid_x = int(centroid[0] / frame_shape[1] * GRID_SIZE) + grid_y = int(centroid[1] / frame_shape[0] * GRID_SIZE) + + cell = region_grid[grid_x][grid_y] + + # if there is no known data, use original region calculation + if not cell or not cell["sizes"]: + return box + + # convert the calculated region size to relative + calc_size = (box[2] - box[0]) / frame_shape[1] + + # if region is within expected size, don't resize + if ( + (cell["mean"] - cell["std_dev"]) + <= calc_size + <= (cell["mean"] + cell["std_dev"]) + ): + return box + # TODO not sure how to handle case where cluster is larger than expected region + elif calc_size > (cell["mean"] + cell["std_dev"]): + return box + + size = cell["mean"] * frame_shape[1] + + # get region based on grid size + return calculate_region( + frame_shape, + max(0, centroid[0] - size / 2), + max(0, centroid[1] - size / 2), + min(frame_shape[1], centroid[0] + size / 2), + min(frame_shape[0], centroid[1] + size / 2), + min_region, + ) + + +def is_object_filtered(obj, objects_to_track, object_filters): + object_name = obj[0] + object_score = obj[1] + object_box = obj[2] + object_area = obj[3] + object_ratio = obj[4] + + if object_name not in objects_to_track: + return True + + if object_name in object_filters: + obj_settings = object_filters[object_name] + + # if the min area is larger than the + # detected object, don't add it to detected objects + if obj_settings.min_area > object_area: + return True + + # if the detected object is larger than the + # max area, don't add it to detected objects + if obj_settings.max_area < object_area: + return True + + # if the score is lower than the min_score, skip + if obj_settings.min_score > object_score: + return True + + # if the object is not proportionally wide enough + if obj_settings.min_ratio > object_ratio: + return True + + # if the object is proportionally too wide + if obj_settings.max_ratio < object_ratio: + return True + + if obj_settings.mask is not None: + # compute the coordinates of the object and make sure + # the location isn't outside the bounds of the image (can happen from rounding) + object_xmin = object_box[0] + object_xmax = object_box[2] + object_ymax = object_box[3] + y_location = min(int(object_ymax), len(obj_settings.mask) - 1) + x_location = min( + int((object_xmax + object_xmin) / 2.0), + len(obj_settings.mask[0]) - 1, + ) + + # if the object is in a masked location, don't add it to detected objects + if obj_settings.mask[y_location][x_location] == 0: + return True + + return False + + +def get_min_region_size(model_config: ModelConfig) -> int: + """Get the min region size.""" + largest_dimension = max(model_config.height, model_config.width) + + if largest_dimension > 320: + # We originally tested allowing any model to have a region down to half of the model size + # but this led to many false positives. In this case we specifically target larger models + # which can benefit from a smaller region in some cases to detect smaller objects. + half = int(largest_dimension / 2) + + if half % 4 == 0: + return half + + return int((half + 3) / 4) * 4 + + return largest_dimension + + +def create_tensor_input(frame, model_config: ModelConfig, region): + if model_config.input_pixel_format == PixelFormatEnum.rgb: + cropped_frame = yuv_region_2_rgb(frame, region) + elif model_config.input_pixel_format == PixelFormatEnum.bgr: + cropped_frame = yuv_region_2_bgr(frame, region) + else: + cropped_frame = yuv_region_2_yuv(frame, region) + + # Resize if needed + if cropped_frame.shape != (model_config.height, model_config.width, 3): + cropped_frame = cv2.resize( + cropped_frame, + dsize=(model_config.width, model_config.height), + interpolation=cv2.INTER_LINEAR, + ) + + # Expand dimensions since the model expects images to have shape: [1, height, width, 3] + return np.expand_dims(cropped_frame, axis=0) + + +def box_overlaps(b1, b2): + if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]: + return False + return True + + +def box_inside(b1, b2): + # check if b2 is inside b1 + if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]: + return True + return False + + +def reduce_boxes(boxes, iou_threshold=0.0): + clusters = [] + + for box in boxes: + matched = 0 + for cluster in clusters: + if intersection_over_union(box, cluster) > iou_threshold: + matched = 1 + cluster[0] = min(cluster[0], box[0]) + cluster[1] = min(cluster[1], box[1]) + cluster[2] = max(cluster[2], box[2]) + cluster[3] = max(cluster[3], box[3]) + + if not matched: + clusters.append(list(box)) + + return [tuple(c) for c in clusters] + + +def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: + """Return a box that is the average of a list of boxes.""" + x_mins = [] + y_mins = [] + x_max = [] + y_max = [] + + for box in boxes: + x_mins.append(box[0]) + y_mins.append(box[1]) + x_max.append(box[2]) + y_max.append(box[3]) + + return [np.mean(x_mins), np.mean(y_mins), np.mean(x_max), np.mean(y_max)] + + +def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: + """Return a box that is the median of a list of boxes.""" + sorted_boxes = sorted(boxes, key=lambda x: area(x)) + return sorted_boxes[int(len(sorted_boxes) / 2.0)] + + +def intersects_any(box_a, boxes): + for box in boxes: + if box_overlaps(box_a, box): + return True + return False + + +def inside_any(box_a, boxes): + for box in boxes: + # check if box_a is inside of box + if box_inside(box, box_a): + return True + return False + + +def get_cluster_boundary(box, min_region): + # compute the max region size for the current box (box is 10% of region) + box_width = box[2] - box[0] + box_height = box[3] - box[1] + max_region_area = abs(box_width * box_height) / 0.1 + max_region_size = max(min_region, int(math.sqrt(max_region_area))) + + centroid = (box_width / 2 + box[0], box_height / 2 + box[1]) + + max_x_dist = int(max_region_size - box_width / 2 * 1.1) + max_y_dist = int(max_region_size - box_height / 2 * 1.1) + + return [ + int(centroid[0] - max_x_dist), + int(centroid[1] - max_y_dist), + int(centroid[0] + max_x_dist), + int(centroid[1] + max_y_dist), + ] + + +def get_cluster_candidates(frame_shape, min_region, boxes): + # and create a cluster of other boxes using it's max region size + # only include boxes where the region is an appropriate(except the region could possibly be smaller?) + # size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset + # determined by the max_region size minus half the box + 20% + # TODO: see if we can do this with numpy + cluster_candidates = [] + used_boxes = [] + # loop over each box + for current_index, b in enumerate(boxes): + if current_index in used_boxes: + continue + cluster = [current_index] + used_boxes.append(current_index) + cluster_boundary = get_cluster_boundary(b, min_region) + # find all other boxes that fit inside the boundary + for compare_index, compare_box in enumerate(boxes): + if compare_index in used_boxes: + continue + + # if the box is not inside the potential cluster area, cluster them + if not box_inside(cluster_boundary, compare_box): + continue + + # get the region if you were to add this box to the cluster + potential_cluster = cluster + [compare_index] + cluster_region = get_cluster_region( + frame_shape, min_region, potential_cluster, boxes + ) + # if region could be smaller and either box would be too small + # for the resulting region, dont cluster + should_cluster = True + if (cluster_region[2] - cluster_region[0]) > min_region: + for b in potential_cluster: + box = boxes[b] + # boxes should be more than 5% of the area of the region + if area(box) / area(cluster_region) < 0.05: + should_cluster = False + break + + if should_cluster: + cluster.append(compare_index) + used_boxes.append(compare_index) + cluster_candidates.append(cluster) + + # return the unique clusters only + unique = {tuple(sorted(c)) for c in cluster_candidates} + return [list(tup) for tup in unique] + + +def get_cluster_region(frame_shape, min_region, cluster, boxes): + min_x = frame_shape[1] + min_y = frame_shape[0] + max_x = 0 + max_y = 0 + for b in cluster: + min_x = min(boxes[b][0], min_x) + min_y = min(boxes[b][1], min_y) + max_x = max(boxes[b][2], max_x) + max_y = max(boxes[b][3], max_y) + return calculate_region( + frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.35 + ) + + +def get_startup_regions( + frame_shape: tuple[int, int], + region_min_size: int, + region_grid: list[list[dict[str, Any]]], +) -> list[list[int]]: + """Get a list of regions to run on startup.""" + # return 8 most popular regions for the camera + all_cells = np.concatenate(region_grid).flat + startup_cells = sorted(all_cells, key=lambda c: len(c["sizes"]), reverse=True)[0:8] + regions = [] + + for cell in startup_cells: + # rest of the cells are empty + if not cell["sizes"]: + break + + x = frame_shape[1] / GRID_SIZE * (0.5 + cell["x"]) + y = frame_shape[0] / GRID_SIZE * (0.5 + cell["y"]) + size = cell["mean"] * frame_shape[1] + regions.append( + calculate_region( + frame_shape, + x - size / 2, + y - size / 2, + x + size / 2, + y + size / 2, + region_min_size, + multiplier=1, + ) + ) + + return regions + + +def reduce_detections( + frame_shape: tuple[int, int], + all_detections: list[tuple[Any]], +) -> list[tuple[Any]]: + """Take a list of detections and reduce overlaps to create a list of confident detections.""" + + def reduce_overlapping_detections(detections: list[tuple[Any]]) -> list[tuple[Any]]: + """apply non-maxima suppression to suppress weak, overlapping bounding boxes.""" + detected_object_groups = defaultdict(lambda: []) + for detection in detections: + detected_object_groups[detection[0]].append(detection) + + selected_objects = [] + for group in detected_object_groups.values(): + label = group[0][0] + # o[2] is the box of the object: xmin, ymin, xmax, ymax + # apply max/min to ensure values do not exceed the known frame size + boxes = [ + ( + o[2][0], + o[2][1], + o[2][2] - o[2][0], + o[2][3] - o[2][1], + ) + for o in group + ] + + # reduce confidences for objects that are on edge of region + # 0.6 should be used to ensure that the object is still considered and not dropped + # due to min score requirement of NMSBoxes + confidences = [0.6 if clipped(o, frame_shape) else o[1] for o in group] + + indices = cv2.dnn.NMSBoxes( + boxes, confidences, 0.5, LABEL_NMS_MAP.get(label, LABEL_NMS_DEFAULT) + ) + + # add objects + for index in indices: + index = index if isinstance(index, np.int32) else index[0] + obj = group[index] + selected_objects.append(obj) + + # set the detections list to only include top objects + return selected_objects + + def get_consolidated_object_detections(detections: list[tuple[Any]]): + """Drop detections that overlap too much.""" + detected_object_groups = defaultdict(lambda: []) + for detection in detections: + detected_object_groups[detection[0]].append(detection) + + consolidated_detections = [] + for group in detected_object_groups.values(): + # if the group only has 1 item, skip + if len(group) == 1: + consolidated_detections.append(group[0]) + continue + + # sort smallest to largest by area + sorted_by_area = sorted(group, key=lambda g: g[3]) + + for current_detection_idx in range(0, len(sorted_by_area)): + current_detection = sorted_by_area[current_detection_idx] + current_label = current_detection[0] + current_box = current_detection[2] + overlap = 0 + for to_check_idx in range( + min(current_detection_idx + 1, len(sorted_by_area)), + len(sorted_by_area), + ): + to_check = sorted_by_area[to_check_idx][2] + + # if area of current detection / area of check < 5% they should not be compared + # this covers cases where a large car parked in a driveway doesn't block detections + # of cars in the street behind it + if area(current_box) / area(to_check) < 0.05: + continue + + intersect_box = intersection(current_box, to_check) + # if % of smaller detection is inside of another detection, consolidate + if intersect_box is not None and area(intersect_box) / area( + current_box + ) > LABEL_CONSOLIDATION_MAP.get( + current_label, LABEL_CONSOLIDATION_DEFAULT + ): + overlap = 1 + break + if overlap == 0: + consolidated_detections.append( + sorted_by_area[current_detection_idx] + ) + + return consolidated_detections + + return get_consolidated_object_detections( + reduce_overlapping_detections(all_detections) + ) diff --git a/sam2-cpu/frigate-dev/frigate/util/process.py b/sam2-cpu/frigate-dev/frigate/util/process.py new file mode 100644 index 0000000..0bef991 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/process.py @@ -0,0 +1,154 @@ +import atexit +import faulthandler +import logging +import multiprocessing as mp +import os +import pathlib +import subprocess +import threading +from logging.handlers import QueueHandler +from multiprocessing.synchronize import Event as MpEvent +from typing import Callable, Optional + +from setproctitle import setproctitle + +import frigate.log +from frigate.config.logger import LoggerConfig +from frigate.const import CONFIG_DIR + + +class BaseProcess(mp.Process): + def __init__( + self, + stop_event: MpEvent, + priority: int, + *, + name: Optional[str] = None, + target: Optional[Callable] = None, + args: tuple = (), + kwargs: dict = {}, + daemon: Optional[bool] = None, + ): + self.priority = priority + self.stop_event = stop_event + super().__init__( + name=name, target=target, args=args, kwargs=kwargs, daemon=daemon + ) + + def start(self, *args, **kwargs): + self.before_start() + super().start(*args, **kwargs) + self.after_start() + + def before_start(self) -> None: + pass + + def after_start(self) -> None: + pass + + +class FrigateProcess(BaseProcess): + logger: logging.Logger + + def before_start(self) -> None: + self.__log_queue = frigate.log.log_listener.queue + self.__memray_tracker = None + + def pre_run_setup(self, logConfig: LoggerConfig | None = None) -> None: + os.nice(self.priority) + setproctitle(self.name) + threading.current_thread().name = f"process:{self.name}" + faulthandler.enable() + + # setup logging + self.logger = logging.getLogger(self.name) + logging.basicConfig(handlers=[], force=True) + logging.getLogger().addHandler(QueueHandler(self.__log_queue)) + + if logConfig: + frigate.log.apply_log_levels( + logConfig.default.value.upper(), logConfig.logs + ) + + self._setup_memray() + + def _setup_memray(self) -> None: + """Setup memray profiling if enabled via environment variable.""" + memray_modules = os.environ.get("FRIGATE_MEMRAY_MODULES", "") + + if not memray_modules: + return + + # Extract module name from process name (e.g., "frigate.capture:camera" -> "frigate.capture") + process_name = self.name + module_name = ( + process_name.split(":")[0] if ":" in process_name else process_name + ) + + enabled_modules = [m.strip() for m in memray_modules.split(",")] + + if module_name not in enabled_modules and process_name not in enabled_modules: + return + + try: + import memray + + reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports" + reports_dir.mkdir(parents=True, exist_ok=True) + safe_name = ( + process_name.replace(":", "_").replace("/", "_").replace("\\", "_") + ) + + binary_file = reports_dir / f"{safe_name}.bin" + + self.__memray_tracker = memray.Tracker(str(binary_file)) + self.__memray_tracker.__enter__() + + # Register cleanup handler to stop tracking and generate HTML report + # atexit runs on normal exits and most signal-based terminations (SIGTERM, SIGINT) + # For hard kills (SIGKILL) or segfaults, the binary file is preserved for manual generation + atexit.register(self._cleanup_memray, safe_name, binary_file) + + self.logger.info( + f"Memray profiling enabled for module {module_name} (process: {self.name}). " + f"Binary file (updated continuously): {binary_file}. " + f"HTML report will be generated on exit: {reports_dir}/{safe_name}.html. " + f"If process crashes, manually generate with: memray flamegraph {binary_file}" + ) + except Exception as e: + self.logger.error(f"Failed to setup memray profiling: {e}", exc_info=True) + + def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None: + """Stop memray tracking and generate HTML report.""" + if self.__memray_tracker is None: + return + + try: + self.__memray_tracker.__exit__(None, None, None) + self.__memray_tracker = None + + reports_dir = pathlib.Path(CONFIG_DIR) / "memray_reports" + html_file = reports_dir / f"{safe_name}.html" + + result = subprocess.run( + ["memray", "flamegraph", "--output", str(html_file), str(binary_file)], + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode == 0: + self.logger.info(f"Memray report generated: {html_file}") + else: + self.logger.error( + f"Failed to generate memray report: {result.stderr}. " + f"Binary file preserved at {binary_file} for manual generation." + ) + + # Keep the binary file for manual report generation if needed + # Users can run: memray flamegraph {binary_file} + + except subprocess.TimeoutExpired: + self.logger.error("Memray report generation timed out") + except Exception as e: + self.logger.error(f"Failed to cleanup memray profiling: {e}", exc_info=True) diff --git a/sam2-cpu/frigate-dev/frigate/util/rknn_converter.py b/sam2-cpu/frigate-dev/frigate/util/rknn_converter.py new file mode 100644 index 0000000..8b2fd00 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/rknn_converter.py @@ -0,0 +1,401 @@ +"""RKNN model conversion utility for Frigate.""" + +import logging +import os +import subprocess +import sys +import time +from pathlib import Path +from typing import Optional + +from frigate.util.file import FileLock + +logger = logging.getLogger(__name__) + +MODEL_TYPE_CONFIGS = { + "yolo-generic": { + "mean_values": [[0, 0, 0]], + "std_values": [[1, 1, 1]], + "target_platform": None, # Will be set dynamically + }, + "yolonas": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "yolox": { + "mean_values": [[0, 0, 0]], + "std_values": [[255, 255, 255]], + "target_platform": None, # Will be set dynamically + }, + "jina-clip-v1-vision": { + "mean_values": [[0.48145466 * 255, 0.4578275 * 255, 0.40821073 * 255]], + "std_values": [[0.26862954 * 255, 0.26130258 * 255, 0.27577711 * 255]], + "target_platform": None, # Will be set dynamically + }, + "arcface-r100": { + "mean_values": [[127.5, 127.5, 127.5]], + "std_values": [[127.5, 127.5, 127.5]], + "target_platform": None, # Will be set dynamically + }, +} + + +def get_rknn_model_type(model_path: str) -> str | None: + if all(keyword in str(model_path) for keyword in ["jina-clip-v1", "vision"]): + return "jina-clip-v1-vision" + + model_name = os.path.basename(str(model_path)).lower() + + if "arcface" in model_name: + return "arcface-r100" + + if any(keyword in model_name for keyword in ["yolo", "yolox", "yolonas"]): + return model_name + + return None + + +def is_rknn_compatible(model_path: str, model_type: str | None = None) -> bool: + """ + Check if a model is compatible with RKNN conversion. + + Args: + model_path: Path to the model file + model_type: Type of the model (if known) + + Returns: + True if the model is RKNN-compatible, False otherwise + """ + soc = get_soc_type() + if soc is None: + return False + + if not model_type: + model_type = get_rknn_model_type(model_path) + + if model_type and model_type in MODEL_TYPE_CONFIGS: + return True + + return False + + +def ensure_torch_dependencies() -> bool: + """Dynamically install torch dependencies if not available.""" + try: + import torch # type: ignore + + logger.debug("PyTorch is already available") + return True + except ImportError: + logger.info("PyTorch not found, attempting to install...") + + try: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "--break-system-packages", + "torch", + "torchvision", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + import torch # type: ignore # noqa: F401 + + logger.info("PyTorch installed successfully") + return True + except (subprocess.CalledProcessError, ImportError) as e: + logger.error(f"Failed to install PyTorch: {e}") + return False + + +def ensure_rknn_toolkit() -> bool: + """Ensure RKNN toolkit is available.""" + try: + from rknn.api import RKNN # type: ignore # noqa: F401 + + logger.debug("RKNN toolkit is already available") + return True + except ImportError as e: + logger.error(f"RKNN toolkit not found. Please ensure it's installed. {e}") + return False + + +def get_soc_type() -> Optional[str]: + """Get the SoC type from device tree.""" + try: + with open("/proc/device-tree/compatible") as file: + content = file.read() + + # Check for Jetson devices + if "nvidia" in content: + return None + + return content.split(",")[-1].strip("\x00") + except FileNotFoundError: + logger.debug("Could not determine SoC type from device tree") + return None + + +def convert_onnx_to_rknn( + onnx_path: str, + output_path: str, + model_type: str, + quantization: bool = False, + soc: Optional[str] = None, +) -> bool: + """ + Convert ONNX model to RKNN format. + + Args: + onnx_path: Path to input ONNX model + output_path: Path for output RKNN model + model_type: Type of model (yolo-generic, yolonas, yolox, ssd) + quantization: Whether to use 8-bit quantization (i8) or 16-bit float (fp16) + soc: Target SoC platform (auto-detected if None) + + Returns: + True if conversion successful, False otherwise + """ + if not ensure_torch_dependencies(): + logger.debug("PyTorch dependencies not available") + return False + + if not ensure_rknn_toolkit(): + logger.debug("RKNN toolkit not available") + return False + + # Get SoC type if not provided + if soc is None: + soc = get_soc_type() + if soc is None: + logger.debug("Could not determine SoC type") + return False + + # Get model config for the specified type + if model_type not in MODEL_TYPE_CONFIGS: + logger.debug(f"Unsupported model type: {model_type}") + return False + + config = MODEL_TYPE_CONFIGS[model_type].copy() + config["target_platform"] = soc + + # RKNN toolkit requires .onnx extension, create temporary copy if needed + temp_onnx_path = None + onnx_model_path = onnx_path + + if not onnx_path.endswith(".onnx"): + import shutil + + temp_onnx_path = f"{onnx_path}.onnx" + logger.debug(f"Creating temporary ONNX copy: {temp_onnx_path}") + try: + shutil.copy2(onnx_path, temp_onnx_path) + onnx_model_path = temp_onnx_path + except Exception as e: + logger.error(f"Failed to create temporary ONNX copy: {e}") + return False + + try: + from rknn.api import RKNN # type: ignore + + logger.info(f"Converting {onnx_path} to RKNN format for {soc}") + rknn = RKNN(verbose=True) + rknn.config(**config) + + if model_type == "jina-clip-v1-vision": + load_output = rknn.load_onnx( + model=onnx_model_path, + inputs=["pixel_values"], + input_size_list=[[1, 3, 224, 224]], + ) + elif model_type == "arcface-r100": + load_output = rknn.load_onnx( + model=onnx_model_path, + inputs=["data"], + input_size_list=[[1, 3, 112, 112]], + ) + else: + load_output = rknn.load_onnx(model=onnx_model_path) + + if load_output != 0: + logger.error("Failed to load ONNX model") + return False + + if rknn.build(do_quantization=quantization) != 0: + logger.error("Failed to build RKNN model") + return False + + if rknn.export_rknn(output_path) != 0: + logger.error("Failed to export RKNN model") + return False + + logger.info(f"Successfully converted model to {output_path}") + return True + + except Exception as e: + logger.error(f"Error during RKNN conversion: {e}") + return False + finally: + # Clean up temporary file if created + if temp_onnx_path and os.path.exists(temp_onnx_path): + try: + os.remove(temp_onnx_path) + logger.debug(f"Removed temporary ONNX file: {temp_onnx_path}") + except Exception as e: + logger.warning(f"Failed to remove temporary ONNX file: {e}") + + +def wait_for_conversion_completion( + model_type: str, rknn_path: Path, lock_file_path: Path, timeout: int = 300 +) -> bool: + """ + Wait for another process to complete the conversion. + + Args: + model_type: Type of model being converted + rknn_path: Path to the expected RKNN model + lock_file_path: Path to the lock file to monitor + timeout: Maximum time to wait in seconds + + Returns: + True if RKNN model appears, False if timeout + """ + start_time = time.time() + lock = FileLock(lock_file_path, stale_timeout=600) + + while time.time() - start_time < timeout: + # Check if RKNN model appeared + if rknn_path.exists(): + logger.info(f"RKNN model appeared: {rknn_path}") + return True + + # Check if lock file is gone (conversion completed or failed) + if not lock_file_path.exists(): + logger.info("Lock file removed, checking for RKNN model...") + if rknn_path.exists(): + logger.info(f"RKNN model found after lock removal: {rknn_path}") + return True + else: + logger.warning( + "Lock file removed but RKNN model not found, conversion may have failed" + ) + return False + + # Check if lock is stale + if lock.is_stale(): + logger.warning("Lock file is stale, attempting to clean up and retry...") + lock._cleanup_stale_lock() + # Try to acquire lock again + retry_lock = FileLock( + lock_file_path, timeout=60, cleanup_stale_on_init=True + ) + if retry_lock.acquire(): + try: + # Check if RKNN file appeared while waiting + if rknn_path.exists(): + logger.info(f"RKNN model appeared while waiting: {rknn_path}") + return True + + # Convert ONNX to RKNN + logger.info( + f"Retrying conversion of {rknn_path} after stale lock cleanup..." + ) + + # Get the original model path from rknn_path + base_path = rknn_path.parent / rknn_path.stem + onnx_path = base_path.with_suffix(".onnx") + + if onnx_path.exists(): + if convert_onnx_to_rknn( + str(onnx_path), str(rknn_path), model_type, False + ): + return True + + logger.error("Failed to convert model after stale lock cleanup") + return False + + finally: + retry_lock.release() + + logger.debug("Waiting for RKNN model to appear...") + time.sleep(1) + + logger.warning(f"Timeout waiting for RKNN model: {rknn_path}") + return False + + +def auto_convert_model( + model_path: str, model_type: str | None = None, quantization: bool = False +) -> Optional[str]: + """ + Automatically convert a model to RKNN format if needed. + + Args: + model_path: Path to the model file + model_type: Type of the model + quantization: Whether to use quantization + + Returns: + Path to the RKNN model if successful, None otherwise + """ + if model_path.endswith(".rknn"): + return model_path + + # Check if equivalent .rknn file exists + base_path = Path(model_path) + if base_path.suffix.lower() in [".onnx", ""]: + base_name = base_path.stem if base_path.suffix else base_path.name + rknn_path = base_path.parent / f"{base_name}.rknn" + + if rknn_path.exists(): + logger.info(f"Found existing RKNN model: {rknn_path}") + return str(rknn_path) + + lock_file_path = base_path.parent / f"{base_name}.conversion.lock" + lock = FileLock(lock_file_path, timeout=300, cleanup_stale_on_init=True) + + if lock.acquire(): + try: + if rknn_path.exists(): + logger.info( + f"RKNN model appeared while waiting for lock: {rknn_path}" + ) + return str(rknn_path) + + logger.info(f"Converting {model_path} to RKNN format...") + rknn_path.parent.mkdir(parents=True, exist_ok=True) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if convert_onnx_to_rknn( + str(base_path), str(rknn_path), model_type, quantization + ): + return str(rknn_path) + else: + logger.error(f"Failed to convert {model_path} to RKNN format") + return None + + finally: + lock.release() + else: + logger.info( + f"Another process is converting {model_path}, waiting for completion..." + ) + + if not model_type: + model_type = get_rknn_model_type(base_path) + + if wait_for_conversion_completion(model_type, rknn_path, lock_file_path): + return str(rknn_path) + else: + logger.error(f"Timeout waiting for conversion of {model_path}") + return None + + return None diff --git a/sam2-cpu/frigate-dev/frigate/util/services.py b/sam2-cpu/frigate-dev/frigate/util/services.py new file mode 100644 index 0000000..c51fe92 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/services.py @@ -0,0 +1,883 @@ +"""Utilities for services.""" + +import asyncio +import json +import logging +import os +import re +import resource +import shutil +import signal +import subprocess as sp +import time +import traceback +from datetime import datetime +from typing import Any, List, Optional, Tuple + +import cv2 +import psutil +import py3nvml.py3nvml as nvml +import requests + +from frigate.const import ( + DRIVER_AMD, + DRIVER_ENV_VAR, + FFMPEG_HWACCEL_NVIDIA, + FFMPEG_HWACCEL_VAAPI, + SHM_FRAMES_VAR, +) +from frigate.util.builtin import clean_camera_user_pass, escape_special_characters + +logger = logging.getLogger(__name__) + + +def restart_frigate(): + proc = psutil.Process(1) + # if this is running via s6, sigterm pid 1 + if proc.name() == "s6-svscan": + proc.terminate() + # otherwise, just try and exit frigate + else: + os.kill(os.getpid(), signal.SIGINT) + + +def print_stack(sig, frame): + traceback.print_stack(frame) + + +def listen(): + signal.signal(signal.SIGUSR1, print_stack) + + +def get_cgroups_version() -> str: + """Determine what version of cgroups is enabled.""" + + cgroup_path = "/sys/fs/cgroup" + + if not os.path.ismount(cgroup_path): + logger.debug(f"{cgroup_path} is not a mount point.") + return "unknown" + + try: + with open("/proc/mounts", "r") as f: + mounts = f.readlines() + + for mount in mounts: + mount_info = mount.split() + if mount_info[1] == cgroup_path: + fs_type = mount_info[2] + if fs_type == "cgroup2fs" or fs_type == "cgroup2": + return "cgroup2" + elif fs_type == "tmpfs": + return "cgroup" + else: + logger.debug( + f"Could not determine cgroups version: unhandled filesystem {fs_type}" + ) + break + except Exception as e: + logger.debug(f"Could not determine cgroups version: {e}") + + return "unknown" + + +def get_docker_memlimit_bytes() -> int: + """Get mem limit in bytes set in docker if present. Returns -1 if no limit detected.""" + + # check running a supported cgroups version + if get_cgroups_version() == "cgroup2": + memlimit_path = "/sys/fs/cgroup/memory.max" + + try: + with open(memlimit_path, "r") as f: + value = f.read().strip() + + if value.isnumeric(): + return int(value) + elif value.lower() == "max": + return -1 + except Exception as e: + logger.debug(f"Unable to get docker memlimit: {e}") + + return -1 + + +def get_cpu_stats() -> dict[str, dict]: + """Get cpu usages for each process id""" + usages = {} + docker_memlimit = get_docker_memlimit_bytes() / 1024 + total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024 + + system_cpu = psutil.cpu_percent( + interval=None + ) # no interval as we don't want to be blocking + system_mem = psutil.virtual_memory() + usages["frigate.full_system"] = { + "cpu": str(system_cpu), + "mem": str(system_mem.percent), + } + + for process in psutil.process_iter(["pid", "name", "cpu_percent", "cmdline"]): + pid = str(process.info["pid"]) + try: + cpu_percent = process.info["cpu_percent"] + cmdline = process.info["cmdline"] + + with open(f"/proc/{pid}/stat", "r") as f: + stats = f.readline().split() + utime = int(stats[13]) + stime = int(stats[14]) + start_time = int(stats[21]) + + with open("/proc/uptime") as f: + system_uptime_sec = int(float(f.read().split()[0])) + + clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) + + process_utime_sec = utime // clk_tck + process_stime_sec = stime // clk_tck + process_start_time_sec = start_time // clk_tck + + process_elapsed_sec = system_uptime_sec - process_start_time_sec + process_usage_sec = process_utime_sec + process_stime_sec + cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec + + with open(f"/proc/{pid}/statm", "r") as f: + mem_stats = f.readline().split() + mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 + + if docker_memlimit > 0: + mem_pct = round((mem_res / docker_memlimit) * 100, 1) + else: + mem_pct = round((mem_res / total_mem) * 100, 1) + + usages[pid] = { + "cpu": str(cpu_percent), + "cpu_average": str(round(cpu_average_usage, 2)), + "mem": f"{mem_pct}", + "cmdline": clean_camera_user_pass(" ".join(cmdline)), + } + except Exception: + continue + + return usages + + +def get_physical_interfaces(interfaces) -> list: + if not interfaces: + return [] + + with open("/proc/net/dev", "r") as file: + lines = file.readlines() + + physical_interfaces = [] + for line in lines: + if ":" in line: + interface = line.split(":")[0].strip() + for int in interfaces: + if interface.startswith(int): + physical_interfaces.append(interface) + + return physical_interfaces + + +def get_bandwidth_stats(config) -> dict[str, dict]: + """Get bandwidth usages for each ffmpeg process id""" + usages = {} + top_command = ["nethogs", "-t", "-v0", "-c5", "-d1"] + get_physical_interfaces( + config.telemetry.network_interfaces + ) + + p = sp.run( + top_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + logger.error(f"Error getting network stats :: {p.stderr}") + return usages + else: + lines = p.stdout.split("\n") + for line in lines: + stats = list(filter(lambda a: a != "", line.strip().split("\t"))) + try: + if re.search( + r"(^ffmpeg|\/go2rtc|frigate\.detector\.[a-z]+)/([0-9]+)/", stats[0] + ): + process = stats[0].split("/") + usages[process[len(process) - 2]] = { + "bandwidth": round(float(stats[1]) + float(stats[2]), 1), + } + except (IndexError, ValueError): + continue + + return usages + + +def is_vaapi_amd_driver() -> bool: + # Use the explicitly configured driver, if available + driver = os.environ.get(DRIVER_ENV_VAR) + if driver: + return driver == DRIVER_AMD + + # Otherwise, ask vainfo what is has autodetected + p = vainfo_hwaccel() + + if p.returncode != 0: + logger.error(f"Unable to poll vainfo: {p.stderr}") + return False + else: + output = p.stdout.decode("unicode_escape").split("\n") + + # VA Info will print out the friendly name of the driver + return any("AMD Radeon Graphics" in line for line in output) + + +def get_amd_gpu_stats() -> Optional[dict[str, str]]: + """Get stats using radeontop.""" + radeontop_command = ["radeontop", "-d", "-", "-l", "1"] + + p = sp.run( + radeontop_command, + encoding="ascii", + capture_output=True, + ) + + if p.returncode != 0: + logger.error(f"Unable to poll radeon GPU stats: {p.stderr}") + return None + else: + usages = p.stdout.split(",") + results: dict[str, str] = {} + + for hw in usages: + if "gpu" in hw: + results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + elif "vram" in hw: + results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')}%" + + return results + + +def get_intel_gpu_stats(intel_gpu_device: Optional[str]) -> Optional[dict[str, str]]: + """Get stats using intel_gpu_top.""" + + def get_stats_manually(output: str) -> dict[str, str]: + """Find global stats via regex when json fails to parse.""" + reading = "".join(output) + results: dict[str, str] = {} + + # render is used for qsv + render = [] + for result in re.findall(r'"Render/3D/0":{[a-z":\d.,%]+}', reading): + packet = json.loads(result[14:]) + single = packet.get("busy", 0.0) + render.append(float(single)) + + if render: + render_avg = sum(render) / len(render) + else: + render_avg = 1 + + # video is used for vaapi + video = [] + for result in re.findall(r'"Video/\d":{[a-z":\d.,%]+}', reading): + packet = json.loads(result[10:]) + single = packet.get("busy", 0.0) + video.append(float(single)) + + if video: + video_avg = sum(video) / len(video) + else: + video_avg = 1 + + results["gpu"] = f"{round((video_avg + render_avg) / 2, 2)}%" + results["mem"] = "-%" + return results + + intel_gpu_top_command = [ + "timeout", + "0.5s", + "intel_gpu_top", + "-J", + "-o", + "-", + "-s", + "1000", # Intel changed this from seconds to milliseconds in 2024+ versions + ] + + if intel_gpu_device: + intel_gpu_top_command += ["-d", intel_gpu_device] + + try: + p = sp.run( + intel_gpu_top_command, + encoding="ascii", + capture_output=True, + ) + except UnicodeDecodeError: + return None + + # timeout has a non-zero returncode when timeout is reached + if p.returncode != 124: + logger.error(f"Unable to poll intel GPU stats: {p.stderr}") + return None + else: + output = "".join(p.stdout.split()) + + try: + data = json.loads(f"[{output}]") + except json.JSONDecodeError: + return get_stats_manually(output) + + results: dict[str, str] = {} + render = {"global": []} + video = {"global": []} + + for block in data: + global_engine = block.get("engines") + + if global_engine: + render_frame = global_engine.get("Render/3D/0", {}).get("busy") + video_frame = global_engine.get("Video/0", {}).get("busy") + + if render_frame is not None: + render["global"].append(float(render_frame)) + + if video_frame is not None: + video["global"].append(float(video_frame)) + + clients = block.get("clients", {}) + + if clients and len(clients): + for client_block in clients.values(): + key = client_block["pid"] + + if render.get(key) is None: + render[key] = [] + video[key] = [] + + client_engine = client_block.get("engine-classes", {}) + + render_frame = client_engine.get("Render/3D", {}).get("busy") + video_frame = client_engine.get("Video", {}).get("busy") + + if render_frame is not None: + render[key].append(float(render_frame)) + + if video_frame is not None: + video[key].append(float(video_frame)) + + if render["global"] and video["global"]: + results["gpu"] = ( + f"{round(((sum(render['global']) / len(render['global'])) + (sum(video['global']) / len(video['global']))) / 2, 2)}%" + ) + results["mem"] = "-%" + + if len(render.keys()) > 1: + results["clients"] = {} + + for key in render.keys(): + if key == "global" or not render[key] or not video[key]: + continue + + results["clients"][key] = ( + f"{round(((sum(render[key]) / len(render[key])) + (sum(video[key]) / len(video[key]))) / 2, 2)}%" + ) + + return results + + +def get_openvino_npu_stats() -> Optional[dict[str, str]]: + """Get NPU stats using openvino.""" + NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time" + + try: + with open(NPU_RUNTIME_PATH, "r") as f: + initial_runtime = float(f.read().strip()) + + initial_time = time.time() + + # Sleep for 1 second to get an accurate reading + time.sleep(1.0) + + # Read runtime value again + with open(NPU_RUNTIME_PATH, "r") as f: + current_runtime = float(f.read().strip()) + + current_time = time.time() + + # Calculate usage percentage + runtime_diff = current_runtime - initial_runtime + time_diff = (current_time - initial_time) * 1000.0 # Convert to milliseconds + + if time_diff > 0: + usage = min(100.0, max(0.0, (runtime_diff / time_diff * 100.0))) + else: + usage = 0.0 + + return {"npu": f"{round(usage, 2)}", "mem": "-"} + except (FileNotFoundError, PermissionError, ValueError): + return None + + +def get_rockchip_gpu_stats() -> Optional[dict[str, str]]: + """Get GPU stats using rk.""" + try: + with open("/sys/kernel/debug/rkrga/load", "r") as f: + content = f.read() + except FileNotFoundError: + return None + + load_values = [] + for line in content.splitlines(): + match = re.search(r"load = (\d+)%", line) + if match: + load_values.append(int(match.group(1))) + + if not load_values: + return None + + average_load = f"{round(sum(load_values) / len(load_values), 2)}%" + return {"gpu": average_load, "mem": "-"} + + +def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: + """Get NPU stats using rk.""" + try: + with open("/sys/kernel/debug/rknpu/load", "r") as f: + npu_output = f.read() + + if "Core0:" in npu_output: + # multi core NPU + core_loads = re.findall(r"Core\d+:\s*(\d+)%", npu_output) + else: + # single core NPU + core_loads = re.findall(r"NPU load:\s+(\d+)%", npu_output) + except FileNotFoundError: + core_loads = None + + if not core_loads: + return None + + percentages = [int(load) for load in core_loads] + mean = round(sum(percentages) / len(percentages), 2) + return {"npu": mean, "mem": "-"} + + +def try_get_info(f, h, default="N/A"): + try: + if h: + v = f(h) + else: + v = f() + except nvml.NVMLError_NotSupported: + v = default + return v + + +def get_nvidia_gpu_stats() -> dict[int, dict]: + names: dict[str, int] = {} + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + gpu_name = nvml.nvmlDeviceGetName(handle) + + # handle case where user has multiple of same GPU + if gpu_name in names: + names[gpu_name] += 1 + gpu_name += f" ({names.get(gpu_name)})" + else: + names[gpu_name] = 1 + + meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle) + util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle) + enc = try_get_info(nvml.nvmlDeviceGetEncoderUtilization, handle) + dec = try_get_info(nvml.nvmlDeviceGetDecoderUtilization, handle) + pstate = try_get_info(nvml.nvmlDeviceGetPowerState, handle, default=None) + + if util != "N/A": + gpu_util = util.gpu + else: + gpu_util = 0 + + if meminfo != "N/A": + gpu_mem_util = meminfo.used / meminfo.total * 100 + else: + gpu_mem_util = -1 + + if enc != "N/A": + enc_util = enc[0] + else: + enc_util = -1 + + if dec != "N/A": + dec_util = dec[0] + else: + dec_util = -1 + + results[i] = { + "name": gpu_name, + "gpu": gpu_util, + "mem": gpu_mem_util, + "enc": enc_util, + "dec": dec_util, + "pstate": pstate or "unknown", + } + except Exception: + pass + finally: + return results + + +def get_jetson_stats() -> Optional[dict[int, dict]]: + results = {} + + try: + results["mem"] = "-" # no discrete gpu memory + + with open("/sys/devices/gpu.0/load", "r") as f: + gpuload = float(f.readline()) / 10 + results["gpu"] = f"{gpuload}%" + except Exception: + return None + + return results + + +def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess: + """Run ffprobe on stream.""" + clean_path = escape_special_characters(path) + + # Base entries that are always included + stream_entries = "codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate" + + # Additional detailed entries + if detailed: + stream_entries += ",codec_name,profile,level,pix_fmt,channels,sample_rate,channel_layout,r_frame_rate" + format_entries = "format_name,size,bit_rate,duration" + else: + format_entries = None + + ffprobe_cmd = [ + ffmpeg.ffprobe_path, + "-timeout", + "1000000", + "-print_format", + "json", + "-show_entries", + f"stream={stream_entries}", + ] + + # Add format entries for detailed mode + if detailed and format_entries: + ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"]) + + ffprobe_cmd.extend(["-loglevel", "error", clean_path]) + + return sp.run(ffprobe_cmd, capture_output=True) + + +def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess: + """Run vainfo.""" + ffprobe_cmd = ( + ["vainfo"] + if not device_name + else ["vainfo", "--display", "drm", "--device", f"/dev/dri/{device_name}"] + ) + return sp.run(ffprobe_cmd, capture_output=True) + + +def get_nvidia_driver_info() -> dict[str, Any]: + """Get general hardware info for nvidia GPU.""" + results = {} + try: + nvml.nvmlInit() + deviceCount = nvml.nvmlDeviceGetCount() + for i in range(deviceCount): + handle = nvml.nvmlDeviceGetHandleByIndex(i) + driver = try_get_info(nvml.nvmlSystemGetDriverVersion, None, default=None) + cuda_compute = try_get_info( + nvml.nvmlDeviceGetCudaComputeCapability, handle, default=None + ) + vbios = try_get_info(nvml.nvmlDeviceGetVbiosVersion, handle, default=None) + results[i] = { + "name": nvml.nvmlDeviceGetName(handle), + "driver": driver or "unknown", + "cuda_compute": cuda_compute or "unknown", + "vbios": vbios or "unknown", + } + except Exception: + pass + finally: + return results + + +def auto_detect_hwaccel() -> str: + """Detect hwaccel args by default.""" + try: + cuda = False + vaapi = False + resp = requests.get("http://127.0.0.1:1984/api/ffmpeg/hardware", timeout=3) + + if resp.status_code == 200: + data: dict[str, list[dict[str, str]]] = resp.json() + for source in data.get("sources", []): + if "cuda" in source.get("url", "") and source.get("name") == "OK": + cuda = True + + if "vaapi" in source.get("url", "") and source.get("name") == "OK": + vaapi = True + except requests.RequestException: + pass + + if cuda: + logger.info("Automatically detected nvidia hwaccel for video decoding") + return FFMPEG_HWACCEL_NVIDIA + + if vaapi: + logger.info("Automatically detected vaapi hwaccel for video decoding") + return FFMPEG_HWACCEL_VAAPI + + logger.warning( + "Did not detect hwaccel, using a GPU for accelerated video decoding is highly recommended" + ) + return "" + + +async def get_video_properties( + ffmpeg, url: str, get_duration: bool = False +) -> dict[str, Any]: + async def probe_with_ffprobe( + url: str, + ) -> tuple[bool, int, int, Optional[str], float]: + """Fallback using ffprobe: returns (valid, width, height, codec, duration).""" + cmd = [ + ffmpeg.ffprobe_path, + "-v", + "quiet", + "-print_format", + "json", + "-show_format", + "-show_streams", + url, + ] + try: + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, _ = await proc.communicate() + if proc.returncode != 0: + return False, 0, 0, None, -1 + + data = json.loads(stdout.decode()) + video_streams = [ + s for s in data.get("streams", []) if s.get("codec_type") == "video" + ] + if not video_streams: + return False, 0, 0, None, -1 + + v = video_streams[0] + width = int(v.get("width", 0)) + height = int(v.get("height", 0)) + codec = v.get("codec_name") + + duration_str = data.get("format", {}).get("duration") + duration = float(duration_str) if duration_str else -1.0 + + return True, width, height, codec, duration + except (json.JSONDecodeError, ValueError, KeyError, asyncio.SubprocessError): + return False, 0, 0, None, -1 + + def probe_with_cv2(url: str) -> tuple[bool, int, int, Optional[str], float]: + """Primary attempt using cv2: returns (valid, width, height, fourcc, duration).""" + cap = cv2.VideoCapture(url) + if not cap.isOpened(): + cap.release() + return False, 0, 0, None, -1 + + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + valid = width > 0 and height > 0 + fourcc = None + duration = -1.0 + + if valid: + fourcc_int = int(cap.get(cv2.CAP_PROP_FOURCC)) + fourcc = fourcc_int.to_bytes(4, "little").decode("latin-1").strip() + + if get_duration: + fps = cap.get(cv2.CAP_PROP_FPS) + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + if fps > 0 and total_frames > 0: + duration = total_frames / fps + + cap.release() + return valid, width, height, fourcc, duration + + # try cv2 first + has_video, width, height, fourcc, duration = probe_with_cv2(url) + + # fallback to ffprobe if needed + if not has_video or (get_duration and duration < 0): + has_video, width, height, fourcc, duration = await probe_with_ffprobe(url) + + result: dict[str, Any] = {"has_valid_video": has_video} + if has_video: + result.update({"width": width, "height": height}) + if fourcc: + result["fourcc"] = fourcc + if get_duration: + result["duration"] = duration + + return result + + +def process_logs( + contents: str, + service: Optional[str] = None, + start: Optional[int] = None, + end: Optional[int] = None, +) -> Tuple[int, List[str]]: + log_lines = [] + last_message = None + last_timestamp = None + repeat_count = 0 + + for raw_line in contents.splitlines(): + clean_line = raw_line.strip() + + if len(clean_line) < 10: + continue + + # Handle cases where S6 does not include date in log line + if " " not in clean_line: + clean_line = f"{datetime.now()} {clean_line}" + + try: + # Find the position of the first double space to extract timestamp and message + date_end = clean_line.index(" ") + timestamp = clean_line[:date_end] + full_message = clean_line[date_end:].strip() + + # For frigate, remove the date part from message comparison + if service == "frigate": + # Skip the date at the start of the message if it exists + date_parts = full_message.split("]", 1) + if len(date_parts) > 1: + message_part = date_parts[1].strip() + else: + message_part = full_message + else: + message_part = full_message + + if message_part == last_message: + repeat_count += 1 + continue + else: + if repeat_count > 0: + # Insert a deduplication message formatted the same way as logs + dedup_message = f"{last_timestamp} [LOGGING] Last message repeated {repeat_count} times" + log_lines.append(dedup_message) + repeat_count = 0 + + log_lines.append(clean_line) + last_timestamp = timestamp + + last_message = message_part + + except ValueError: + # If we can't parse the line properly, just add it as is + log_lines.append(clean_line) + continue + + # If there were repeated messages at the end, log the count + if repeat_count > 0: + dedup_message = ( + f"{last_timestamp} [LOGGING] Last message repeated {repeat_count} times" + ) + log_lines.append(dedup_message) + + return len(log_lines), log_lines[start:end] + + +def set_file_limit() -> None: + # Newer versions of containerd 2.X+ impose a very low soft file limit of 1024 + # This applies to OSs like HA OS (see https://github.com/home-assistant/operating-system/issues/4110) + # Attempt to increase this limit + soft_limit = int(os.getenv("SOFT_FILE_LIMIT", "65536") or "65536") + + current_soft, current_hard = resource.getrlimit(resource.RLIMIT_NOFILE) + logger.debug(f"Current file limits - Soft: {current_soft}, Hard: {current_hard}") + + new_soft = min(soft_limit, current_hard) + resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, current_hard)) + logger.debug( + f"File limit set. New soft limit: {new_soft}, Hard limit remains: {current_hard}" + ) + + +def get_fs_type(path: str) -> str: + bestMatch = "" + fsType = "" + for part in psutil.disk_partitions(all=True): + if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint): + fsType = part.fstype + bestMatch = part.mountpoint + return fsType + + +def calculate_shm_requirements(config) -> dict: + try: + storage_stats = shutil.disk_usage("/dev/shm") + except (FileNotFoundError, OSError): + return {} + + total_mb = round(storage_stats.total / pow(2, 20), 1) + used_mb = round(storage_stats.used / pow(2, 20), 1) + free_mb = round(storage_stats.free / pow(2, 20), 1) + + # required for log files + nginx cache + min_req_shm = 40 + 10 + + if config.birdseye.restream: + min_req_shm += 8 + + available_shm = total_mb - min_req_shm + cam_total_frame_size = 0.0 + + for camera in config.cameras.values(): + if camera.enabled_in_config and camera.detect.width and camera.detect.height: + cam_total_frame_size += round( + (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, + 1, + ) + + # leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them. + cam_total_frame_size += 2 * round( + (1280 * 720 * 1.5 + 270480) / 1048576, + 1, + ) + + shm_frame_count = min( + int(os.environ.get(SHM_FRAMES_VAR, "50")), + int(available_shm / cam_total_frame_size), + ) + + # minimum required shm recommendation + min_shm = round(min_req_shm + cam_total_frame_size * 20) + + return { + "total": total_mb, + "used": used_mb, + "free": free_mb, + "mount_type": get_fs_type("/dev/shm"), + "available": round(available_shm, 1), + "camera_frame_size": cam_total_frame_size, + "shm_frame_count": shm_frame_count, + "min_shm": min_shm, + } diff --git a/sam2-cpu/frigate-dev/frigate/util/time.py b/sam2-cpu/frigate-dev/frigate/util/time.py new file mode 100644 index 0000000..1e7b49c --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/time.py @@ -0,0 +1,100 @@ +"""Time utilities.""" + +import datetime +import logging +from typing import Tuple +from zoneinfo import ZoneInfoNotFoundError + +import pytz +from tzlocal import get_localzone + +logger = logging.getLogger(__name__) + + +def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]: + seconds_offset = ( + datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() + ) + hours_offset = int(seconds_offset / 60 / 60) + minutes_offset = int(seconds_offset / 60 - hours_offset * 60) + hour_modifier = f"{hours_offset} hour" + minute_modifier = f"{minutes_offset} minute" + return hour_modifier, minute_modifier, seconds_offset + + +def get_tomorrow_at_time(hour: int) -> datetime.datetime: + """Returns the datetime of the following day at 2am.""" + try: + tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1) + except ZoneInfoNotFoundError: + tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( + days=1 + ) + logger.warning( + "Using utc for maintenance due to missing or incorrect timezone set" + ) + + return tomorrow.replace(hour=hour, minute=0, second=0).astimezone( + datetime.timezone.utc + ) + + +def is_current_hour(timestamp: int) -> bool: + """Returns if timestamp is in the current UTC hour.""" + start_of_next_hour = ( + datetime.datetime.now(datetime.timezone.utc).replace( + minute=0, second=0, microsecond=0 + ) + + datetime.timedelta(hours=1) + ).timestamp() + return timestamp < start_of_next_hour + + +def get_dst_transitions( + tz_name: str, start_time: float, end_time: float +) -> list[tuple[float, float]]: + """ + Find DST transition points and return time periods with consistent offsets. + + Args: + tz_name: Timezone name (e.g., 'America/New_York') + start_time: Start timestamp (UTC) + end_time: End timestamp (UTC) + + Returns: + List of (period_start, period_end, seconds_offset) tuples representing + continuous periods with the same UTC offset + """ + try: + tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + # If timezone is invalid, return single period with no offset + return [(start_time, end_time, 0)] + + periods = [] + current = start_time + + # Get initial offset + dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) + local_dt = dt.astimezone(tz) + prev_offset = local_dt.utcoffset().total_seconds() + period_start = start_time + + # Check each day for offset changes + while current <= end_time: + dt = datetime.datetime.utcfromtimestamp(current).replace(tzinfo=pytz.UTC) + local_dt = dt.astimezone(tz) + current_offset = local_dt.utcoffset().total_seconds() + + if current_offset != prev_offset: + # Found a transition - close previous period + periods.append((period_start, current, prev_offset)) + period_start = current + prev_offset = current_offset + + current += 86400 # Check daily + + # Add final period + periods.append((period_start, end_time, prev_offset)) + + return periods diff --git a/sam2-cpu/frigate-dev/frigate/util/velocity.py b/sam2-cpu/frigate-dev/frigate/util/velocity.py new file mode 100644 index 0000000..61f1a0d --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/util/velocity.py @@ -0,0 +1,132 @@ +import math + +import numpy as np + + +def order_points_clockwise(points): + """ + Ensure points are sorted in clockwise order starting from the top left + + :param points: Array of zone corner points in pixel coordinates + :return: Ordered list of points + """ + top_left = min( + points, key=lambda p: (p[1], p[0]) + ) # Find the top-left point (min y, then x) + + # Remove the top-left point from the list of points + remaining_points = [p for p in points if not np.array_equal(p, top_left)] + + # Sort the remaining points based on the angle relative to the top-left point + def angle_from_top_left(point): + x, y = point[0] - top_left[0], point[1] - top_left[1] + return math.atan2(y, x) + + sorted_points = sorted(remaining_points, key=angle_from_top_left) + + return [top_left] + sorted_points + + +def create_ground_plane(zone_points, distances): + """ + Create a ground plane that accounts for perspective distortion using real-world dimensions for each side of the zone. + + :param zone_points: Array of zone corner points in pixel coordinates + [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] + :param distances: Real-world dimensions ordered by A, B, C, D + :return: Function that calculates real-world distance per pixel at any coordinate + """ + A, B, C, D = zone_points + + # Calculate pixel lengths of each side + AB_px = np.linalg.norm(np.array(B) - np.array(A)) + BC_px = np.linalg.norm(np.array(C) - np.array(B)) + CD_px = np.linalg.norm(np.array(D) - np.array(C)) + DA_px = np.linalg.norm(np.array(A) - np.array(D)) + + AB, BC, CD, DA = map(float, distances) + + AB_scale = AB / AB_px + BC_scale = BC / BC_px + CD_scale = CD / CD_px + DA_scale = DA / DA_px + + def distance_per_pixel(x, y): + """ + Calculate the real-world distance per pixel at a given (x, y) coordinate. + + :param x: X-coordinate in the image + :param y: Y-coordinate in the image + :return: Real-world distance per pixel at the given (x, y) coordinate + """ + + # Return 0 if divide by zero would occur + if (B[0] - A[0]) == 0 or (D[1] - A[1]) == 0: + return 0 + + # Normalize x and y within the zone + x_norm = (x - A[0]) / (B[0] - A[0]) + y_norm = (y - A[1]) / (D[1] - A[1]) + + # Interpolate scales horizontally and vertically + vertical_scale = AB_scale + (CD_scale - AB_scale) * y_norm + horizontal_scale = DA_scale + (BC_scale - DA_scale) * x_norm + + # Combine horizontal and vertical scales + return (vertical_scale + horizontal_scale) / 2 + + return distance_per_pixel + + +def calculate_real_world_speed( + zone_contour, + distances, + velocity_pixels, + position, + camera_fps, +): + """ + Calculate the real-world speed of a tracked object, accounting for perspective, + directly from the zone string. + + :param zone_contour: Array of absolute zone points + :param distances: List of distances of each side, ordered by A, B, C, D + :param velocity_pixels: List of tuples representing velocity in pixels/frame + :param position: Current position of the object (x, y) in pixels + :param camera_fps: Frames per second of the camera + :return: speed and velocity angle direction + """ + # order the zone_contour points clockwise starting at top left + ordered_zone_contour = order_points_clockwise(zone_contour) + + # find the indices that would sort the original zone_contour to match ordered_zone_contour + sort_indices = [ + np.where((zone_contour == point).all(axis=1))[0][0] + for point in ordered_zone_contour + ] + + # Reorder distances to match the new order of zone_contour + distances = np.array(distances) + ordered_distances = distances[sort_indices] + + ground_plane = create_ground_plane(ordered_zone_contour, ordered_distances) + + if not isinstance(velocity_pixels, np.ndarray): + velocity_pixels = np.array(velocity_pixels) + + avg_velocity_pixels = velocity_pixels.mean(axis=0) + + # get the real-world distance per pixel at the object's current position and calculate real speed + scale = ground_plane(position[0], position[1]) + speed_real = avg_velocity_pixels * scale * camera_fps + + # euclidean speed in real-world units/second + speed_magnitude = np.linalg.norm(speed_real) + + # movement direction + dx, dy = avg_velocity_pixels + angle = math.degrees(math.atan2(dy, dx)) + if angle < 0: + angle += 360 + + return speed_magnitude, angle diff --git a/sam2-cpu/frigate-dev/frigate/video.py b/sam2-cpu/frigate-dev/frigate/video.py new file mode 100755 index 0000000..a139c25 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/video.py @@ -0,0 +1,1092 @@ +import logging +import queue +import subprocess as sp +import threading +import time +from datetime import datetime, timedelta, timezone +from multiprocessing import Queue, Value +from multiprocessing.synchronize import Event as MpEvent +from typing import Any + +import cv2 + +from frigate.camera import CameraMetrics, PTZMetrics +from frigate.comms.inter_process import InterProcessRequestor +from frigate.comms.recordings_updater import ( + RecordingsDataSubscriber, + RecordingsDataTypeEnum, +) +from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig +from frigate.config.camera.camera import CameraTypeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) +from frigate.const import ( + PROCESS_PRIORITY_HIGH, + REQUEST_REGION_GRID, +) +from frigate.log import LogPipe +from frigate.motion import MotionDetector +from frigate.motion.improved_motion import ImprovedMotionDetector +from frigate.object_detection.base import RemoteObjectDetector +from frigate.ptz.autotrack import ptz_moving_at_frame_time +from frigate.track import ObjectTracker +from frigate.track.norfair_tracker import NorfairTracker +from frigate.track.tracked_object import TrackedObjectAttribute +from frigate.util.builtin import EventsPerSecond +from frigate.util.image import ( + FrameManager, + SharedMemoryFrameManager, + draw_box_with_label, +) +from frigate.util.object import ( + create_tensor_input, + get_cluster_candidates, + get_cluster_region, + get_cluster_region_from_grid, + get_min_region_size, + get_startup_regions, + inside_any, + intersects_any, + is_object_filtered, + reduce_detections, +) +from frigate.util.process import FrigateProcess +from frigate.util.time import get_tomorrow_at_time + +logger = logging.getLogger(__name__) + + +def stop_ffmpeg(ffmpeg_process: sp.Popen[Any], logger: logging.Logger): + logger.info("Terminating the existing ffmpeg process...") + ffmpeg_process.terminate() + try: + logger.info("Waiting for ffmpeg to exit gracefully...") + ffmpeg_process.communicate(timeout=30) + except sp.TimeoutExpired: + logger.info("FFmpeg didn't exit. Force killing...") + ffmpeg_process.kill() + ffmpeg_process.communicate() + ffmpeg_process = None + + +def start_or_restart_ffmpeg( + ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None +) -> sp.Popen[Any]: + if ffmpeg_process is not None: + stop_ffmpeg(ffmpeg_process, logger) + + if frame_size is None: + process = sp.Popen( + ffmpeg_cmd, + stdout=sp.DEVNULL, + stderr=logpipe, + stdin=sp.DEVNULL, + start_new_session=True, + ) + else: + process = sp.Popen( + ffmpeg_cmd, + stdout=sp.PIPE, + stderr=logpipe, + stdin=sp.DEVNULL, + bufsize=frame_size * 10, + start_new_session=True, + ) + return process + + +def capture_frames( + ffmpeg_process: sp.Popen[Any], + config: CameraConfig, + shm_frame_count: int, + frame_index: int, + frame_shape: tuple[int, int], + frame_manager: FrameManager, + frame_queue, + fps: Value, + skipped_fps: Value, + current_frame: Value, + stop_event: MpEvent, +) -> None: + frame_size = frame_shape[0] * frame_shape[1] + frame_rate = EventsPerSecond() + frame_rate.start() + skipped_eps = EventsPerSecond() + skipped_eps.start() + config_subscriber = CameraConfigUpdateSubscriber( + None, {config.name: config}, [CameraConfigUpdateEnum.enabled] + ) + + def get_enabled_state(): + """Fetch the latest enabled state from ZMQ.""" + config_subscriber.check_for_updates() + return config.enabled + + try: + while not stop_event.is_set(): + if not get_enabled_state(): + logger.debug(f"Stopping capture thread for disabled {config.name}") + break + + fps.value = frame_rate.eps() + skipped_fps.value = skipped_eps.eps() + current_frame.value = datetime.now().timestamp() + frame_name = f"{config.name}_frame{frame_index}" + frame_buffer = frame_manager.write(frame_name) + try: + frame_buffer[:] = ffmpeg_process.stdout.read(frame_size) + except Exception: + # shutdown has been initiated + if stop_event.is_set(): + break + + logger.error( + f"{config.name}: Unable to read frames from ffmpeg process." + ) + + if ffmpeg_process.poll() is not None: + logger.error( + f"{config.name}: ffmpeg process is not running. exiting capture thread..." + ) + break + + continue + + frame_rate.update() + + # don't lock the queue to check, just try since it should rarely be full + try: + # add to the queue + frame_queue.put((frame_name, current_frame.value), False) + frame_manager.close(frame_name) + except queue.Full: + # if the queue is full, skip this frame + skipped_eps.update() + + frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1 + finally: + config_subscriber.stop() + + +class CameraWatchdog(threading.Thread): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + frame_queue: Queue, + camera_fps, + skipped_fps, + ffmpeg_pid, + stop_event, + ): + threading.Thread.__init__(self) + self.logger = logging.getLogger(f"watchdog.{config.name}") + self.config = config + self.shm_frame_count = shm_frame_count + self.capture_thread = None + self.ffmpeg_detect_process = None + self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.detect") + self.ffmpeg_other_processes: list[dict[str, Any]] = [] + self.camera_fps = camera_fps + self.skipped_fps = skipped_fps + self.ffmpeg_pid = ffmpeg_pid + self.frame_queue = frame_queue + self.frame_shape = self.config.frame_shape_yuv + self.frame_size = self.frame_shape[0] * self.frame_shape[1] + self.fps_overflow_count = 0 + self.frame_index = 0 + self.stop_event = stop_event + self.sleeptime = self.config.ffmpeg.retry_interval + + self.config_subscriber = CameraConfigUpdateSubscriber( + None, + {config.name: config}, + [CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record], + ) + self.requestor = InterProcessRequestor() + self.was_enabled = self.config.enabled + + self.segment_subscriber = RecordingsDataSubscriber(RecordingsDataTypeEnum.all) + self.latest_valid_segment_time: float = 0 + self.latest_invalid_segment_time: float = 0 + self.latest_cache_segment_time: float = 0 + + def _update_enabled_state(self) -> bool: + """Fetch the latest config and update enabled state.""" + self.config_subscriber.check_for_updates() + return self.config.enabled + + def reset_capture_thread( + self, terminate: bool = True, drain_output: bool = True + ) -> None: + if terminate: + self.ffmpeg_detect_process.terminate() + try: + self.logger.info("Waiting for ffmpeg to exit gracefully...") + + if drain_output: + self.ffmpeg_detect_process.communicate(timeout=30) + else: + self.ffmpeg_detect_process.wait(timeout=30) + except sp.TimeoutExpired: + self.logger.info("FFmpeg did not exit. Force killing...") + self.ffmpeg_detect_process.kill() + + if drain_output: + self.ffmpeg_detect_process.communicate() + else: + self.ffmpeg_detect_process.wait() + + # Wait for old capture thread to fully exit before starting a new one + if self.capture_thread is not None and self.capture_thread.is_alive(): + self.logger.info("Waiting for capture thread to exit...") + self.capture_thread.join(timeout=5) + + if self.capture_thread.is_alive(): + self.logger.warning( + f"Capture thread for {self.config.name} did not exit in time" + ) + + self.logger.error( + "The following ffmpeg logs include the last 100 lines prior to exit." + ) + self.logpipe.dump() + self.logger.info("Restarting ffmpeg...") + self.start_ffmpeg_detect() + + def run(self) -> None: + if self._update_enabled_state(): + self.start_all_ffmpeg() + + time.sleep(self.sleeptime) + while not self.stop_event.wait(self.sleeptime): + enabled = self._update_enabled_state() + if enabled != self.was_enabled: + if enabled: + self.logger.debug(f"Enabling camera {self.config.name}") + self.start_all_ffmpeg() + + # reset all timestamps + self.latest_valid_segment_time = 0 + self.latest_invalid_segment_time = 0 + self.latest_cache_segment_time = 0 + else: + self.logger.debug(f"Disabling camera {self.config.name}") + self.stop_all_ffmpeg() + + # update camera status + self.requestor.send_data( + f"{self.config.name}/status/detect", "disabled" + ) + self.requestor.send_data( + f"{self.config.name}/status/record", "disabled" + ) + self.was_enabled = enabled + continue + + if not enabled: + continue + + while True: + update = self.segment_subscriber.check_for_update(timeout=0) + + if update == (None, None): + break + + raw_topic, payload = update + if raw_topic and payload: + topic = str(raw_topic) + camera, segment_time, _ = payload + + if camera != self.config.name: + continue + + if topic.endswith(RecordingsDataTypeEnum.valid.value): + self.logger.debug( + f"Latest valid recording segment time on {camera}: {segment_time}" + ) + self.latest_valid_segment_time = segment_time + elif topic.endswith(RecordingsDataTypeEnum.invalid.value): + self.logger.warning( + f"Invalid recording segment detected for {camera} at {segment_time}" + ) + self.latest_invalid_segment_time = segment_time + elif topic.endswith(RecordingsDataTypeEnum.latest.value): + if segment_time is not None: + self.latest_cache_segment_time = segment_time + else: + self.latest_cache_segment_time = 0 + + now = datetime.now().timestamp() + + if not self.capture_thread.is_alive(): + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") + self.camera_fps.value = 0 + self.logger.error( + f"Ffmpeg process crashed unexpectedly for {self.config.name}." + ) + self.reset_capture_thread(terminate=False) + elif self.camera_fps.value >= (self.config.detect.fps + 10): + self.fps_overflow_count += 1 + + if self.fps_overflow_count == 3: + self.requestor.send_data( + f"{self.config.name}/status/detect", "offline" + ) + self.fps_overflow_count = 0 + self.camera_fps.value = 0 + self.logger.info( + f"{self.config.name} exceeded fps limit. Exiting ffmpeg..." + ) + self.reset_capture_thread(drain_output=False) + elif now - self.capture_thread.current_frame.value > 20: + self.requestor.send_data(f"{self.config.name}/status/detect", "offline") + self.camera_fps.value = 0 + self.logger.info( + f"No frames received from {self.config.name} in 20 seconds. Exiting ffmpeg..." + ) + self.reset_capture_thread() + else: + # process is running normally + self.requestor.send_data(f"{self.config.name}/status/detect", "online") + self.fps_overflow_count = 0 + + for p in self.ffmpeg_other_processes: + poll = p["process"].poll() + + if self.config.record.enabled and "record" in p["roles"]: + now_utc = datetime.now().astimezone(timezone.utc) + + latest_cache_dt = ( + datetime.fromtimestamp( + self.latest_cache_segment_time, tz=timezone.utc + ) + if self.latest_cache_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + latest_valid_dt = ( + datetime.fromtimestamp( + self.latest_valid_segment_time, tz=timezone.utc + ) + if self.latest_valid_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + latest_invalid_dt = ( + datetime.fromtimestamp( + self.latest_invalid_segment_time, tz=timezone.utc + ) + if self.latest_invalid_segment_time > 0 + else now_utc - timedelta(seconds=1) + ) + + # ensure segments are still being created and that they have valid video data + cache_stale = now_utc > (latest_cache_dt + timedelta(seconds=120)) + valid_stale = now_utc > (latest_valid_dt + timedelta(seconds=120)) + invalid_stale_condition = ( + self.latest_invalid_segment_time > 0 + and now_utc > (latest_invalid_dt + timedelta(seconds=120)) + and self.latest_valid_segment_time + <= self.latest_invalid_segment_time + ) + invalid_stale = invalid_stale_condition + + if cache_stale or valid_stale or invalid_stale: + if cache_stale: + reason = "No new recording segments were created" + elif valid_stale: + reason = "No new valid recording segments were created" + else: # invalid_stale + reason = ( + "No valid segments created since last invalid segment" + ) + + self.logger.error( + f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..." + ) + p["process"] = start_or_restart_ffmpeg( + p["cmd"], + self.logger, + p["logpipe"], + ffmpeg_process=p["process"], + ) + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role}", "offline" + ) + + continue + else: + self.requestor.send_data( + f"{self.config.name}/status/record", "online" + ) + p["latest_segment_time"] = self.latest_cache_segment_time + + if poll is None: + continue + + for role in p["roles"]: + self.requestor.send_data( + f"{self.config.name}/status/{role}", "offline" + ) + + p["logpipe"].dump() + p["process"] = start_or_restart_ffmpeg( + p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"] + ) + + self.stop_all_ffmpeg() + self.logpipe.close() + self.config_subscriber.stop() + self.segment_subscriber.stop() + + def start_ffmpeg_detect(self): + ffmpeg_cmd = [ + c["cmd"] for c in self.config.ffmpeg_cmds if "detect" in c["roles"] + ][0] + self.ffmpeg_detect_process = start_or_restart_ffmpeg( + ffmpeg_cmd, self.logger, self.logpipe, self.frame_size + ) + self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid + self.capture_thread = CameraCaptureRunner( + self.config, + self.shm_frame_count, + self.frame_index, + self.ffmpeg_detect_process, + self.frame_shape, + self.frame_queue, + self.camera_fps, + self.skipped_fps, + self.stop_event, + ) + self.capture_thread.start() + + def start_all_ffmpeg(self): + """Start all ffmpeg processes (detection and others).""" + logger.debug(f"Starting all ffmpeg processes for {self.config.name}") + self.start_ffmpeg_detect() + for c in self.config.ffmpeg_cmds: + if "detect" in c["roles"]: + continue + logpipe = LogPipe( + f"ffmpeg.{self.config.name}.{'_'.join(sorted(c['roles']))}" + ) + self.ffmpeg_other_processes.append( + { + "cmd": c["cmd"], + "roles": c["roles"], + "logpipe": logpipe, + "process": start_or_restart_ffmpeg(c["cmd"], self.logger, logpipe), + } + ) + + def stop_all_ffmpeg(self): + """Stop all ffmpeg processes (detection and others).""" + logger.debug(f"Stopping all ffmpeg processes for {self.config.name}") + if self.capture_thread is not None and self.capture_thread.is_alive(): + self.capture_thread.join(timeout=5) + if self.capture_thread.is_alive(): + self.logger.warning( + f"Capture thread for {self.config.name} did not stop gracefully." + ) + if self.ffmpeg_detect_process is not None: + stop_ffmpeg(self.ffmpeg_detect_process, self.logger) + self.ffmpeg_detect_process = None + for p in self.ffmpeg_other_processes[:]: + if p["process"] is not None: + stop_ffmpeg(p["process"], self.logger) + p["logpipe"].close() + self.ffmpeg_other_processes.clear() + + +class CameraCaptureRunner(threading.Thread): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + frame_index: int, + ffmpeg_process, + frame_shape: tuple[int, int], + frame_queue: Queue, + fps: Value, + skipped_fps: Value, + stop_event: MpEvent, + ): + threading.Thread.__init__(self) + self.name = f"capture:{config.name}" + self.config = config + self.shm_frame_count = shm_frame_count + self.frame_index = frame_index + self.frame_shape = frame_shape + self.frame_queue = frame_queue + self.fps = fps + self.stop_event = stop_event + self.skipped_fps = skipped_fps + self.frame_manager = SharedMemoryFrameManager() + self.ffmpeg_process = ffmpeg_process + self.current_frame = Value("d", 0.0) + self.last_frame = 0 + + def run(self): + capture_frames( + self.ffmpeg_process, + self.config, + self.shm_frame_count, + self.frame_index, + self.frame_shape, + self.frame_manager, + self.frame_queue, + self.fps, + self.skipped_fps, + self.current_frame, + self.stop_event, + ) + + +class CameraCapture(FrigateProcess): + def __init__( + self, + config: CameraConfig, + shm_frame_count: int, + camera_metrics: CameraMetrics, + stop_event: MpEvent, + log_config: LoggerConfig | None = None, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name=f"frigate.capture:{config.name}", + daemon=True, + ) + self.config = config + self.shm_frame_count = shm_frame_count + self.camera_metrics = camera_metrics + self.log_config = log_config + + def run(self) -> None: + self.pre_run_setup(self.log_config) + camera_watchdog = CameraWatchdog( + self.config, + self.shm_frame_count, + self.camera_metrics.frame_queue, + self.camera_metrics.camera_fps, + self.camera_metrics.skipped_fps, + self.camera_metrics.ffmpeg_pid, + self.stop_event, + ) + camera_watchdog.start() + camera_watchdog.join() + + +class CameraTracker(FrigateProcess): + def __init__( + self, + config: CameraConfig, + model_config: ModelConfig, + labelmap: dict[int, str], + detection_queue: Queue, + detected_objects_queue, + camera_metrics: CameraMetrics, + ptz_metrics: PTZMetrics, + region_grid: list[list[dict[str, Any]]], + stop_event: MpEvent, + log_config: LoggerConfig | None = None, + ) -> None: + super().__init__( + stop_event, + PROCESS_PRIORITY_HIGH, + name=f"frigate.process:{config.name}", + daemon=True, + ) + self.config = config + self.model_config = model_config + self.labelmap = labelmap + self.detection_queue = detection_queue + self.detected_objects_queue = detected_objects_queue + self.camera_metrics = camera_metrics + self.ptz_metrics = ptz_metrics + self.region_grid = region_grid + self.log_config = log_config + + def run(self) -> None: + self.pre_run_setup(self.log_config) + frame_queue = self.camera_metrics.frame_queue + frame_shape = self.config.frame_shape + + motion_detector = ImprovedMotionDetector( + frame_shape, + self.config.motion, + self.config.detect.fps, + name=self.config.name, + ptz_metrics=self.ptz_metrics, + ) + object_detector = RemoteObjectDetector( + self.config.name, + self.labelmap, + self.detection_queue, + self.model_config, + self.stop_event, + ) + + object_tracker = NorfairTracker(self.config, self.ptz_metrics) + + frame_manager = SharedMemoryFrameManager() + + # create communication for region grid updates + requestor = InterProcessRequestor() + + process_frames( + requestor, + frame_queue, + frame_shape, + self.model_config, + self.config, + frame_manager, + motion_detector, + object_detector, + object_tracker, + self.detected_objects_queue, + self.camera_metrics, + self.stop_event, + self.ptz_metrics, + self.region_grid, + ) + + # empty the frame queue + logger.info(f"{self.config.name}: emptying frame queue") + while not frame_queue.empty(): + (frame_name, _) = frame_queue.get(False) + frame_manager.delete(frame_name) + + logger.info(f"{self.config.name}: exiting subprocess") + + +def detect( + detect_config: DetectConfig, + object_detector, + frame, + model_config: ModelConfig, + region, + objects_to_track, + object_filters, +): + tensor_input = create_tensor_input(frame, model_config, region) + + detections = [] + region_detections = object_detector.detect(tensor_input) + for d in region_detections: + box = d[2] + size = region[2] - region[0] + x_min = int(max(0, (box[1] * size) + region[0])) + y_min = int(max(0, (box[0] * size) + region[1])) + x_max = int(min(detect_config.width - 1, (box[3] * size) + region[0])) + y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1])) + + # ignore objects that were detected outside the frame + if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1): + continue + + width = x_max - x_min + height = y_max - y_min + area = width * height + ratio = width / max(1, height) + det = (d[0], d[1], (x_min, y_min, x_max, y_max), area, ratio, region) + # apply object filters + if is_object_filtered(det, objects_to_track, object_filters): + continue + detections.append(det) + return detections + + +def process_frames( + requestor: InterProcessRequestor, + frame_queue: Queue, + frame_shape: tuple[int, int], + model_config: ModelConfig, + camera_config: CameraConfig, + frame_manager: FrameManager, + motion_detector: MotionDetector, + object_detector: RemoteObjectDetector, + object_tracker: ObjectTracker, + detected_objects_queue: Queue, + camera_metrics: CameraMetrics, + stop_event: MpEvent, + ptz_metrics: PTZMetrics, + region_grid: list[list[dict[str, Any]]], + exit_on_empty: bool = False, +): + next_region_update = get_tomorrow_at_time(2) + config_subscriber = CameraConfigUpdateSubscriber( + None, + {camera_config.name: camera_config}, + [ + CameraConfigUpdateEnum.detect, + CameraConfigUpdateEnum.enabled, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, + ], + ) + + fps_tracker = EventsPerSecond() + fps_tracker.start() + + startup_scan = True + stationary_frame_counter = 0 + camera_enabled = True + + region_min_size = get_min_region_size(model_config) + + attributes_map = model_config.attributes_map + all_attributes = model_config.all_attributes + + # remove license_plate from attributes if this camera is a dedicated LPR cam + if camera_config.type == CameraTypeEnum.lpr: + modified_attributes_map = model_config.attributes_map.copy() + + if ( + "car" in modified_attributes_map + and "license_plate" in modified_attributes_map["car"] + ): + modified_attributes_map["car"] = [ + attr + for attr in modified_attributes_map["car"] + if attr != "license_plate" + ] + + attributes_map = modified_attributes_map + + all_attributes = [ + attr for attr in model_config.all_attributes if attr != "license_plate" + ] + + while not stop_event.is_set(): + updated_configs = config_subscriber.check_for_updates() + + if "enabled" in updated_configs: + prev_enabled = camera_enabled + camera_enabled = camera_config.enabled + + if "motion" in updated_configs: + motion_detector.config = camera_config.motion + motion_detector.update_mask() + + if ( + not camera_enabled + and prev_enabled != camera_enabled + and camera_metrics.frame_queue.empty() + ): + logger.debug( + f"Camera {camera_config.name} disabled, clearing tracked objects" + ) + prev_enabled = camera_enabled + + # Clear norfair's dictionaries + object_tracker.tracked_objects.clear() + object_tracker.disappeared.clear() + object_tracker.stationary_box_history.clear() + object_tracker.positions.clear() + object_tracker.track_id_map.clear() + + # Clear internal norfair states + for trackers_by_type in object_tracker.trackers.values(): + for tracker in trackers_by_type.values(): + tracker.tracked_objects = [] + for tracker in object_tracker.default_tracker.values(): + tracker.tracked_objects = [] + + if not camera_enabled: + time.sleep(0.1) + continue + + if datetime.now().astimezone(timezone.utc) > next_region_update: + region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name) + next_region_update = get_tomorrow_at_time(2) + + try: + if exit_on_empty: + frame_name, frame_time = frame_queue.get(False) + else: + frame_name, frame_time = frame_queue.get(True, 1) + except queue.Empty: + if exit_on_empty: + logger.info("Exiting track_objects...") + break + continue + + camera_metrics.detection_frame.value = frame_time + ptz_metrics.frame_time.value = frame_time + + frame = frame_manager.get(frame_name, (frame_shape[0] * 3 // 2, frame_shape[1])) + + if frame is None: + logger.debug( + f"{camera_config.name}: frame {frame_time} is not in memory store." + ) + continue + + # look for motion if enabled + motion_boxes = motion_detector.detect(frame) + + regions = [] + consolidated_detections = [] + + # if detection is disabled + if not camera_config.detect.enabled: + object_tracker.match_and_update(frame_name, frame_time, []) + else: + # get stationary object ids + # check every Nth frame for stationary objects + # disappeared objects are not stationary + # also check for overlapping motion boxes + if stationary_frame_counter == camera_config.detect.stationary.interval: + stationary_frame_counter = 0 + stationary_object_ids = [] + else: + stationary_frame_counter += 1 + stationary_object_ids = [ + obj["id"] + for obj in object_tracker.tracked_objects.values() + # if it has exceeded the stationary threshold + if obj["motionless_count"] + >= camera_config.detect.stationary.threshold + # and it hasn't disappeared + and object_tracker.disappeared[obj["id"]] == 0 + # and it doesn't overlap with any current motion boxes when not calibrating + and not intersects_any( + obj["box"], + [] if motion_detector.is_calibrating() else motion_boxes, + ) + ] + + # get tracked object boxes that aren't stationary + tracked_object_boxes = [ + ( + # use existing object box for stationary objects + obj["estimate"] + if obj["motionless_count"] + < camera_config.detect.stationary.threshold + else obj["box"] + ) + for obj in object_tracker.tracked_objects.values() + if obj["id"] not in stationary_object_ids + ] + object_boxes = tracked_object_boxes + object_tracker.untracked_object_boxes + + # get consolidated regions for tracked objects + regions = [ + get_cluster_region( + frame_shape, region_min_size, candidate, object_boxes + ) + for candidate in get_cluster_candidates( + frame_shape, region_min_size, object_boxes + ) + ] + + # only add in the motion boxes when not calibrating and a ptz is not moving via autotracking + # ptz_moving_at_frame_time() always returns False for non-autotracking cameras + if not motion_detector.is_calibrating() and not ptz_moving_at_frame_time( + frame_time, + ptz_metrics.start_time.value, + ptz_metrics.stop_time.value, + ): + # find motion boxes that are not inside tracked object regions + standalone_motion_boxes = [ + b for b in motion_boxes if not inside_any(b, regions) + ] + + if standalone_motion_boxes: + motion_clusters = get_cluster_candidates( + frame_shape, + region_min_size, + standalone_motion_boxes, + ) + motion_regions = [ + get_cluster_region_from_grid( + frame_shape, + region_min_size, + candidate, + standalone_motion_boxes, + region_grid, + ) + for candidate in motion_clusters + ] + regions += motion_regions + + # if starting up, get the next startup scan region + if startup_scan: + for region in get_startup_regions( + frame_shape, region_min_size, region_grid + ): + regions.append(region) + startup_scan = False + + # resize regions and detect + # seed with stationary objects + detections = [ + ( + obj["label"], + obj["score"], + obj["box"], + obj["area"], + obj["ratio"], + obj["region"], + ) + for obj in object_tracker.tracked_objects.values() + if obj["id"] in stationary_object_ids + ] + + for region in regions: + detections.extend( + detect( + camera_config.detect, + object_detector, + frame, + model_config, + region, + camera_config.objects.track, + camera_config.objects.filters, + ) + ) + + consolidated_detections = reduce_detections(frame_shape, detections) + + # if detection was run on this frame, consolidate + if len(regions) > 0: + tracked_detections = [ + d for d in consolidated_detections if d[0] not in all_attributes + ] + # now that we have refined our detections, we need to track objects + object_tracker.match_and_update( + frame_name, frame_time, tracked_detections + ) + # else, just update the frame times for the stationary objects + else: + object_tracker.update_frame_times(frame_name, frame_time) + + # group the attribute detections based on what label they apply to + attribute_detections: dict[str, list[TrackedObjectAttribute]] = {} + for label, attribute_labels in attributes_map.items(): + attribute_detections[label] = [ + TrackedObjectAttribute(d) + for d in consolidated_detections + if d[0] in attribute_labels + ] + + # build detections + detections = {} + for obj in object_tracker.tracked_objects.values(): + detections[obj["id"]] = {**obj, "attributes": []} + + # find the best object for each attribute to be assigned to + all_objects: list[dict[str, Any]] = object_tracker.tracked_objects.values() + for attributes in attribute_detections.values(): + for attribute in attributes: + filtered_objects = filter( + lambda o: attribute.label in attributes_map.get(o["label"], []), + all_objects, + ) + selected_object_id = attribute.find_best_object(filtered_objects) + + if selected_object_id is not None: + detections[selected_object_id]["attributes"].append( + attribute.get_tracking_data() + ) + + # debug object tracking + if False: + bgr_frame = cv2.cvtColor( + frame, + cv2.COLOR_YUV2BGR_I420, + ) + object_tracker.debug_draw(bgr_frame, frame_time) + cv2.imwrite( + f"debug/frames/track-{'{:.6f}'.format(frame_time)}.jpg", bgr_frame + ) + # debug + if False: + bgr_frame = cv2.cvtColor( + frame, + cv2.COLOR_YUV2BGR_I420, + ) + + for m_box in motion_boxes: + cv2.rectangle( + bgr_frame, + (m_box[0], m_box[1]), + (m_box[2], m_box[3]), + (0, 0, 255), + 2, + ) + + for b in tracked_object_boxes: + cv2.rectangle( + bgr_frame, + (b[0], b[1]), + (b[2], b[3]), + (255, 0, 0), + 2, + ) + + for obj in object_tracker.tracked_objects.values(): + if obj["frame_time"] == frame_time: + thickness = 2 + color = model_config.colormap.get(obj["label"], (255, 255, 255)) + else: + thickness = 1 + color = (255, 0, 0) + + # draw the bounding boxes on the frame + box = obj["box"] + + draw_box_with_label( + bgr_frame, + box[0], + box[1], + box[2], + box[3], + obj["label"], + obj["id"], + thickness=thickness, + color=color, + ) + + for region in regions: + cv2.rectangle( + bgr_frame, + (region[0], region[1]), + (region[2], region[3]), + (0, 255, 0), + 2, + ) + + cv2.imwrite( + f"debug/frames/{camera_config.name}-{'{:.6f}'.format(frame_time)}.jpg", + bgr_frame, + ) + # add to the queue if not full + if detected_objects_queue.full(): + frame_manager.close(frame_name) + continue + else: + fps_tracker.update() + camera_metrics.process_fps.value = fps_tracker.eps() + detected_objects_queue.put( + ( + camera_config.name, + frame_name, + frame_time, + detections, + motion_boxes, + regions, + ) + ) + camera_metrics.detection_fps.value = object_detector.fps.eps() + frame_manager.close(frame_name) + + motion_detector.stop() + requestor.stop() + config_subscriber.stop() diff --git a/sam2-cpu/frigate-dev/frigate/watchdog.py b/sam2-cpu/frigate-dev/frigate/watchdog.py new file mode 100644 index 0000000..4c49de1 --- /dev/null +++ b/sam2-cpu/frigate-dev/frigate/watchdog.py @@ -0,0 +1,41 @@ +import datetime +import logging +import threading +import time +from multiprocessing.synchronize import Event as MpEvent + +from frigate.object_detection.base import ObjectDetectProcess +from frigate.util.services import restart_frigate + +logger = logging.getLogger(__name__) + + +class FrigateWatchdog(threading.Thread): + def __init__(self, detectors: dict[str, ObjectDetectProcess], stop_event: MpEvent): + super().__init__(name="frigate_watchdog") + self.detectors = detectors + self.stop_event = stop_event + + def run(self) -> None: + time.sleep(10) + while not self.stop_event.wait(10): + now = datetime.datetime.now().timestamp() + + # check the detection processes + for detector in self.detectors.values(): + detection_start = detector.detection_start.value # type: ignore[attr-defined] + # issue https://github.com/python/typeshed/issues/8799 + # from mypy 0.981 onwards + if detection_start > 0.0 and now - detection_start > 10: + logger.info( + "Detection appears to be stuck. Restarting detection process..." + ) + detector.start_or_restart() + elif ( + detector.detect_process is not None + and not detector.detect_process.is_alive() + ): + logger.info("Detection appears to have stopped. Exiting Frigate...") + restart_frigate() + + logger.info("Exiting watchdog...") diff --git a/sam2-cpu/frigate-dev/generate_config_translations.py b/sam2-cpu/frigate-dev/generate_config_translations.py new file mode 100644 index 0000000..c19578f --- /dev/null +++ b/sam2-cpu/frigate-dev/generate_config_translations.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +Generate English translation JSON files from Pydantic config models. + +This script dynamically extracts all top-level config sections from FrigateConfig +and generates JSON translation files with titles and descriptions for the web UI. +""" + +import json +import logging +import shutil +from pathlib import Path +from typing import Any, Dict, Optional, get_args, get_origin + +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from frigate.config.config import FrigateConfig + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def get_field_translations(field_info: FieldInfo) -> Dict[str, str]: + """Extract title and description from a Pydantic field.""" + translations = {} + + if field_info.title: + translations["label"] = field_info.title + + if field_info.description: + translations["description"] = field_info.description + + return translations + + +def process_model_fields(model: type[BaseModel]) -> Dict[str, Any]: + """ + Recursively process a Pydantic model to extract translations. + + Returns a nested dictionary structure matching the config schema, + with title and description for each field. + """ + translations = {} + + model_fields = model.model_fields + + for field_name, field_info in model_fields.items(): + field_translations = get_field_translations(field_info) + + # Get the field's type annotation + field_type = field_info.annotation + + # Handle Optional types + origin = get_origin(field_type) + + if origin is Optional or ( + hasattr(origin, "__name__") and origin.__name__ == "UnionType" + ): + args = get_args(field_type) + field_type = next( + (arg for arg in args if arg is not type(None)), field_type + ) + + # Handle Dict types (like Dict[str, CameraConfig]) + if get_origin(field_type) is dict: + dict_args = get_args(field_type) + + if len(dict_args) >= 2: + value_type = dict_args[1] + + if isinstance(value_type, type) and issubclass(value_type, BaseModel): + nested_translations = process_model_fields(value_type) + + if nested_translations: + field_translations["properties"] = nested_translations + elif isinstance(field_type, type) and issubclass(field_type, BaseModel): + nested_translations = process_model_fields(field_type) + if nested_translations: + field_translations["properties"] = nested_translations + + if field_translations: + translations[field_name] = field_translations + + return translations + + +def generate_section_translation( + section_name: str, field_info: FieldInfo +) -> Dict[str, Any]: + """ + Generate translation structure for a top-level config section. + """ + section_translations = get_field_translations(field_info) + field_type = field_info.annotation + origin = get_origin(field_type) + + if origin is Optional or ( + hasattr(origin, "__name__") and origin.__name__ == "UnionType" + ): + args = get_args(field_type) + field_type = next((arg for arg in args if arg is not type(None)), field_type) + + # Handle Dict types (like detectors, cameras, camera_groups) + if get_origin(field_type) is dict: + dict_args = get_args(field_type) + if len(dict_args) >= 2: + value_type = dict_args[1] + if isinstance(value_type, type) and issubclass(value_type, BaseModel): + nested = process_model_fields(value_type) + if nested: + section_translations["properties"] = nested + + # If the field itself is a BaseModel, process it + elif isinstance(field_type, type) and issubclass(field_type, BaseModel): + nested = process_model_fields(field_type) + if nested: + section_translations["properties"] = nested + + return section_translations + + +def main(): + """Main function to generate config translations.""" + + # Define output directory + output_dir = Path(__file__).parent / "web" / "public" / "locales" / "en" / "config" + + logger.info(f"Output directory: {output_dir}") + + # Clean and recreate the output directory + if output_dir.exists(): + logger.info(f"Removing existing directory: {output_dir}") + shutil.rmtree(output_dir) + + logger.info(f"Creating directory: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + + config_fields = FrigateConfig.model_fields + logger.info(f"Found {len(config_fields)} top-level config sections") + + for field_name, field_info in config_fields.items(): + if field_name.startswith("_"): + continue + + logger.info(f"Processing section: {field_name}") + section_data = generate_section_translation(field_name, field_info) + + if not section_data: + logger.warning(f"No translations found for section: {field_name}") + continue + + output_file = output_dir / f"{field_name}.json" + with open(output_file, "w", encoding="utf-8") as f: + json.dump(section_data, f, indent=2, ensure_ascii=False) + + logger.info(f"Generated: {output_file}") + + logger.info("Translation generation complete!") + + +if __name__ == "__main__": + main() diff --git a/sam2-cpu/frigate-dev/labelmap.txt b/sam2-cpu/frigate-dev/labelmap.txt new file mode 100644 index 0000000..79fff17 --- /dev/null +++ b/sam2-cpu/frigate-dev/labelmap.txt @@ -0,0 +1,91 @@ +0 person +1 bicycle +2 car +3 motorcycle +4 airplane +5 bus +6 train +7 car +8 boat +9 traffic light +10 fire hydrant +11 street sign +12 stop sign +13 parking meter +14 bench +15 bird +16 cat +17 dog +18 horse +19 sheep +20 cow +21 elephant +22 bear +23 zebra +24 giraffe +25 hat +26 backpack +27 umbrella +28 shoe +29 eye glasses +30 handbag +31 tie +32 suitcase +33 frisbee +34 skis +35 snowboard +36 sports ball +37 kite +38 baseball bat +39 baseball glove +40 skateboard +41 surfboard +42 tennis racket +43 bottle +44 plate +45 wine glass +46 cup +47 fork +48 knife +49 spoon +50 bowl +51 banana +52 apple +53 sandwich +54 orange +55 broccoli +56 carrot +57 hot dog +58 pizza +59 donut +60 cake +61 chair +62 couch +63 potted plant +64 bed +65 mirror +66 dining table +67 window +68 desk +69 toilet +70 door +71 tv +72 laptop +73 mouse +74 remote +75 keyboard +76 cell phone +77 microwave +78 oven +79 toaster +80 sink +81 refrigerator +82 blender +83 book +84 clock +85 vase +86 scissors +87 teddy bear +88 hair drier +89 toothbrush +90 hair brush \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/migrations/001_create_events_table.py b/sam2-cpu/frigate-dev/migrations/001_create_events_table.py new file mode 100644 index 0000000..57f9aa6 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/001_create_events_table.py @@ -0,0 +1,38 @@ +"""Peewee migrations -- 001_create_events_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "event" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "label" VARCHAR(20) NOT NULL, "camera" VARCHAR(20) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "top_score" REAL NOT NULL, "false_positive" INTEGER NOT NULL, "zones" JSON NOT NULL, "thumbnail" TEXT NOT NULL)' + ) + migrator.sql('CREATE INDEX IF NOT EXISTS "event_label" ON "event" ("label")') + migrator.sql('CREATE INDEX IF NOT EXISTS "event_camera" ON "event" ("camera")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/002_add_clip_snapshot.py b/sam2-cpu/frigate-dev/migrations/002_add_clip_snapshot.py new file mode 100644 index 0000000..47a46f5 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/002_add_clip_snapshot.py @@ -0,0 +1,40 @@ +"""Peewee migrations -- 002_add_clip_snapshot.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + has_clip=pw.BooleanField(default=True), + has_snapshot=pw.BooleanField(default=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["has_clip", "has_snapshot"]) diff --git a/sam2-cpu/frigate-dev/migrations/003_create_recordings_table.py b/sam2-cpu/frigate-dev/migrations/003_create_recordings_table.py new file mode 100644 index 0000000..3956ae9 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/003_create_recordings_table.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 003_create_recordings_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "recordings" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_camera" ON "recordings" ("camera")' + ) + migrator.sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "recordings_path" ON "recordings" ("path")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_start_time_end_time" ON "recordings" (start_time, end_time)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/004_add_bbox_region_area.py b/sam2-cpu/frigate-dev/migrations/004_add_bbox_region_area.py new file mode 100644 index 0000000..a1aa35a --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/004_add_bbox_region_area.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + region=JSONField(default=[]), + box=JSONField(default=[]), + area=pw.IntegerField(default=0), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["region", "box", "area"]) diff --git a/sam2-cpu/frigate-dev/migrations/005_make_end_time_nullable.py b/sam2-cpu/frigate-dev/migrations/005_make_end_time_nullable.py new file mode 100644 index 0000000..d80d31d --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/005_make_end_time_nullable.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null(Event, "end_time") + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/006_add_motion_active_objects.py b/sam2-cpu/frigate-dev/migrations/006_add_motion_active_objects.py new file mode 100644 index 0000000..2fe1f90 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/006_add_motion_active_objects.py @@ -0,0 +1,43 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + objects=pw.IntegerField(null=True), + motion=pw.IntegerField(null=True), + ) + migrator.sql( + 'CREATE INDEX "recordings_activity" ON "recordings" ("camera", "start_time" DESC, "regions")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["objects", "motion"]) diff --git a/sam2-cpu/frigate-dev/migrations/007_add_retain_indefinitely.py b/sam2-cpu/frigate-dev/migrations/007_add_retain_indefinitely.py new file mode 100644 index 0000000..e5d07ab --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/007_add_retain_indefinitely.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 007_add_retain_indefinitely.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + retain_indefinitely=pw.BooleanField(default=False), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["retain_indefinitely"]) diff --git a/sam2-cpu/frigate-dev/migrations/008_add_sub_label.py b/sam2-cpu/frigate-dev/migrations/008_add_sub_label.py new file mode 100644 index 0000000..bba3834 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/008_add_sub_label.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 008_add_sub_label.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + sub_label=pw.CharField(max_length=20, null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["sub_label"]) diff --git a/sam2-cpu/frigate-dev/migrations/009_add_object_filter_ratio.py b/sam2-cpu/frigate-dev/migrations/009_add_object_filter_ratio.py new file mode 100644 index 0000000..77a25eb --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/009_add_object_filter_ratio.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 009_add_object_filter_ratio.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + ratio=pw.FloatField(default=1.0), # Assume that existing detections are square + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["ratio"]) diff --git a/sam2-cpu/frigate-dev/migrations/010_add_plus_image_id.py b/sam2-cpu/frigate-dev/migrations/010_add_plus_image_id.py new file mode 100644 index 0000000..d403dbb --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/010_add_plus_image_id.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 010_add_plus_image_id.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + plus_id=pw.CharField(max_length=30, null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Event, ["plus_id"]) diff --git a/sam2-cpu/frigate-dev/migrations/011_update_indexes.py b/sam2-cpu/frigate-dev/migrations/011_update_indexes.py new file mode 100644 index 0000000..6d411d3 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/011_update_indexes.py @@ -0,0 +1,40 @@ +"""Peewee migrations -- 011_update_indexes.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX "event_start_time_end_time" ON "event" ("start_time" DESC, "end_time" DESC)' + ) + migrator.sql("DROP INDEX recordings_start_time_end_time") + migrator.sql( + 'CREATE INDEX "recordings_end_time_start_time" ON "recordings" ("end_time" DESC, "start_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/012_add_segment_size.py b/sam2-cpu/frigate-dev/migrations/012_add_segment_size.py new file mode 100644 index 0000000..8ea91a1 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/012_add_segment_size.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 012_add_segment_size.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + segment_size=pw.FloatField(default=0), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["segment_size"]) diff --git a/sam2-cpu/frigate-dev/migrations/013_create_timeline_table.py b/sam2-cpu/frigate-dev/migrations/013_create_timeline_table.py new file mode 100644 index 0000000..9ed2606 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/013_create_timeline_table.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 013_create_timeline_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "timeline" ("timestamp" DATETIME NOT NULL, "camera" VARCHAR(20) NOT NULL, "source" VARCHAR(20) NOT NULL, "source_id" VARCHAR(30), "class_type" VARCHAR(50) NOT NULL, "data" JSON)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_camera" ON "timeline" ("camera")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_source" ON "timeline" ("source")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "timeline_source_id" ON "timeline" ("source_id")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/014_event_updates_for_fp.py b/sam2-cpu/frigate-dev/migrations/014_event_updates_for_fp.py new file mode 100644 index 0000000..f44f6c9 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/014_event_updates_for_fp.py @@ -0,0 +1,45 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Event, + score=pw.FloatField(null=True), + model_hash=pw.CharField(max_length=32, null=True), + detector_type=pw.CharField(max_length=32, null=True), + model_type=pw.CharField(max_length=32, null=True), + ) + + migrator.drop_not_null(Event, "area", "false_positive") + migrator.add_default(Event, "false_positive", 0) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/015_event_refactor.py b/sam2-cpu/frigate-dev/migrations/015_event_refactor.py new file mode 100644 index 0000000..92d8a16 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/015_event_refactor.py @@ -0,0 +1,43 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null( + Event, "top_score", "score", "region", "box", "area", "ratio" + ) + migrator.add_fields( + Event, + data=JSONField(default={}), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/016_sublabel_increase.py b/sam2-cpu/frigate-dev/migrations/016_sublabel_increase.py new file mode 100644 index 0000000..66411ff --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/016_sublabel_increase.py @@ -0,0 +1,11 @@ +import peewee as pw + +from frigate.models import Event + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.change_fields(Event, sub_label=pw.CharField(max_length=100, null=True)) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.change_fields(Event, sub_label=pw.CharField(max_length=20, null=True)) diff --git a/sam2-cpu/frigate-dev/migrations/017_update_indexes.py b/sam2-cpu/frigate-dev/migrations/017_update_indexes.py new file mode 100644 index 0000000..63685ea --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/017_update_indexes.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 017_update_indexes.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX "recordings_camera_segment_size" ON "recordings" ("camera", "segment_size")' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/018_add_dbfs.py b/sam2-cpu/frigate-dev/migrations/018_add_dbfs.py new file mode 100644 index 0000000..485e954 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/018_add_dbfs.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 018_add_dbfs.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + dBFS=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["dBFS"]) diff --git a/sam2-cpu/frigate-dev/migrations/019_create_regions_table.py b/sam2-cpu/frigate-dev/migrations/019_create_regions_table.py new file mode 100644 index 0000000..961aaf8 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/019_create_regions_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 019_create_regions_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "regions" ("camera" VARCHAR(20) NOT NULL PRIMARY KEY, "last_update" DATETIME NOT NULL, "grid" JSON)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/020_update_index_recordings.py b/sam2-cpu/frigate-dev/migrations/020_update_index_recordings.py new file mode 100644 index 0000000..d6af71c --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/020_update_index_recordings.py @@ -0,0 +1,41 @@ +"""Peewee migrations -- 020_update_index_recordings.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql("DROP INDEX recordings_end_time_start_time") + migrator.sql( + 'CREATE INDEX "recordings_camera_start_time_end_time" ON "recordings" ("camera", "start_time" DESC, "end_time" DESC)' + ) + migrator.sql( + 'CREATE INDEX "recordings_api_recordings_summary" ON "recordings" ("camera", "start_time" DESC, "duration", "motion", "objects")' + ) + migrator.sql('CREATE INDEX "recordings_start_time" ON "recordings" ("start_time")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/021_create_previews_table.py b/sam2-cpu/frigate-dev/migrations/021_create_previews_table.py new file mode 100644 index 0000000..b775360 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/021_create_previews_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 021_create_previews_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "previews" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/022_create_review_segment_table.py b/sam2-cpu/frigate-dev/migrations/022_create_review_segment_table.py new file mode 100644 index 0000000..91d0c8c --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/022_create_review_segment_table.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 022_create_review_segment_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "reviewsegment" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME, "has_been_reviewed" INTEGER NOT NULL, "severity" VARCHAR(30) NOT NULL, "thumb_path" VARCHAR(255) NOT NULL, "data" JSON NOT NULL)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "review_segment_camera" ON "reviewsegment" ("camera")' + ) + migrator.sql( + 'CREATE INDEX "review_segment_start_time_end_time" ON "reviewsegment" ("start_time" DESC, "end_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/023_add_regions.py b/sam2-cpu/frigate-dev/migrations/023_add_regions.py new file mode 100644 index 0000000..7649baa --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/023_add_regions.py @@ -0,0 +1,39 @@ +"""Peewee migrations -- 023_add_regions.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Recordings + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + regions=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["regions"]) diff --git a/sam2-cpu/frigate-dev/migrations/024_create_export_table.py b/sam2-cpu/frigate-dev/migrations/024_create_export_table.py new file mode 100644 index 0000000..8de2f17 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/024_create_export_table.py @@ -0,0 +1,37 @@ +"""Peewee migrations -- 024_create_export_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "export" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "name" VARCHAR(100) NOT NULL, "date" DATETIME NOT NULL, "video_path" VARCHAR(255) NOT NULL, "thumb_path" VARCHAR(255) NOT NULL, "in_progress" INTEGER NOT NULL)' + ) + migrator.sql('CREATE INDEX IF NOT EXISTS "export_camera" ON "export" ("camera")') + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/025_create_user_table.py b/sam2-cpu/frigate-dev/migrations/025_create_user_table.py new file mode 100644 index 0000000..dec57d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/025_create_user_table.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 025_create_user_table.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "user" ("username" VARCHAR(30) NOT NULL PRIMARY KEY, "password_hash" VARCHAR(120) NOT NULL)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/026_add_notification_tokens.py b/sam2-cpu/frigate-dev/migrations/026_add_notification_tokens.py new file mode 100644 index 0000000..23860c5 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/026_add_notification_tokens.py @@ -0,0 +1,40 @@ +"""Peewee migrations + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw +from playhouse.sqlite_ext import JSONField + +from frigate.models import User + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + User, + notification_tokens=JSONField(default=[]), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + pass diff --git a/sam2-cpu/frigate-dev/migrations/027_create_explore_index.py b/sam2-cpu/frigate-dev/migrations/027_create_explore_index.py new file mode 100644 index 0000000..f08c0bb --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/027_create_explore_index.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 027_create_explore_index.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "event_label_start_time" ON "event" ("label", "start_time" DESC)' + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('DROP INDEX IF EXISTS "event_label_start_time"') diff --git a/sam2-cpu/frigate-dev/migrations/028_optional_event_thumbnail.py b/sam2-cpu/frigate-dev/migrations/028_optional_event_thumbnail.py new file mode 100644 index 0000000..5217700 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/028_optional_event_thumbnail.py @@ -0,0 +1,36 @@ +"""Peewee migrations -- 028_optional_event_thumbnail.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import Event + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.drop_not_null(Event, "thumbnail") + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.add_not_null(Event, "thumbnail") diff --git a/sam2-cpu/frigate-dev/migrations/029_add_user_role.py b/sam2-cpu/frigate-dev/migrations/029_add_user_role.py new file mode 100644 index 0000000..e0fb1bb --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/029_add_user_role.py @@ -0,0 +1,37 @@ +"""Peewee migrations -- 029_add_user_role.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + 'ALTER TABLE "user" ADD COLUMN "role" VARCHAR(20) NOT NULL DEFAULT \'admin\'' + ) + migrator.sql('UPDATE "user" SET "role" = \'admin\' WHERE "role" IS NULL') + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('ALTER TABLE "user" DROP COLUMN "role"') diff --git a/sam2-cpu/frigate-dev/migrations/030_create_user_review_status.py b/sam2-cpu/frigate-dev/migrations/030_create_user_review_status.py new file mode 100644 index 0000000..ddcf063 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/030_create_user_review_status.py @@ -0,0 +1,89 @@ +"""Peewee migrations -- 030_create_user_review_status.py. + +This migration creates the UserReviewStatus table to track per-user review states, +migrates existing has_been_reviewed data from ReviewSegment to all users in the user table, +and drops the has_been_reviewed column. Rollback drops UserReviewStatus and restores the column. + +Some examples (model - class or model_name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +from frigate.models import User, UserReviewStatus + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + User._meta.database = database + UserReviewStatus._meta.database = database + + migrator.sql( + """ + CREATE TABLE IF NOT EXISTS "userreviewstatus" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "user_id" VARCHAR(30) NOT NULL, + "review_segment_id" VARCHAR(30) NOT NULL, + "has_been_reviewed" INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY ("review_segment_id") REFERENCES "reviewsegment" ("id") ON DELETE CASCADE + ) + """ + ) + + # Add unique index on (user_id, review_segment_id) + migrator.sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "userreviewstatus_user_segment" ON "userreviewstatus" ("user_id", "review_segment_id")' + ) + + # Migrate existing has_been_reviewed data to UserReviewStatus for all users + def migrate_data(): + # Use raw SQL to avoid ORM issues with columns that don't exist yet + cursor = database.execute_sql('SELECT "username" FROM "user"') + all_users = cursor.fetchall() + if not all_users: + return + + cursor = database.execute_sql( + 'SELECT "id" FROM "reviewsegment" WHERE "has_been_reviewed" = 1' + ) + reviewed_segment_ids = [row[0] for row in cursor.fetchall()] + # also migrate for anonymous (unauthenticated users) + usernames = [user[0] for user in all_users] + ["anonymous"] + + for segment_id in reviewed_segment_ids: + for username in usernames: + UserReviewStatus.create( + user_id=username, + review_segment=segment_id, + has_been_reviewed=True, + ) + + if not fake: # Only run data migration if not faking + migrator.run(migrate_data) + + migrator.sql('ALTER TABLE "reviewsegment" DROP COLUMN "has_been_reviewed"') + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql('DROP TABLE IF EXISTS "userreviewstatus"') + # Restore has_been_reviewed column to reviewsegment (no data restoration) + migrator.sql( + 'ALTER TABLE "reviewsegment" ADD COLUMN "has_been_reviewed" INTEGER NOT NULL DEFAULT 0' + ) diff --git a/sam2-cpu/frigate-dev/migrations/031_create_trigger_table.py b/sam2-cpu/frigate-dev/migrations/031_create_trigger_table.py new file mode 100644 index 0000000..c2ac2e0 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/031_create_trigger_table.py @@ -0,0 +1,50 @@ +"""Peewee migrations -- 031_create_trigger_table.py. + +This migration creates the Trigger table to track semantic search triggers for cameras. + +Some examples (model - class or model_name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + CREATE TABLE IF NOT EXISTS trigger ( + camera VARCHAR(20) NOT NULL, + name VARCHAR NOT NULL, + type VARCHAR(10) NOT NULL, + model VARCHAR(30) NOT NULL, + data TEXT NOT NULL, + threshold REAL, + embedding BLOB, + triggering_event_id VARCHAR(30), + last_triggered DATETIME, + PRIMARY KEY (camera, name) + ) + """ + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql("DROP TABLE IF EXISTS trigger") diff --git a/sam2-cpu/frigate-dev/migrations/032_add_password_changed_at.py b/sam2-cpu/frigate-dev/migrations/032_add_password_changed_at.py new file mode 100644 index 0000000..5382c12 --- /dev/null +++ b/sam2-cpu/frigate-dev/migrations/032_add_password_changed_at.py @@ -0,0 +1,42 @@ +"""Peewee migrations -- 032_add_password_changed_at.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import peewee as pw + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + ALTER TABLE user ADD COLUMN password_changed_at DATETIME NULL + """ + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.sql( + """ + ALTER TABLE user DROP COLUMN password_changed_at + """ + ) diff --git a/sam2-cpu/frigate-dev/netlify.toml b/sam2-cpu/frigate-dev/netlify.toml new file mode 100644 index 0000000..59362d2 --- /dev/null +++ b/sam2-cpu/frigate-dev/netlify.toml @@ -0,0 +1,7 @@ +[build] + base = "docs/" + publish = "build" + command = "npm run build" + environment = { NODE_VERSION = "20" } + + diff --git a/sam2-cpu/frigate-dev/notebooks/README.md b/sam2-cpu/frigate-dev/notebooks/README.md new file mode 100644 index 0000000..23afa6e --- /dev/null +++ b/sam2-cpu/frigate-dev/notebooks/README.md @@ -0,0 +1,10 @@ +# Notebooks + +## YOLO-NAS Pretrained + +You can build and download a compatible model with pre-trained weights using [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb). + +> [!WARNING] +> The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html + +The input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired. By default, YOLO_NAS_S is built with YOLO_NAS_M and YOLO_NAS_L sizes also being available for export. \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb b/sam2-cpu/frigate-dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb new file mode 100644 index 0000000..e9ee223 --- /dev/null +++ b/sam2-cpu/frigate-dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb @@ -0,0 +1,88 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rmuF9iKWTbdk" + }, + "outputs": [], + "source": [ + "! pip install -q git+https://github.com/Deci-AI/super-gradients.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NiRCt917KKcL" + }, + "outputs": [], + "source": [ + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/pretrained_models.py\n", + "! sed -i 's/sghub.deci.ai/sg-hub-nv.s3.amazonaws.com/' /usr/local/lib/python3.12/dist-packages/super_gradients/training/utils/checkpoint_utils.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dTB0jy_NNSFz" + }, + "outputs": [], + "source": [ + "from super_gradients.common.object_names import Models\n", + "from super_gradients.conversion import DetectionOutputFormatMode\n", + "from super_gradients.training import models\n", + "\n", + "model = models.get(Models.YOLO_NAS_S, pretrained_weights=\"coco\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GymUghyCNXem" + }, + "outputs": [], + "source": [ + "# export the model for compatibility with Frigate\n", + "\n", + "model.export(\"yolo_nas_s.onnx\",\n", + " output_predictions_format=DetectionOutputFormatMode.FLAT_FORMAT,\n", + " max_predictions_per_image=20,\n", + " num_pre_nms_predictions=300,\n", + " confidence_threshold=0.4,\n", + " input_image_shape=(320,320),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uBhXV5g4Nh42" + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "files.download('yolo_nas_s.onnx')" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/sam2-cpu/frigate-dev/package-lock.json b/sam2-cpu/frigate-dev/package-lock.json new file mode 100644 index 0000000..13ac760 --- /dev/null +++ b/sam2-cpu/frigate-dev/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "frigate", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/sam2-cpu/frigate-dev/process_clip.py b/sam2-cpu/frigate-dev/process_clip.py new file mode 100644 index 0000000..6f474de --- /dev/null +++ b/sam2-cpu/frigate-dev/process_clip.py @@ -0,0 +1,321 @@ +import csv +import json +import logging +import multiprocessing as mp +import os +import subprocess as sp +import sys + +import click +import cv2 +import numpy as np + +sys.path.append("/workspace/frigate") + +from frigate.config import FrigateConfig # noqa: E402 +from frigate.motion import MotionDetector # noqa: E402 +from frigate.object_detection.base import LocalObjectDetector # noqa: E402 +from frigate.track.centroid_tracker import CentroidTracker # noqa: E402 +from frigate.track.object_processing import CameraState # noqa: E402 +from frigate.util import ( # noqa: E402 + EventsPerSecond, + SharedMemoryFrameManager, + draw_box_with_label, +) +from frigate.video import ( # noqa: E402 + capture_frames, + process_frames, + start_or_restart_ffmpeg, +) + +logging.basicConfig(level=logging.DEBUG) + +logger = logging.getLogger(__name__) + + +def get_frame_shape(source): + ffprobe_cmd = [ + "ffprobe", + "-v", + "panic", + "-show_error", + "-show_streams", + "-of", + "json", + source, + ] + p = sp.run(ffprobe_cmd, capture_output=True) + info = json.loads(p.stdout) + + video_info = [s for s in info["streams"] if s["codec_type"] == "video"][0] + + if video_info["height"] != 0 and video_info["width"] != 0: + return (video_info["height"], video_info["width"], 3) + + # fallback to using opencv if ffprobe didn't succeed + video = cv2.VideoCapture(source) + ret, frame = video.read() + frame_shape = frame.shape + video.release() + return frame_shape + + +class ProcessClip: + def __init__(self, clip_path, frame_shape, config: FrigateConfig): + self.clip_path = clip_path + self.camera_name = "camera" + self.config = config + self.camera_config = self.config.cameras["camera"] + self.frame_shape = self.camera_config.frame_shape + self.ffmpeg_cmd = [ + c["cmd"] for c in self.camera_config.ffmpeg_cmds if "detect" in c["roles"] + ][0] + self.frame_manager = SharedMemoryFrameManager() + self.frame_queue = mp.Queue() + self.detected_objects_queue = mp.Queue() + self.camera_state = CameraState(self.camera_name, config, self.frame_manager) + + def load_frames(self): + fps = EventsPerSecond() + skipped_fps = EventsPerSecond() + current_frame = mp.Value("d", 0.0) + frame_size = ( + self.camera_config.frame_shape_yuv[0] + * self.camera_config.frame_shape_yuv[1] + ) + ffmpeg_process = start_or_restart_ffmpeg( + self.ffmpeg_cmd, logger, sp.DEVNULL, frame_size + ) + capture_frames( + ffmpeg_process, + self.camera_name, + self.camera_config.frame_shape_yuv, + self.frame_manager, + self.frame_queue, + fps, + skipped_fps, + current_frame, + ) + ffmpeg_process.wait() + ffmpeg_process.communicate() + + def process_frames( + self, object_detector, objects_to_track=["person"], object_filters={} + ): + mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8) + mask[:] = 255 + motion_detector = MotionDetector(self.frame_shape, self.camera_config.motion) + motion_detector.save_images = False + + object_tracker = CentroidTracker(self.camera_config.detect) + process_info = { + "process_fps": mp.Value("d", 0.0), + "detection_fps": mp.Value("d", 0.0), + "detection_frame": mp.Value("d", 0.0), + } + + detection_enabled = mp.Value("d", 1) + motion_enabled = mp.Value("d", True) + stop_event = mp.Event() + + process_frames( + self.camera_name, + self.frame_queue, + self.frame_shape, + self.config.model, + self.camera_config.detect, + self.frame_manager, + motion_detector, + object_detector, + object_tracker, + self.detected_objects_queue, + process_info, + objects_to_track, + object_filters, + detection_enabled, + motion_enabled, + stop_event, + exit_on_empty=True, + ) + + def stats(self, debug_path=None): + total_regions = 0 + total_motion_boxes = 0 + object_ids = set() + total_frames = 0 + + while not self.detected_objects_queue.empty(): + ( + camera_name, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = self.detected_objects_queue.get() + + if debug_path: + self.save_debug_frame( + debug_path, frame_time, current_tracked_objects.values() + ) + + self.camera_state.update( + frame_time, current_tracked_objects, motion_boxes, regions + ) + total_regions += len(regions) + total_motion_boxes += len(motion_boxes) + top_score = 0 + for id, obj in self.camera_state.tracked_objects.items(): + if not obj.false_positive: + object_ids.add(id) + if obj.top_score > top_score: + top_score = obj.top_score + + total_frames += 1 + + self.frame_manager.delete(self.camera_state.previous_frame_id) + + return { + "total_regions": total_regions, + "total_motion_boxes": total_motion_boxes, + "true_positive_objects": len(object_ids), + "total_frames": total_frames, + "top_score": top_score, + } + + def save_debug_frame(self, debug_path, frame_time, tracked_objects): + current_frame = cv2.cvtColor( + self.frame_manager.get( + f"{self.camera_name}{frame_time}", self.camera_config.frame_shape_yuv + ), + cv2.COLOR_YUV2BGR_I420, + ) + # draw the bounding boxes on the frame + for obj in tracked_objects: + thickness = 2 + color = (0, 0, 175) + if obj["frame_time"] != frame_time: + thickness = 1 + color = (255, 0, 0) + else: + color = (255, 255, 0) + + # draw the bounding boxes on the frame + box = obj["box"] + draw_box_with_label( + current_frame, + box[0], + box[1], + box[2], + box[3], + obj["id"], + f"{int(obj['score'] * 100)}% {int(obj['area'])}", + thickness=thickness, + color=color, + ) + # draw the regions on the frame + region = obj["region"] + draw_box_with_label( + current_frame, + region[0], + region[1], + region[2], + region[3], + "region", + "", + thickness=1, + color=(0, 255, 0), + ) + + cv2.imwrite( + f"{os.path.join(debug_path, os.path.basename(self.clip_path))}.{int(frame_time * 1000000)}.jpg", + current_frame, + ) + + +@click.command() +@click.option("-p", "--path", required=True, help="Path to clip or directory to test.") +@click.option("-l", "--label", default="person", help="Label name to detect.") +@click.option("-o", "--output", default=None, help="File to save csv of data") +@click.option("--debug-path", default=None, help="Path to output frames for debugging.") +def process(path, label, output, debug_path): + clips = [] + if os.path.isdir(path): + files = os.listdir(path) + files.sort() + clips = [os.path.join(path, file) for file in files] + elif os.path.isfile(path): + clips.append(path) + + json_config = { + "mqtt": {"enabled": False}, + "detectors": {"coral": {"type": "edgetpu", "device": "usb"}}, + "cameras": { + "camera": { + "ffmpeg": { + "inputs": [ + { + "path": "path.mp4", + "global_args": "-hide_banner", + "input_args": "-loglevel info", + "roles": ["detect"], + } + ] + }, + "record": {"enabled": False}, + } + }, + } + + object_detector = LocalObjectDetector(labels="/labelmap.txt") + + results = [] + for c in clips: + logger.info(c) + frame_shape = get_frame_shape(c) + + json_config["cameras"]["camera"]["detect"] = { + "height": frame_shape[0], + "width": frame_shape[1], + } + json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c + + frigate_config = FrigateConfig(**json_config) + process_clip = ProcessClip(c, frame_shape, frigate_config) + process_clip.load_frames() + process_clip.process_frames(object_detector, objects_to_track=[label]) + + results.append((c, process_clip.stats(debug_path))) + + positive_count = sum( + 1 for result in results if result[1]["true_positive_objects"] > 0 + ) + print( + f"Objects were detected in {positive_count}/{len(results)}({positive_count / len(results) * 100:.2f}%) clip(s)." + ) + + if output: + # now we will open a file for writing + data_file = open(output, "w") + + # create the csv writer object + csv_writer = csv.writer(data_file) + + # Counter variable used for writing + # headers to the CSV file + count = 0 + + for result in results: + if count == 0: + # Writing headers of CSV file + header = ["file"] + list(result[1].keys()) + csv_writer.writerow(header) + count += 1 + + # Writing data of CSV file + csv_writer.writerow([result[0]] + list(result[1].values())) + + data_file.close() + + +if __name__ == "__main__": + process() diff --git a/sam2-cpu/frigate-dev/pyproject.toml b/sam2-cpu/frigate-dev/pyproject.toml new file mode 100644 index 0000000..d17a60e --- /dev/null +++ b/sam2-cpu/frigate-dev/pyproject.toml @@ -0,0 +1,3 @@ +[tool.ruff.lint] +ignore = ["E501","E711","E712"] +extend-select = ["I"] diff --git a/sam2-cpu/frigate-dev/web/.eslintrc.cjs b/sam2-cpu/frigate-dev/web/.eslintrc.cjs new file mode 100644 index 0000000..ab64df6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/.eslintrc.cjs @@ -0,0 +1,74 @@ +module.exports = { + root: true, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:vitest-globals/recommended", + "plugin:prettier/recommended", + ], + env: { browser: true, es2021: true, "vitest-globals/env": true }, + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: "latest", + sourceType: "module", + }, + settings: { + jest: { + version: 27, + }, + }, + ignorePatterns: ["*.d.ts", "/src/components/ui/*"], + plugins: ["react-hooks", "react-refresh"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "comma-dangle": [ + "error", + { + objects: "always-multiline", + arrays: "always-multiline", + imports: "always-multiline", + }, + ], + "no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "no-console": "error", + "prettier/prettier": [ + "warn", + { + plugins: ["prettier-plugin-tailwindcss"], + }, + ], + }, + overrides: [ + { + files: ["**/*.{ts,tsx}"], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + }, + ], +}; diff --git a/sam2-cpu/frigate-dev/web/.gitignore b/sam2-cpu/frigate-dev/web/.gitignore new file mode 100644 index 0000000..1cac559 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/.prettierrc b/sam2-cpu/frigate-dev/web/.prettierrc new file mode 100644 index 0000000..b4bfed3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/sam2-cpu/frigate-dev/web/.vscode/extensions.json b/sam2-cpu/frigate-dev/web/.vscode/extensions.json new file mode 100644 index 0000000..7dea559 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] + } \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/README.md b/sam2-cpu/frigate-dev/web/README.md new file mode 100644 index 0000000..b30fa73 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/README.md @@ -0,0 +1,25 @@ +This is the Frigate frontend which connects to and provides a User Interface to the Python backend. + +# Web Development + +## Installing Web Dependencies Via NPM + +Within `/web`, run: + +```bash +npm install +``` + +## Running development frontend + +Within `/web`, run: + +```bash +PROXY_HOST= npm run dev +``` + +The Proxy Host can point to your existing Frigate instance. Otherwise defaults to `localhost:5000` if running Frigate on the same machine. + +## Extensions +Install these IDE extensions for an improved development experience: +- eslint diff --git a/sam2-cpu/frigate-dev/web/components.json b/sam2-cpu/frigate-dev/web/components.json new file mode 100644 index 0000000..679fbd7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.cjs", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/sam2-cpu/frigate-dev/web/images/branding/LICENSE b/sam2-cpu/frigate-dev/web/images/branding/LICENSE new file mode 100644 index 0000000..42913f9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/images/branding/LICENSE @@ -0,0 +1,33 @@ +# COPYRIGHT AND TRADEMARK NOTICE + +The images, logos, and icons contained in this directory (the "Brand Assets") are +proprietary to Frigate LLC and are NOT covered by the MIT License governing the +rest of this repository. + +1. TRADEMARK STATUS + The "Frigate" name and the accompanying logo are common law trademarks™ of + Frigate LLC. Frigate LLC reserves all rights to these marks. + +2. LIMITED PERMISSION FOR USE + Permission is hereby granted to display these Brand Assets strictly for the + following purposes: + a. To execute the software interface on a local machine. + b. To identify the software in documentation or reviews (nominative use). + +3. RESTRICTIONS + You may NOT: + a. Use these Brand Assets to represent a derivative work (fork) as an official + product of Frigate LLC. + b. Use these Brand Assets in a way that implies endorsement, sponsorship, or + commercial affiliation with Frigate LLC. + c. Modify or alter the Brand Assets. + +If you fork this repository with the intent to distribute a modified or competing +version of the software, you must replace these Brand Assets with your own +original content. + +For full usage guidelines, strictly see the TRADEMARK.md file in the +repository root. + +ALL RIGHTS RESERVED. +Copyright (c) 2025 Frigate LLC. diff --git a/sam2-cpu/frigate-dev/web/images/branding/apple-touch-icon.png b/sam2-cpu/frigate-dev/web/images/branding/apple-touch-icon.png new file mode 100644 index 0000000..a0ca9e8 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/apple-touch-icon.png differ diff --git a/sam2-cpu/frigate-dev/web/images/branding/favicon-16x16.png b/sam2-cpu/frigate-dev/web/images/branding/favicon-16x16.png new file mode 100644 index 0000000..bbe3207 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/favicon-16x16.png differ diff --git a/sam2-cpu/frigate-dev/web/images/branding/favicon-32x32.png b/sam2-cpu/frigate-dev/web/images/branding/favicon-32x32.png new file mode 100644 index 0000000..20e64b2 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/favicon-32x32.png differ diff --git a/sam2-cpu/frigate-dev/web/images/branding/favicon.ico b/sam2-cpu/frigate-dev/web/images/branding/favicon.ico new file mode 100644 index 0000000..1de8ec8 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/favicon.ico differ diff --git a/sam2-cpu/frigate-dev/web/images/branding/favicon.png b/sam2-cpu/frigate-dev/web/images/branding/favicon.png new file mode 100644 index 0000000..60bf469 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/favicon.png differ diff --git a/sam2-cpu/frigate-dev/web/images/branding/favicon.svg b/sam2-cpu/frigate-dev/web/images/branding/favicon.svg new file mode 100644 index 0000000..066268a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/images/branding/favicon.svg @@ -0,0 +1,46 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/sam2-cpu/frigate-dev/web/images/branding/mstile-150x150.png b/sam2-cpu/frigate-dev/web/images/branding/mstile-150x150.png new file mode 100644 index 0000000..63ecc61 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/branding/mstile-150x150.png differ diff --git a/sam2-cpu/frigate-dev/web/images/marker.png b/sam2-cpu/frigate-dev/web/images/marker.png new file mode 100644 index 0000000..3591e0a Binary files /dev/null and b/sam2-cpu/frigate-dev/web/images/marker.png differ diff --git a/sam2-cpu/frigate-dev/web/index.html b/sam2-cpu/frigate-dev/web/index.html new file mode 100644 index 0000000..0805dec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/index.html @@ -0,0 +1,36 @@ + + + + + + + Frigate + + + + + + + + + + +
+ + + + diff --git a/sam2-cpu/frigate-dev/web/login.html b/sam2-cpu/frigate-dev/web/login.html new file mode 100644 index 0000000..fc0fb55 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/login.html @@ -0,0 +1,36 @@ + + + + + + + Frigate + + + + + + + + + + +
+ + + + diff --git a/sam2-cpu/frigate-dev/web/package-lock.json b/sam2-cpu/frigate-dev/web/package-lock.json new file mode 100644 index 0000000..8df3356 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/package-lock.json @@ -0,0 +1,10372 @@ +{ + "name": "web-new", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-new", + "version": "0.0.0", + "dependencies": { + "@cycjimmy/jsmpeg-player": "^6.1.2", + "@hookform/resolvers": "^3.9.0", + "@melloware/react-logviewer": "^6.1.2", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-aspect-ratio": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.6", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.2.8", + "apexcharts": "^3.52.0", + "axios": "^1.7.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "copy-to-clipboard": "^3.3.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.2.0", + "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", + "hls.js": "^1.5.20", + "i18next": "^24.2.0", + "i18next-http-backend": "^3.0.1", + "idb-keyval": "^6.2.1", + "immer": "^10.1.1", + "konva": "^9.3.18", + "lodash": "^4.17.21", + "lucide-react": "^0.477.0", + "monaco-yaml": "^5.3.1", + "next-themes": "^0.3.0", + "nosleep.js": "^0.12.0", + "react": "^18.3.1", + "react-apexcharts": "^1.4.1", + "react-day-picker": "^9.7.0", + "react-device-detect": "^2.2.3", + "react-dom": "^18.3.1", + "react-dropzone": "^14.3.8", + "react-grid-layout": "^1.5.0", + "react-hook-form": "^7.52.1", + "react-i18next": "^15.2.0", + "react-icons": "^5.5.0", + "react-konva": "^18.2.10", + "react-router-dom": "^6.26.0", + "react-swipeable": "^7.0.2", + "react-tracked": "^2.0.1", + "react-transition-group": "^4.4.5", + "react-use-websocket": "^4.8.1", + "react-zoom-pan-pinch": "3.4.4", + "recoil": "^0.7.7", + "scroll-into-view-if-needed": "^3.1.0", + "sonner": "^1.5.0", + "sort-by": "^1.2.0", + "strftime": "^0.10.3", + "swr": "^2.3.2", + "tailwind-merge": "^2.4.0", + "tailwind-scrollbar": "^3.1.0", + "tailwindcss-animate": "^1.0.7", + "use-long-press": "^3.2.0", + "vaul": "^0.9.1", + "vite-plugin-monaco-editor": "^1.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.9", + "@testing-library/jest-dom": "^6.6.2", + "@types/lodash": "^4.17.12", + "@types/node": "^20.14.10", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.3.0", + "@types/react-grid-layout": "^1.3.5", + "@types/react-icons": "^3.0.0", + "@types/react-transition-group": "^4.4.10", + "@types/strftime": "^0.9.8", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitejs/plugin-react-swc": "^3.8.0", + "@vitest/coverage-v8": "^3.0.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.8", + "eslint-plugin-vitest-globals": "^1.5.0", + "fake-indexeddb": "^6.0.0", + "jest-websocket-mock": "^2.5.0", + "jsdom": "^24.1.1", + "msw": "^2.3.5", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.9", + "typescript": "^5.8.2", + "vite": "^6.2.0", + "vitest": "^3.0.7" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@cycjimmy/jsmpeg-player": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.2.tgz", + "integrity": "sha512-U9DBDe5fxHmbwQww9rFxMLNI2Wlg7DhPzI7AVFpq8GehiUP7+NwuMPXpP4zAd52sgkxtOqOeMjgE5g0ZLnQZ0w==", + "license": "MIT" + }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.7.tgz", + "integrity": "sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.4.tgz", + "integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@melloware/react-logviewer": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@melloware/react-logviewer/-/react-logviewer-6.1.2.tgz", + "integrity": "sha512-WDw3VIGqhoXxDn93HFDicwRhi4+FQyaKiVTB07bWerT82gTgyWV7bOciVV33z25N3WJrz62j5FKVzvFZCu17/A==", + "license": "MPL-2.0", + "dependencies": { + "hotkeys-js": "3.13.9", + "mitt": "3.0.1", + "react-string-replace": "1.1.1", + "virtua": "0.39.3" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", + "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", + "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", + "integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", + "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz", + "integrity": "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz", + "integrity": "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz", + "integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@swc/core": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.7.tgz", + "integrity": "sha512-ICuzjyfz8Hh3U16Mb21uCRJeJd/lUgV999GjgvPhJSISM1L8GDSB5/AMNcwuGs7gFywTKI4vAeeXWyCETUXHAg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.19" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.7", + "@swc/core-darwin-x64": "1.11.7", + "@swc/core-linux-arm-gnueabihf": "1.11.7", + "@swc/core-linux-arm64-gnu": "1.11.7", + "@swc/core-linux-arm64-musl": "1.11.7", + "@swc/core-linux-x64-gnu": "1.11.7", + "@swc/core-linux-x64-musl": "1.11.7", + "@swc/core-win32-arm64-msvc": "1.11.7", + "@swc/core-win32-ia32-msvc": "1.11.7", + "@swc/core-win32-x64-msvc": "1.11.7" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.7.tgz", + "integrity": "sha512-3+LhCP2H50CLI6yv/lhOtoZ5B/hi7Q/23dye1KhbSDeDprLTm/KfLJh/iQqwaHUponf5m8C2U0y6DD+HGLz8Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.7.tgz", + "integrity": "sha512-1diWpJqwX1XmOghf9ENFaeRaTtqLiqlZIW56RfOqmeZ7tPp3qS7VygWb9akptBsO5pEA5ZwNgSerD6AJlQcjAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.7.tgz", + "integrity": "sha512-MV8+hLREf0NN23NuSKemsjFaWjl/HnqdOkE7uhXTnHzg8WTwp6ddVtU5Yriv15+d/ktfLWPVAOhLHQ4gzaoa8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.7.tgz", + "integrity": "sha512-5GNs8ZjHQy/UTSnzzn+gm1RCUpCYo43lsxYOl8mpcnZSfxkNFVpjfylBv0QuJ5qhdfZ2iU55+v4iJCwCMtw0nA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.7.tgz", + "integrity": "sha512-cTydaYBwDbVV5CspwVcCp9IevYWpGD1cF5B5KlBdjmBzxxeWyTAJRtKzn8w5/UJe/MfdAptarpqMPIs2f33YEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.7.tgz", + "integrity": "sha512-YAX2KfYPlbDsnZiVMI4ZwotF3VeURUrzD+emJgFf1g26F4eEmslldgnDrKybW7V+bObsH22cDqoy6jmQZgpuPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.7.tgz", + "integrity": "sha512-mYT6FTDZyYx5pailc8xt6ClS2yjKmP8jNHxA9Ce3K21n5qkKilI5M2N7NShwXkd3Ksw3F29wKrg+wvEMXTRY/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.7.tgz", + "integrity": "sha512-uLDQEcv0BHcepypstyxKkNsW6KfLyI5jVxTbcxka+B2UnMcFpvoR87nGt2JYW0grO2SNZPoFz+UnoKL9c6JxpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.7.tgz", + "integrity": "sha512-wiq5G3fRizdxAJVFcon7zpyfbfrb+YShuTy+TqJ4Nf5PC0ueMOXmsmeuyQGApn6dVWtGCyymYQYt77wHeQajdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.7.tgz", + "integrity": "sha512-/zQdqY4fHkSORxEJ2cKtRBOwglvf/8gs6Tl4Q6VMx2zFtFpIOwFQstfY5u8wBNN2Z+PkAzyUCPoi8/cQFK8HLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", + "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", + "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", + "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "devOptional": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-grid-layout": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz", + "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-icons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz", + "integrity": "sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==", + "deprecated": "This is a stub types definition. react-icons provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "react-icons": "*" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", + "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.4.tgz", + "integrity": "sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==", + "dev": true + }, + "node_modules/@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", + "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/type-utils": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", + "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/utils": "7.12.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", + "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", + "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", + "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", + "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/visitor-keys": "7.12.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", + "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.12.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.8.0.tgz", + "integrity": "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.10.15" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "debug": "^4.4.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.52.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz", + "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==", + "license": "MIT", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.2.0.tgz", + "integrity": "sha512-rf2GIX8rab9E6ZZN0Uhz05746qu2KrDje9IfFyHzjwxLwhvGjUt6y9+uaY1Sf+B0OPSa3sgas7BE2hWZCtopTA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.2.0.tgz", + "integrity": "sha512-dWqbmaEBQjeAcy/EKrcAX37beVr0ubXuHPuLZkx27z58V1FIvRbbMb4/c3cLZx0PAv/ofngX2QFrwUB+62SPnw==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.2.0", + "embla-carousel-reactive-utils": "8.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.2.0.tgz", + "integrity": "sha512-ZdaPNgMydkPBiDRUv+wRIz3hpZJ3LKrTyz+XWi286qlwPyZFJDjbzPBiXnC3czF9N/nsabSc7LTRvGauUzwKEg==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.6.0.tgz", + "integrity": "sha512-YG28E1/MIKwnz+e2H7VwYPzHUYU4aMa19w0yGcwXnnmJH6EfgHahTJ2un3IyraUxNfnz/KUhJAFXNNwWPo12tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", + "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.12.0", + "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/typescript-estree": "7.12.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz", + "integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-vitest-globals": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz", + "integrity": "sha512-ZSsVOaOIig0oVLzRTyk8lUfBfqzWxr/J3/NFMfGGRIkGQPejJYmDH3gXmSJxAojts77uzAGB/UmVrwi2DC4LYA==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", + "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fake-indexeddb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz", + "integrity": "sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz", + "integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz", + "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", + "dev": true + }, + "node_modules/hls.js": { + "version": "1.5.20", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz", + "integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ==", + "license": "Apache-2.0" + }, + "node_modules/hotkeys-js": { + "version": "3.13.9", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz", + "integrity": "sha512-3TRCj9u9KUH6cKo25w4KIdBfdBfNRjfUwrljCLDC2XhmPDG0SjAZFcFZekpUZFmXzfYoGhFDcdx2gX/vUVtztQ==", + "license": "MIT", + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/i18next": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.1.tgz", + "integrity": "sha512-XT2lYSkbAtDE55c6m7CtKxxrsfuRQO3rUfHzj8ZyRtY9CkIX3aRGwXGTkUhpGWce+J8n7sfu3J0f2wTzo7Lw0A==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/its-fine": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.1.3.tgz", + "integrity": "sha512-mncCA+yb6tuh5zK26cHqKlsSyxm4zdm4YgJpxycyx6p9fgxgK5PLu3iDVpKhzTn57Yrv3jk/r0aK0RFTT1OjFw==", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-websocket-mock": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.5.0.tgz", + "integrity": "sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig==", + "dev": true, + "dependencies": { + "jest-diff": "^29.2.0", + "mock-socket": "^9.3.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", + "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/konva": { + "version": "9.3.18", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.18.tgz", + "integrity": "sha512-ad5h0Y9phUrinBrKXyIISbURRHQO7Rx5cz7mAEEfdVCs45gDqRD8Y0I0nJRk8S6iqEbiRE87CEZu5GVSnU8oow==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.477.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.477.0.tgz", + "integrity": "sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "peer": true + }, + "node_modules/monaco-languageserver-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/monaco-languageserver-types/-/monaco-languageserver-types-0.4.0.tgz", + "integrity": "sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==", + "license": "MIT", + "dependencies": { + "monaco-types": "^0.1.0", + "vscode-languageserver-protocol": "^3.0.0", + "vscode-uri": "^3.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-marker-data-provider": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz", + "integrity": "sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "monaco-editor": ">=0.30.0" + } + }, + "node_modules/monaco-types": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/monaco-types/-/monaco-types-0.1.0.tgz", + "integrity": "sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/monaco-worker-manager": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz", + "integrity": "sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==", + "peerDependencies": { + "monaco-editor": ">=0.30.0" + } + }, + "node_modules/monaco-yaml": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/monaco-yaml/-/monaco-yaml-5.3.1.tgz", + "integrity": "sha512-1MN8i1Tnc8d8RugQGqv5jp+Ce2xtNhrnbm0ZZbe5ceExj9C2PkKZfHJhY9kbdUS4G7xSVwKlVdMTmLlStepOtw==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "jsonc-parser": "^3.0.0", + "monaco-languageserver-types": "^0.4.0", + "monaco-marker-data-provider": "^1.0.0", + "monaco-types": "^0.1.0", + "monaco-worker-manager": "^2.0.0", + "path-browserify": "^1.0.0", + "prettier": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.0", + "vscode-languageserver-types": "^3.0.0", + "vscode-uri": "^3.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "monaco-editor": ">=0.36" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.3.tgz", + "integrity": "sha512-+mycXv8l2fEAjFZ5sjrtjJDmm2ceKGjrNbBr1durRg6VkU9fNUE/gsmQ51hWbHqs+l35W1iM+ZsmOD9Fd6lspw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz", + "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==" + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-path": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz", + "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz", + "integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/proxy-compare": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.0.tgz", + "integrity": "sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-apexcharts": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz", + "integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": "^3.41.0", + "react": ">=0.13" + } + }, + "node_modules/react-day-picker": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz", + "integrity": "sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-day-picker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/react-device-detect": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz", + "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==", + "dependencies": { + "ua-parser-js": "^1.0.33" + }, + "peerDependencies": { + "react": ">= 0.14.0", + "react-dom": ">= 0.14.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-grid-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz", + "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.5", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", + "integrity": "sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/react-konva": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", + "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "dependencies": { + "@types/react-reconciler": "^0.28.2", + "its-fine": "^1.1.1", + "react-reconciler": "~0.29.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", + "integrity": "sha512-wa0fGj7Zht1EYMRhKWwoo1H9GApxYLBuhoAuXN0TlltESAjDssB+Apf0T/DngVqaMyPypDmabL37vw/2aRM98Q==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, + "node_modules/react-router": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz", + "integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz", + "integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.0", + "react-router": "6.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-string-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", + "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-swipeable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz", + "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-tracked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-2.0.1.tgz", + "integrity": "sha512-qjbmtkO2IcW+rB2cFskRWDTjKs/w9poxvNnduacjQA04LWxOoLy9J8WfIEq1ahifQ/tVJQECrQPBm+UEzKRDtg==", + "license": "MIT", + "dependencies": { + "proxy-compare": "^3.0.0", + "use-context-selector": "^2.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "scheduler": ">=0.19.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-use-websocket": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz", + "integrity": "sha512-FTXuG5O+LFozmu1BRfrzl7UIQngECvGJmL7BHsK4TYXuVt+mCizVA8lT0hGSIF0Z0TedF7bOo1nRzOUdginhDw==", + "peerDependencies": { + "react": ">= 18.0.0", + "react-dom": ">= 18.0.0" + } + }, + "node_modules/react-zoom-pan-pinch": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.4.4.tgz", + "integrity": "sha512-lGTu7D9lQpYEQ6sH+NSlLA7gicgKRW8j+D/4HO1AbSV2POvKRFzdWQ8eI0r3xmOsl4dYQcY+teV6MhULeg1xBw==", + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/sort-by": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz", + "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==", + "dependencies": { + "object-path": "0.6.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/strftime": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.3.tgz", + "integrity": "sha512-DZrDUeIF73eKJ4/GgGuv8UHWcUQPYDYfDeQFj3jrx+JZl6GQE656MbHIpvbo4mEG9a5DgS8GRCc5DxJXD2udDQ==", + "license": "MIT", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/swr": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz", + "integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/synckit": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.6.tgz", + "integrity": "sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwind-merge": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz", + "integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-scrollbar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", + "integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "3.x" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", + "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-context-selector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz", + "integrity": "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==", + "peerDependencies": { + "react": ">=18.0.0", + "scheduler": ">=0.19.0" + } + }, + "node_modules/use-long-press": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz", + "integrity": "sha512-uq5o2qFR1VRjHn8Of7Fl344/AGvgk7C5Mcb4aSb1ZRVp6PkgdXJJLdRrlSTJQVkkQcDuqFbFc3mDX4COg7mRTA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vaul": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.1.tgz", + "integrity": "sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==", + "dependencies": { + "@radix-ui/react-dialog": "^1.0.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/virtua": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/virtua/-/virtua-0.39.3.tgz", + "integrity": "sha512-Ep3aiJXSGPm1UUniThr5mGDfG0upAleP7pqQs5mvvCgM1wPhII1ZKa7eNCWAJRLkC+InpXKokKozyaaj/aMYOQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0", + "solid-js": ">=1.0", + "svelte": ">=5.0", + "vue": ">=3.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-monaco-editor": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz", + "integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==", + "peerDependencies": { + "monaco-editor": ">=0.33.0" + } + }, + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", + "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/package.json b/sam2-cpu/frigate-dev/web/package.json new file mode 100644 index 0000000..27256bd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/package.json @@ -0,0 +1,128 @@ +{ + "name": "web-new", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc && vite build --base=/BASE_PATH/", + "lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .", + "lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .", + "preview": "vite preview", + "prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"", + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "dependencies": { + "@cycjimmy/jsmpeg-player": "^6.1.2", + "@hookform/resolvers": "^3.9.0", + "@melloware/react-logviewer": "^6.1.2", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-aspect-ratio": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.6", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.2.8", + "apexcharts": "^3.52.0", + "axios": "^1.7.7", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "copy-to-clipboard": "^3.3.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.2.0", + "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", + "hls.js": "^1.5.20", + "i18next": "^24.2.0", + "i18next-http-backend": "^3.0.1", + "idb-keyval": "^6.2.1", + "immer": "^10.1.1", + "konva": "^9.3.18", + "lodash": "^4.17.21", + "lucide-react": "^0.477.0", + "monaco-yaml": "^5.3.1", + "next-themes": "^0.3.0", + "nosleep.js": "^0.12.0", + "react": "^18.3.1", + "react-apexcharts": "^1.4.1", + "react-day-picker": "^9.7.0", + "react-device-detect": "^2.2.3", + "react-dom": "^18.3.1", + "react-dropzone": "^14.3.8", + "react-grid-layout": "^1.5.0", + "react-hook-form": "^7.52.1", + "react-i18next": "^15.2.0", + "react-icons": "^5.5.0", + "react-konva": "^18.2.10", + "react-router-dom": "^6.26.0", + "react-swipeable": "^7.0.2", + "react-tracked": "^2.0.1", + "react-transition-group": "^4.4.5", + "react-use-websocket": "^4.8.1", + "react-zoom-pan-pinch": "3.4.4", + "recoil": "^0.7.7", + "scroll-into-view-if-needed": "^3.1.0", + "sonner": "^1.5.0", + "sort-by": "^1.2.0", + "strftime": "^0.10.3", + "swr": "^2.3.2", + "tailwind-merge": "^2.4.0", + "tailwind-scrollbar": "^3.1.0", + "tailwindcss-animate": "^1.0.7", + "use-long-press": "^3.2.0", + "vaul": "^0.9.1", + "vite-plugin-monaco-editor": "^1.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.9", + "@testing-library/jest-dom": "^6.6.2", + "@types/lodash": "^4.17.12", + "@types/node": "^20.14.10", + "@types/react": "^18.3.2", + "@types/react-dom": "^18.3.0", + "@types/react-grid-layout": "^1.3.5", + "@types/react-icons": "^3.0.0", + "@types/react-transition-group": "^4.4.10", + "@types/strftime": "^0.9.8", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitejs/plugin-react-swc": "^3.8.0", + "@vitest/coverage-v8": "^3.0.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.2.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.8", + "eslint-plugin-vitest-globals": "^1.5.0", + "fake-indexeddb": "^6.0.0", + "jest-websocket-mock": "^2.5.0", + "jsdom": "^24.1.1", + "msw": "^2.3.5", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.5", + "tailwindcss": "^3.4.9", + "typescript": "^5.8.2", + "vite": "^6.2.0", + "vitest": "^3.0.7" + } +} diff --git a/sam2-cpu/frigate-dev/web/postcss.config.js b/sam2-cpu/frigate-dev/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Black.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Black.woff2 new file mode 100644 index 0000000..18b35db Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Black.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-BlackItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..02c9d8e Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-BlackItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Bold.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..0f1b157 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Bold.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-BoldItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..bc50f24 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-BoldItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBold.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..b113368 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBold.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBoldItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..a5b76ca Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraBoldItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLight.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLight.woff2 new file mode 100644 index 0000000..1d77ae8 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLight.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLightItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLightItalic.woff2 new file mode 100644 index 0000000..8c68492 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ExtraLightItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Italic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Italic.woff2 new file mode 100644 index 0000000..4c24ce2 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Italic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Light.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Light.woff2 new file mode 100644 index 0000000..dbe6143 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Light.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-LightItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-LightItalic.woff2 new file mode 100644 index 0000000..a40d042 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-LightItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Medium.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..0fd2ee7 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Medium.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-MediumItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..9676715 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-MediumItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Regular.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..b8699af Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Regular.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBold.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..95c48b1 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBold.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBoldItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..ddfe19e Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-SemiBoldItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-Thin.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Thin.woff2 new file mode 100644 index 0000000..0790960 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-Thin.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/fonts/Inter-ThinItalic.woff2 b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ThinItalic.woff2 new file mode 100644 index 0000000..a7bf213 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/fonts/Inter-ThinItalic.woff2 differ diff --git a/sam2-cpu/frigate-dev/web/public/images/android-chrome-192x192.png b/sam2-cpu/frigate-dev/web/public/images/android-chrome-192x192.png new file mode 100644 index 0000000..80c3820 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/images/android-chrome-192x192.png differ diff --git a/sam2-cpu/frigate-dev/web/public/images/android-chrome-512x512.png b/sam2-cpu/frigate-dev/web/public/images/android-chrome-512x512.png new file mode 100644 index 0000000..a23e25c Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/images/android-chrome-512x512.png differ diff --git a/sam2-cpu/frigate-dev/web/public/images/apple-touch-icon.png b/sam2-cpu/frigate-dev/web/public/images/apple-touch-icon.png new file mode 100644 index 0000000..a0ca9e8 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/images/apple-touch-icon.png differ diff --git a/sam2-cpu/frigate-dev/web/public/images/maskable-badge.png b/sam2-cpu/frigate-dev/web/public/images/maskable-badge.png new file mode 100644 index 0000000..856f142 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/images/maskable-badge.png differ diff --git a/sam2-cpu/frigate-dev/web/public/images/maskable-icon.png b/sam2-cpu/frigate-dev/web/public/images/maskable-icon.png new file mode 100644 index 0000000..eb77fe2 Binary files /dev/null and b/sam2-cpu/frigate-dev/web/public/images/maskable-icon.png differ diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ab/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/audio.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/common.json b/sam2-cpu/frigate-dev/web/public/locales/ab/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/common.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/input.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ab/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/components/player.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ab/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/objects.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/events.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/live.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/search.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ab/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ab/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ab/views/system.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ar/audio.json new file mode 100644 index 0000000..b72a52c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/audio.json @@ -0,0 +1,78 @@ +{ + "bark": "نُبَاح", + "snort": "نَفْخَة", + "heartbeat": "نَبْض القَلْب", + "pets": "حَيَوَانَات أَلِيفَة", + "whoop": "هتاف", + "humming": "هَمْهَمَة", + "chewing": "مَضْغ", + "yodeling": "غناء متقلب", + "howl": "عُوَاء", + "speech": "تحدث", + "hiccup": "فُوَاق", + "dog": "كَلْب", + "yip": "نُبَيْحَة", + "babbling": "ثرثرة", + "yell": "صراخ", + "bellow": "زمجرة", + "whispering": "همس", + "laughter": "ضحك", + "snicker": "ضحكة خفيفه", + "crying": "بكاء", + "sigh": "تنهد", + "singing": "غناء", + "choir": "فرقة غناء", + "chant": "تَرْنِيم", + "mantra": "تَرْنِيمَة", + "child_singing": "غِنَاء طِفْل", + "synthetic_singing": "غِنَاء اِصْطِنَاعِيّ", + "rapping": "رَاب", + "groan": "أَنِين", + "grunt": "خَنِين", + "whistling": "صَفِير", + "breathing": "تَنَفُّس", + "wheeze": "أَزِيز", + "snoring": "شَخِير", + "gasp": "شَهْقَة", + "pant": "لَهَث", + "cough": "سُعَال", + "throat_clearing": "تَنْحِيم", + "sneeze": "عُطَاس", + "sniff": "شَمَّ", + "run": "رَكْض", + "shuffle": "خَلْط", + "footsteps": "خُطُوَات", + "biting": "عَضّ", + "gargling": "غَرْغَرَة", + "stomach_rumble": "قَرْقَرَة المَعِدَة", + "burping": "تَجَشُّؤ", + "fart": "ضُرَاط", + "hands": "أَيْدِي", + "finger_snapping": "طَقْطَقَة الأَصَابِع", + "clapping": "تَصْفِيق", + "heart_murmur": "لَغَط القَلْب", + "cheering": "صِيَاح", + "applause": "تَصْفِيق", + "chatter": "حَدِيث", + "crowd": "جُمْهُور", + "children_playing": "لَعِب الأَطْفَال", + "animal": "حَيَوَان", + "bow_wow": "نُبَاح الكَلْب", + "growling": "زَمْجَرَ", + "whimper_dog": "أَنِين الكَلْب", + "cat": "قِطّ", + "purr": "خَرْخَرَة", + "meow": "مُوَاء", + "hiss": "فَحِيح", + "caterwaul": "صُرَاخ مُتَوَاصِل", + "livestock": "مَاشِيَة", + "horse": "حِصَان", + "clip_clop": "حَوَافِر الخَيْل", + "car": "سيارة", + "motorcycle": "دراجة نارية", + "bicycle": "دراجة هوائية", + "bus": "حافلة", + "train": "قطار", + "boat": "زورق", + "bird": "طائر" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/common.json b/sam2-cpu/frigate-dev/web/public/locales/ar/common.json new file mode 100644 index 0000000..92390a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/common.json @@ -0,0 +1,20 @@ +{ + "time": { + "untilForTime": "حتى {{time}}", + "untilForRestart": "حتى يعاد تشغيل فرايجيت.", + "untilRestart": "حتى إعادة التشغيل", + "ago": "منذ {{timeAgo}}", + "justNow": "في التو", + "today": "اليوم", + "last14": "آخر 14 يومًا", + "last30": "آخر 30 يومًا", + "thisWeek": "هذا الأسبوع", + "lastWeek": "الأسبوع الماضي", + "thisMonth": "هذا الشهر", + "yesterday": "بالأمس", + "last7": "آخر 7 أيام", + "lastMonth": "الشهر المنصرم", + "5minutes": "5 دقائق", + "10minutes": "10 دقائق" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/auth.json new file mode 100644 index 0000000..1c8eabf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "password": "كلمة السر", + "user": "أسم المستخدم", + "login": "تسجيل الدخول", + "errors": { + "usernameRequired": "اسم المستخدم مطلوب", + "passwordRequired": "كلمة المرور مطلوبة", + "rateLimit": "تجاوز الحد الأقصى للمعدل. حاول مرة أخرى في وقت لاحق.", + "webUnknownError": "خطأ غير معروف. تحقق من سجلات وحدة التحكم.", + "loginFailed": "فشل تسجيل الدخول", + "unknownError": "خطأ غير معروف. تحقق من السجلات." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/camera.json new file mode 100644 index 0000000..9bc19f1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/camera.json @@ -0,0 +1,51 @@ +{ + "group": { + "label": "مجموعات الكاميرات", + "add": "إضافة مجموعة الكاميرات", + "edit": "تعديل مجموعة الكاميرات", + "delete": { + "label": "حذف مجموعة الكاميرات", + "confirm": { + "title": "تأكيد الحذف", + "desc": "هل أنت متأكد أنك تريد حذف مجموعة الكاميرات {{name}}؟" + } + }, + "name": { + "errorMessage": { + "mustLeastCharacters": "يجب أن يتكون اسم مجموعة الكاميرا من حرفين على الأقل.", + "exists": "اسم مجموعة الكاميرا موجود بالفعل.", + "nameMustNotPeriod": "يجب ألا يحتوي اسم مجموعة الكاميرا على نقطة.", + "invalid": "اسم مجموعة الكاميرا غير صالح." + }, + "label": "الاسم", + "placeholder": "أدخل اسمًا…" + }, + "cameras": { + "label": "الكاميرات", + "desc": "اختر الكاميرات لهذه المجموعة." + }, + "icon": "أيقونة", + "camera": { + "setting": { + "streamMethod": { + "placeholder": "إختيار طريقة البث", + "method": { + "noStreaming": { + "label": "لايوجد بث", + "desc": "صور الكاميرا سيتم تحديثها مرة واحدة فقط كل دقيقة من دون بث حي." + }, + "smartStreaming": { + "label": "البث الذكي (ينصح به)" + }, + "continuousStreaming": { + "label": "بث متواصل" + } + } + } + } + } + }, + "debug": { + "timestamp": "الختم الزمني" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/dialog.json new file mode 100644 index 0000000..4291873 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/dialog.json @@ -0,0 +1,38 @@ +{ + "restart": { + "title": "هل أنت متأكد أنك تريد إعادة تشغيل فرايجيت؟", + "button": "إعادة التشغيل", + "restarting": { + "title": "يتم إعادة تشغيل فرايجيت", + "content": "العد التنازلي", + "button": "فرض إعادة التحميل الآن" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "التقديم إلى Frigate+", + "desc": "الكائنات الموجودة في الأماكن التي تريد تجنبها ليست ضمن النتائج الإيجابية الخاطئة. إرسالها كنتائج إيجابية خاطئة سيؤدي إلى إرباك النموذج." + }, + "review": { + "state": { + "submitted": "تم تقديمه" + }, + "question": { + "label": "تأكد من صحة هذه التسمية لـ Frigate Plus", + "ask_a": "هل هذا الكائن هو {{label}}؟", + "ask_an": "هل هذا الكائن هو {{label}}؟", + "ask_full": "هل هذا الكائن هو {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "عرض في التاريخ" + } + }, + "export": { + "time": { + "fromTimeline": "اختر من التسلسل الزمني" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/filter.json new file mode 100644 index 0000000..954d69f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/filter.json @@ -0,0 +1,31 @@ +{ + "filter": "ترشيح", + "labels": { + "label": "التسميات", + "all": { + "title": "كل التسميات", + "short": "المسمّيات" + } + }, + "classes": { + "label": "فئات", + "all": { + "title": "جميع الفئات" + }, + "count_one": "{{عدد}} الفئة", + "count_other": "{{count}} الفئات" + }, + "zones": { + "label": "المناطق", + "all": { + "title": "جميع المناطق", + "short": "المناطق" + } + }, + "dates": { + "selectPreset": "اختر إعدادًا مسبقًا…", + "all": { + "title": "جميع التواريخ" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/icons.json new file mode 100644 index 0000000..6002ba6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "اختر أيقونة", + "search": { + "placeholder": "ابحث عن أيقونة…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/input.json new file mode 100644 index 0000000..f58f8be --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "تحميل الفيديو", + "toast": { + "success": "تم بدأ تحميل فيديو عنصر المراجعة." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ar/components/player.json new file mode 100644 index 0000000..5a3e87d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/components/player.json @@ -0,0 +1,29 @@ +{ + "noRecordingsFoundForThisTime": "لا يوجد تسجيلات في هذا التوقيت", + "noPreviewFound": "لا يوجد معاينة", + "noPreviewFoundFor": "لا يوجد معاينة لـ{{cameraName}}", + "submitFrigatePlus": { + "title": "هل ترغب بإرسال هذه الصوره الى Frigate+؟", + "submit": "تقديم" + }, + "livePlayerRequiredIOSVersion": "مطلوب نظام iOS 17.1 أو أكبر لهذا النوع من البث المباشر.", + "cameraDisabled": "الكاميرا معطلة", + "stats": { + "streamType": { + "title": "نوع الدفق:", + "short": "النوع" + }, + "bandwidth": { + "title": "العرض الترددي:", + "short": "العرض الترددي" + }, + "latency": { + "title": "التأخير:", + "value": "{{seconds}} ثانية" + } + }, + "streamOffline": { + "title": "البث دون اتصال بالإنترنت", + "desc": "لم يتم استلام أي إطارات على دفق {{cameraName}} detect، تحقق من سجلات الأخطاء" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ar/objects.json new file mode 100644 index 0000000..4aff9d7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/objects.json @@ -0,0 +1,22 @@ +{ + "dog": "كَلْب", + "cat": "قِطّ", + "horse": "حِصَان", + "animal": "حَيَوَان", + "bark": "نُبَاح", + "person": "شخص", + "bicycle": "دراجة هوائية", + "car": "سيارة", + "motorcycle": "دراجة نارية", + "airplane": "طائرة", + "bus": "حافلة", + "traffic_light": "إشارة المرور", + "fire_hydrant": "حنفية إطفاء الحريق", + "street_sign": "لافتة شارع", + "stop_sign": "إشارة توقف", + "parking_meter": "عداد موقف سيارات", + "train": "قطار", + "boat": "زورق", + "bench": "مقعدة", + "bird": "طائر" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/configEditor.json new file mode 100644 index 0000000..6387006 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "محرر الإعدادات - فرايجيت", + "configEditor": "محرر الإعدادات", + "copyConfig": "نسخ الإعدادات", + "saveAndRestart": "حفظ وإعادة تشغيل", + "safeConfigEditor": "محرر التكوين في ( الوضع الامن )", + "safeModeDescription": "أصبح Frigate في الوضع الآمن بسبب خطأ في التحقق من صحة التكوين.", + "toast": { + "success": { + "copyToClipboard": "تم نسخ التكوين إلى الحافظة." + }, + "error": { + "savingError": "خطأ في حفظ التكوين" + } + }, + "saveOnly": "احفظ فقط", + "confirm": "أتود الخروج دون حفظ؟" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/events.json new file mode 100644 index 0000000..41312c9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/events.json @@ -0,0 +1,25 @@ +{ + "detections": "الإكتشافات", + "alerts": "الإنذارات", + "motion": { + "label": "الحركة", + "only": "حركة فقط" + }, + "allCameras": "كافة الكاميرات", + "empty": { + "alert": "لا توجد تنبيهات لمراجعتها", + "detection": "لا توجد عمليات كشف لمراجعتها", + "motion": "لم يتم العثور على بيانات الحركة" + }, + "timeline": "التسلسل الزمني", + "timeline.aria": "اختر التسلسل الزمني", + "events": { + "label": "اﻷحداث", + "aria": "اختر الأحداث", + "noFoundForTimePeriod": "لم يتم العثور على أي أحداث لهذه الفترة الزمنية." + }, + "documentTitle": "مراجعة - Frigate", + "recordings": { + "documentTitle": "التسجيلات - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/explore.json new file mode 100644 index 0000000..4b54ed1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/explore.json @@ -0,0 +1,30 @@ +{ + "exploreMore": "اكتشف المزيد من أجسام {{label}}", + "documentTitle": "اكتشف - فرايجيت", + "generativeAI": "ذكاء اصطناعي مولد", + "exploreIsUnavailable": { + "title": "المتصفح غير متاح", + "embeddingsReindexing": { + "context": "يمكن استخدام الاستكشاف بعد انتهاء تضمين الكائنات المتعقبة من إعادة الفهرسة.", + "startingUp": "إبتدا التشغيل…", + "step": { + "thumbnailsEmbedded": "الصور المصغرة المضمنة: ", + "descriptionsEmbedded": "الأوصاف المضمنة: ", + "trackedObjectsProcessed": "الأشياء المتعقبة التي تمت معالجتها: " + }, + "estimatedTime": "الزمن المتبقي المقدر:", + "finishingShortly": "سينتهي قريبًا" + }, + "downloadingModels": { + "context": "تقوم Frigate بتنزيل نماذج التضمين اللازمة لدعم ميزة البحث الدلالي. قد يستغرق ذلك عدة دقائق حسب سرعة اتصالك بالإنترنت.", + "setup": { + "visionModel": "نموذج الرؤية", + "visionModelFeatureExtractor": "مستخرج ميزات نموذج الرؤية", + "textModel": "نموذج النص" + } + } + }, + "details": { + "timestamp": "الطابع الزمني" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/exports.json new file mode 100644 index 0000000..318ec2f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/exports.json @@ -0,0 +1,17 @@ +{ + "search": "بحث", + "noExports": "لا يوجد تصديرات", + "documentTitle": "التصدير - فرايجيت", + "deleteExport": "حذف التصدير", + "deleteExport.desc": "هل أنت متأكد من رغبتك في حذف{{exportName}}؟", + "editExport": { + "title": "إعادة تسمية التصدير", + "desc": "قم بإدخال اسم جديد لهذا التصدير.", + "saveExport": "حفظ التصدير" + }, + "toast": { + "error": { + "renameExportFailed": "فشل إعادة تسمية التصدير: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/faceLibrary.json new file mode 100644 index 0000000..c6c2c39 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/faceLibrary.json @@ -0,0 +1,26 @@ +{ + "description": { + "addFace": "قم بإضافة مجموعة جديدة لمكتبة الأوجه.", + "invalidName": "أسم غير صالح. يجب أن يشمل الأسم فقط على الحروف، الأرقام، المسافات، الفاصلة العليا، الشرطة التحتية، والشرطة الواصلة.", + "placeholder": "أدخل أسم لهذه المجموعة" + }, + "details": { + "person": "شخص", + "subLabelScore": "نتيجة العلامة الفرعية", + "timestamp": "الطابع الزمني", + "unknown": "غير معروف", + "scoreInfo": "النتيجة الفرعية هي النتيجة المرجحة لجميع درجات الثقة المعترف بها للوجه، لذلك قد تختلف عن النتيجة الموضحة في اللقطة.", + "face": "تفاصيل الوجه", + "faceDesc": "تفاصيل الكائن المتتبع الذي أنشأ هذا الوجه" + }, + "documentTitle": "مكتبة الوجوه - Frigate", + "uploadFaceImage": { + "title": "رفع صورة الوجه", + "desc": "قم بتحميل صورة لمسح الوجوه وإدراجها في {{pageToggle}}" + }, + "collections": "المجموعات", + "createFaceLibrary": { + "title": "إنشاء المجاميع", + "desc": "إنشاء مجموعة جديدة" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/live.json new file mode 100644 index 0000000..6e4f32d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/live.json @@ -0,0 +1,39 @@ +{ + "documentTitle": "بث حي - فرايجيت", + "documentTitle.withCamera": "{{camera}} - بث حي - فرايجيت", + "lowBandwidthMode": "وضع موفر للبيانات", + "twoWayTalk": { + "enable": "تفعيل المكالمات ثنائية الاتجاه", + "disable": "تعطيل المحادثة ثنائية الاتجاه" + }, + "cameraAudio": { + "enable": "تمكين صوت الكاميرا", + "disable": "تعطيل صوت الكاميرا" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "تمكين النقر للتحريك", + "disable": "تعطيل النقر للتحريك", + "label": "سينتهي قريبًا" + }, + "left": { + "label": "حرك الكاميرا PTZ إلى اليسار" + }, + "up": { + "label": "حرك كاميرا PTZ لأعلى" + }, + "down": { + "label": "حرك كاميرا PTZ لأسفل" + }, + "right": { + "label": "حرك الكاميرا PTZ إلى اليمين" + } + }, + "zoom": { + "in": { + "label": "تقريب كاميرا PTZ" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/recording.json new file mode 100644 index 0000000..c12dfda --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "ترشيح", + "export": "إرسال", + "calendar": "التقويم", + "filters": "المنقيات", + "toast": { + "error": { + "noValidTimeSelected": "لم يتم تحديد نطاق زمني صحيح", + "endTimeMustAfterStartTime": "يجب أن يكون وقت الانتهاء بعد وقت بدء التشغيل" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/search.json new file mode 100644 index 0000000..7964a0f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/search.json @@ -0,0 +1,23 @@ +{ + "search": "بحث", + "savedSearches": "عمليات البحث المحفوظة", + "searchFor": "البحث عن {{inputValue}}", + "button": { + "clear": "محو البحث", + "save": "احفظ البحث", + "delete": "حذف البحث المحفوظ", + "filterInformation": "تصفية المعلومات", + "filterActive": "الفلتر النشط" + }, + "trackedObjectId": "مُعرف الكائن المتعقّب", + "filter": { + "label": { + "cameras": "الكاميرات", + "labels": "الملصقات", + "zones": "مناطق", + "search_type": "نوع البحث", + "sub_labels": "العلامات الفرعية", + "time_range": "النطاق الزمني" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/settings.json new file mode 100644 index 0000000..6a40658 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/settings.json @@ -0,0 +1,35 @@ +{ + "documentTitle": { + "camera": "إعدادات الكاميرا - فرايجيت", + "default": "الإعدادات - فرايجيت", + "authentication": "إعدادات المصادقة - فرايجيت", + "enrichments": "إحصاء الاعدادات", + "masksAndZones": "القناع ومحرر المنطقة - Frigate", + "motionTuner": "مضبط الحركة - Firgate", + "object": "تصحيح الأخطاء - Frigate", + "general": "الإعدادات العامة - Frigate", + "notifications": "إعدادات الإشعارات - Frigate" + }, + "menu": { + "ui": "واجهة المستخدم", + "enrichments": "التحسينات", + "cameras": "إعدادات الكاميرا", + "masksAndZones": "أقنعة / مناطق", + "motionTuner": "مضبط الحركة", + "debug": "تصحيح", + "users": "المستخدمون", + "notifications": "إشعارات" + }, + "dialog": { + "unsavedChanges": { + "title": "لديك تغييرات غير محفوظة.", + "desc": "هل تريد حفظ تغييراتك قبل المتابعة؟" + } + }, + "cameraSetting": { + "camera": "كاميرا" + }, + "general": { + "title": "الإعدادات العامة" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ar/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ar/views/system.json new file mode 100644 index 0000000..e68d544 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ar/views/system.json @@ -0,0 +1,80 @@ +{ + "documentTitle": { + "cameras": "إحصاءات الكاميرات - فرايجيت", + "storage": "إحصاءات التخزين - فرايجيت", + "general": "إحصاءات عامة - فرايجيت", + "enrichments": "إحصاء العمليات", + "logs": { + "frigate": "سجلات Frigate - Frigate", + "go2rtc": "Go2RTC سجلات - Frigate", + "nginx": "سجلات إنجنإكس - Frigate" + } + }, + "metrics": "مقاييس النظام", + "logs": { + "download": { + "label": "تنزيل السجلات" + }, + "copy": { + "label": "نسخ إلى الحافظة", + "success": "نسخ السجلات إلى الحافظة", + "error": "تعذر نسخ السجلات إلى الحافظة" + }, + "type": { + "label": "النوع", + "timestamp": "الختم الزمني" + }, + "tips": "يتم بث السجلات من الخادم" + }, + "title": "النظام", + "general": { + "hardwareInfo": { + "gpuEncoder": "مشفر ترميز GPU", + "gpuDecoder": "مفكك ترميز GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "مخرجات Vainfo", + "processOutput": "ناتج العملية:", + "processError": "خطأ في العملية:" + }, + "nvidiaSMIOutput": { + "title": "مخرجات Nvidia SMI", + "name": "الاسم: {{name}}", + "driver": "برنامج التشغيل: {{driver}}", + "cudaComputerCapability": "قدرة الحوسبة CUDA: {{cuda_compute}}" + } + }, + "title": "معلومات الاجهزة المادية", + "gpuUsage": "مقدار استخدام GPU", + "gpuMemory": "ذاكرة GPU" + }, + "title": "لمحة عامة", + "detector": { + "title": "أجهزة الكشف", + "inferenceSpeed": "سرعة استنتاج الكاشف", + "temperature": "درجة حرارة الكاشف", + "cpuUsage": "كشف استخدام CPU", + "memoryUsage": "كشف استخدام الذاكرة" + }, + "otherProcesses": { + "title": "عمليات أخرى", + "processCpuUsage": "استخدام وحدة المعالجة المركزية (CPU)", + "processMemoryUsage": "استخدام ذاكرة العملية" + } + }, + "storage": { + "title": "التخزين", + "overview": "نظرة عامة", + "recordings": { + "title": "التسجيلات", + "tips": "تمثل هذه القيمة إجمالي مساحة التخزين المستخدمة للتسجيلات في قاعدة بيانات Frigate. لا يتتبع Frigate استخدام مساحة التخزين لجميع الملفات الموجودة على القرص.", + "earliestRecording": "أقدم تسجيل متاح:" + } + }, + "cameras": { + "overview": "نظرة عامة", + "info": { + "unknown": "غير معروف" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/audio.json b/sam2-cpu/frigate-dev/web/public/locales/bg/audio.json new file mode 100644 index 0000000..e59baf8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/audio.json @@ -0,0 +1,268 @@ +{ + "babbling": "Бърборене", + "whispering": "Шепнене", + "laughter": "Смях", + "crying": "Плача", + "sigh": "Въздишка", + "singing": "Подписвам", + "choir": "Хор", + "yodeling": "Йоделинг", + "mantra": "Мантра", + "child_singing": "Дете пее", + "rapping": "Рапиране", + "humming": "Тананикане", + "groan": "Пъшкане", + "whistling": "Подсвиркване", + "breathing": "Дишане", + "snoring": "Хъркане", + "cough": "Кашлица", + "throat_clearing": "Прокашляне", + "sneeze": "Кихане", + "sniff": "Подсмърчане", + "run": "Бяг", + "shuffle": "Разбъркване", + "footsteps": "Стъпки", + "chewing": "Дъвчене", + "biting": "Хапане", + "gargling": "Гаргара", + "burping": "Оригване", + "hiccup": "Хълцане", + "fart": "Пръдня", + "hands": "Ръце", + "finger_snapping": "Щтракане с пръсти", + "clapping": "Ръкопляскане", + "applause": "Овации", + "chatter": "Говорене", + "crowd": "Тълпа", + "children_playing": "Деца си играят", + "animal": "Животно", + "pets": "Домашен любимец", + "dog": "Куче", + "bark": "Лай", + "cat": "Котка", + "purr": "Мър", + "meow": "Мял", + "hiss": "Съскане", + "livestock": "Добитък", + "horse": "Кон", + "neigh": "Иииааа", + "moo": "Муу", + "cowbell": "Хлопка", + "pig": "Прасе", + "oink": "Грух", + "goat": "Коза", + "sheep": "Овца", + "chicken": "Пиле", + "cluck": "Ко-ко", + "cock_a_doodle_doo": "Кукуригу", + "turkey": "Пуйка", + "gobble": "Пулюпулю", + "duck": "Патка", + "quack": "Ква", + "goose": "Гъска", + "wild_animals": "Диви животни", + "roaring_cats": "Ревящи котки", + "roar": "Рев", + "bird": "Птица", + "pigeon": "Гълъб", + "coo": "Гуу", + "crow": "Гарван", + "caw": "Га", + "owl": "Сова", + "hoot": "Бухуу", + "flapping_wings": "Плясък на крила", + "dogs": "Кучета", + "rats": "Плъхове", + "mouse": "Мишка", + "insect": "Насекомо", + "cricket": "Щурец", + "mosquito": "Комар", + "fly": "Муха", + "buzz": "Бръм", + "frog": "Жаба", + "croak": "Квак", + "snake": "Змия", + "whale_vocalization": "Вик на кит", + "music": "Музика", + "musical_instrument": "Музикален инструмент", + "plucked_string_instrument": "Струнен инструмент", + "guitar": "Китара", + "electric_guitar": "Електрическа китара", + "bass_guitar": "Бас китара", + "acoustic_guitar": "Акустична китара", + "steel_guitar": "Метална китара", + "banjo": "Банджо", + "sitar": "Ситар", + "mandolin": "Мандолина", + "ukulele": "Укулеле", + "keyboard": "Клавир", + "piano": "Пиано", + "electric_piano": "Електрическо пиано", + "organ": "Орган", + "electronic_organ": "Електрически орган", + "hammond_organ": "Хамонд орган", + "synthesizer": "Синтезатор", + "sampler": "Семплър", + "percussion": "Перкуции", + "drum_kit": "Сет барабани", + "drum_machine": "Дръм машина", + "drum": "Барабан", + "drum_roll": "Туш", + "timpani": "Тимпани", + "tabla": "Табла", + "cymbal": "Цимбал", + "tambourine": "Тамбура", + "maraca": "Маракас", + "gong": "Гонг", + "vibraphone": "Вибрафон", + "orchestra": "Оркестър", + "brass_instrument": "Брас инструмент", + "french_horn": "Валдхорна", + "trumpet": "Тромпет", + "trombone": "Тромбон", + "bowed_string_instrument": "Струнен инструмент с лък", + "violin": "Цигулка", + "pizzicato": "Пицикато", + "cello": "Чело", + "double_bass": "Контрабас", + "wind_instrument": "Духов инструмент", + "flute": "Флейта", + "saxophone": "Саксофон", + "clarinet": "Кларинет", + "harp": "Арфа", + "bell": "Камбана", + "church_bell": "Църковна камбана", + "bicycle_bell": "Вело звънец", + "tuning_fork": "Камертон", + "harmonica": "Хармоника", + "accordion": "Акордеон", + "bagpipes": "Гайда", + "didgeridoo": "Диджириду", + "theremin": "Теремин", + "scratching": "Чесане", + "pop_music": "Поп музика", + "hip_hop_music": "Хип-хоп музика", + "beatboxing": "Бийтбокс", + "rock_music": "Рок музика", + "heavy_metal": "Хеви метъл", + "punk_rock": "Пънк рок", + "grunge": "Гръндж", + "progressive_rock": "Прогресивен рок", + "rock_and_roll": "Рок енд рол", + "psychedelic_rock": "Психаделичен рок", + "rhythm_and_blues": "Ритъм и блуз", + "soul_music": "Соул музика", + "reggae": "Реге", + "country": "Кънтри", + "swing_music": "Суинг музика", + "bluegrass": "Блуграс", + "funk": "Фънк", + "folk_music": "Фолк музика", + "middle_eastern_music": "Маанета", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Класическа музика", + "opera": "Опера", + "electronic_music": "Електронна музика", + "house_music": "Хаус музика", + "techno": "Техно", + "dubstep": "Дъбстеп", + "drum_and_bass": "Дръм и бас", + "electronica": "Електроника", + "trance_music": "Транс музика", + "music_of_latin_america": "Латино музика", + "salsa_music": "Салса музика", + "flamenco": "Фламенко", + "blues": "Блус", + "music_for_children": "Детска музика", + "a_capella": "Акапела", + "music_of_africa": "Африканска музика", + "afrobeat": "Афроритъм", + "gospel_music": "Госпел", + "music_of_asia": "Азиатска музика", + "ska": "Ска", + "song": "Песен", + "background_music": "Фонова музика", + "jingle": "Джингъл", + "thunderstorm": "Гръмотевична буря", + "thunder": "Гръмотевица", + "water": "Вода", + "rain": "Дъжд", + "raindrop": "Дъждовна капка", + "stream": "Поток", + "waterfall": "Водопад", + "ocean": "Океан", + "waves": "Вълни", + "steam": "Пара", + "fire": "Огън", + "vehicle": "Превозно средство", + "boat": "Лодка", + "sailboat": "Ветроходна лодка", + "rowboat": "Гребна лодка", + "motorboat": "Моторна лодка", + "ship": "Кораб", + "motor_vehicle": "МПС", + "car": "Кола", + "car_alarm": "Аларма на кола", + "skidding": "Поднасяне", + "tire_squeal": "Скърцане на гуми", + "car_passing_by": "Преминаваща кола", + "race_car": "Състезателна кола", + "truck": "Камион", + "air_brake": "Въздушна спирачка", + "air_horn": "Тромба", + "reversing_beeps": "Звуков сигнал за задна скорост", + "ice_cream_truck": "Камион за сладолед", + "bus": "Автобус", + "police_car": "Полицейска кола", + "ambulance": "Линейка", + "fire_engine": "Пожарна кола", + "motorcycle": "Мотоциклет", + "traffic_noise": "Шум от трафик", + "rail_transport": "Железопътен транспорт", + "train": "Влак", + "train_whistle": "Влакова свирка", + "train_horn": "Влаков клаксон", + "railroad_car": "Вагон", + "train_wheels_squealing": "Скърцане на ЖП спирачки", + "subway": "Метро", + "aircraft": "Самолет", + "aircraft_engine": "Самолетен двигател", + "jet_engine": "Реактивен двигател", + "propeller": "Витло", + "helicopter": "Хеликоптер", + "fixed-wing_aircraft": "Самолет с твърди крила", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "engine": "Двигател", + "dental_drill's_drill": "Зълболекарско борче", + "lawn_mower": "Косачка", + "chainsaw": "Моторен трион", + "engine_starting": "Стартиране на двигател", + "idling": "Празен ход", + "accelerating": "Ускорение", + "door": "Врата", + "doorbell": "Звънец", + "ding-dong": "Динг-донг", + "sliding_door": "Плъзгаща врата", + "slam": "Затръшване", + "knock": "Чук", + "tap": "Почукване", + "squeak": "Скръц", + "drawer_open_or_close": "Чекмедже отвори или затвори", + "dishes": "Чинии", + "cutlery": "Прибори за хранене", + "chopping": "Рязане", + "frying": "Пържене", + "microwave_oven": "Микровълнова фурна", + "blender": "Блендер", + "water_tap": "Кран за вода", + "speech": "Реч", + "yell": "Викане", + "bellow": "Под", + "whoop": "Уупс", + "pant": "Здъхване", + "stomach_rumble": "Къркорене на стомах", + "heartbeat": "Сърцебиене", + "scream": "Вик" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/common.json b/sam2-cpu/frigate-dev/web/public/locales/bg/common.json new file mode 100644 index 0000000..94e85dd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/common.json @@ -0,0 +1,119 @@ +{ + "time": { + "today": "Днес", + "yesterday": "Вчера", + "month_one": "{{time}} месец", + "month_other": "{{time}} месеца", + "day_one": "{{time}} ден", + "day_other": "{{time}} дни", + "hour_one": "{{time}} час", + "hour_other": "{{time}} часа", + "minute_one": "{{time}} минута", + "minute_other": "{{time}} минути", + "second_one": "{{time}} секунда", + "second_other": "{{time}} секунди", + "year_one": "{{time}} година", + "year_other": "{{time}} години", + "justNow": "Сега", + "last7": "Изминалите 7 дни", + "last14": "Изминалите 14 дни", + "last30": "Изминалите 30 дни", + "thisWeek": "Тази седмица", + "lastWeek": "Предходната седмица", + "thisMonth": "Този месец", + "lastMonth": "Предходния месец", + "5minutes": "5 минути", + "10minutes": "10 минути", + "30minutes": "30 минути", + "1hour": "1 час", + "12hours": "12 часа", + "24hours": "24 часа", + "pm": "pm", + "am": "am", + "yr": "г", + "d": "{{time}}д", + "h": "{{time}}ч", + "formattedTimestamp": { + "12hour": "МММ д, ч:мм:сс ааа", + "24hour": "МММ д, ЧЧ:мм:сс" + }, + "formattedTimestamp2": { + "12hour": "ММ/дд ч:мм:сса", + "24hour": "д МММ ЧЧ:мм:сс" + }, + "formattedTimestampHourMinute": { + "12hour": "ч:мм ааа", + "24hour": "ЧЧ:мм" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ч:мм:сс ааа", + "24hour": "ЧЧ:мм:сс" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "МММ д, ч:мм ааа", + "24hour": "МММ д, ЧЧ:мм" + }, + "formattedTimestampMonthDayYear": { + "12hour": "МММ д, гггг", + "24hour": "МММ д, гггг" + }, + "ago": "Преди {{timeAgo}}", + "untilForTime": "До {{time}}", + "untilForRestart": "Докато Frigate рестартира.", + "untilRestart": "До рестарт", + "mo": "{{time}}мес", + "m": "{{time}}м", + "s": "{{time}}с" + }, + "button": { + "apply": "Приложи", + "reset": "Нулиране", + "done": "Готово", + "disabled": "Деактивирано", + "save": "Запази", + "saving": "Запазване…", + "cancel": "Отказ", + "close": "Затвори", + "copy": "Копирай", + "edit": "Редактирай", + "copyCoordinates": "Копирай координати", + "delete": "Изтриване", + "yes": "Да", + "download": "Изтегляне", + "enabled": "Активирано", + "history": "История", + "back": "Назад", + "fullscreen": "Цял екран", + "exitFullscreen": "Излез от цял екран", + "pictureInPicture": "Картина в картина", + "twoWayTalk": "Двупосочни разговори", + "cameraAudio": "Аудио на камерата", + "on": "Включено", + "off": "Изключено", + "no": "Не", + "info": "Информация", + "suspended": "Спряно", + "unsuspended": "Възобновяване", + "play": "Пускане", + "unselect": "Демаркиране", + "export": "Експортиране", + "deleteNow": "Изтрии сега", + "next": "Следващ", + "disable": "Деактивирай", + "enable": "Активирай" + }, + "menu": { + "live": { + "title": "Наживо", + "cameras": { + "count_one": "{{count}} камера", + "count_other": "{{count}} камери" + } + } + }, + "label": { + "back": "Върни се" + }, + "selectItem": "Избери {{item}}", + "readTheDocumentation": "Прочетете документацията" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/auth.json new file mode 100644 index 0000000..56a13f7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/auth.json @@ -0,0 +1,6 @@ +{ + "form": { + "user": "Потребителско име", + "password": "Парола" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/camera.json new file mode 100644 index 0000000..e95016a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/camera.json @@ -0,0 +1,45 @@ +{ + "group": { + "add": "Добави група за камери", + "label": "Групи камери", + "edit": "Редактирай група за камери", + "delete": { + "label": "Изтрий група за камери", + "confirm": { + "title": "Потвърди изтриването", + "desc": "Сигурни ли сте, че искате да изтриете група {{name}}?" + } + }, + "name": { + "label": "Име", + "placeholder": "Въведете име…", + "errorMessage": { + "mustLeastCharacters": "Името на групата камери трябва да е поне 2 символа.", + "exists": "Групата камери вече съществува.", + "nameMustNotPeriod": "Името на групата камери не трябва да съръжа точка.", + "invalid": "Невалидно име за група камери." + } + }, + "cameras": { + "label": "Камери", + "desc": "Изберете камери за тази група." + }, + "icon": "Икона", + "success": "Група камери ({{name}}) беше записана.", + "camera": { + "setting": { + "stream": "Поток", + "placeholder": "Изберете поток", + "streamMethod": { + "label": "Метод на стийминг", + "placeholder": "Избери метод на стрийминг", + "method": { + "noStreaming": { + "label": "Без стрийминг" + } + } + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/dialog.json new file mode 100644 index 0000000..d704890 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/dialog.json @@ -0,0 +1,16 @@ +{ + "export": { + "name": { + "placeholder": "Име на експорта" + }, + "time": { + "lastHour_one": "Последният час", + "lastHour_other": "Последните {{count}} часа" + }, + "select": "Избери" + }, + "restart": { + "title": "Сигурен ли сте, че искате да рестартирате Frigate?", + "button": "Рестартирай" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/filter.json new file mode 100644 index 0000000..3aa7b61 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/filter.json @@ -0,0 +1,34 @@ +{ + "filter": "Филтър", + "cameras": { + "all": { + "title": "Всички камери", + "short": "Камери" + } + }, + "logSettings": { + "allLogs": "Всички логове" + }, + "subLabels": { + "all": "Всички подетикети", + "label": "Подетикети" + }, + "labels": { + "all": { + "title": "Всички етикети", + "short": "Етикети" + } + }, + "zones": { + "all": { + "title": "Всички зони", + "short": "Зони" + } + }, + "dates": { + "all": { + "title": "Всички дати", + "short": "Дати" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/icons.json new file mode 100644 index 0000000..a978fa3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Изберете иконка", + "search": { + "placeholder": "Потърси за икона…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/input.json new file mode 100644 index 0000000..9bd41d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Свали видео", + "toast": { + "success": "Вашето видео за преглеждане почна да се изтегля." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/bg/components/player.json new file mode 100644 index 0000000..39d9699 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFoundFor": "Не е намерен предварителен преглед за {{cameraName}}", + "stats": { + "latency": { + "short": { + "value": "{{seconds}} сек", + "title": "Закъснение" + }, + "title": "Закъснение:", + "value": "{{seconds}} секунди" + }, + "streamType": { + "title": "Тип поток:", + "short": "Тип" + }, + "bandwidth": { + "title": "Трафик:", + "short": "Трафик" + }, + "totalFrames": "Общо кадри:", + "droppedFrames": { + "title": "Пропуснати кадри:", + "short": { + "title": "Пропуснати", + "value": "{{droppedFrames}} кадри" + } + }, + "decodedFrames": "Декодирани кадри:", + "droppedFrameRate": "Честота на пропуснатати кадри:" + }, + "streamOffline": { + "desc": "Не са получени кадри в потока за разпознаване на {{cameraName}}, проверете лог файловете за грешки", + "title": "Потокът е офлайн" + }, + "submitFrigatePlus": { + "title": "Да се изпрати ли този кадър към Frigate+?", + "submit": "Изпрати" + }, + "noPreviewFound": "Не е намерен предварителен преглед", + "noRecordingsFoundForThisTime": "За това време не са намерени записи", + "livePlayerRequiredIOSVersion": "За този тип поток на живо се изисква iOS 17.1 или по-нова версия.", + "cameraDisabled": "Камерата е изключена", + "toast": { + "success": { + "submittedFrigatePlus": "Успешно изпратен кадър към Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Неуспешно изпратен кадър към Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/objects.json b/sam2-cpu/frigate-dev/web/public/locales/bg/objects.json new file mode 100644 index 0000000..a12c53c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/objects.json @@ -0,0 +1,23 @@ +{ + "animal": "Животно", + "dog": "Куче", + "bark": "Лай", + "cat": "Котка", + "horse": "Кон", + "goat": "Коза", + "sheep": "Овца", + "bird": "Птица", + "mouse": "Мишка", + "keyboard": "Клавир", + "vehicle": "Превозно средство", + "boat": "Лодка", + "car": "Кола", + "bus": "Автобус", + "motorcycle": "Мотоциклет", + "train": "Влак", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "door": "Врата", + "blender": "Блендер", + "person": "Човек" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/classificationModel.json new file mode 100644 index 0000000..685eefe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/classificationModel.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Модели за класификация" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/configEditor.json new file mode 100644 index 0000000..b2507c3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/configEditor.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "Настройки на конфигурацията - Фригейт", + "configEditor": "Настройки на конфигурацията" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/events.json new file mode 100644 index 0000000..3b82600 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/events.json @@ -0,0 +1,15 @@ +{ + "timeline.aria": "Избери хронология", + "timeline": "Хронология", + "calendarFilter": { + "last24Hours": "Последните 24 часа" + }, + "events": { + "label": "Събития", + "aria": "Избери събития", + "noFoundForTimePeriod": "Няма намерени събития за този времеви период." + }, + "allCameras": "Всички камери", + "alerts": "Известия", + "detections": "Засичания" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/explore.json new file mode 100644 index 0000000..f896493 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/explore.json @@ -0,0 +1,14 @@ +{ + "details": { + "item": { + "tips": { + "mismatch_one": "{{count}} недостъпен обект беше открит и включен в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити.", + "mismatch_other": "{{count}} недостъпни обекта бяха открити и включени в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити." + } + } + }, + "trackedObjectsCount_one": "{{count}} проследен обект ", + "trackedObjectsCount_other": "{{count}} проследени обекта ", + "documentTitle": "Разгледай - Фригейт", + "generativeAI": "Генериращ Изкъствен Интелект" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/exports.json new file mode 100644 index 0000000..ae366d5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/exports.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "Експорт - Frigate", + "search": "Търси" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/faceLibrary.json new file mode 100644 index 0000000..4c9b15b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/faceLibrary.json @@ -0,0 +1,18 @@ +{ + "deleteFaceAttempts": { + "desc_one": "Сигурни ли сте, че искате да изтриете {{count}} лице? Това действие не може да бъде отменено.", + "desc_other": "Сигурни ли сте, че искате да изтриете {{count}} лица? Това действие не може да бъде отменено." + }, + "toast": { + "success": { + "deletedFace_one": "Успешно изтрито {{count}} лице.", + "deletedFace_other": "Успешно изтрити {{count}} лица.", + "deletedName_one": "{{count}} лице бе изтрито успешно.", + "deletedName_other": "{{count}} лица бяха изтрити успешно." + } + }, + "description": { + "addFace": "Добавете нова колекция във библиотеката за лица при качването на първата ви снимка.", + "placeholder": "Напишете име за тази колекция" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/live.json new file mode 100644 index 0000000..01b3a5c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/live.json @@ -0,0 +1,69 @@ +{ + "stream": { + "playInBackground": { + "tips": "Активирайте тази опция, за да продължите поточното предаване, когато плейърът е скрит." + } + }, + "cameraAudio": { + "enable": "Включи звука на камерата", + "disable": "Изключи звука на камерата" + }, + "twoWayTalk": { + "enable": "Включи двупосочен разговор", + "disable": "Изключи двупосочен разговор" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Включи кликване за преместване", + "disable": "Изключи кликване за преместване" + } + } + }, + "muteCameras": { + "enable": "Заглушаване на всички камери", + "disable": "Включване на звука на всички камери" + }, + "recording": { + "enable": "Включи запис", + "disable": "Изключи запис" + }, + "snapshots": { + "enable": "Включи моментни снимки", + "disable": "Изключи моментни снимки" + }, + "audioDetect": { + "enable": "Включи аудио разпознаване", + "disable": "Изключи аудио разпознаване" + }, + "camera": { + "enable": "Включи камера", + "disable": "Изключи камера" + }, + "detect": { + "enable": "Включи разпознаване", + "disable": "Изключи разпознаване" + }, + "autotracking": { + "enable": "Включи автоматично проследяване", + "disable": "Изключи автоматично проследяване" + }, + "streamStats": { + "enable": "Показване на статистика на потока", + "disable": "Скриване на статистиката на потока" + }, + "manualRecording": { + "playInBackground": { + "desc": "Активирайте тази опция, за да продължите поточното предаване, когато плейърът е скрит." + }, + "showStats": { + "desc": "Активирайте тази опция, за да покажете статистиката на потока като наслагване върху канала на камерата." + }, + "recordDisabledTips": "Тъй като записът е изключен или ограничен в конфигурацията за тази камера, ще бъде запазена само моментна снимка." + }, + "cameraSettings": { + "cameraEnabled": "Камерата е включена" + }, + "documentTitle": "Наживо - Frigate", + "documentTitle.withCamera": "{{camera}} - На живо - Фригейт" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/recording.json new file mode 100644 index 0000000..e600636 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Филтър", + "export": "Експорт", + "calendar": "Календар", + "filters": "Филтри", + "toast": { + "error": { + "noValidTimeSelected": "Не е избран валиден времеви диапазон", + "endTimeMustAfterStartTime": "Крайното време трябва да бъде след началеният час" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/search.json new file mode 100644 index 0000000..e92f488 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/search.json @@ -0,0 +1,7 @@ +{ + "button": { + "save": "Запазване на търсенето" + }, + "search": "Търси", + "savedSearches": "Запазени търсения" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/settings.json new file mode 100644 index 0000000..08395e4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/settings.json @@ -0,0 +1,20 @@ +{ + "masksAndZones": { + "motionMasks": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + }, + "objectMasks": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + }, + "zones": { + "point_one": "{{count}} точка", + "point_other": "{{count}} точки" + } + }, + "documentTitle": { + "default": "Настройки - Фригейт", + "authentication": "Настройки за сигурността - Фругейт" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/bg/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/bg/views/system.json new file mode 100644 index 0000000..ec5f0ec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/bg/views/system.json @@ -0,0 +1,9 @@ +{ + "stats": { + "healthy": "Системата е изправна" + }, + "documentTitle": { + "cameras": "Статистики за Камери - Фригейт", + "storage": "Статистика за паметта - Фригейт" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ca/audio.json new file mode 100644 index 0000000..27c44b4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Parla", + "babbling": "Balbuceig", + "yell": "Crit", + "whispering": "Xiuxiuejant", + "laughter": "Riure", + "snicker": "Rialleta", + "crying": "Plor", + "bellow": "Bram", + "sigh": "Suspir", + "singing": "Cant", + "choir": "Cor", + "yodeling": "Cant Tirolès", + "chant": "Càntic", + "mantra": "Mantra", + "child_singing": "Cant Infantil", + "synthetic_singing": "Cant Sintètic", + "rapping": "Rap", + "humming": "Taral·leig", + "groan": "Gemec", + "grunt": "Grunyo", + "whistling": "Xiulet", + "wheeze": "Sibilància", + "snoring": "Ronc", + "gasp": "Jadeig", + "cat": "Gat", + "dog": "Gos", + "animal": "Animal", + "bark": "Escorça", + "horse": "Cavall", + "sheep": "Ovella", + "goat": "Cabra", + "bird": "Ocell", + "mouse": "Ratolí", + "keyboard": "Teclat", + "vehicle": "Vehicle", + "boat": "Vaixell", + "car": "Cotxe", + "motorcycle": "Motocicleta", + "bus": "Autobús", + "bicycle": "Bicicleta", + "train": "Tren", + "skateboard": "Monopatí", + "door": "Porta", + "hair_dryer": "Assecador de cabell", + "sink": "Aigüera", + "blender": "Batedora", + "toothbrush": "Raspall de dents", + "scissors": "Tisores", + "clock": "Rellotge", + "breathing": "Respiració", + "fart": "Pet", + "stomach_rumble": "Barbolló d'estómac", + "hands": "Mans", + "burping": "Eructe", + "hiccup": "Singlot", + "whoop": "Crit d'alegria", + "pant": "Esbufec", + "snort": "Esbufec nasal", + "cough": "Tos", + "throat_clearing": "Carraspeig", + "sneeze": "Esternut", + "sniff": "Fregit nasal", + "run": "Córrer", + "shuffle": "Passos arrossegats", + "footsteps": "Passos", + "chewing": "Masticació", + "biting": "Mossegada", + "gargling": "Gàrgares", + "finger_snapping": "Claqueig de dits", + "heartbeat": "Batec del cor", + "heart_murmur": "Sospit cardíac", + "cheering": "Ovacions", + "applause": "Aplaudiments", + "clapping": "Cop de mans", + "chatter": "Xerrameca", + "crowd": "Multitud", + "children_playing": "Nens jugant", + "pets": "Animals de companyia", + "camera": "Càmera", + "wild_animals": "Animals salvatges", + "heavy_engine": "Motor pesat", + "wedding_music": "Música de casament", + "yip": "Crit agut", + "howl": "Udol", + "bow_wow": "Lladruc", + "growling": "Grunyit", + "whimper_dog": "Gemec de gos", + "purr": "Ronroneig", + "meow": "Miol", + "hiss": "Siseig", + "caterwaul": "Udol estrident", + "livestock": "Bestiar", + "clip_clop": "Clip-clop", + "neigh": "Relinxo", + "cattle": "Bestiar boví", + "moo": "Mugir", + "cowbell": "Esquellot", + "pig": "Porc", + "oink": "Oink", + "bleat": "Brama", + "fowl": "Au de corral", + "chicken": "Pollastre", + "cluck": "Cloqueig", + "cock_a_doodle_doo": "Quiquiriquí", + "turkey": "Gall dindi", + "gobble": "Gorgoriteig", + "duck": "Ànec", + "quack": "Quac", + "goose": "Oca", + "honk": "Cluc-cluc", + "roaring_cats": "Gats que rugen", + "roar": "Rugit", + "chirp": "Piulet", + "squawk": "Xerric", + "pigeon": "Colom", + "coo": "Arruix", + "crow": "Corb", + "caw": "Cric", + "owl": "Mussol", + "hoot": "Ulul", + "flapping_wings": "Batuda d’ales", + "dogs": "Gossos", + "rats": "Rates", + "patter": "Repic", + "insect": "Insecte", + "cricket": "Grill", + "mosquito": "Mosquit", + "fly": "Mosca", + "buzz": "Brunzit", + "frog": "Granota", + "croak": "Grall", + "snake": "Serp", + "rattle": "Cascavell", + "whale_vocalization": "Vocalització de balena", + "music": "Música", + "musical_instrument": "Instrument musical", + "plucked_string_instrument": "Instrument de corda pinçada", + "guitar": "Guitarra", + "electric_guitar": "Guitarra elèctrica", + "bass_guitar": "Baix", + "acoustic_guitar": "Guitarra acústica", + "steel_guitar": "Guitarra steel", + "tapping": "Tapping", + "strum": "Rasgueig", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cítara", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Piano elèctric", + "organ": "Orgue", + "electronic_organ": "Orgue electrònic", + "hammond_organ": "Orgue Hammond", + "synthesizer": "Sintetitzador", + "sampler": "Sampler", + "harpsichord": "Clavicèmbal", + "percussion": "Percussió", + "drum_kit": "Bateria", + "drum_machine": "Caixa de ritmes", + "drum": "Tambor", + "snare_drum": "Caixa", + "rimshot": "Rimshot", + "drum_roll": "Rul·lat de tambor", + "bass_drum": "Bombo", + "timpani": "Timpà", + "tabla": "Tabla", + "cymbal": "Plat", + "hi_hat": "Charles", + "wood_block": "Bloc de fusta", + "tambourine": "Pandereta", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Campanes tubulars", + "mallet_percussion": "Percussió amb baquetes", + "marimba": "Marimba", + "glockenspiel": "Carilló", + "vibraphone": "Vibràfon", + "steelpan": "Steelpan", + "orchestra": "Orquestra", + "brass_instrument": "Instrument de metall", + "french_horn": "Corn francès", + "trumpet": "Trompeta", + "trombone": "Trombó", + "bowed_string_instrument": "Instrument de corda fregada", + "string_section": "Secció de corda", + "violin": "Violí", + "pizzicato": "Pizzicato", + "cello": "Violoncel", + "double_bass": "Contrabaix", + "wind_instrument": "Instrument de vent", + "flute": "Flauta", + "saxophone": "Saxòfon", + "clarinet": "Clarinet", + "harp": "Arpa", + "bell": "Campana", + "church_bell": "Campana d'església", + "jingle_bell": "Campaneta", + "bicycle_bell": "Timbre de bicicleta", + "tuning_fork": "Diapasó", + "chime": "Timbre", + "wind_chime": "Campanes de vent", + "harmonica": "Harmònica", + "accordion": "Acordió", + "bagpipes": "Gaites", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Bol tibetà", + "scratching": "Esgarrapar", + "pop_music": "Música pop", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatboxing", + "rock_music": "Música rock", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Rock progressiu", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock psicodèlic", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Música soul", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Música swing", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Música folk", + "middle_eastern_music": "Música d'Orient Mitjà", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música clàssica", + "opera": "Òpera", + "electronic_music": "Música electrònica", + "house_music": "Música house", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electrònica", + "electronic_dance_music": "Música electrònica de ball", + "ambient_music": "Música ambient", + "trance_music": "Música trance", + "music_of_latin_america": "Música d'Amèrica Llatina", + "salsa_music": "Música salsa", + "flamenco": "Flamenc", + "blues": "Blues", + "music_for_children": "Música per a nens", + "new-age_music": "Música new age", + "vocal_music": "Música vocal", + "a_capella": "A capella", + "music_of_africa": "Música d'Àfrica", + "afrobeat": "Afrobeat", + "christian_music": "Música cristiana", + "gospel_music": "Música gospel", + "music_of_asia": "Música d'Àsia", + "carnatic_music": "Música carnàtica", + "music_of_bollywood": "Música de bollywood", + "ska": "Ska", + "traditional_music": "Música tradicional", + "independent_music": "Música independent", + "song": "Cançó", + "background_music": "Música de fons", + "theme_music": "Música temàtica", + "jingle": "Jingle", + "soundtrack_music": "Música de banda sonora", + "lullaby": "Lullaby", + "video_game_music": "Música de videojocs", + "christmas_music": "Música nadalenca", + "dance_music": "Música dance", + "happy_music": "Música alegre", + "sad_music": "Música trista", + "tender_music": "Música tendra", + "exciting_music": "Música emocionant", + "angry_music": "Música enfadada", + "scary_music": "Música de por", + "wind": "Vent", + "wind_noise": "Soroll del vent", + "thunderstorm": "Tempesta", + "thunder": "Tro", + "water": "Aigua", + "rain": "Pluja", + "raindrop": "Gota de pluja", + "rain_on_surface": "Pluja en superfície", + "stream": "Rierol", + "waterfall": "Cascada", + "ocean": "Oceà", + "waves": "Ones", + "steam": "Vapor", + "fire": "Foc", + "sailboat": "Veler", + "rowboat": "Barca de rems", + "ship": "Vaixell", + "motor_vehicle": "Vehicle de motor", + "car_alarm": "Alarma del cotxe", + "car_passing_by": "Cotxe passant", + "race_car": "Cotxe de curses", + "truck": "Camió", + "air_brake": "Fre d'aire", + "air_horn": "Bocina d'aire", + "ice_cream_truck": "Camió de gelats", + "emergency_vehicle": "Vehicle d'emergència", + "police_car": "Cotxe de policia", + "ambulance": "Ambulància", + "fire_engine": "Camió de bombers", + "traffic_noise": "Soroll de trànsit", + "rail_transport": "Transport ferroviari", + "train_whistle": "Xiulet de tren", + "train_horn": "Bocina de tren", + "railroad_car": "Vagó de tren", + "subway": "Metro", + "aircraft": "Aeronau", + "aircraft_engine": "Motor d'aeronau", + "propeller": "Hèlix", + "helicopter": "Helicòpter", + "fixed-wing_aircraft": "Aeronau d'Ala Fixa", + "engine": "Motor", + "light_engine": "Motor lleuger", + "dental_drill's_drill": "Trepant dental", + "lawn_mower": "Talla-gespa", + "chainsaw": "Motoserra", + "medium_engine": "Motor mitjà", + "engine_starting": "Arranc del motor", + "idling": "Ralentí", + "accelerating": "Accelerant", + "doorbell": "Timbre", + "ding-dong": "Ding-dong", + "sliding_door": "Porta corredissa", + "slam": "Cop de porta", + "knock": "Toc", + "tap": "Toc suau", + "squeak": "Grinyol", + "cupboard_open_or_close": "Obertura o tancament d'armari", + "drawer_open_or_close": "Obertura o tancament de calaix", + "dishes": "Plats", + "cutlery": "Coberteria", + "rustling_leaves": "Sons de fulles", + "gurgling": "Borbolleig", + "crackle": "Cremoreig", + "motorboat": "Llanxa a motor", + "toot": "Botzinada", + "skidding": "Derrapada", + "reversing_beeps": "Bips de marxa enrere", + "jet_engine": "Motor a reacció", + "train_wheels_squealing": "Xiulet de rodes de tren", + "engine_knocking": "Cop de motor", + "chopping": "Tallant", + "frying": "Fregint", + "electric_shaver": "Afeitadora elèctrica", + "shuffling_cards": "Barrejar cartes", + "alarm": "Alarma", + "alarm_clock": "Despertador", + "siren": "Sirena", + "buzzer": "Brunzidor", + "gears": "Engranatges", + "pulleys": "Politges", + "glass": "Vidre", + "chop": "Tall", + "splinter": "Astella", + "scream": "Crit", + "field_recording": "Enregistrament de camp", + "tire_squeal": "Xiulet de rodes", + "explosion": "Explosió", + "wood": "Fusta", + "crack": "Esquerda", + "air_conditioning": "Aire condicionat", + "tick-tock": "Tic-tac", + "sewing_machine": "Màquina de cosir", + "writing": "Escrivint", + "telephone": "Telèfon", + "environmental_noise": "Soroll ambiental", + "zipper": "Cremallera", + "smoke_detector": "Detector de fums", + "sound_effect": "Efecte sonor", + "microwave_oven": "Forn microones", + "water_tap": "Aixeta d'aigua", + "toilet_flush": "Cisterna del vàter", + "electric_toothbrush": "Raspall de dents elèctric", + "vacuum_cleaner": "Aspiradora", + "keys_jangling": "Claus repicant", + "bathtub": "Banyera", + "coin": "Moneda", + "typing": "Mecanografia", + "computer_keyboard": "Teclat d'ordinador", + "telephone_dialing": "Marcatge telefònic", + "dial_tone": "To de marcatge", + "telephone_bell_ringing": "Timbre del telèfon sonant", + "typewriter": "Màquina d'escriure", + "ringtone": "To de trucada", + "busy_signal": "Senyal d'ocupat", + "fire_alarm": "Alarma d'incendis", + "civil_defense_siren": "Sirena de defensa civil", + "foghorn": "Bocina de boira", + "whistle": "Xiulet", + "steam_whistle": "Xiulet de vapor", + "mechanical_fan": "Ventall mecànic", + "cash_register": "Caixa registradora", + "single-lens_reflex_camera": "Càmera reflex de lent fixa", + "mechanisms": "Mecanismes", + "ratchet": "Trinquet", + "tick": "Tic", + "printer": "Impressora", + "tools": "Eines", + "hammer": "Martell", + "jackhammer": "Martell neumàtic", + "sawing": "Serratge", + "filing": "Llimar", + "sanding": "Poliment", + "power_tool": "Eina elèctrica", + "machine_gun": "Ametralladora", + "cap_gun": "Pistola de joguina", + "drill": "Trepant", + "gunshot": "Tret", + "fusillade": "Ràfega de trets", + "fireworks": "Focs artificials", + "firecracker": "Petard", + "chink": "Clinc", + "shatter": "Trencar", + "silence": "Silenci", + "static": "Estàtic", + "white_noise": "Soroll blanc", + "burst": "Explosió", + "eruption": "Erupció", + "boom": "Boom", + "television": "Televisió", + "radio": "Ràdio", + "pink_noise": "Soroll rosa", + "power_windows": "Finestres elèctriques", + "artillery_fire": "Foc d'artilleria", + "sodeling": "Cant a la tirolesa", + "vibration": "Vibració", + "throbbing": "Palpitant", + "cacophony": "Cacofonia", + "sidetone": "To local", + "distortion": "Distorsió", + "mains_hum": "brunzit", + "noise": "Soroll", + "echo": "Echo", + "reverberation": "Reverberació", + "inside": "Interior", + "pulse": "Pols", + "outside": "Fora", + "chirp_tone": "To de grinyol", + "harmonic": "Harmònic", + "sine_wave": "Ona sinus", + "crunch": "Cruixit", + "hum": "Taral·lejar", + "plop": "Chof", + "clickety_clack": "Clic-Clac", + "clicking": "Clicant", + "clatter": "Soroll", + "chird": "Piular", + "liquid": "Líquid", + "splash": "Xof", + "slosh": "Xip-xap", + "boing": "Boing", + "zing": "Fiu", + "rumble": "Bum-bum", + "sizzle": "Xiu-xiu", + "whir": "Brrrm", + "rustle": "Fru-Fru", + "creak": "Clic-clac", + "clang": "Clang", + "squish": "Xaf", + "drip": "Plic-plic", + "pour": "Glug-glug", + "trickle": "Xiulet", + "gush": "Xuuuix", + "fill": "Glug-glug", + "ding": "Ding", + "ping": "Ping", + "beep": "Bip", + "squeal": "Xiscle", + "crumpling": "Arrugant-se", + "rub": "Fregar", + "scrape": "Raspar", + "scratch": "Rasca", + "whip": "Fuet", + "bouncing": "Rebotant", + "breaking": "Trencant", + "smash": "Aixafar", + "whack": "Cop", + "slap": "Bufetada", + "bang": "Bang", + "basketball_bounce": "Rebot de bàsquet", + "chorus_effect": "Efecte de cor", + "effects_unit": "Unitat d'Efectes", + "electronic_tuner": "Afinador electrònic", + "thunk": "Bruix", + "thump": "Cop fort", + "whoosh": "Xiuxiueig", + "arrow": "Fletxa", + "sonar": "Sonar", + "boiling": "Bullint", + "stir": "Remenar", + "pump": "Bomba", + "spray": "Esprai", + "shofar": "Xofar", + "crushing": "Aixafament", + "change_ringing": "Toc de campanes", + "flap": "Cop de peu", + "roll": "Rodament", + "tearing": "Esquinçat" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/common.json b/sam2-cpu/frigate-dev/web/public/locales/ca/common.json new file mode 100644 index 0000000..03d217a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/common.json @@ -0,0 +1,304 @@ +{ + "role": { + "title": "Rol", + "viewer": "Visualitzador", + "admin": "Administrador", + "desc": "Els administradors tenen accés complet a totes les característiques de la interfície d'usuari de Frigate. Els visualitzadors es limiten a visualitzar càmeres, articles de revisió i imatges històriques a la interfície d'usuari." + }, + "menu": { + "language": { + "yue": "粵語 (Cantonès)", + "zhCN": "简体中文 (Xinès simplificat)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francès)", + "ar": "العربية (Àrab)", + "de": "Deutsch (Alemany)", + "sv": "Svenska (Suec)", + "cs": "Čeština (Txec)", + "nb": "Norsk Bokmål (Noruec Bokmål)", + "ko": "한국어 (Coreà)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "hu": "Magyar (Hongarès)", + "fi": "Suomi (Finlandès)", + "en": "English (Anglès)", + "pt": "Português (Portuguès)", + "ja": "日本語 (Japonès)", + "es": "Español (Espanyol)", + "withSystem": { + "label": "Utilitzeu la configuració del sistema per a l'idioma" + }, + "tr": "Türkçe (Turc)", + "it": "Italiano (Italià)", + "he": "עברית (Hebreu)", + "el": "Ελληνικά (Grec)", + "ro": "Română (Romanès)", + "nl": "Nederlands (Holandès)", + "pl": "Polski (Polonès)", + "uk": "Українська (Ucraïnès)", + "da": "Dansk (Danès)", + "sk": "Slovenčina (Eslovac)", + "ru": "Русский (Rus)", + "th": "ไทย (Tailandès)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portuguès Brasiler)", + "sr": "Српски (Serbi)", + "sl": "Slovenščina (Sloveni)", + "lt": "Lietuvių (Lituà)", + "bg": "Български (Búlgar)", + "gl": "Galego (Gallec)", + "id": "Bahasa Indonesia (Indonesi)", + "ur": "اردو (Urdú)" + }, + "system": "Sistema", + "systemMetrics": "Mètriques del sistema", + "configuration": "Configuració", + "systemLogs": "Registres del sistema", + "configurationEditor": "Editor de configuració", + "languages": "Idiomes", + "settings": "Opcions", + "darkMode": { + "light": "Clar", + "dark": "Fosc", + "withSystem": { + "label": "Utilitzeu la configuració del sistema per al mode clar o fosc" + }, + "label": "Mode fosc" + }, + "withSystem": "Sistema", + "appearance": "Aspecte", + "theme": { + "blue": "Blau", + "green": "Verd", + "nord": "Nord", + "red": "Vermell", + "default": "Per defecte", + "highcontrast": "Contrast Alt", + "label": "Tema" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentació", + "label": "Documentació de Frigate" + }, + "restart": "Reinicia Frigate", + "live": { + "title": "Directe", + "allCameras": "Totes les càmeres", + "cameras": { + "title": "Càmeres", + "count_one": "{{count}} Càmera", + "count_many": "{{count}} Càmeres", + "count_other": "{{count}} Càmeres" + } + }, + "review": "Revisió", + "explore": "Explora", + "export": "Exportar", + "uiPlayground": "Zona de proves de la interfície d'usuari", + "faceLibrary": "Biblioteca de cares", + "user": { + "title": "Usuari", + "setPassword": "Estableix Contrasenya", + "account": "Compte", + "anonymous": "Anònim", + "logout": "Tanca la sessió", + "current": "Usuari actual: {{user}}" + }, + "classification": "Classificació" + }, + "pagination": { + "previous": { + "label": "Ves a pàgina anterior", + "title": "Anterior" + }, + "next": { + "label": "Ves a pàgina següent", + "title": "Següent" + }, + "more": "Més pàgines", + "label": "paginació" + }, + "time": { + "untilForTime": "Fins les {{time}}", + "untilForRestart": "Fins que Frigate es reiniciï.", + "untilRestart": "Fins que es reiniciï", + "ago": "Fa {{timeAgo}}", + "justNow": "Ara mateix", + "today": "Avui", + "yesterday": "Ahir", + "last7": "Últims 7 dies", + "last14": "Últims 14 dies", + "last30": "Últims 30 dies", + "thisWeek": "Aquesta setmana", + "lastWeek": "La setmana passada", + "thisMonth": "Aquest mes", + "lastMonth": "El mes passat", + "5minutes": "5 minuts", + "10minutes": "10 minuts", + "30minutes": "30 minuts", + "1hour": "1 hora", + "12hours": "12 hores", + "24hours": "24 hores", + "pm": "pm", + "am": "am", + "yr": "{{time}}any", + "year_one": "{{time}} any", + "year_many": "{{time}} anys", + "year_other": "{{time}} anys", + "mo": "{{time}}mes", + "month_one": "{{time}} mes", + "month_many": "{{time}} mesos", + "month_other": "{{time}} mesos", + "h": "{{time}}h", + "d": "{{time}}d", + "day_one": "{{time}} dia", + "day_many": "{{time}} dies", + "day_other": "{{time}} dies", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} hores", + "hour_other": "{{time}} hores", + "m": "{{time}} m", + "minute_one": "{{time}} minut", + "minute_many": "{{time}} minuts", + "minute_other": "{{time}} minuts", + "s": "{{time}}s", + "second_one": "{{time}} segon", + "second_many": "{{time}} segons", + "second_other": "{{time}} segons", + "formattedTimestamp": { + "12hour": "MMM d, h::mm::ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "inProgress": "En curs", + "invalidStartTime": "Hora d'inici no vàlida", + "invalidEndTime": "Hora de finalització no vàlida" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "peus", + "meters": "metres" + }, + "data": { + "kbps": "Kb/s", + "mbps": "Mb/s", + "gbps": "Gb/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "label": { + "back": "Torna enrere", + "hide": "Oculta {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Cap", + "all": "Tots" + }, + "button": { + "apply": "Aplicar", + "reset": "Restablir", + "done": "Fet", + "disabled": "Deshabilitat", + "disable": "Deshabilitar", + "save": "Guardar", + "copy": "Copiar", + "back": "Enrere", + "pictureInPicture": "Imatge en Imatge", + "twoWayTalk": "Xerrada bidireccional", + "cameraAudio": "Àudio de la càmera", + "no": "No", + "yes": "Sí", + "download": "Descarregar", + "info": "Informació", + "suspended": "Suspès", + "export": "Exportar", + "deleteNow": "Eliminar ara", + "next": "Següent", + "saving": "Guardant…", + "cancel": "Cancelar", + "edit": "Editar", + "copyCoordinates": "Copiar coordenades", + "delete": "Elimina", + "unsuspended": "Reactivar", + "play": "Reproduir", + "close": "Tancar", + "history": "Historial", + "fullscreen": "Pantalla completa", + "exitFullscreen": "Sortir de pantalla completa", + "on": "ENCÈS", + "off": "APAGAT", + "unselect": "Desseleccionar", + "enable": "Habilitar", + "enabled": "Habilitat", + "continue": "Continua" + }, + "toast": { + "copyUrlToClipboard": "URL copiada al porta-retalls.", + "save": { + "title": "Guardar", + "error": { + "title": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}", + "noMessage": "No s'han pogut guardar els canvis de configuració" + } + } + }, + "accessDenied": { + "desc": "No teniu permís per veure aquesta pàgina.", + "documentTitle": "Accés Denegat - Frigate", + "title": "Accés Denegat" + }, + "notFound": { + "documentTitle": "No s'ha trobat - Frigate", + "title": "404", + "desc": "Pàgina no trobada" + }, + "selectItem": "Selecciona {{item}}", + "readTheDocumentation": "Llegir la documentació", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}}, i {{last}}", + "separatorWithSpace": ",· " + }, + "field": { + "optional": "Opcional", + "internalID": "L'ID intern que Frigate s'utilitza a la configuració i a la base de dades" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/auth.json new file mode 100644 index 0000000..1ca91ee --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Usuari", + "password": "Contrasenya", + "login": "Iniciar sessió", + "errors": { + "usernameRequired": "El nom d'usuari és obligatori", + "passwordRequired": "La contrasenya és obligatoria", + "rateLimit": "S'ha superat el límit d'intents. Torna-ho a provar més tard.", + "loginFailed": "Error en l'inici de sessió", + "unknownError": "Error desconegut. Comproveu els registres.", + "webUnknownError": "Error desconegut. Comproveu els registres de la consola." + }, + "firstTimeLogin": "Intentar iniciar sessió per primera vegada? Les credencials s'imprimeixen als registres de Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/camera.json new file mode 100644 index 0000000..bfa8ea1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "add": "Afegir grup de càmeres", + "edit": "Editar grup de càmeres", + "delete": { + "label": "Eliminar grup de càmeres", + "confirm": { + "title": "Confirmar eliminació", + "desc": "Estàs segur que vols eliminar el grup de càmeres {{name}}?" + } + }, + "name": { + "placeholder": "Introduïu un nom…", + "errorMessage": { + "mustLeastCharacters": "El nom del grup de càmeres ha de ser de com a mínim 2 caràcters.", + "exists": "El nom del grup de càmeres ja existeix.", + "nameMustNotPeriod": "El nom del grup de càmeres no pot contenir un punt.", + "invalid": "Nom del grup de càmeres no vàlid." + }, + "label": "Nom" + }, + "cameras": { + "label": "Càmeres", + "desc": "Seleccioneu càmeres per a aquest grup." + }, + "camera": { + "setting": { + "label": "Paràmetres de transmissió de la càmera", + "title": "Paràmetres de transmissió de {{cameraName}}", + "audioIsAvailable": "L'àudio està disponible per a aquesta transmissió", + "audioIsUnavailable": "L'audio no està disponible per a aquesta transmissió", + "audio": { + "tips": { + "document": "Llegir la documentació · ", + "title": "L'audio ha de venir de la càmera i estar configurat a go2rtc per a aquesta transmissió." + } + }, + "streamMethod": { + "label": "Mètode de transmissió", + "method": { + "noStreaming": { + "label": "Sense transmissió", + "desc": "Les imatges de la càmera només s'actualitzaran una vegada per minut i no hi haurà transmissió en viu." + }, + "smartStreaming": { + "label": "Transmissió intel·ligent (recomanat)", + "desc": "La transmissió intel·ligent actualitzarà la imatge de la teva càmera una vegada per minut quan no es detecti activitat per a conservar amplada de banda i recursos. Quan es detecti activitat, la imatge canviarà automàticament a una transmissió en directe." + }, + "continuousStreaming": { + "label": "Transmissió contínua", + "desc": { + "title": "La imatge de la càmera sempre serà una transmissió en directe quan estigui visible al panell de control, tot i que no hi hagi cap activitat detectada.", + "warning": "La transmissió contínua pot provocar problemes d'ús elevat d'amplada de banda i rendiment. Feu servir amb precaució." + } + } + }, + "placeholder": "Tria un mètode de transmissió" + }, + "compatibilityMode": { + "label": "Mode de compatibilitat", + "desc": "Activeu aquesta opció només si la transmissió en directe de la càmera mostra artefactes de color i té una línia diagonal a la part dreta de la imatge." + }, + "desc": "Cambia les opcions de transmissió en viu del panell de control d'aquest grup de càmeres. Aquest paràmetres son específics del dispositiu/navegador.", + "stream": "Transmissió", + "placeholder": "Seleccionar una transmissió" + }, + "birdseye": "Ull d'ocell" + }, + "success": "El grup de càmeres ({{name}}) ha estat guardat.", + "icon": "Icona", + "label": "Grups de Càmeres" + }, + "debug": { + "options": { + "title": "Opcions", + "showOptions": "Mostra opcions", + "hideOptions": "Amaga opcions", + "label": "Paràmetres" + }, + "boundingBox": "Caixa delimitadora", + "timestamp": "Marca temporal", + "zones": "Zones", + "mask": "Màscara", + "motion": "Moviment", + "regions": "Regions" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/dialog.json new file mode 100644 index 0000000..79e4bd8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "Estàs segur que vols reiniciar Frigate?", + "button": "Reiniciar", + "restarting": { + "title": "Frigate s'està reiniciant", + "content": "Aquesta pàgina es tornarà a carregar d'aquí a {{countdown}} segons.", + "button": "Forçar la recàrrega ara" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar a Frigate+", + "desc": "Els objectes a les ubicacions que voleu evitar no són falsos positius. Enviar-los com a falsos positius confondrà el model." + }, + "review": { + "question": { + "label": "Confirmar aquesta etiqueta per a Frigate Plus", + "ask_a": "Aquest objecte és un {{label}}?", + "ask_an": "Aquest objecte és un {{label}}?", + "ask_full": "Aquest objecte és un {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Enviat" + } + } + }, + "video": { + "viewInHistory": "Veure a l'historial" + } + }, + "export": { + "time": { + "custom": "Personalitzat", + "fromTimeline": "Seleccionar a la línia de temps", + "lastHour_one": "L'última hora", + "lastHour_many": "Les últimes {{count}} hores", + "lastHour_other": "Les últimes {{count}} hores", + "start": { + "title": "Hora d'inci", + "label": "Seleccionar una hora d'inici" + }, + "end": { + "title": "Hora de finalització", + "label": "Seleccionar una hora de finalització" + } + }, + "name": { + "placeholder": "Nom de l'exportació" + }, + "select": "Seleccionar", + "export": "Exportar", + "selectOrExport": "Seleccionar o exportar", + "toast": { + "success": "Exportació inciada amb èxit. Pots veure l'arxiu a la pàgina d'exportacions.", + "error": { + "endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici", + "noVaildTimeSelected": "No s'ha seleccionat un rang de temps vàlid", + "failed": "No s'ha pogut inciar l'exportació: {{error}}" + }, + "view": "Vista" + }, + "fromTimeline": { + "saveExport": "Guardar exportació", + "previewExport": "Previsualitzar exportació" + } + }, + "streaming": { + "label": "Transmissió", + "restreaming": { + "disabled": "La retransmissió no està habilitada per a aquesta càmera.", + "desc": { + "title": "Configurar go2rtc per a àudio i opcions addicionals de visualització en directe per a aquesta càmera.", + "readTheDocumentation": "Llegir la documentació" + } + }, + "showStats": { + "label": "Mostrar les estadístiques de la transmissió", + "desc": "Activa aquesta opció per a mostrar les estadístiques de la transmissió superposades a la imatge de la càmera." + }, + "debugView": "Vista de depuració" + }, + "search": { + "saveSearch": { + "label": "Desar la cerca", + "desc": "Propocioneu un nom per a aquesta cerca desada.", + "placeholder": "Introduïu un nom per a la vostra cerca", + "success": "La cerca {{searchName}} ha sigut desada.", + "overwrite": "{{searchName}} ja existeix. Si deseu, es sobreescriurà el valor existent.", + "button": { + "save": { + "label": "Desar aquesta cerca" + } + } + } + }, + "recording": { + "button": { + "deleteNow": "Suprimir ara", + "export": "Exportar", + "markAsReviewed": "Marcar com a revisat", + "markAsUnreviewed": "Marcar com no revisat" + }, + "confirmDelete": { + "title": "Confirmar la supressió", + "desc": { + "selected": "Esteu segurs que voleu suprimir tots els vídeos enregistrats associats a aquest element de revisió?

Manteniu premuda la tecla Maj per ometre aquest diàleg en el futur." + }, + "toast": { + "success": "Els enregistraments de vídeo associats als elements de revisió seleccionats s’han suprimit correctament.", + "error": "No s'ha pogut suprimir: {{error}}" + } + } + }, + "imagePicker": { + "selectImage": "Selecciona la miniatura d'un objecte rastrejat", + "search": { + "placeholder": "Cerca per etiqueta o subetiqueta..." + }, + "noImages": "No s'han trobat miniatures per a aquesta càmera", + "unknownLabel": "Imatge activadora desada" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/filter.json new file mode 100644 index 0000000..e178972 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/filter.json @@ -0,0 +1,136 @@ +{ + "labels": { + "all": { + "short": "Etiquetes", + "title": "Totes les etiquetes" + }, + "label": "Etiquetes", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetes" + }, + "filter": "Filtre", + "zones": { + "label": "Zones", + "all": { + "title": "Totes les zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Totes les dates", + "short": "Dates" + }, + "selectPreset": "Selecciona un preajust…" + }, + "more": "Més filtres", + "reset": { + "label": "Restablir filtres als valors predeterminats" + }, + "timeRange": "Rang de temps", + "subLabels": { + "label": "Subetiquetes", + "all": "Totes les subetiquetes" + }, + "score": "Puntuació", + "estimatedSpeed": "Velocitat estimada ({{unit}})", + "features": { + "label": "Característiques", + "hasSnapshot": "Té una instantània", + "hasVideoClip": "Té un clip de vídeo", + "submittedToFrigatePlus": { + "label": "Enviat a Frigate+", + "tips": "Primer heu de filtrar els objectes de seguiment que tenen una instantània.

Els objectes de seguiment sense una instantània no es poden enviar a Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendent)", + "dateDesc": "Data (Descendent)", + "scoreAsc": "Puntuació de l'objecte (Ascendent)", + "scoreDesc": "Puntuació de l'objecte (Descendent)", + "speedAsc": "Velocitat estimada (Ascendent)", + "speedDesc": "Velocitat estimada (descendent)", + "relevance": "Rellevància" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "Resum", + "unfilteredGrid": "Quadrícula sense filtrar", + "title": "Vista per defecte", + "desc": "Quan no s'ha seleccionat cap filtre, mostreu un resum dels objectes de seguiment més recents per etiqueta, o visualitzeu una quadrícula sense filtrar." + }, + "gridColumns": { + "title": "Columnes de la quadrícula", + "desc": "Seleccionar el nombre de columnes a la vista de quadrícula." + }, + "searchSource": { + "label": "Font de cerca", + "options": { + "description": "Descripció", + "thumbnailImage": "Imatge en miniatura" + }, + "desc": "Trieu si voleu cercar les miniatures o les descripcions dels objectes de seguiment." + }, + "title": "Configuració" + }, + "date": { + "selectDateBy": { + "label": "Seleccionr a una data per filtrar" + } + } + }, + "logSettings": { + "disableLogStreaming": "Deshabilitar la transmissió de registres", + "allLogs": "Tots els registres", + "loading": { + "desc": "Quan el panell de registre es desplaça cap a la part inferior, els registres nous apareixen automàticament a mesura que s'afegeixen.", + "title": "Carregant" + }, + "label": "Filtrar nivell de registre", + "filterBySeverity": "Filtrar registre per gravetat" + }, + "trackedObjectDelete": { + "title": "Confirmar la supressió", + "desc": "En suprimir aquests {{objectLength}} objectes de seguiment, s'elimina la instatània, les incrustacions desades, i els registres de temps de vida. Les imatges gravades d'aquests objectes NO es suprimiran de l'historial.

Està segur que vol continuar?

Manteniu la tecla Shift per ometre aquest diàleg en el futur.", + "toast": { + "success": "Els objectes amb seguiment s'han suprimit correctament.", + "error": "No s'han pogut suprimir els objectes de seguiment: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrar per màscara de zona" + }, + "recognizedLicensePlates": { + "title": "Matrícules reconegudes", + "loadFailed": "No s'han pogut carregar les matrícules reconegudes.", + "loading": "Carregant les matrícules reconegudes…", + "placeholder": "Escriu per a buscar matrícules…", + "noLicensePlatesFound": "No s'han trobat matrícules.", + "selectPlatesFromList": "Seleccioni una o més matrícules de la llista.", + "selectAll": "Seleccionar tots", + "clearAll": "Netejar tot" + }, + "cameras": { + "label": "Filtre de càmeres", + "all": { + "title": "Totes les càmeres", + "short": "Càmeres" + } + }, + "review": { + "showReviewed": "Mostrar els revisats" + }, + "motion": { + "showMotionOnly": "Mostar només el moviment" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Totes les classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/icons.json new file mode 100644 index 0000000..88fba47 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona una icona", + "search": { + "placeholder": "Buscar icona…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/input.json new file mode 100644 index 0000000..7b36418 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descarregar vídeo", + "toast": { + "success": "S’ha començat a descarregar el vídeo de l’element de revisió." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ca/components/player.json new file mode 100644 index 0000000..1fed78e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/components/player.json @@ -0,0 +1,51 @@ +{ + "stats": { + "latency": { + "short": { + "title": "Latència", + "value": "{{seconds}} s" + }, + "title": "Latència:", + "value": "{{seconds}} segons" + }, + "streamType": { + "title": "Tipus de transmissió:", + "short": "Tipus" + }, + "bandwidth": { + "title": "Ample de banda:", + "short": "Ample de banda" + }, + "totalFrames": "Fotogrames totals:", + "droppedFrames": { + "title": "Fotogrames perduts:", + "short": { + "title": "Perduts", + "value": "{{droppedFrames}} fotogrames" + } + }, + "decodedFrames": "Fotogrames decodificats:", + "droppedFrameRate": "Taxa de fotogrames perduts:" + }, + "noRecordingsFoundForThisTime": "No s'han trobat enregistraments en aquesta hora", + "noPreviewFound": "No s'ha trobat previsualització", + "noPreviewFoundFor": "No s'ha trobat cap previsualització per a {{cameraName}}", + "submitFrigatePlus": { + "title": "Enviar aquesta imatge a Frigate+?", + "submit": "Enviar" + }, + "livePlayerRequiredIOSVersion": "Es requereix iOS 17.1 o superior per a aquest tipus de reproducció en directe.", + "streamOffline": { + "title": "Transmissió desconnectada", + "desc": "No s’han rebut imatges a la transmissió detect de la càmera {{cameraName}}. Comprova els registres d’errors" + }, + "cameraDisabled": "La càmera està desactivada", + "toast": { + "success": { + "submittedFrigatePlus": "Fotograma enviat correctament a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Error al enviar fotograma a Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ca/objects.json new file mode 100644 index 0000000..253e275 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Persona", + "bicycle": "Bicicleta", + "car": "Cotxe", + "motorcycle": "Motocicleta", + "airplane": "Avió", + "boat": "Vaixell", + "traffic_light": "Llum del trànsit", + "fire_hydrant": "Boca d'incendi", + "street_sign": "Senyal de trànsit", + "stop_sign": "Senyal de stop", + "parking_meter": "Parquímetre", + "bench": "Banc", + "bird": "Ocell", + "cat": "Gat", + "dog": "Gos", + "horse": "Cavall", + "sheep": "Ovella", + "cow": "Vaca", + "elephant": "Elefant", + "bear": "Ós", + "zebra": "Zebra", + "giraffe": "Girafa", + "hat": "Barret", + "backpack": "Motxilla", + "umbrella": "Paraigües", + "shoe": "Sabata", + "eye_glasses": "Ulleres", + "tie": "Corbata", + "suitcase": "Maleta", + "frisbee": "Frisbee", + "skis": "Esquís", + "snowboard": "Snowboard", + "sports_ball": "Pilota d'esports", + "kite": "Estel", + "baseball_bat": "Bat de beisbol", + "baseball_glove": "Guant de beisbol", + "skateboard": "Monopatí", + "surfboard": "Taula de surf", + "tennis_racket": "Raqueta de tenis", + "bottle": "Ampolla", + "plate": "Placa", + "wine_glass": "Got de vi", + "cup": "Copa", + "fork": "Forquilla", + "knife": "Ganivet", + "spoon": "Cullera", + "bowl": "Bol", + "apple": "Poma", + "sandwich": "Sandvitx", + "orange": "Taronja", + "broccoli": "Bròquil", + "carrot": "Pastanaga", + "hot_dog": "Frankfurt", + "pizza": "Pizza", + "donut": "Dònut", + "cake": "Pastís", + "chair": "Cadira", + "couch": "Sofà", + "potted_plant": "Planta en test", + "bed": "Llit", + "mirror": "Mirall", + "dining_table": "Taula de menjador", + "window": "Finestra", + "desk": "Escriptori", + "toilet": "Bany", + "door": "Porta", + "tv": "TV", + "mouse": "Ratolí", + "remote": "Comandament", + "keyboard": "Teclat", + "cell_phone": "Telèfon mòbil", + "microwave": "Microones", + "oven": "Forn", + "toaster": "Torradora", + "sink": "Aigüera", + "refrigerator": "Nevera", + "blender": "Batedora", + "book": "Llibre", + "clock": "Rellotge", + "vase": "Gerro", + "scissors": "Tisores", + "teddy_bear": "Ós de peluix", + "hair_dryer": "Assecador de cabell", + "toothbrush": "Raspall de dents", + "hair_brush": "Raspall de cabell", + "vehicle": "Vehicle", + "squirrel": "Esquirol", + "deer": "Cérvol", + "animal": "Animal", + "bark": "Escorça", + "fox": "Guineu", + "goat": "Cabra", + "rabbit": "Conill", + "raccoon": "Ós rentador", + "robot_lawnmower": "Robot tallagespa", + "handbag": "Bossa de mà", + "banana": "Plàtan", + "train": "Tren", + "bus": "Autobús", + "laptop": "Portàtil", + "waste_bin": "Paperera", + "face": "Cara", + "on_demand": "Sota demanda", + "license_plate": "Matrícula", + "package": "Paquet", + "bbq_grill": "Barbacoa", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "dpd": "DPD", + "gls": "GLS" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/classificationModel.json new file mode 100644 index 0000000..568a38d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Models de classificació - Frigate", + "button": { + "deleteClassificationAttempts": "Suprimeix les imatges de classificació", + "renameCategory": "Reanomena la classe", + "deleteCategory": "Suprimeix la classe", + "deleteImages": "Suprimeix les imatges", + "trainModel": "Model de tren", + "addClassification": "Afegeix una classificació", + "deleteModels": "Suprimeix els models", + "editModel": "Edita el model" + }, + "toast": { + "success": { + "deletedCategory": "Classe suprimida", + "deletedImage": "Imatges suprimides", + "categorizedImage": "Imatge classificada amb èxit", + "trainedModel": "Model entrenat amb èxit.", + "trainingModel": "S'ha iniciat amb èxit la formació de models.", + "deletedModel_one": "S'ha suprimit correctament {{count}} model", + "deletedModel_many": "S'han suprimit correctament els {{count}} models", + "deletedModel_other": "S'han suprimit correctament els {{count}} models", + "updatedModel": "S'ha actualitzat correctament la configuració del model", + "renamedCategory": "S'ha canviat el nom de la classe a {{name}}" + }, + "error": { + "deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}", + "deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}", + "categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}", + "trainingFailed": "Ha fallat l'entrenament del model. Comproveu els registres de fragata per a més detalls.", + "deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}", + "updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}", + "renameCategoryFailed": "No s'ha pogut canviar el nom de la classe: {{errorMessage}}", + "trainingFailedToStart": "Errar en arrencar l'entrenament del model: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Suprimeix la classe", + "desc": "Esteu segur que voleu suprimir la classe {{name}}? Això suprimirà permanentment totes les imatges associades i requerirà tornar a entrenar el model.", + "minClassesTitle": "No es pot suprimir la classe", + "minClassesDesc": "Un model de classificació ha de tenir almenys 2 classes. Afegeix una altra classe abans d'eliminar aquesta." + }, + "deleteDatasetImages": { + "title": "Suprimeix les imatges del conjunt de dades", + "desc_one": "Esteu segur que voleu suprimir {{count}} imatge de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.", + "desc_many": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model.", + "desc_other": "Esteu segur que voleu suprimir {{count}} imatges de {{dataset}}? Aquesta acció no es pot desfer i requerirà tornar a entrenar el model." + }, + "deleteTrainImages": { + "title": "Suprimeix les imatges del tren", + "desc_one": "Esteu segur que voleu suprimir {{count}} imatge? Aquesta acció no es pot desfer.", + "desc_many": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer.", + "desc_other": "Esteu segur que voleu suprimir {{count}} imatges? Aquesta acció no es pot desfer." + }, + "renameCategory": { + "title": "Reanomena la classe", + "desc": "Introduïu un nom nou per {{name}}. Se us requerirà que torneu a entrenar el model per al canvi de nom a afectar." + }, + "description": { + "invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions." + }, + "train": { + "title": "Classificacions recents", + "aria": "Selecciona les classificacions recents", + "titleShort": "Recent" + }, + "categories": "Classes", + "createCategory": { + "new": "Crea una classe nova" + }, + "categorizeImageAs": "Classifica la imatge com a:", + "categorizeImage": "Classifica la imatge", + "noModels": { + "object": { + "title": "No hi ha models de classificació d'objectes", + "description": "Crea un model personalitzat per classificar els objectes detectats.", + "buttonText": "Crea un model d'objecte" + }, + "state": { + "title": "Cap model de classificació d'estat", + "description": "Crea un model personalitzat per a monitoritzar i classificar els canvis d'estat en àrees de càmera específiques.", + "buttonText": "Crea un model d'estat" + } + }, + "wizard": { + "title": "Crea una classificació nova", + "steps": { + "nameAndDefine": "Nom i definició", + "stateArea": "Àrea estatal", + "chooseExamples": "Trieu exemples" + }, + "step1": { + "description": "Els models estatals monitoritzen àrees de càmera fixes per als canvis (p. ex., porta oberta/tancada). Els models d'objectes afegeixen classificacions als objectes detectats (per exemple, animals coneguts, persones de lliurament, etc.).", + "name": "Nom", + "namePlaceholder": "Introduïu el nom del model...", + "type": "Tipus", + "typeState": "Estat", + "typeObject": "Objecte", + "objectLabel": "Etiqueta de l'objecte", + "objectLabelPlaceholder": "Selecciona el tipus d'objecte...", + "classificationType": "Tipus de classificació", + "classificationTypeTip": "Apreneu sobre els tipus de classificació", + "classificationTypeDesc": "Les subetiquetes afegeixen text addicional a l'etiqueta de l'objecte (p. ex., 'Person: UPS'). Els atributs són metadades cercables emmagatzemades per separat a les metadades de l'objecte.", + "classificationSubLabel": "Subetiqueta", + "classificationAttribute": "Atribut", + "classes": "Classes", + "classesTip": "Aprèn sobre les classes", + "classesStateDesc": "Defineix els diferents estats en què pot estar la teva àrea de càmera. Per exemple: \"obert\" i \"tancat\" per a una porta de garatge.", + "classesObjectDesc": "Defineix les diferents categories en què classificar els objectes detectats. Per exemple: 'lliuramentpersonpersona', 'resident', 'amenaça' per a la classificació de persones.", + "classPlaceholder": "Introduïu el nom de la classe...", + "errors": { + "nameRequired": "Es requereix el nom del model", + "nameLength": "El nom del model ha de tenir 64 caràcters o menys", + "nameOnlyNumbers": "El nom del model no pot contenir només números", + "classRequired": "Es requereix com a mínim 1 classe", + "classesUnique": "Els noms de classe han de ser únics", + "stateRequiresTwoClasses": "Els models d'estat requereixen almenys 2 classes", + "objectLabelRequired": "Seleccioneu una etiqueta d'objecte", + "objectTypeRequired": "Seleccioneu un tipus de classificació" + }, + "states": "Estats" + }, + "step2": { + "description": "Seleccioneu les càmeres i definiu l'àrea a monitoritzar per a cada càmera. El model classificarà l'estat d'aquestes àrees.", + "cameras": "Càmeres", + "selectCamera": "Selecciona la càmera", + "noCameras": "Feu clic a + per a afegir càmeres", + "selectCameraPrompt": "Seleccioneu una càmera de la llista per definir la seva àrea de monitoratge" + }, + "step3": { + "selectImagesPrompt": "Selecciona totes les imatges amb: {{className}}", + "selectImagesDescription": "Feu clic a les imatges per a seleccionar-les. Feu clic a Continua quan hàgiu acabat amb aquesta classe.", + "generating": { + "title": "S'estan generant imatges de mostra", + "description": "Frigate està traient imatges representatives dels vostres enregistraments. Això pot trigar un moment..." + }, + "training": { + "title": "Model d'entrenament", + "description": "El teu model s'està entrenant en segon pla. Tanqueu aquest diàleg i el vostre model començarà a funcionar tan aviat com s'hagi completat l'entrenament." + }, + "retryGenerate": "Torna a provar la generació", + "noImages": "No s'ha generat cap imatge de mostra", + "classifying": "Classificació i formació...", + "trainingStarted": "L'entrenament s'ha iniciat amb èxit", + "errors": { + "noCameras": "No s'ha configurat cap càmera", + "noObjectLabel": "No s'ha seleccionat cap etiqueta d'objecte", + "generateFailed": "No s'han pogut generar exemples: {{error}}", + "generationFailed": "Ha fallat la generació. Torneu-ho a provar.", + "classifyFailed": "No s'han pogut classificar les imatges: {{error}}" + }, + "generateSuccess": "Imatges de mostra generades amb èxit", + "allImagesRequired_one": "Classifiqueu totes les imatges. Queda {{count}} imatge.", + "allImagesRequired_many": "Classifiqueu totes les imatges. Queden {{count}} imatges.", + "allImagesRequired_other": "Classifiqueu totes les imatges. Queden {{count}} imatges.", + "modelCreated": "El model s'ha creat correctament. Utilitzeu la vista Classificacions recents per a afegir imatges per als estats que falten i, a continuació, entrenar el model.", + "missingStatesWarning": { + "title": "Falten exemples d'estat", + "description": "Es recomana seleccionar exemples per a tots els estats per obtenir els millors resultats. Podeu continuar sense seleccionar tots els estats, però el model no serà entrenat fins que tots els estats tinguin imatges. Després de continuar, utilitzeu la vista Classificacions recents per classificar imatges per als estats que falten, i després entrenar el model." + } + } + }, + "deleteModel": { + "title": "Suprimeix el model de classificació", + "single": "Esteu segur que voleu suprimir {{name}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_one": "Esteu segur que voleu suprimir el model {{count}}? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_many": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer.", + "desc_other": "Esteu segur que voleu suprimir {{count}} models? Això suprimirà permanentment totes les dades associades, incloses les imatges i les dades d'entrenament. Aquesta acció no es pot desfer." + }, + "menu": { + "objects": "Objectes", + "states": "Estats" + }, + "details": { + "scoreInfo": "La puntuació representa la confiança mitjana de la classificació en totes les deteccions d'aquest objecte." + }, + "edit": { + "title": "Edita el model de classificació", + "descriptionState": "Edita les classes per a aquest model de classificació d'estats. Els canvis requeriran tornar a entrenar el model.", + "descriptionObject": "Edita el tipus d'objecte i el tipus de classificació per a aquest model de classificació d'objectes.", + "stateClassesInfo": "Nota: Canviar les classes d'estat requereix tornar a entrenar el model amb les classes actualitzades." + }, + "tooltip": { + "trainingInProgress": "El model s'està entrenant actualment", + "noNewImages": "Sense noves imatges per entrenar. Classifica més imatges primer.", + "modelNotReady": "El model no está preparat per entrenar", + "noChanges": "No hi ha canvis al conjunt de dades des de l'última formació." + }, + "none": "Cap" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/configEditor.json new file mode 100644 index 0000000..bd3149a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de Configuració - Frigate", + "configEditor": "Editor de configuració", + "copyConfig": "Copiar configuració", + "saveAndRestart": "Desa i reinicia", + "saveOnly": "Només desar", + "toast": { + "success": { + "copyToClipboard": "Configuració copiada al porta-retalls." + }, + "error": { + "savingError": "Error al desar la configuració" + } + }, + "confirm": "Sortir sense desar?", + "safeConfigEditor": "Editor de Configuració (Mode Segur)", + "safeModeDescription": "Frigate està en mode segur a causa d'un error de validació de la configuració." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/events.json new file mode 100644 index 0000000..960d6a2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/events.json @@ -0,0 +1,63 @@ +{ + "detected": "detectat", + "alerts": "Alertes", + "detections": "Deteccions", + "motion": { + "label": "Moviment", + "only": "Només moviment" + }, + "allCameras": "Totes les càmeres", + "empty": { + "alert": "Hi ha cap alerta per revisar", + "detection": "Hi ha cap detecció per revisar", + "motion": "No s'haan trobat dades de moviment" + }, + "timeline": "Línia de temps", + "timeline.aria": "Seleccionar línia de temps", + "events": { + "label": "Esdeveniments", + "aria": "Seleccionar esdeveniments", + "noFoundForTimePeriod": "No s'han trobat esdeveniments per aquest període de temps." + }, + "documentTitle": "Revisió - Frigate", + "recordings": { + "documentTitle": "Enregistraments - Frigate" + }, + "calendarFilter": { + "last24Hours": "Últimes 24 hores" + }, + "markAsReviewed": "Marcar com a revisat", + "markTheseItemsAsReviewed": "Marca aquests elements com a revisats", + "newReviewItems": { + "label": "Veure nous elements de revisió", + "button": "Nous elements per revisar" + }, + "camera": "Càmera", + "selected_one": "{{count}} seleccionats", + "selected_other": "{{count}} seleccionats", + "suspiciousActivity": "Activitat sospitosa", + "threateningActivity": "Activitat amenaçadora", + "detail": { + "noDataFound": "No hi ha dades detallades a revisar", + "trackedObject_one": "{{count}} objecte", + "aria": "Canvia la vista de detall", + "trackedObject_other": "{{count}} objectes", + "noObjectDetailData": "No hi ha dades de detall d'objecte disponibles.", + "label": "Detall", + "settings": "Configuració de la vista detallada", + "alwaysExpandActive": { + "title": "Expandeix sempre actiu", + "desc": "Expandeix sempre els detalls de l'objecte de la revisió activa quan estigui disponible." + } + }, + "objectTrack": { + "clickToSeek": "Feu clic per cercar aquesta hora", + "trackedPoint": "Punt de seguiment" + }, + "zoomIn": "Amplia", + "zoomOut": "Redueix", + "normalActivity": "Normal", + "needsReview": "Necessita revisió", + "securityConcern": "Preocupació per la seguretat", + "select_all": "Tots" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/explore.json new file mode 100644 index 0000000..dec2973 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/explore.json @@ -0,0 +1,295 @@ +{ + "exploreIsUnavailable": { + "downloadingModels": { + "tips": { + "context": "Potser voldreu reindexar les incrustacions dels objectes seguits un cop s'hagin descarregat els models.", + "documentation": "Llegir la documentació" + }, + "context": "Frigate està descarregant els models d'embeddings necessaris per a donar suport a la funció de cerca semàntica. Això pot trigar diversos minuts, depenent de la velocitat de la teva connexió de xarxa.", + "setup": { + "visionModel": "Model de visió", + "visionModelFeatureExtractor": "Extractor de característiques del model de visió", + "textModel": "Model de text", + "textTokenizer": "Tokenitzador de text" + }, + "error": "S'ha produït un error. Comproveu els registres de Frigate." + }, + "embeddingsReindexing": { + "context": "Explorar pot ser utilitzat després d’haver completat la reindexació d’objectes rastrejats.", + "startingUp": "Iniciant…", + "finishingShortly": "Finalitzant en breus", + "step": { + "thumbnailsEmbedded": "Miniatures integrades: ", + "descriptionsEmbedded": "Descripcions integrades: ", + "trackedObjectsProcessed": "Objectes processats: " + }, + "estimatedTime": "Temps restant estimat:" + }, + "title": "Explorar no està disponible" + }, + "documentTitle": "Explora - Frigate", + "generativeAI": "IA Generativa", + "objectLifecycle": { + "createObjectMask": "Crear màscara per a l'objecte", + "title": "Cicle de vida de l'objecte", + "noImageFound": "No s'ha trobat cap imatge per a aquesta marca temporal.", + "adjustAnnotationSettings": "Ajustar els paràmetres de les anotacions", + "scrollViewTips": "Desplaça't per veure els moments significatius del cicle de vida d'aquest objecte.", + "lifecycleItemDesc": { + "entered_zone": "{{label}} ha entrat a {{zones}}", + "active": "{{label}} s'ha activat", + "stationary": "{{label}} ha esdevingut estacionari", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat per a {{label}}", + "other": "{{label}} reconegut com a {{attribute}}" + }, + "header": { + "zones": "Zones", + "ratio": "Proporció", + "area": "Àrea" + }, + "heard": "{{label}} escoltat", + "external": "{{label}} detectat", + "gone": "{{label}} ha marxat", + "visible": "{{label}} detectat" + }, + "annotationSettings": { + "offset": { + "documentation": "Llegir la documentació ", + "label": "Desplaçament de l'anotació", + "desc": "Aquestes dades provenen de la detecció d'objectes, però se superposen a les imatges d’enregistrament. És poc probable que les dues transmissions estiguin perfectament sincronitzades. Per aquest motiu, la capsa delimitadora i les imatges poden no coincidir exactament. Tanmateix, es pot utilitzar el camp annotation_offset per ajustar-ho.", + "tips": "CONSELL: Imagina que hi ha la captura d'un esdeveniment on una persona camina d'esquerra a dreta. Si la caixa delimitadora de l'objecte està constantment a l'esquerra de la persona, llavors el valor s'hauria de disminuir. Si, per contra, la caixa delimitadora està constantment per davant de la persona (a la seva dreta en aquest exemple), llavors el valor s'hauria d'augmentar.", + "toast": { + "success": "El desplaçament d'anotació per {{camera}} s'ha guardat al fitxer de configuració. Reinicia Frigate per aplicar els canvis." + }, + "millisecondsToOffset": "Mil·lisegons a desplaçar les anotacions de detecció: Per Defecte: 0" + }, + "title": "Paràmetres de les anotacions", + "showAllZones": { + "title": "Mostra totes les zones", + "desc": "Mostra sempre les zones en fotogrames on hi hagin aparegut objectes." + } + }, + "carousel": { + "next": "Diapositiva següent", + "previous": "Diapositiva anterior" + }, + "autoTrackingTips": "Les posicions dels recuadres delimitadors seràn inexactes per a càmeres amb seguiment automàtic.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punt seguit" + }, + "exploreMore": "Explora més {{label}} objectes", + "trackedObjectDetails": "Detalls de l'objecte rastrejat", + "type": { + "details": "detalls", + "snapshot": "instantània", + "video": "vídeo", + "object_lifecycle": "cicle de vida de l'objecte", + "thumbnail": "miniatura", + "tracking_details": "detalls del seguiment" + }, + "details": { + "timestamp": "Marca temporal", + "item": { + "button": { + "viewInExplore": "Veure a Explorar", + "share": "Comparteix aquest element de revisió" + }, + "toast": { + "success": { + "updatedSublabel": "Subetiqueta actualitzada amb èxit.", + "updatedLPR": "Matrícula actualitzada amb èxit.", + "regenerate": "El {{provider}} ha sol·licitat una nova descripció. En funció de la velocitat del vostre proveïdor, la nova descripció pot trigar un temps a regenerar-se.", + "audioTranscription": "S'ha sol·licitat correctament la transcripció d'àudio. Depenent de la velocitat del vostre servidor Frigate, la transcripció pot trigar una estona a completar-se." + }, + "error": { + "regenerate": "No s'ha pogut contactar amb {{provider}} per obtenir una nova descripció: {{errorMessage}}", + "updatedSublabelFailed": "No s'ha pogut actualitzar la subetiqueta: {{errorMessage}}", + "updatedLPRFailed": "No s'ha pogut actualitzar la matrícula: {{errorMessage}}", + "audioTranscription": "Error en demanar la transcripció d'audio {{errorMessage}}" + } + }, + "title": "Revisar detalls de l'element", + "desc": "Revisar detalls de l'element", + "tips": { + "hasMissingObjects": "Ajusta la configuració si vols que Frigate guardi els objectes rastrejat de les seguents etiquetes: {{objects}}", + "mismatch_one": "{{count}} objecte no disponible ha estat detectat i inclòs en aquest element de revisió. Aquest objecte tampoc no s'han calificat com una alerta o detecció o ja ha estat netejat mes amunt/eliminat.", + "mismatch_many": "{{count}} objectes no disponibles han estat detectats i inclosos en aquest element de revisió. Aquests objectes tampoc no s'han calificat com una alerta o detecció o ja han estat netejats mes amunt/eliminats.", + "mismatch_other": "{{count}} objectes no disponibles han estat detectats i inclosos en aquest element de revisió. Aquests objectes tampoc no s'han calificat com una alerta o detecció o ja han estat netejats mes amunt/eliminats." + } + }, + "label": "Etiqueta", + "topScore": { + "label": "Puntuació màxima", + "info": "El resultat superior és la mediana més alta per l'objecte seguit, així que pot diferir des del resultat mostrat en thumbnail de la búsqueda de recerca." + }, + "estimatedSpeed": "Velocitat estimada", + "button": { + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descripció d'objecte rastrejat" + }, + "findSimilar": "Cercar similars" + }, + "expandRegenerationMenu": "Amplia el menú de regeneració", + "regenerateFromSnapshot": "Regenerar desde instantània", + "regenerateFromThumbnails": "Regenerar desde miniatures", + "tips": { + "descriptionSaved": "Descripció desada amb èxit", + "saveDescriptionFailed": "No s'ha pogut actualitzar la descripció: {{errorMessage}}" + }, + "description": { + "placeholder": "Descripció de l'objecte rastrejat", + "label": "Descripció", + "aiTips": "Frigate no sol·licitarà una descripció al teu proveïdor d'intel·ligència artificial generativa fins que el cicle de vida de l'objecte rastrejat hagi acabat." + }, + "objects": "Objectes", + "camera": "Càmera", + "editSubLabel": { + "title": "Editar subetiqueta", + "descNoLabel": "Introdueix una nova subetiqueta per a aquest objecte rastrejat", + "desc": "Introdueix una nova subetiqueta per a aquesta {{label}}" + }, + "zones": "Zones", + "recognizedLicensePlate": "Matrícula reconeguda", + "snapshotScore": { + "label": "Puntuació d'instantània" + }, + "editLPR": { + "title": "Editar matrícula", + "descNoLabel": "Introdueix un nou valor de matrícula per a aquest objecte rastrejat", + "desc": "Introdueix un nou valor per a la matrícula per aquesta {{label}}" + }, + "score": { + "label": "Puntuació" + } + }, + "searchResult": { + "tooltip": "S'ha identificat {{type}} amb una confiança del {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "L'objectes amb seguiment s'ha suprimit correctament.", + "error": "No s'ha pogut suprimir l'objecte rastrejat: {{errorMessage}}" + } + }, + "nextTrackedObject": "Següent objecte rastrejat", + "previousTrackedObject": "Objecte rastrejat anterior" + }, + "itemMenu": { + "downloadVideo": { + "aria": "Descarregar vídeo", + "label": "Descarregar vídeo" + }, + "submitToPlus": { + "aria": "Enviar a Frigate Plus", + "label": "Enviar a Frigate+" + }, + "downloadSnapshot": { + "label": "Descarregar instantània", + "aria": "Descarregar instantània" + }, + "findSimilar": { + "label": "Cercar similars", + "aria": "Trobar objectes de seguiment similars" + }, + "viewObjectLifecycle": { + "label": "Veure el cicle de vida de l'objecte", + "aria": "Mostrar el cicle de vida de l'objecte" + }, + "viewInHistory": { + "label": "Veure a l'historial", + "aria": "Veure a l'historial" + }, + "deleteTrackedObject": { + "label": "Suprimeix aquest objecte rastrejat" + }, + "addTrigger": { + "label": "Afegir disparador", + "aria": "Afegir disparador per aquest objecte" + }, + "audioTranscription": { + "label": "Transcriu", + "aria": "Demanar una transcripció d'audio" + }, + "showObjectDetails": { + "label": "Mostra la ruta de l'objecte" + }, + "hideObjectDetails": { + "label": "Amaga la ruta de l'objecte" + }, + "viewTrackingDetails": { + "label": "Veure detalls de seguiment", + "aria": "Mostra els detalls de seguiment" + }, + "downloadCleanSnapshot": { + "label": "Descarrega la instantània neta", + "aria": "Descarrega la instantània neta" + } + }, + "noTrackedObjects": "No s'han trobat objectes rastrejats", + "dialog": { + "confirmDelete": { + "title": "Confirmar la supressió", + "desc": "Eliminant aquest objecte seguit borrarà l'snapshot, qualsevol embedding gravat, i qualsevol detall de seguiment. Les imatges gravades d'aquest objecte seguit en l'historial NO seràn eliminades.

Estas segur que vols continuar?" + } + }, + "fetchingTrackedObjectsFailed": "Error al obtenir objectes rastrejats: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objecte rastrejat ", + "trackedObjectsCount_many": "{{count}} objectes rastrejats ", + "trackedObjectsCount_other": "{{count}} objectes rastrejats ", + "aiAnalysis": { + "title": "Anàlisi d'IA" + }, + "concerns": { + "label": "Preocupacions" + }, + "trackingDetails": { + "title": "Detalls de seguiment", + "noImageFound": "No s'ha trobat cap imatge amb aquesta hora.", + "createObjectMask": "Crear màscara d'objecte", + "adjustAnnotationSettings": "Ajustar configuració d'anotacions", + "scrollViewTips": "Feu clic per veure els moments significatius del cicle de vida d'aquest objecte.", + "autoTrackingTips": "Limitar les posicións de la caixa serà inacurat per càmeras de seguiment automàtic.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punt Seguit", + "lifecycleItemDesc": { + "visible": "{{label}} detectat", + "entered_zone": "{{label}} ha entrat a {{zones}}", + "active": "{{label}} ha esdevingut actiu", + "stationary": "{{label}} ha esdevingut estacionari", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat per {{label}}", + "other": "{{label}} reconegut com a {{attribute}}" + }, + "gone": "{{label}} esquerra", + "heard": "{{label}} sentit", + "external": "{{label}} detectat", + "header": { + "zones": "Zones", + "ratio": "Ràtio", + "area": "Àrea", + "score": "Puntuació" + } + }, + "annotationSettings": { + "title": "Configuració d'anotacions", + "showAllZones": { + "title": "Mostra totes les Zones", + "desc": "Mostra sempre les zones amb marcs on els objectes hagin entrat a la zona." + }, + "offset": { + "label": "Òfset d'Anotació", + "desc": "Aquestes dades provenen del flux de detecció de la càmera, però se superposen a les imatges del flux de gravació. És poc probable que els dos fluxos estiguin perfectament sincronitzats. Com a resultat, el quadre delimitador i les imatges no s'alinearan perfectament. Tanmateix, es pot utilitzar el camp annotation_offset per ajustar-ho.", + "millisecondsToOffset": "Millisegons per l'òfset de detecció d'anotacions per. Per defecte: 0", + "tips": "Reduïu el valor si la reproducció del vídeo es troba per davant dels quadres i els punts de ruta, i augmenteu-lo si es troba per darrere. Aquest valor pot ser negatiu.", + "toast": { + "success": "El desplaçament de l'anotació per {{camera}} s'ha desat al fitxer de configuració." + } + } + }, + "carousel": { + "previous": "Diapositiva anterior", + "next": "Dispositiva posterior" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/exports.json new file mode 100644 index 0000000..dec2726 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Buscar", + "noExports": "No s'han trobat exportacions", + "deleteExport": "Suprimeix l'exportació", + "deleteExport.desc": "Estàs segur que vols eliminar {{exportName}}?", + "editExport": { + "title": "Renombrar exportació", + "desc": "Introdueix un nou nom per a aquesta exportació.", + "saveExport": "Desar exportació" + }, + "toast": { + "error": { + "renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Comparteix l'exportació", + "downloadVideo": "Baixa el vídeo", + "editName": "Edita el nom", + "deleteExport": "Suprimeix l'exportació" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/faceLibrary.json new file mode 100644 index 0000000..d2a5fcf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "selectItem": "Selecciona {{item}}", + "details": { + "subLabelScore": "Puntuació de la subetiqueta", + "scoreInfo": "La puntuació de la subetiqueta és la puntuació ponderada de totes la confidència dels rostres reconeguts, de manera que pot ser diferent de la puntuació que es mostra a la instantània.", + "unknown": "Desconegut", + "person": "Persona", + "faceDesc": "Detalls de l'objecte que ha generat aquest rostre", + "timestamp": "Marca temporal", + "face": "Detalls del rostre" + }, + "collections": "Col·leccions", + "train": { + "empty": "No hi ha intents recents de reconeixement de rostres", + "title": "Reconeixements recents", + "aria": "Selecciona els reconeixements recents", + "titleShort": "Recent" + }, + "description": { + "addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.", + "placeholder": "Introduïu un nom per a aquesta col·lecció", + "invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions." + }, + "documentTitle": "Biblioteca de rostres - Frigate", + "uploadFaceImage": { + "title": "Puja una imatge del rostre", + "desc": "Carregar una imatge per escanejar els rostres i incloure per a {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Crear Col·lecció", + "desc": "Crear una nova col·lecció", + "new": "Crear un nou rostre", + "nextSteps": "Per establir una base sòlida:
  • Utilitza la pestanya Entrenament per seleccionar i entrenar imatges de cada persona detectada.
  • Centra’t en imatges frontals per obtenir millors resultats; evita imatges d’entrenament amb rostres en angle.
  • " + }, + "steps": { + "faceName": "Introduir el nom del rostre", + "uploadFace": "Puja una imatge del rostre", + "nextSteps": "Següents passos", + "description": { + "uploadFace": "Puja una imatge de {{name}} que mostri el seu rostre de cares. No cal que la imatge estigui retallada només al rostre." + } + }, + "selectFace": "Seleccionar rostre", + "deleteFaceLibrary": { + "desc": "Estàs segur que vols eliminar la col·lecció {{name}}? Això eliminarà permanentment tots els rostres associats.", + "title": "Suprimir nom" + }, + "renameFace": { + "desc": "Introduïu un nou nom per a {{name}}", + "title": "Canviar nom del rostre" + }, + "imageEntry": { + "dropActive": "Arrossegueu la imatge aquí…", + "validation": { + "selectImage": "Siusplau, selecciona un fixer d'imatge." + }, + "maxSize": "Mida màxima: {{size}}MB", + "dropInstructions": "Arrossegueu i deixeu anar o enganxeu una imatge aquí, o feu clic per seleccionar" + }, + "button": { + "uploadImage": "Pujar imatge", + "addFace": "Afegir rostre", + "deleteFaceAttempts": "Suprimir rostres", + "renameFace": "Renombrar rostre", + "deleteFace": "Suprimeix rostre", + "reprocessFace": "Reprocessar rostre" + }, + "toast": { + "success": { + "trainedFace": "Rostre entrenat amb èxit.", + "updatedFaceScore": "S'ha actualitzat correctament la puntuació de la cara a {{name}} ({{score}}).", + "uploadedImage": "Imatge pujada amb èxit.", + "addFaceLibrary": "{{name}} s'ha afegit amb èxit a la biblioteca de rostres!", + "deletedName_one": "{{count}} rostre s'ha suprimit amb èxit.", + "deletedName_many": "{{count}} rostres s'han suprimit amb èxit.", + "deletedName_other": "{{count}} rostres s'han suprimit amb èxit.", + "deletedFace_one": "{{count}} rostre suprimit amb èxit.", + "deletedFace_many": "{{count}} rostres suprimits amb èxit.", + "deletedFace_other": "{{count}} rostres suprimits amb èxit.", + "renamedFace": "Rostre renombrat amb èxit a {{name}}" + }, + "error": { + "uploadingImageFailed": "No s'ha pogut penjar la imatge: {{errorMessage}}", + "trainFailed": "No s'ha pogut entrenar: {{errorMessage}}", + "deleteFaceFailed": "No s'ha pogut suprimir: {{errorMessage}}", + "deleteNameFailed": "No s'ha pogut suprimir el nom: {{errorMessage}}", + "updateFaceScoreFailed": "No s'ha pogut actualitzar la puntuació de rostre: {{errorMessage}}", + "addFaceLibraryFailed": "No s'ha pogut establir el nom del rostre: {{errorMessage}}", + "renameFaceFailed": "No s'ha pogut renombrar el rostre: {{errorMessage}}" + } + }, + "nofaces": "No hi han rostres disponibles", + "deleteFaceAttempts": { + "title": "Suprimir rostres", + "desc_one": "Estàs segur que vols suprimir {{count}} rostre? Aquesta acció no es pot desfer.", + "desc_many": "Estàs segur que vols suprimir {{count}} rostres? Aquesta acció no es pot desfer.", + "desc_other": "Estàs segur que vols suprimir {{count}} rostres? Aquesta acció no es pot desfer." + }, + "pixels": "{{area}}px", + "trainFace": "Entrenar rostre", + "readTheDocs": "Llegir la documentació", + "trainFaceAs": "Entrenar rostre com a:" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/live.json new file mode 100644 index 0000000..d9245fe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/live.json @@ -0,0 +1,189 @@ +{ + "ptz": { + "zoom": { + "in": { + "label": "Apropar la càmera PTZ" + }, + "out": { + "label": "Allunyar la càmera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Fes clic a la imatge per centrar la càmera", + "enable": "Habilita clic per moure", + "disable": "Deshabilita clic per moure" + }, + "left": { + "label": "Moure la càmera PTZ a l'esquerra" + }, + "up": { + "label": "Moure la càmera PTZ cap amunt" + }, + "down": { + "label": "Moure la càmera PTZ cap avall" + }, + "right": { + "label": "Moure la càmera PTZ a la dreta" + } + }, + "frame": { + "center": { + "label": "Fer clic a la imatge per centrar la càmera PTZ" + } + }, + "presets": "Predefinits de la càmera PTZ", + "focus": { + "in": { + "label": "Enfoca la càmera PTZ aprop" + }, + "out": { + "label": "Enfoca la càmera PTZ lluny" + } + } + }, + "documentTitle": "Directe - Frigate", + "documentTitle.withCamera": "{{camera}} - Directe - Frigate", + "lowBandwidthMode": "Mode de baix ample de banda", + "twoWayTalk": { + "enable": "Activa la comunicació bidireccional", + "disable": "Desactiva la comunicació bidireccional" + }, + "cameraAudio": { + "enable": "Habilitar l'àudio de la càmera", + "disable": "Deshabilita l'àudio de la càmera" + }, + "camera": { + "enable": "Habilitar la càmera", + "disable": "Deshabilita la càmera" + }, + "muteCameras": { + "enable": "Silencia totes les càmeres", + "disable": "Activar el so de totes les càmeres" + }, + "detect": { + "enable": "Habilita la detecció", + "disable": "Deshabilitar detecció" + }, + "recording": { + "enable": "Habilitar gravació", + "disable": "Deshabilita l'enregistrament" + }, + "snapshots": { + "enable": "Habilita captura d'instantània", + "disable": "Deshabilitar instantànies" + }, + "audioDetect": { + "enable": "Habilita la detecció d'àudio", + "disable": "Deshabilitar la detecció d'àudio" + }, + "autotracking": { + "enable": "Habilitar seguiment automàtic", + "disable": "Deshabilitar seguiment automàtic" + }, + "streamStats": { + "enable": "Mostrar les estadístiques de la transmissió", + "disable": "Amaga estadístiques de la transmissió" + }, + "manualRecording": { + "title": "Sota demanda", + "tips": "Baixeu una instantània o inicieu un esdeveniment manual basat en la configuració de retenció d'enregistrament d'aquesta càmera.", + "playInBackground": { + "label": "Reproduir en segon pla", + "desc": "Habilita aquesta opció per a continuar la transmissió quan el reproductor està amagat." + }, + "showStats": { + "label": "Mostrar les estadístiques", + "desc": "Habilita aquesta opció per mostrar les estadístiques de transmissió com una superposició de la transmissió de la càmera." + }, + "start": "Iniciar enregistrament sota demanda", + "started": "Gravació sota demanda manual inciada.", + "ended": "Gravació sota demanda manual finalitzada.", + "debugView": "Vista de depuració", + "end": "Finalitzar gravació sota demanda", + "failedToStart": "No s'ha pogut iniciar la gravació manual sota demanda.", + "recordDisabledTips": "Com que la gravació està deshabilitada o restringida a la configuració d'aquesta càmera, només es guardarà una instantània.", + "failedToEnd": "No s'ha pogut acabar la gravació manual sota demanda." + }, + "notifications": "Notificacions", + "audio": "Àudio", + "stream": { + "title": "Transmissió", + "audio": { + "tips": { + "documentation": "Llegir la documentació ", + "title": "L'àudio ha de provenir de la càmera i estar configurat amb go2rtc per a aquesta transmissió." + }, + "available": "L'àudio està disponible per a aquesta transmissió", + "unavailable": "L'audio no està disponible per a aquesta transmissió" + }, + "twoWayTalk": { + "tips.documentation": "Llegir la documentació ", + "tips": "El teu dispositiu ha de suportar la funció i WebRTC ha d'estar configurat per a conversa bidireccional.", + "available": "La conversa bidireccional està disponible per a aquesta transmissió", + "unavailable": "La conversa bidireccional no està disponible per a aquesta transmissió" + }, + "lowBandwidth": { + "resetStream": "Restablir transmissió", + "tips": "La vista en directe està en mode de baix ample de banda a causa d'errors de transmissió o de buffering." + }, + "playInBackground": { + "label": "Reproduir en segon pla", + "tips": "Habilita aquesta opció per a contiuar la transmissió tot i que el reproductor estigui ocult." + }, + "debug": { + "picker": "Selecció de stream no disponible en mode debug. La vista debug sempre fa servir el stream assignat pel rol de detecció." + } + }, + "streamingSettings": "Paràmetres de transmissió", + "suspend": { + "forTime": "Suspèn per: " + }, + "cameraSettings": { + "title": "{{camera}} Paràmetres", + "cameraEnabled": "Càmera habilitada", + "recording": "Gravació", + "snapshots": "Instantànies", + "autotracking": "Seguiment automàtic", + "objectDetection": "Detecció d'objectes", + "audioDetection": "Detecció d'àudio", + "transcription": "Transcripció d'audio" + }, + "history": { + "label": "Mostrar gravacions històriques" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tot", + "motion": "Moviment", + "active_objects": "Objectes actius" + }, + "notAllTips": "El vostre {{source}} registre de configuració de retenció s'ha posat en el mode : {{effectiveRetainMode}}, així que la gravaciò a demanda només seguirà segments amb {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar el disseny", + "group": { + "label": "Editar grup de càmeres" + }, + "exitEdit": "Sortir de l'edició" + }, + "transcription": { + "enable": "Habilita la transcripció d'àudio en temps real", + "disable": "Deshabilita la transcripció d'àudio en temps real" + }, + "snapshot": { + "takeSnapshot": "Descarregar una instantània", + "noVideoSource": "No hi ha cap font de video per fer una instantània.", + "captureFailed": "Error capturant una instantània.", + "downloadStarted": "Inici de baixada d'instantània." + }, + "noCameras": { + "title": "No s'ha configurat cap càmera", + "description": "Comenceu connectant una càmera a Frigate.", + "buttonText": "Afegeix una càmera", + "restricted": { + "title": "No hi ha càmeres disponibles", + "description": "No teniu permís per veure cap càmera en aquest grup." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/recording.json new file mode 100644 index 0000000..e78f5ef --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "No s'ha seleccionat un rang de temps vàlid", + "endTimeMustAfterStartTime": "L'hora de finalització ha de ser posterior a l'hora d'inici" + } + }, + "export": "Exportar", + "calendar": "Calendari", + "filters": "Filtres" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/search.json new file mode 100644 index 0000000..dec4537 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/search.json @@ -0,0 +1,72 @@ +{ + "filter": { + "label": { + "time_range": "Rang de temps", + "cameras": "Càmeres", + "search_type": "Tipus de cerca", + "labels": "Etiquetes", + "zones": "Zones", + "sub_labels": "Subetiquetes", + "before": "Abans", + "after": "Després", + "min_score": "Puntuació mínima", + "max_score": "Puntuació màxima", + "min_speed": "Velocitat mínima", + "max_speed": "Velocitat màxima", + "recognized_license_plate": "Matrícula reconeguda", + "has_clip": "Té Clip", + "has_snapshot": "Té instantània" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descripció" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "La data 'abans' ha de ser posterior a la data 'després'.", + "afterDatebeEarlierBefore": "La data 'després' ha de ser anterior a la data 'abans'.", + "minScoreMustBeLessOrEqualMaxScore": "La \"puntuació mínima\" ha de ser menor o igual que la \"puntuació màxima\".", + "maxScoreMustBeGreaterOrEqualMinScore": "La \"puntuació màxima\" ha de ser major o igual que la \"puntuació mínima\".", + "minSpeedMustBeLessOrEqualMaxSpeed": "La \"velocitat mínima\" ha de ser menor o igual que la \"velocitat màxima\".", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La \"velocitat màxima\" ha de ser major o igual que la \"velocitat mínima\"." + } + }, + "tips": { + "title": "Com utilitzar filtres de text", + "desc": { + "step2": "Selecciona un valor de les suggerències o escriu-ne un de propi.", + "step3": "Utilitza múltiples filtres afegint-los un rere l'altre amb un espai entremig.", + "exampleLabel": "Exemple:", + "step1": "Escriviu un nom de clau de filtre seguit de dos punts (p. ex., \"càmeres:\").", + "text": "Els filtres t'ajuden a acotar els resultats de cerca. Aquí tens com utilitzar-los al camp d’entrada:", + "step4": "Els filtres de data {abans: i després:) fan servir el format {{DateFormat}}.", + "step5": "El filtre de rang de temps fa servir el format {{exampleTime}}.", + "step6": "Suprimeix els filtres fent clic a la 'x' que tenen al costat." + } + }, + "header": { + "noFilters": "Filtres", + "currentFilterType": "Valors del filtre", + "activeFilters": "Filtres actius" + } + }, + "search": "Buscar", + "savedSearches": "Cerques desades", + "searchFor": "Buscar {{inputValue}}", + "button": { + "clear": "Netejar cerca", + "save": "Desa la cerca", + "delete": "Elimina la recerca desada", + "filterInformation": "Informació del filtre", + "filterActive": "Filtres actius" + }, + "trackedObjectId": "ID de l'objecte rastrejat", + "placeholder": { + "search": "Cercar…" + }, + "similaritySearch": { + "title": "Cerca per similitud", + "active": "Cerca per similitud habilitada", + "clear": "Netejar cerca per similitud" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/settings.json new file mode 100644 index 0000000..1fcd974 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/settings.json @@ -0,0 +1,1248 @@ +{ + "documentTitle": { + "enrichments": "Parmàmetres complementaris - Frigate", + "motionTuner": "Ajust de moviment - Frigate", + "object": "Depurar - Frigate", + "default": "Paràmetres - Frigate", + "authentication": "Configuració d'autenticació - Frigate", + "camera": "Paràmetres de càmera - Frigate", + "masksAndZones": "Editor de màscares i zones - Frigate", + "general": "Configuració de la interfície d'usuari - Fragata", + "frigatePlus": "Paràmetres de Frigate+ - Frigate", + "notifications": "Paràmetres de notificació - Frigate", + "cameraManagement": "Gestionar càmeres - Frigate", + "cameraReview": "Configuració Revisió de Càmeres - Frigate" + }, + "menu": { + "ui": "Interfície d'usuari", + "cameras": "Paràmetres de la càmera", + "masksAndZones": "Màscares / Zones", + "motionTuner": "Ajust de detecció de moviment", + "users": "Usuaris", + "notifications": "Notificacions", + "debug": "Depuració", + "frigateplus": "Frigate+", + "enrichments": "Enriquiments", + "triggers": "Disparadors", + "cameraManagement": "Gestió", + "cameraReview": "Revisió", + "roles": "Rols" + }, + "dialog": { + "unsavedChanges": { + "title": "Hi ha canvis no guardats.", + "desc": "Desar els canvis abans de continuar?" + } + }, + "cameraSetting": { + "camera": "Càmera", + "noCamera": "Cap càmera" + }, + "general": { + "title": "Paràmetres de la interfície d'usuari", + "liveDashboard": { + "title": "Panell en directe", + "automaticLiveView": { + "label": "Vista en directe automàtica", + "desc": "Canvia automàticament a la vista en directe d’una càmera quan es detecta activitat. Desactivar aquesta opció fa que les imatges estàtiques de la càmera al panell en directe s’actualitzin només un cop per minut." + }, + "playAlertVideos": { + "label": "Reproduir vídeos d’alerta", + "desc": "Per defecte, les alertes recents al tauler en directe es reprodueixen com a vídeos petits en bucle. Desactiva aquesta opció per mostrar només una imatge estàtica de les alertes recents en aquest dispositiu/navegador." + }, + "displayCameraNames": { + "label": "Mostra sempre els noms de la càmera", + "desc": "Mostra sempre els noms de les càmeres en un xip al tauler de visualització en directe multicàmera." + }, + "liveFallbackTimeout": { + "label": "Temps d'espera per a la reserva del jugador en directe", + "desc": "Quan el flux en viu d'alta qualitat d'una càmera no està disponible, torneu al mode d'amplada de banda baixa després d'aquests molts segons. Per defecte: 3." + } + }, + "storedLayouts": { + "title": "Disposicions desades", + "desc": "La disposició de les càmeres en un grup es pot arrossegar i redimensionar. Les posicions es guarden a l’emmagatzematge local del teu navegador.", + "clearAll": "Esborra tots les disposicions" + }, + "cameraGroupStreaming": { + "desc": "La configuració de la transmissió per a cada grup de càmeres s'emmagatzema de manera local al vostre navegador.", + "title": "Parmàmetres de transmissió del grup de càmeres", + "clearAll": "Esborra tots els paràmetres de transmissió" + }, + "recordingsViewer": { + "title": "Visor d'enregistraments", + "defaultPlaybackRate": { + "label": "Velocitat de reproducció predeterminada", + "desc": "Velocitat de reproducció predeterminada per a la reproducció de gravacions." + } + }, + "calendar": { + "firstWeekday": { + "monday": "Dilluns", + "label": "Primer dia de la setmana", + "sunday": "Diumenge", + "desc": "El dia en que comencen les setmanes del calendari de revisions." + }, + "title": "Calendari" + }, + "toast": { + "success": { + "clearStoredLayout": "Disposició emmagatzemada esborrada per {{cameraName}}", + "clearStreamingSettings": "S'han suprimit els paràmetres de la transmissió per tots els grups de càmeres." + }, + "error": { + "clearStoredLayoutFailed": "Error en suprimir la disposició desada: {{errorMessage}}", + "clearStreamingSettingsFailed": "Error en esborrar els paràmetres de la transmissió: {{errorMessage}}" + } + } + }, + "masksAndZones": { + "form": { + "polygonDrawing": { + "snapPoints": { + "false": "No ajustar punts", + "true": "Punts d'ajust" + }, + "delete": { + "success": "{{name}} s'ha suprimit.", + "title": "Confirmar la supressió", + "desc": "Estas segur que vols suprimir el {{type}} {{name}}?" + }, + "removeLastPoint": "Eliminar l'últim punt", + "reset": { + "label": "Neteja tots els punts" + }, + "error": { + "mustBeFinished": "El dibuix del polígon s'ha d'acabar abans de desar." + } + }, + "zoneName": { + "error": { + "hasIllegalCharacter": "El nom de la zona conté caràcters il·legals.", + "mustBeAtLeastTwoCharacters": "El nom de la zona ha de contenir com a mínim 2 caràcters.", + "mustNotContainPeriod": "El nom de la zona no pot contenir punts.", + "alreadyExists": "Ja existeix una zona amb aquest nom per a aquesta càmera.", + "mustNotBeSameWithCamera": "El nom de la zona no pot ser el mateix que el nom de la càmera.", + "mustHaveAtLeastOneLetter": "El nom de la zona ha de tenir almenys una lletra." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inèrcia ha de ser superior a 0." + } + }, + "distance": { + "error": { + "text": "La distància ha de ser major o igual a 0.1.", + "mustBeFilled": "Cal omplir tots els camps de distància per poder utilitzar l’estimació de la velocitat." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "El llindar de velocitat ha de ser major o igual a 0.1." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "El temps de merodeig ha de ser mes gran o igual a 0." + } + } + }, + "zones": { + "objects": { + "title": "Objectes", + "desc": "Llista d'objectes que apliquen per aquesta zona." + }, + "speedEstimation": { + "lineDDistance": "Distància de la línia D ({{unit}})", + "title": "Estimació de velocitat", + "docs": "Llegir la documentació", + "lineADistance": "Distància de la línia A ({{unit}})", + "lineBDistance": "Distància de la línia B ({{unit}})", + "lineCDistance": "Distància de la línia C ({{unit}})", + "desc": "Habilita l'estimació de velocitat per a objectes dins d'aquesta zona. La zona ha de tenir exactament 4 punts." + }, + "inertia": { + "title": "Inèrcia", + "desc": "Especifica quants fotogrames ha d’estar un objecte dins d’una zona abans de considerar-se que hi és. Per defecte: 3" + }, + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "name": { + "inputPlaceHolder": "Introduïu un nom…", + "title": "Nom", + "tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona en aquesta càmera." + }, + "label": "Zones", + "desc": { + "documentation": "Documentació", + "title": "Les zones permeten definir una àrea específica de la imatge per tal de determinar si un objecte es troba dins d'una àrea concreta o no." + }, + "add": "Afegir Zona", + "edit": "Editar zona", + "loiteringTime": { + "title": "Temps de merodeig", + "desc": "Estableix el temps mínim, en segons, que l'objecte ha d'estar dins la zona perquè s'activi. Per defecte: 0" + }, + "allObjects": "Tots els objectes", + "documentTitle": "Edita zona - Frigate", + "speedThreshold": { + "title": "Llindar de velocitat ({{unit}})", + "toast": { + "error": { + "loiteringTimeError": "Zones amb temps de merodeig superior a 0 no s'han d'utilitzar per a l'estimació de velocitat.", + "pointLengthError": "L'estimació de velocitat s'ha desactivat per a aquesta zona. Les zones amb estimació de velocitat han de tenir exactament 4 points." + } + }, + "desc": "Especifica una velocitat mínima dels objectes per ser considerats dins d'aquesta zona." + }, + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "success": "S'ha desat la zona ({{zoneName}})." + } + }, + "filter": { + "all": "Totes les màscares i zones" + }, + "motionMasks": { + "desc": { + "documentation": "Documentació", + "title": "Les màscares de moviment s’utilitzen per evitar que certs tipus de moviment no desitjats activin la detecció. Si s’aplica una màscara excessiva, es dificultarà el seguiment dels objectes." + }, + "context": { + "documentation": "Llegir la documentació", + "title": "Les màscares de moviment s’utilitzen per evitar que certs tipus de moviment no desitjats activin la detecció (per exemple: branques d’arbres, marques temporals). Les màscares de moviment s’han d’utilitzar amb molta moderació, un excés de màscares dificultarà el seguiment dels objectes." + }, + "polygonAreaTooLarge": { + "documentation": "Llegir la documentació", + "tips": "Les màscares de moviment no impedeixen la detecció d'objectes. Hauries de fer servir una zona requerida en el seu lloc.", + "title": "La màscara de moviment cobreix el {{polygonArea}}% del camp de visió de la càmera. Les màscares de moviment molt grans no son recomanables." + }, + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "label": "Màscara de moviment", + "add": "Nova màscara de moviment", + "edit": "Edita la màscara de moviment", + "documentTitle": "Editar la màscara de moviment - Frigate", + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "success": { + "title": "{{polygonName}} s'ha desat.", + "noName": "La màscara de moviment ha estat desada." + } + } + }, + "objectMasks": { + "documentTitle": "Editar la màscara d'objecte - Frigate", + "label": "Màscares d'objecte", + "edit": "Editar la màscara d'objecte", + "point_one": "{{count}} punt", + "point_many": "{{count}} punts", + "point_other": "{{count}} punts", + "objects": { + "allObjectTypes": "Tots els tipus d’objecte", + "title": "Objectes", + "desc": "El tipus d'objecte que s'aplica a la màscara d'objectes." + }, + "add": "Afegir màscara d'objecte", + "desc": { + "documentation": "Documentació", + "title": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació." + }, + "clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.", + "toast": { + "success": { + "title": "{{polygonName}} s'ha desat.", + "noName": "La màscara d'objectes ha estat desada." + } + }, + "context": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació." + }, + "restart_required": "Reinici necessari (canvi de màscares o zones)", + "motionMaskLabel": "Màscara de moviment {{number}}", + "objectMaskLabel": "Màscara d'objecte {{number}} ({{label}})", + "toast": { + "success": { + "copyCoordinates": "S'han copiat les coordenades per a {{polyName}} al porta-retalls." + }, + "error": { + "copyCoordinatesFailed": "No s'han pogut copiar les coordenades al porta-retalls." + } + } + }, + "notification": { + "email": { + "title": "Correu electrònic", + "placeholder": "p. ex. exemple@email.com", + "desc": "Es requereix un correu electrònic vàlid que s’utilitzarà per notificar-te si hi ha algun problema amb el servei de notificacions push." + }, + "notificationSettings": { + "documentation": "Llegir la documentació", + "title": "Paràmetres de notificació", + "desc": "Frigate pot enviar notificacions push directament al teu dispositiu quan s’executa des del navegador o està instal·lat com a PWA (aplicació web progressiva)." + }, + "deviceSpecific": "Paràmetres específics del dispositiu", + "registerDevice": "Registrar aquest dispositiu", + "unregisterDevice": "Desegistrar aquest dispositiu", + "cancelSuspension": "Cancel·la la suspensió", + "suspendTime": { + "untilRestart": "Suspendre fins al reinici", + "30minutes": "Suspèn durant 30 minuts", + "1hour": "Suspèn durant 1 hora", + "12hours": "Suspèn durant 12 hores", + "suspend": "Suspendre", + "24hours": "Suspèn durant 24 hores", + "10minutes": "Suspèn durant 10 minuts", + "5minutes": "Suspèn durant 5 minuts" + }, + "toast": { + "success": { + "settingSaved": "Els paràmetres de notificacions s'han desat.", + "registered": "Registre de notificacions realitzat amb èxit. Cal reiniciar Frigate perquè es puguin enviar notificacions (inclosa una notificació de prova)." + }, + "error": { + "registerFailed": "No s'ha pogut desar el registre de notificacions." + } + }, + "cameras": { + "title": "Càmeres", + "noCameras": "Sense càmeres disponibles", + "desc": "Selecciona per a quines càmeres s'han d'habilitar les notificacions." + }, + "title": "Notificacions", + "notificationUnavailable": { + "title": "Notificacions no disponibles", + "documentation": "Llegir la documentació", + "desc": "Les notificacions push web requereixen un context segur (https://…). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions." + }, + "unsavedChanges": "Canvis de notificació no desats", + "globalSettings": { + "title": "Paràmetres globals", + "desc": "Suspendre temporalment les notificacions per a certes càmeres per tots els dispositius registrats." + }, + "active": "Notificacions actives", + "suspended": "Notificacions suspeses {{time}}", + "unsavedRegistrations": "Registres de notificació no desats", + "sendTestNotification": "Enviar una notificació de prova" + }, + "camera": { + "streams": { + "title": "Transmissions", + "desc": "Desactiva temporalment una càmera fins que es reiniciï Frigate. La desactivació d'una càmera atura completament el processament de les transmissions d'aquesta càmera per part de Frigate. La detecció, gravació i depuració no estaran disponibles.
    Nota: Això no desactiva les retransmissions de go2rtc." + }, + "title": "Paràmetres de la càmera", + "reviewClassification": { + "title": "Revisar la classificació", + "readTheDocumentation": "Llegir la documentació", + "selectAlertsZones": "Seleccionar zones per alertes", + "limitDetections": "Limitar deteccions a zones específiques", + "selectDetectionsZones": "Seleccionar zones per deteccions", + "unsavedChanges": "Paràmetres de la revisió de classificació no guardats per a {{camera}}", + "noDefinedZones": "No s'han definit zones per a aquesta càmera.", + "objectAlertsTips": "Tots els objectes {{alertsLabels}} a {{cameraName}} es mostraràn com a Alertes.", + "zoneObjectAlertsTips": "Tots els objectes {{alertsLabels}} detectats a la {{zone}} de {{cameraName}} es mostraràn com a Alertes.", + "toast": { + "success": "S'ha desat la configuració de la classificació de revisió. Reinicia Frigate per aplicar els canvis." + }, + "zoneObjectDetectionsTips": { + "text": "Tots els objectes {{detectionsLabels}} no classificats a la {{zone}} de {{cameraName}} es mostraràn com a Deteccions.", + "notSelectDetections": "Tots els objectes {{detectionsLabels}} detectats a {{zone}} de la càmera {{cameraName}} que no estiguin categoritzats com a Alertes es mostraran com a Deteccions, independentment de la zona en què es trobin.", + "regardlessOfZoneObjectDetectionsTips": "Tots els objectes {{detectionsLabels}} no categoritzats a {{cameraName}} es mostraran com a Deteccions independentment de la zona en què es trobin." + }, + "objectDetectionsTips": "Tots els objectes {{detectionsLabels}} no categoritzats a {{cameraName}} es mostraran com a Deteccions independentment de la zona en què es trobin.", + "desc": "Frigate categoritza els elements de revisió com a Alertes i Deteccions. Per defecte, tots els objectes de tipus persona i cotxe es consideren Alertes. Pots afinar la categorització dels teus elements de revisió configurant zones requerides per a aquests." + }, + "review": { + "alerts": "Alertes ", + "detections": "Deteccions ", + "title": "Revisar", + "desc": "Habilita o deshabilita temporalment les alertes i deteccions per a aquesta càmera fins que es reiniciï Frigate. Quan estigui desactivat, no es generaran nous elements de revisió. " + }, + "object_descriptions": { + "title": "Descripció d'objectes per IA generativa", + "desc": "Activar/desactivar temporalment la IA generativa de descripcions per aquesta càmera. Quan està desactivat, les descripcions d'IA generativa no seran requerides per als objectes seguits per aquesta càmera." + }, + "review_descriptions": { + "title": "Revisar las descripcions d'IA generativa", + "desc": "Activar/desactivals temporalment les descripcions d'IA generativa per aquesta càmera. Quan estan desactivades, les descripcions d'IA generativa no serán requerides per revisar els items en aquesta càmera." + }, + "addCamera": "Afegir Nova Càmera", + "editCamera": "Editar Càmera:", + "selectCamera": "Seleccionar Càmera", + "backToSettings": "Tornar a la Configuració de Càmera", + "cameraConfig": { + "add": "Afegir Càmera", + "edit": "Editar Càmera", + "description": "Configurar la càmera incloent les entrades y rols.", + "name": "Nom de Càmera", + "nameRequired": "El nom de càmera es necesari", + "nameLength": "El nom de la càmera ha de ser com a mínim de 24 caràcters.", + "namePlaceholder": "e.x., porta_entrada", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Entrades", + "path": "Direcció d'entrada", + "pathRequired": "Direcció d'entrada necesaria", + "pathPlaceholder": "rtsp://...", + "roles": "Rols", + "rolesRequired": "Com a mínin un rol es necesari", + "rolesUnique": "Cada rol (audio, detecció, gravació) pot ser assiganda a una entrada", + "addInput": "Afegir una entrada", + "removeInput": "Esborrar una entrada", + "inputsRequired": "Com a mínim una entrada es necesaria" + }, + "toast": { + "success": "La càmera {{cameraName}} s'ha guardat correctament" + } + } + }, + "motionDetectionTuner": { + "Threshold": { + "title": "Llindar", + "desc": "El valor del llindar determina quanta variació en la luminància d’un píxel cal perquè es consideri moviment. Per defecte: 30" + }, + "contourArea": { + "title": "Àrea de contorn", + "desc": "El valor de l’àrea del contorn s’utilitza per decidir quins grups de píxels canviats es consideren moviment. Valor per defecte: 10" + }, + "desc": { + "documentation": "Llegeix la guia d'ajust de detecció de moviment", + "title": "Frigate utilitza la detecció de moviment com a primer filtre per comprovar si hi ha alguna activitat a la imatge que valgui la pena analitzar amb la detecció d’objectes." + }, + "improveContrast": { + "title": "Millorar contrast", + "desc": "Millora el contrast per les escenes fosques. Predeterminat: ACTIVAT" + }, + "title": "Afinador de detecció de moviment", + "toast": { + "success": "Els ajustos de la detecció de moviment s'han desat." + }, + "unsavedChanges": "Canvis no desats en l'ajust de moviment {{camera}}" + }, + "debug": { + "title": "Depuració", + "objectList": "Llista d'objectes", + "noObjects": "Cap objecte", + "debugging": "Depurant", + "mask": { + "title": "Màscares de moviment", + "desc": "Mostra els polígons de la màscara de moviment" + }, + "regions": { + "title": "Regions", + "desc": "Mostre un requadre de la regió d'interés enviat al detector d'objectes", + "tips": "

    Requadres de Regió


    Requadres verds es sobreposaran a les àrees d’interès de la imatge que s'envien al detector d’objectes.

    " + }, + "objectShapeFilterDrawing": { + "score": "Puntuació", + "document": "Llegir la documentació ", + "ratio": "Proporció", + "area": "Àrea", + "title": "Dibuix del filtre de forma de l'objecte", + "desc": "Dibuixa un rectangle a la imatge per veure detalls d'àrea i proporció", + "tips": "Habilita aquesta opció per dibuixar un rectangle a la imatge de la càmera que mostri la seva àrea i proporció. Aquests valors es poden utilitzar després per configurar els paràmetres del filtre de forma d’objecte a la teva configuració." + }, + "zones": { + "title": "Zones", + "desc": "Mostra el contorn per a qualsevol zona definida" + }, + "timestamp": { + "title": "Marca temporal", + "desc": "Superposa una marca temporal a la imatge" + }, + "boundingBoxes": { + "title": "Caixes delimitadores", + "colors": { + "label": "Colors de la caixa delimitadora de l'objecte", + "info": "
  • En iniciar, s'assignarà un color diferent a cada etiqueta d’objecte
  • Una línia fina de color blau fosc indica que l’objecte no està detectat en aquest moment
  • Una línia fina de color gris indica que l’objecte està detectat com a estacionari
  • Una línia gruixuda indica que l’objecte és el subjecte de l’autoseguiment (quan està activat)
  • " + }, + "desc": "Mostra les caixes delimitadores al voltant dels objectes rastrejats" + }, + "motion": { + "title": "Caixes de moviment", + "desc": "Mostra requadres al voltant de les àrees on s'ha detectat moviment", + "tips": "

    Caixes de moviment


    Es sobreposaran requadres vermells a les àrees del fotograma on actualment s’estigui detectant moviment.

    " + }, + "detectorDesc": "Frigate fa servir els teus detectors ({{detectors}}) per a detectar objectes a les imatges de la teva càmera.", + "desc": "La vista de depuració mostra en temps real els objectes rastrejats i les seves estadístiques. La llista d’objectes mostra un resum amb retard temporal dels objectes detectats.", + "openCameraWebUI": "Obrir la interficie d'usuari de {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "No hi ha deteccions d'audio", + "score": "puntuació", + "currentRMS": "RMS Actual", + "currentdbFS": "dbFS Actual" + }, + "paths": { + "title": "Rutes", + "desc": "Mostrar els punts significatius de la ruta dels objectes seguits", + "tips": "

    Rutes


    Les línies i cercles indicarán els punts significatius dels objectes seguits durant el seu cicle de vida.

    " + } + }, + "users": { + "table": { + "username": "Usuari", + "password": "Contrasenya", + "deleteUser": "Suprimir usuari", + "noUsers": "No s'han trobat usuaris.", + "changeRole": "Canviar la funció d’usuari", + "actions": "Accions", + "role": "Rol" + }, + "toast": { + "error": { + "deleteUserFailed": "No s'ha pogut eliminar l'usuari: {{errorMessage}}", + "roleUpdateFailed": "No s'ha pogut actualitzar la funció: {{errorMessage}}", + "setPasswordFailed": "Error en guardar la contrasenya: {{errorMessage}}", + "createUserFailed": "No s'ha pogut crear l'usuari: {{errorMessage}}" + }, + "success": { + "deleteUser": "L'usuari {{user}} s'ha suprimit amb èxit", + "createUser": "L'Usuari {{user}} s'ha creat amb èxit", + "updatePassword": "Contrasenya actualitzada amb èxit.", + "roleUpdated": "Funció actualitzada per {{user}}" + } + }, + "dialog": { + "form": { + "user": { + "title": "Nom d'usuari", + "placeholder": "Introdueix el nom d'usuari", + "desc": "Només es permeten lletres, números, punts i guions baixos." + }, + "password": { + "confirm": { + "placeholder": "Confirma contrasenya", + "title": "Confirma contrasenya" + }, + "strength": { + "title": "Seguretat de la contrasenya: ", + "weak": "Dèbil", + "strong": "Fort", + "veryStrong": "Molt forta", + "medium": "Mitjana" + }, + "notMatch": "Les contrasenyes no coincideixen", + "match": "Les contrasenyes coincideixen", + "placeholder": "Introdueix la contrasenya", + "title": "Contrasenya", + "show": "Mostra contrasenya", + "hide": "Amaga contrasenya", + "requirements": { + "title": "Requisits contrasenya:", + "length": "Com a mínim 8 carácters", + "uppercase": "Com a mínim una majúscula", + "digit": "Com a mínim un digit", + "special": "Com a mínim un carácter especial (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nova contrasenya", + "placeholder": "Introduïu una nova contrasenya", + "confirm": { + "placeholder": "Re-entrar contrasenya nova" + } + }, + "usernameIsRequired": "El nom d'usuari és obligatori", + "passwordIsRequired": "La contrasenya és obligatoria", + "currentPassword": { + "title": "Constrasenya actual", + "placeholder": "Entra l'actual contrasenya" + } + }, + "passwordSetting": { + "updatePassword": "Contrasenya actualitzada per {{username}}", + "setPassword": "Estableix Contrasenya", + "cannotBeEmpty": "La contrasenya no pot ser buida", + "doNotMatch": "Les contrasenyes no coincideixen", + "desc": "Crea un nova contrasenya segura per protegir aquest compte.", + "currentPasswordRequired": "L'actual contrasenya es requerida", + "incorrectCurrentPassword": "L'actual contrasenya es incorrecte", + "passwordVerificationFailed": "Falla en la verificació de la contrasenya", + "multiDeviceWarning": "Serà necesari loguejarte en qualsevol altre dispositiu en que estiguis loguejat en {{refresh_time}}.", + "multiDeviceAdmin": "També pots forçar a tots els usuaris a tornar a autenticar-se immediatament rotant el teu secret JWT." + }, + "deleteUser": { + "title": "Suprimir usuari", + "warn": "Estàs segur que vols suprimir {{username}}?", + "desc": "Aquesta acció no es pot desfer. Això eliminarà permanentment el compte d'usuari i suprimirà totes les dades associades." + }, + "changeRole": { + "roleInfo": { + "viewer": "Visualitzador", + "admin": "Administrador", + "adminDesc": "Accés complet a totes les funcionalitats.", + "intro": "Selecciona el rol adequat per a aquest usuari:", + "viewerDesc": "Limitat només a panells en directe, revisió, exporació i exportació.", + "customDesc": "Rol personalitzat per accés específic a una cámera." + }, + "title": "Canviar la funció d’usuari", + "desc": "Actualitzar permisos per a {{username}}", + "select": "Seleccioneu un paper" + }, + "createUser": { + "title": "Crear un nou usuari", + "confirmPassword": "Siusplau, confirma la contrasenya", + "usernameOnlyInclude": "El nom d'usuari només pot contenir lletres, números, . o _", + "desc": "Afegeix un nou compte d'usuari i especifica un rol per accedir a àrees de la interfície de Frigate." + } + }, + "title": "Usuaris", + "addUser": "Afegir usuari", + "management": { + "title": "Gestió d'usuaris", + "desc": "Gestioneu els comptes d'usuari d'aquesta instància de Frigate." + }, + "updatePassword": "Actualitzar contrasenya" + }, + "frigatePlus": { + "snapshotConfig": { + "table": { + "camera": "Càmera", + "snapshots": "Instantànies", + "cleanCopySnapshots": "clean_copy Instantànies" + }, + "title": "Configuració d'instantànies", + "documentation": "Llegir la documentació", + "desc": "Per a enviar a Frigate+ fa falta que tan la instantània com la instantània clean_copy estiguin habilitades a la configuració.", + "cleanCopyWarning": "Algunes càmeres tenen les captures d'imatge activades però la còpia neta desactivada. Cal habilitar clean_copy a la configuració de captures per poder enviar imatges d’aquestes càmeres a Frigate+." + }, + "modelInfo": { + "baseModel": "Model base", + "modelType": "Tipus de model", + "trainDate": "Data d'entrenament", + "title": "Informació del model", + "supportedDetectors": "Detectors compatibles", + "availableModels": "Models disponibles", + "cameras": "Càmeres", + "plusModelType": { + "userModel": "Afinat", + "baseModel": "Model base" + }, + "loadingAvailableModels": "Carregant models disponibles…", + "loading": "Carregant informació del model…", + "error": "No s'ha pogut carregar la informació del model", + "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector." + }, + "apiKey": { + "plusLink": "Llegeix més sobre Frigate+", + "title": "Clau API de Frigate+", + "validated": "La clau API de Frigate+ ha estat detectada i validada", + "notValidated": "La clau API de Frigate+ no ha estat detectada o no ha estat validada", + "desc": "La clau API de Frigate+ habilita la integració amb el servei Frigate+." + }, + "unsavedChanges": "Canvis dels paràmetres de Frigate+ sense desar", + "title": "Paràmetres de Frigate+", + "toast": { + "error": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}", + "success": "Els paràmetres de Frigate+ han estat desats. Reincia Frigate per aplicar els canvis." + }, + "restart_required": "Es necessari un reinici (El model de Frigate+ ha cambiat)" + }, + "enrichments": { + "semanticSearch": { + "modelSize": { + "small": { + "title": "petit", + "desc": "L’opció small fa servir una versió quantitzada del model que consumeix menys RAM i s’executa més ràpidament a la CPU, amb una diferència gairebé inapreciable en la qualitat de les incrustacions (embeddings)." + }, + "label": "Mida del model", + "large": { + "title": "gran", + "desc": "L’opció large fa servir el model complet de Jina i s’executarà automàticament a la GPU si està disponible." + }, + "desc": "La mida del model utilitzat per incrustacions de cerca semàntica." + }, + "reindexNow": { + "confirmButton": "Reindexar", + "success": "La reindexació ha començat amb èxit.", + "label": "Reindexar ara", + "confirmTitle": "Confirmar la reindexació", + "desc": "La reindexació regenerarà les incrustacions (embeddings) de tots els objectes seguits. Aquest procés s’executa en segon pla i pot arribar a saturar la CPU, així com trigar una bona estona depenent del nombre d’objectes seguits que tinguis.", + "confirmDesc": "Estàs segur que vols reindexar totes les incrustacions (embeddings) dels objectes seguits? Aquest procés s’executarà en segon pla, però pot arribar a saturar la CPU i trigar bastant temps. Pots seguir-ne el progrés a la pàgina d’Explora.", + "alreadyInProgress": "La reindexació ja està en curs.", + "error": "Error en iniciar la reindexació: {{errorMessage}}" + }, + "readTheDocumentation": "Llegir la documentació", + "title": "Cerca semàntica", + "desc": "La cerca semàntica a Frigate permet trobar objectes rastrejats dins dels elements de revisió utilitzant la pròpia imatge, una descripció de text definida per l'usuari o una de generada automàticament." + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "petit", + "desc": "Fer servir la opció petit fa servir un model d'embedding de rostre de FaceNet que d'executa de manera eficient a la majoria de les CPUs." + }, + "large": { + "title": "gran", + "desc": "L’opció large fa servir el model d'embedding de rostres d'ArcFace i s’executarà automàticament a la GPU si està disponible." + }, + "label": "Mida del model", + "desc": "La mida del model utilitzat per al reconeixement facial." + }, + "readTheDocumentation": "Llegir la documentació", + "title": "Reconeixement de rostres", + "desc": "El reconeixement facial permet a les persones assignar noms i quan es reconeix la seva cara Frigate assignarà el nom de la persona com a subetiqueta. Aquesta informació s'inclou en la interfície d'usuari, filtres, així com en les notificacions." + }, + "unsavedChanges": "Canvis dels paràmetres complementaris sense desar", + "licensePlateRecognition": { + "readTheDocumentation": "Llegir la documentació", + "title": "Reconeixement de matrícules", + "desc": "Frigate pot reconèixer les plaques de matrícula en vehicles i afegir automàticament els caràcters detectats al camp de la placa reconeguda o un nom conegut com a sub_etiqueta en objectes que són de tipus cotxe. Un cas d'ús comú pot ser llegir les plaques de matrícula dels cotxes que entren en un lloc o els cotxes que passen per un carrer." + }, + "birdClassification": { + "title": "Classificació d'ocells", + "desc": "La classificació d’ocells identifica ocells coneguts mitjançant un model TensorFlow quantitzat. Quan es reconeix un ocell conegut, el seu nom comú s’afegeix com a subetiqueta. Aquesta informació es mostra a la interfície d’usuari, als filtres i també a les notificacions." + }, + "title": "Parmàmetres complementaris", + "toast": { + "error": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}", + "success": "Els paràmetres complementaris s'han desat. Reinicia Frigate per aplicar els canvis." + }, + "restart_required": "És necessari reiniciar (Han cambiat paràmetres complementaris)" + }, + "triggers": { + "table": { + "actions": "Accions", + "noTriggers": "No hi ha disparadors configurats en aquesta càmera.", + "edit": "Editar", + "deleteTrigger": "Esborrar Disparador", + "lastTriggered": "Últim Disparo", + "name": "Nom", + "type": "Tipus", + "content": "Contingut", + "threshold": "Llindar" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descripció" + }, + "actions": { + "alert": "Marcar com Alerta", + "notification": "Enviar Notificació", + "sub_label": "Afegeix una subetiqueta", + "attribute": "Afegeix un atribut" + }, + "dialog": { + "createTrigger": { + "title": "Crear Disparador", + "desc": "Crear disparador per una càmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Disparador", + "desc": "Editar la configuració per al disparador de càmera {{camera}}" + }, + "deleteTrigger": { + "title": "Esborrar Disparador", + "desc": "Estas segur que vols esborrar el disparador {{triggerName}}? Aquesta acció no es pot desfer." + }, + "form": { + "name": { + "title": "Nom", + "placeholder": "Anomena aquest activador", + "error": { + "minLength": "El camp ha de tenir almenys 2 caràcters.", + "invalidCharacters": "El camp només pot contenir lletres, números, guions baixos i guions.", + "alreadyExists": "El disparador amb aquest nom ja existeix per aquesta càmera." + }, + "description": "Introduïu un nom o una descripció únics per a identificar aquest activador" + }, + "enabled": { + "description": "Activar o desactivar aquest disparador" + }, + "type": { + "title": "Tipus", + "placeholder": "Selecciona un tipus de disparador", + "description": "Activa quan es detecta una descripció similar d'un objecte rastrejat", + "thumbnail": "Activa quan es detecti una miniatura d'objecte rastrejada similar" + }, + "content": { + "title": "Contingut", + "imagePlaceholder": "Selecciona una miniatura", + "textPlaceholder": "Entra el contingut de text", + "imageDesc": "Només es mostren les 100 miniatures més recents. Si no podeu trobar la miniatura desitjada, reviseu els objectes anteriors a Explora i configureu un activador des del menú.", + "textDesc": "Entra el text per disparar aquesta acció quan es detecti una descripció d'objecte a rastrejar similar.", + "error": { + "required": "Contigunt requerit." + } + }, + "threshold": { + "title": "Llindar", + "error": { + "min": "El llindar ha de ser mínim 0", + "max": "El llindar ha de ser máxim 1" + }, + "desc": "Estableix el llindar de similitud per a aquest activador. Un llindar més alt significa que es requereix una coincidència més propera per disparar el disparador." + }, + "actions": { + "title": "Accions", + "desc": "Per defecte, Frigate dispara un missatge MQTT per a tots els activadors. Subetiquetes afegeix el nom de l'activador a l'etiqueta de l'objecte. Els atributs són metadades cercables emmagatzemades per separat a les metadades de l'objecte rastrejat.", + "error": { + "min": "S'ha de seleccionar una acció com a mínim." + } + }, + "friendly_name": { + "title": "Nom amistós", + "placeholder": "Nom o descripció d'aquest disparador", + "description": "Un nom opcional amistós o text descriptiu per a aquest activador." + } + } + }, + "toast": { + "success": { + "createTrigger": "El disparador {{name}} s'ha creat existosament.", + "updateTrigger": "El disparador {{name}} s'ha actualitzat correctament.", + "deleteTrigger": "El disparador {{name}} s'ha borrat correctament." + }, + "error": { + "createTriggerFailed": "Error al crear el disparador: {{errorMessage}}", + "updateTriggerFailed": "Error a l'actualitzar el disparador: {{errorMessage}}", + "deleteTriggerFailed": "Error a l'esborrar el disparador: {{errorMessage}}" + } + }, + "documentTitle": "Disparadors", + "management": { + "title": "Activadors", + "desc": "Gestionar els disparadors de {{camera}}. Usa les tipus de miniatures per disparar miniatures similars a l'objecte a seguir seleccionat, i el tipus de descripció per disparar en cas de descripcions similars a l'especificada." + }, + "addTrigger": "Afegir disaprador", + "semanticSearch": { + "desc": "La cerca semàntica ha d'estar activada per a utilitzar els activadors.", + "title": "La cerca semàntica està desactivada" + }, + "wizard": { + "title": "Crea un activador", + "step1": { + "description": "Configura la configuració bàsica per al vostre activador." + }, + "step2": { + "description": "Configura el contingut que activarà aquesta acció." + }, + "step3": { + "description": "Configura el llindar i les accions d'aquest activador." + }, + "steps": { + "nameAndType": "Nom i tipus", + "configureData": "Configura les dades", + "thresholdAndActions": "Llindar i accions" + } + } + }, + "roles": { + "dialog": { + "form": { + "cameras": { + "required": "Almenys has de seleccionar una càmera.", + "title": "Càmeres", + "desc": "Selecciona les càmeres que tingui accés aquest rol. Com a mínim s'ha de seleccionar una càmera." + }, + "role": { + "title": "Nom del Rol", + "placeholder": "Entra el nom del rol", + "desc": "Només lletres, números, els punts i subrallats están permesos.", + "roleIsRequired": "Nom del Rol requerit", + "roleOnlyInclude": "El nom de Rol només pot incloure lletres, nombres, . o _", + "roleExists": "Ja existeis un rol amb aquest nom." + } + }, + "createRole": { + "title": "Crear nou Rol", + "desc": "Afegir nou rol y especificar permisos d'accés." + }, + "editCameras": { + "title": "Editar Càmeres Rol", + "desc": "Actualitza l'acces a les càmeres per al rol {{role}}." + }, + "deleteRole": { + "title": "Eliminar Rol", + "desc": "Aquesta acció no pot ser restablerta. S'esborrarà permenentment el rol y els usuaris asignats amb aquest rol de 'visor', que els dona accés a totes les càmeres.", + "warn": "Estas segur que vols eliminar {{role}}?", + "deleting": "Eliminant..." + } + }, + "management": { + "title": "Gestió del Rols de Visors", + "desc": "Gestiona els rols visors personalitzats y els seus permisos d'accés per aquesta instancia de Frigate." + }, + "addRole": "Afegir Rol", + "table": { + "role": "Rol", + "cameras": "Càmeres", + "actions": "Accions", + "noRoles": "No s'han trobat rols personalitzats.", + "editCameras": "Editar Càmeres", + "deleteRole": "Eliminar Rol" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} creat exitosament", + "updateCameras": "Càmeres actualitzades per al rol {{role}}", + "deleteRole": "Rol {{role}} eliminat exitosament", + "userRolesUpdated_one": "{{count}} l'usuari assignat a aquest rol s'ha actualitzat a 'visor', que té accés a totes les càmeres.", + "userRolesUpdated_many": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres.", + "userRolesUpdated_other": "{{count}} usuaris assignats a aquest rol s'han actualitzat a 'visor', que té accés a totes les càmeres." + }, + "error": { + "createRoleFailed": "Error al crear el rol: {{errorMessage}}", + "updateCamerasFailed": "Error a l'actualitzar les càmeres: {{errorMessage}}", + "deleteRoleFailed": "Error a l'eliminar el rol: {{errorMessage}}", + "userUpdateFailed": "Error a l'actualitzar els ros d'usuari: {{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "Afegir Càmera", + "description": "Seguiu els passos de sota per afegir una nova càmera a la instal·lació.", + "steps": { + "nameAndConnection": "Nom i connexió", + "streamConfiguration": "Configuració de stream", + "validationAndTesting": "Validació i proves", + "probeOrSnapshot": "Prova o instantània" + }, + "step1": { + "cameraBrand": "Marca de la càmera", + "description": "Introduïu els detalls de la càmera i trieu provar la càmera o seleccionar manualment la marca.", + "cameraName": "Nom de la càmera", + "cameraNamePlaceholder": "p. ex., vista general de la porta davantera o de la barra posterior", + "host": "Adreça de l'amfitrió/IP", + "port": "Port", + "username": "Nom d'usuari", + "usernamePlaceholder": "Opcional", + "password": "Contrasenya", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecciona el protocol de transport", + "brandInformation": "Informació de marca", + "brandUrlFormat": "Per a càmeres amb el format d'URL RTSP com: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "testConnection": "Prova la connexió", + "testSuccess": "Prova de connexió correcta!", + "testFailed": "Ha fallat la prova de connexió. Si us plau, comproveu la vostra entrada i torneu-ho a provar.", + "streamDetails": "Detalls del flux", + "warnings": { + "noSnapshot": "No s'ha pogut obtenir una instantània del flux configurat." + }, + "errors": { + "brandOrCustomUrlRequired": "Seleccioneu una marca de càmera amb host/IP o trieu 'Altres' amb un URL personalitzat", + "nameRequired": "Es requereix el nom de la càmera", + "nameLength": "El nom de la càmera ha de tenir 64 caràcters o menys", + "invalidCharacters": "El nom de la càmera conté caràcters no vàlids", + "nameExists": "El nom de la càmera ja existeix", + "brands": { + "reolink-rtsp": "No es recomana Reolink RST. Es recomana habilitar HTTP a la configuració de la càmera i reiniciar l'assistent de la càmera." + }, + "customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\". Es requereix configuració manual per a fluxos de càmera no RTSP." + }, + "selectBrand": "Seleccioneu la marca de la càmera per a la plantilla d'URL", + "customUrl": "URL de flux personalitzat", + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "S'estan provant les metadades de la càmera...", + "fetchingSnapshot": "S'està recuperant la instantània de la càmera..." + }, + "connectionSettings": "Configuració de la connexió", + "detectionMethod": "Mètode de detecció de flux", + "onvifPort": "ONVIF Port", + "probeMode": "Càmera de prova", + "manualMode": "Selecció manual", + "detectionMethodDescription": "Proveu la càmera amb ONVIF (si és compatible) per trobar URL de flux de càmera, o seleccioneu manualment la marca de càmera per utilitzar URL predefinits. Per a introduir un URL RTSP personalitzat, trieu el mètode manual i seleccioneu \"Altres\".", + "onvifPortDescription": "Per a les càmeres que suporten ONVIF, això sol ser 80 o 8080.", + "useDigestAuth": "Utilitza l'autenticació digest", + "useDigestAuthDescription": "Usa l'autenticació de resum HTTP per a ONVIF. Algunes càmeres poden requerir un nom d'usuari/contrasenya ONVIF dedicat en lloc de l'usuari administrador estàndard." + }, + "save": { + "failure": "SS'ha produït un error en desar {{cameraName}}.", + "success": "S'ha desat correctament la càmera nova {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolució", + "video": "Vídeo", + "audio": "Àudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Proporcioneu un URL de flux vàlid", + "testFailed": "Ha fallat la prova de flux: {{error}}" + }, + "step2": { + "description": "Proveu la càmera per als fluxos disponibles o configureu la configuració manual basada en el mètode de detecció seleccionat.", + "streamsTitle": "Fluxos de la càmera", + "addStream": "Afegeix un flux", + "addAnotherStream": "Afegeix un altre flux", + "streamTitle": "Flux {{number}}", + "streamUrl": "URL del flux", + "url": "URL", + "resolution": "Resolució", + "selectResolution": "Selecciona la resolució", + "quality": "Qualitat", + "selectQuality": "Selecciona la qualitat", + "roleLabels": { + "detect": "Detecció d'objectes", + "record": "Enregistrament", + "audio": "Àudio" + }, + "testStream": "Prova la connexió", + "testSuccess": "Prova de connexió correcta!", + "testFailed": "Ha fallat la prova de connexió. Si us plau, comproveu la vostra entrada i torneu-ho a provar.", + "testFailedTitle": "Ha fallat la prova", + "connected": "Connectat", + "notConnected": "No connectat", + "featuresTitle": "Característiques", + "go2rtc": "Redueix les connexions a la càmera", + "detectRoleWarning": "Almenys un flux ha de tenir el rol de \"detecte\" per continuar.", + "rolesPopover": { + "title": "Rols de flux", + "detect": "Canal principal per a la detecció d'objectes.", + "record": "Desa els segments del canal de vídeo basats en la configuració.", + "audio": "Canal per a la detecció basada en àudio." + }, + "featuresPopover": { + "title": "Característiques del flux", + "description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera." + }, + "roles": "Rols", + "streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "streamDetails": "Detalls del flux", + "probing": "Provant càmera...", + "retry": "Intentar de nou", + "testing": { + "probingMetadata": "S'estan provant les metadades de la càmera...", + "fetchingSnapshot": "S'està recuperant la instantània de la càmera..." + }, + "probeFailed": "No s'ha pogut provar la càmera: {{error}}", + "probingDevice": "Provant dispositiu...", + "probeSuccessful": "Prova exitosa", + "probeError": "Error de prova", + "probeNoSuccess": "La prova no ha tingut èxit", + "deviceInfo": "Informació del dispositiu", + "manufacturer": "Fabricant", + "model": "Model", + "firmware": "Firmware", + "profiles": "Perfils", + "ptzSupport": "Suport PTZ", + "autotrackingSupport": "Implementació de seguiment automàtic", + "presets": "Predefinits", + "rtspCandidates": "Candidats RTSP", + "rtspCandidatesDescription": "S'han trobat els següents URL RTSP de la sonda de la càmera. Proveu la connexió per a veure les metadades del flux.", + "noRtspCandidates": "No s'ha trobat cap URL RTSP a la càmera. Les vostres credencials poden ser incorrectes, o la càmera pot no admetre ONVIF o el mètode utilitzat per recuperar els URL RTSP. Torneu enrere i introduïu l'URL RTSP manualment.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Utilitza", + "uriCopy": "Copia", + "uriCopied": "URI copiat al porta-retalls", + "testConnection": "Prova la connexió", + "toggleUriView": "Feu clic per a commutar la vista completa de l'URI", + "errors": { + "hostRequired": "Es requereix l'adreça de l'amfitrió/IP" + } + }, + "step3": { + "none": "Cap", + "error": "Error", + "saveAndApply": "Desa una càmera nova", + "saveError": "Configuració no vàlida. Si us plau, comproveu la configuració.", + "issues": { + "title": "Validació del flux", + "videoCodecGood": "El còdec de vídeo és {{codec}}.", + "audioCodecGood": "El còdec d'àudio és {{codec}}.", + "noAudioWarning": "No s'ha detectat cap àudio per a aquest flux, els enregistraments no tindran àudio.", + "audioCodecRecordError": "El còdec d'àudio AAC és necessari per a suportar l'àudio en els enregistraments.", + "audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.", + "restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.", + "dahua": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "hikvision": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.", + "resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits." + }, + "description": "Configura els rols de flux i afegeix fluxos addicionals per a la càmera.", + "validationTitle": "Validació del flux", + "connectAllStreams": "Connecta tots els fluxos", + "reconnectionSuccess": "S'ha reconnectat correctament.", + "reconnectionPartial": "Alguns fluxos no s'han pogut tornar a connectar.", + "streamUnavailable": "La vista prèvia del flux no està disponible", + "reload": "Torna a carregar", + "connecting": "Connectant...", + "streamTitle": "Flux {{number}}", + "valid": "Vàlid", + "failed": "Ha fallat", + "notTested": "No provat", + "connectStream": "Connecta", + "connectingStream": "Connectant", + "disconnectStream": "Desconnecta", + "estimatedBandwidth": "Amplada de banda estimad", + "roles": "Rols", + "streamValidated": "El flux {{number}} s'ha validat correctament", + "streamValidationFailed": "Ha fallat la validació del flux {{number}}", + "ffmpegModule": "Usa el mode de compatibilitat del flux", + "ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.", + "streamsTitle": "Fluxos de la càmera", + "addStream": "Afegeix un flux", + "addAnotherStream": "Afegeix un altre flux", + "streamUrl": "URL del flux", + "streamUrlPlaceholder": "rtsp://usuari:contrasenya@host:port/ruta", + "selectStream": "Selecciona un flux", + "searchCandidates": "Cerca candidats...", + "noStreamFound": "No s'ha trobat cap flux", + "url": "URL", + "resolution": "Resolució", + "selectResolution": "Selecciona la resolució", + "quality": "Qualitat", + "selectQuality": "Selecciona la qualitat", + "roleLabels": { + "detect": "Detecció d'objectes", + "record": "Enregistrament", + "audio": "Àudio" + }, + "testStream": "Prova la connexió", + "testSuccess": "Prova de flux amb èxit!", + "testFailed": "Ha fallat la prova del flux", + "testFailedTitle": "Ha fallat la prova", + "connected": "Connectat", + "notConnected": "No connectat", + "featuresTitle": "Característiques", + "go2rtc": "Redueix les connexions a la càmera", + "detectRoleWarning": "Almenys un flux ha de tenir el rol de \"detecte\" per continuar.", + "rolesPopover": { + "title": "Roles de flux", + "detect": "Canal principal per a la detecció d'objectes.", + "record": "Desa els segments del canal de vídeo basats en la configuració.", + "audio": "Canal per a la detecció basada en àudio." + }, + "featuresPopover": { + "title": "Característiques del flux", + "description": "Utilitzeu el restreaming go2rtc per reduir les connexions a la càmera." + } + }, + "step4": { + "description": "Validació i anàlisi final abans de desar la nova càmera. Connecta cada flux abans de desar-lo.", + "validationTitle": "Validació del flux", + "connectAllStreams": "Connecta tots els fluxos", + "reconnectionSuccess": "S'ha reconnectat correctament.", + "reconnectionPartial": "Alguns fluxos no s'han pogut tornar a connecta.", + "streamUnavailable": "La vista prèvia del flux no està disponible", + "reload": "Torna a carregar", + "connecting": "S'està connectant...", + "streamTitle": "Flux {{number}}", + "valid": "Vàlid", + "failed": "Ha fallat", + "notTested": "No provat", + "connectStream": "Connecta", + "connectingStream": "Connectant", + "disconnectStream": "Desconnecta", + "estimatedBandwidth": "Amplada de banda estimada", + "roles": "Roles", + "ffmpegModule": "Usa el mode de compatibilitat del flux", + "ffmpegModuleDescription": "Si el flux no es carrega després de diversos intents, proveu d'activar-ho. Quan està activat, Frigate utilitzarà el mòdul ffmpeg amb go2rtc. Això pot proporcionar una millor compatibilitat amb alguns fluxos de càmera.", + "none": "Cap", + "error": "Error", + "streamValidated": "El flux {{number}} s'ha validat correctament", + "streamValidationFailed": "Ha fallat la validació del flux {{number}}", + "saveAndApply": "Desa una càmera nova", + "saveError": "Configuració no vàlida. Si us plau, comproveu la configuració.", + "issues": { + "title": "Validació del flux", + "videoCodecGood": "El còdec de vídeo és {{codec}}.", + "audioCodecGood": "El còdec d'àudio és {{codec}}.", + "resolutionHigh": "Una resolució de {{resolution}} pot causar un ús més gran dels recursos.", + "resolutionLow": "Una resolució de {{resolution}} pot ser massa baixa per a la detecció fiable d'objectes petits.", + "noAudioWarning": "No s'ha detectat cap àudio per a aquest flux, els enregistraments no tindran àudio.", + "audioCodecRecordError": "El còdec d'àudio AAC és necessari per a suportar l'àudio en els enregistraments.", + "audioCodecRequired": "Es requereix un flux d'àudio per admetre la detecció d'àudio.", + "restreamingWarning": "Reduir les connexions a la càmera per al flux de registre pot augmentar lleugerament l'ús de la CPU.", + "brands": { + "reolink-rtsp": "No és racomana utilitzar Reolink RSTP. Activeu HTTP a la configuració del microprogramari de la càmera i reinicieu l'assistent.", + "reolink-http": "Els fluxos HTTP de reenllaç haurien d'utilitzar FFmpeg per a una millor compatibilitat. Habilita «Utilitza el mode de compatibilitat del flux» per a aquest flux." + }, + "dahua": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Dahua / Amcrest / EmpireTech suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + }, + "hikvision": { + "substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles." + } + } + } + }, + "cameraManagement": { + "title": "Gestiona les càmeres", + "addCamera": "Afegeix una càmera nova", + "editCamera": "Edita la càmera:", + "selectCamera": "Selecciona una càmera", + "backToSettings": "Torna a la configuració de la càmera", + "streams": { + "title": "Habilita / Inhabilita les càmeres", + "desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.
    Nota: això no desactiva les retransmissions de go2rtc." + }, + "cameraConfig": { + "add": "Afegeix una càmera", + "edit": "Edita la càmera", + "description": "Configura la configuració de la càmera, incloses les entrades i els rols de flux.", + "name": "Nom de la càmera", + "nameRequired": "Es requereix el nom de la càmera", + "nameLength": "El nom de la càmera ha de tenir menys de 64 caràcters.", + "namePlaceholder": "p. ex., vista general de la porta davantera o de la barra posterior", + "enabled": "Habilitat", + "ffmpeg": { + "inputs": "Fluxos d'entrada", + "path": "Camí del flux", + "pathRequired": "Es requereix un camí de flux", + "pathPlaceholder": "rtsp://...", + "rolesRequired": "Es requereix almenys un rol", + "rolesUnique": "Cada rol (àudio, detecta, registra) només es pot assignar a un flux", + "addInput": "Afegeix un flux d'entrada", + "removeInput": "Elimina el flux d'entrada", + "inputsRequired": "Es requereix com a mínim un flux d'entrada", + "roles": "Rols" + }, + "go2rtcStreams": "go2rtc Fluxos", + "streamUrls": "URL de flux", + "addUrl": "Afegeix un URL", + "addGo2rtcStream": "Afegeix go2rtc flux", + "toast": { + "success": "La càmera {{cameraName}} s'ha desat correctament" + } + } + }, + "cameraReview": { + "object_descriptions": { + "title": "Descripcions d'objectes generadors d'IA", + "desc": "Activa/desactiva temporalment les descripcions d'objectes generatius d'IA per a aquesta càmera. Quan està desactivat, les descripcions generades per IA no se sol·licitaran per als objectes rastrejats en aquesta càmera." + }, + "review_descriptions": { + "title": "Descripcions de la IA generativa", + "desc": "Activa/desactiva temporalment les descripcions de revisió de la IA generativa per a aquesta càmera. Quan està desactivat, les descripcions generades per IA no se sol·licitaran per als elements de revisió d'aquesta càmera." + }, + "review": { + "title": "Revisió", + "desc": "Activa/desactiva temporalment les alertes i deteccions d'aquesta càmera fins que es reiniciï Frigate. Si està desactivat, no es generaran nous elements de revisió. ", + "alerts": "Alertes. ", + "detections": "Deteccions. " + }, + "reviewClassification": { + "title": "Revisió de la classificació", + "desc": "Frigate categoritza els articles de revisió com Alertes i Deteccions. Per defecte, tots els objectes persona i cotxe es consideren Alertes. Podeu refinar la categorització dels elements de revisió configurant-los les zones requerides.", + "noDefinedZones": "No hi ha zones definides per a aquesta càmera.", + "selectAlertsZones": "Selecciona zones per a les alertes", + "selectDetectionsZones": "Selecció de zones per a les deteccions", + "limitDetections": "Limita les deteccions a zones específiques", + "toast": { + "success": "S'ha desat la configuració de la classificació de la revisió. Reinicia la fragata per aplicar canvis." + }, + "unsavedChanges": "Paràmetres de classificació de revisions sense desar per {{camera}}", + "objectAlertsTips": "Totes els objectes {{alertsLabels}} de {{cameraName}} es mostraran com avisos.", + "zoneObjectAlertsTips": "Tots els objectes{{alertsLabels}} detectats en {{zone}} de {{cameraName}} es mostraran com a avisos.", + "objectDetectionsTips": "Tots els objectes {{detectionsLabels}} que no estiguin categoritzats de {{cameraName}} es mostraran com a Deteccions independentment de la zona on es trobin.", + "zoneObjectDetectionsTips": { + "text": "Tots els objectes {{detectionsLabels}} no categoritzats a {{zone}} de {{cameraName}} es mostraran com a Deteccions.", + "notSelectDetections": "Tots els objectes {{detectionsLabels}} detectats a {{zone}} de{{cameraName}} no categoritzats com a alertes es mostraran com a Deteccions independentment de la zona on es trobin.", + "regardlessOfZoneObjectDetectionsTips": "Tots els objectes {{detectionsLabels}} que no estiguin categoritzats de {{cameraName}} es mostraran com a Deteccions independentment de la zona on es trobin." + } + }, + "title": "Paràmetres de Revisió de la Càmera" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ca/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ca/views/system.json new file mode 100644 index 0000000..f610e6a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ca/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "cameras": "Estadístiques de càmera - Frigate", + "storage": "Estadístiques d'emmagatzematge - Frigate", + "general": "Estadístiques generals - Frigate", + "logs": { + "frigate": "Registres de Frigate - Frigate", + "go2rtc": "Registres de Go2RTC - Frigate", + "nginx": "Registres de Nginix - Frigate" + }, + "enrichments": "Estadístiques complementàries - Frigate" + }, + "title": "Sistema", + "metrics": "Mètriques del sistema", + "logs": { + "download": { + "label": "Descarregar registres" + }, + "copy": { + "label": "Copiar al porta-retalls", + "success": "Registres copiats al porta-retalls", + "error": "No s'han pogut copiar els registres al porta-retalls" + }, + "type": { + "label": "Tipus", + "timestamp": "Marca temporal", + "tag": "Etiqueta", + "message": "Missatge" + }, + "tips": "Els registres s'estàn transmetent des del servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Error al obtenir els registres: {{errorMessage}}", + "whileStreamingLogs": "Error en la transmissió dels registres: {{errorMessage}}" + } + } + }, + "general": { + "detector": { + "memoryUsage": "Ús de memòria del detector", + "title": "Detectors", + "inferenceSpeed": "Velocitat d'inferència del detector", + "cpuUsage": "Ús de CPU del detector", + "temperature": "Temperatura del detector", + "cpuUsageInformation": "CPU usada en la preparació d'entrades i sortides desde/cap als models de detecció. Aquest valor no mesura l'utilització d'inferència, encara que usis una GPU o accelerador." + }, + "title": "General", + "hardwareInfo": { + "title": "Informació de maquinari", + "gpuUsage": "Ús de la GPU", + "gpuMemory": "Memòria de GPU", + "gpuDecoder": "Decodificador de GPU", + "gpuEncoder": "Codificador de GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Sortida de Vainfo", + "processOutput": "Sortida del procés:", + "processError": "Error de procés:", + "returnCode": "Codi de retorn: {{code}}" + }, + "nvidiaSMIOutput": { + "title": "Sortida de Nvidia SMI", + "vbios": "Informació de VBios: {{vbios}}", + "cudaComputerCapability": "Capacitat de càlcul CUDA: {{cuda_compute}}", + "name": "Nom: {{name}}", + "driver": "Controlador: {{driver}}" + }, + "closeInfo": { + "label": "Tancar informació de GPU" + }, + "copyInfo": { + "label": "Copiar informació de GPU" + }, + "toast": { + "success": "Informació de GPU copiada al porta-retalls" + } + }, + "npuUsage": "Ús de NPU", + "npuMemory": "Memòria de NPU", + "intelGpuWarning": { + "title": "Avís d'estadístiques de la GPU d'Intel", + "message": "Estadístiques de GPU no disponibles", + "description": "Aquest és un error conegut en les eines d'informació de les estadístiques de GPU d'Intel (intel.gpu.top) on es trencarà i retornarà repetidament un ús de GPU del 0% fins i tot en els casos en què l'acceleració del maquinari i la detecció d'objectes s'executen correctament a la (i)GPU. Això no és un error de fragata. Podeu reiniciar l'amfitrió per a corregir temporalment el problema i confirmar que la GPU funciona correctament. Això no afecta el rendiment." + } + }, + "otherProcesses": { + "title": "Altres processos", + "processMemoryUsage": "Ús de memòria de procés", + "processCpuUsage": "Ús de la CPU del procés" + } + }, + "storage": { + "title": "Emmagatzematge", + "recordings": { + "title": "Gravacions", + "earliestRecording": "Gravació més antiga disponible:", + "tips": "Aquest valor representa l'emmagatzematge total utilitzat per les gravacions a la base de dades de Frigate. Frigate no registre l'ús de tots els arxius del disc." + }, + "cameraStorage": { + "camera": "Càmera", + "unusedStorageInformation": "Informació d'emmagatzematge no utilitzat", + "bandwidth": "Ample de banda", + "storageUsed": "Emmagatzematge", + "title": "Emmagatzematge de càmera", + "unused": { + "title": "Sense utilitzar", + "tips": "Aquest valor pot no de forma exacta representar l'espai lliure disponible a Frigate si tens altres fitxers emmagatzemats en la vostra unitat més enllà dels registres de Frigate. Frigate no rastreja l'ús d'emmagatzematge extern als seus registres." + }, + "percentageOfTotalUsed": "Percentatge del total" + }, + "overview": "Visió general", + "shm": { + "title": "Ubicació de SHM (memória compartida)", + "warning": "El tamany de la SHM oh {{total}}MB es massa petita. Augmenta almenys fins a {{min_shm}}MB." + } + }, + "cameras": { + "framesAndDetections": "Fotogrames / Deteccions", + "label": { + "capture": "captura", + "cameraDetect": "{{camName}} detectar", + "cameraCapture": "{{camName}} captura", + "camera": "càmera", + "skipped": "omès", + "ffmpeg": "FFmpeg", + "detect": "detectar", + "overallFramesPerSecond": "Fotogrames per segon globals", + "overallDetectionsPerSecond": "Deteccions per segon globals", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} fotogrames per segon", + "cameraDetectionsPerSecond": "{{camName}} deteccions per segon", + "overallSkippedDetectionsPerSecond": "Nombre total de deteccions descartades per segon", + "cameraSkippedDetectionsPerSecond": "Nombre de deteccions descartades per segon a {{camName}}" + }, + "info": { + "codec": "Còdec:", + "fps": "FPS:", + "resolution": "Resolució:", + "video": "Vídeo:", + "unknown": "Desconegut", + "stream": "Transmissió {{idx}}", + "error": "Error: {{error}}", + "fetching": "Obtenint dades de càmera", + "aspectRatio": "relació d'aspecte", + "tips": { + "title": "Informació del sondeig de la càmera" + }, + "audio": "Àudio:", + "cameraProbeInfo": "Informació del sondeig de la càmera {{camera}}", + "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe." + }, + "title": "Càmeres", + "overview": "Visió general", + "toast": { + "success": { + "copyToClipboard": "S'han copiat les dades de sondeig al porta-retalls." + }, + "error": { + "unableToProbeCamera": "No s'ha pogut sondejar la càmera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Darrera actualització: ", + "stats": { + "reindexingEmbeddings": "Reindexant incrustacions ({{processed}}% completat)", + "healthy": "El sistema és saludable", + "cameraIsOffline": "{{camera}} està fora de línia", + "ffmpegHighCpuUsage": "{{camera}} te un ús elevat de CPU per FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} te un ús elevat de CPU per la detecció ({{detectAvg}}%)", + "detectIsVerySlow": "{{detect}} és molt lent ({{speed}} ms)", + "detectIsSlow": "{{detect}} és lent ({{speed}} ms)", + "shmTooLow": "/dev/shm directori ({{total}} MB) hauria de ser incrementat com a mínim {{min}} MB." + }, + "enrichments": { + "title": "Enriquiments", + "embeddings": { + "face_recognition_speed": "Velocitat de reconeixement facial", + "image_embedding": "Incrustació d'imatges", + "text_embedding": "Incrustació de text", + "face_recognition": "Reconeixement de rostres", + "plate_recognition": "Reconeixemnt de matrícules", + "image_embedding_speed": "Velocitat d'ncrustació d'imatges", + "face_embedding_speed": "Velocitat d'incrustació de rostres", + "plate_recognition_speed": "Velocitat de reconeixement de matrícules", + "text_embedding_speed": "Velocitat d'incrustació de text", + "yolov9_plate_detection": "Detecció de matrícules YOLOv9", + "yolov9_plate_detection_speed": "Velocitat de detecció de matrícules YOLOv9", + "review_description": "Descripció de la revisió", + "review_description_speed": "Velocitat de la descripció de la revisió", + "review_description_events_per_second": "Descripció de la revisió", + "object_description": "Descripció de l'objecte", + "object_description_speed": "Velocitat de la descripció de l'objecte", + "object_description_events_per_second": "Descripció de l'objecte" + }, + "infPerSecond": "Inferències per segon", + "averageInf": "Temps mitjà d'inferència" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/audio.json b/sam2-cpu/frigate-dev/web/public/locales/cs/audio.json new file mode 100644 index 0000000..8876626 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/audio.json @@ -0,0 +1,429 @@ +{ + "yell": "Křik", + "child_singing": "Dětský zpěv", + "speech": "Řeč", + "babbling": "Blábolení", + "bellow": "Řev", + "whoop": "Výskání", + "whispering": "Šeptání", + "snicker": "Chichotání", + "crying": "Pláč", + "sigh": "Povzdech", + "singing": "Zpěv", + "choir": "Sbor", + "yodeling": "Jódlování", + "synthetic_singing": "Umělý zpěv", + "humming": "Bzukot", + "groan": "Sténání", + "whistling": "Pískot", + "breathing": "Dech", + "wheeze": "Sípání", + "snoring": "Chrapot", + "snort": "Funění", + "cough": "Kašel", + "throat_clearing": "Odkašlávání", + "sneeze": "Kýchání", + "footsteps": "Kroky", + "chewing": "Žvýkání", + "biting": "Kousání", + "burping": "Krkání", + "hiccup": "Škytání", + "fart": "Prdění", + "hands": "Ruce", + "finger_snapping": "Luskání prstem", + "clapping": "Tleskání", + "heartbeat": "Tluk srdce", + "cheering": "Jásání", + "applause": "Potlesk", + "chatter": "Klábosení", + "crowd": "Dav", + "children_playing": "Hrající si děti", + "bark": "Štěkot", + "howl": "Vytí", + "growling": "Vrkot", + "whimper_dog": "Psí kníkot", + "cat": "Kočka", + "purr": "Předení", + "meow": "Mňouk", + "hiss": "Sykot", + "livestock": "Hospodářská zvířata", + "horse": "Kůň", + "neigh": "Řehtání", + "cattle": "Dobytek", + "moo": "Bučení", + "cowbell": "Kravský zvonec", + "pig": "Prase", + "oink": "Chrochtanie", + "fowl": "Drůbež", + "chicken": "Slepice", + "cluck": "Kvokání", + "cock_a_doodle_doo": "Kykyryký", + "turkey": "Krocan", + "gobble": "Hudrování", + "duck": "Kachna", + "quack": "Kvákání", + "goose": "Husa", + "honk": "Kejhání", + "wild_animals": "Divoká zvířata", + "roaring_cats": "Řvoucí kočky", + "roar": "Řev", + "bird": "Pták", + "chirp": "Cvrlikání", + "pigeon": "Holub", + "coo": "Vrkání", + "squawk": "Skřekání", + "crow": "Vrána", + "caw": "Krákání", + "hoot": "Houkání", + "flapping_wings": "Mávání křídel", + "dogs": "Psi", + "mouse": "Myš", + "insect": "Hmyz", + "fly": "Moucha", + "buzz": "Bzučení", + "frog": "Žába", + "snake": "Had", + "croak": "Kvákání žáby", + "rattle": "Chrastění", + "whale_vocalization": "Velrybí zpěv", + "music": "Hudba", + "guitar": "Kytara", + "bass_guitar": "Basová kytara", + "steel_guitar": "Ocelová kytara", + "tapping": "Ťukání", + "banjo": "Banjo", + "sitar": "Sitár", + "mandolin": "Mandolína", + "zither": "Citera", + "ukulele": "Ukulele", + "keyboard": "Klávesnice", + "electric_piano": "Elektrický klavír", + "electronic_organ": "Elektronické varhany", + "hammond_organ": "Hammondovy varhany", + "synthesizer": "Syntezátor", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkuse", + "drum_kit": "Bubny", + "drum_machine": "Bicí automat", + "drum": "Buben", + "snare_drum": "Malý buben", + "rimshot": "Rána na obruč", + "drum_roll": "Víření", + "timpani": "Tympány", + "tabla": "Tabla", + "cymbal": "Činel", + "hi_hat": "Hi-hat", + "wood_block": "Dřevěný blok", + "tambourine": "Tamburína", + "maraca": "Maraka", + "gong": "Gong", + "marimba": "Marimba", + "vibraphone": "Vibrafon", + "steelpan": "Ocelový buben", + "orchestra": "Orchestr", + "brass_instrument": "Žesťový nástroj", + "french_horn": "Lesní roh", + "trumpet": "Trubka", + "trombone": "Trombón", + "violin": "Housle", + "saxophone": "Saxofon", + "church_bell": "Kostelní zvon", + "bicycle_bell": "Cyklistický zvonek", + "tuning_fork": "Ladička", + "chime": "Zvonění", + "harmonica": "Harmonika", + "accordion": "Akordeon", + "bagpipes": "Dudy", + "didgeridoo": "Didžeridu", + "theremin": "Theremin", + "scratching": "Škrábání", + "pop_music": "Popová muzika", + "hip_hop_music": "Hip-hopová muzika", + "rock_music": "Rocková muzika", + "heavy_metal": "Heavy metal", + "music_for_children": "Hudba pro děti", + "song": "Píseň", + "thunderstorm": "Bouře", + "wind": "Vítr", + "rustling_leaves": "Šustění listů", + "wind_noise": "Zvuk větru", + "thunder": "Hrom", + "water": "Voda", + "rain": "Déšť", + "raindrop": "Dešťové kapky", + "stream": "Potok", + "waterfall": "Vodopád", + "ocean": "Moře", + "waves": "Vlny", + "steam": "Pára", + "fire": "Oheň", + "crackle": "Praskání", + "vehicle": "Vozidlo", + "sailboat": "Plachetnice", + "boat": "Člun", + "ship": "Loď", + "rowboat": "Loďka", + "motorboat": "Motorový člun", + "motor_vehicle": "Motorové vozidlo", + "car": "Auto", + "laughter": "Smích", + "sniff": "Čichání", + "stomach_rumble": "Kručení v břiše", + "gargling": "Kloktání", + "dog": "Pes", + "run": "Běh", + "cricket": "Cvrček", + "glockenspiel": "Paličková zvonkohra", + "cello": "Cello", + "pets": "Domácí mazlíčci", + "opera": "Opera", + "harp": "Harfa", + "animal": "Zvíře", + "electric_guitar": "Elektrická kytara", + "piano": "Klavír", + "goat": "Koza", + "bleat": "Mečení", + "sheep": "Ovce", + "owl": "Sova", + "musical_instrument": "Hudební nástroj", + "organ": "Varhany", + "rats": "Krysy", + "mosquito": "Komár", + "strum": "Brnkání", + "tubular_bells": "Trubicové zvony", + "acoustic_guitar": "Akustická kytara", + "bass_drum": "Basový buben", + "jazz": "Jazz", + "flute": "Flétna", + "clarinet": "Klarinet", + "bell": "Zvon", + "techno": "Techno", + "electronic_music": "Elektronická muzika", + "car_alarm": "Autoalarm", + "power_windows": "Elektrická okénka", + "skidding": "Smyk", + "tire_squeal": "Kvílení pneumatik", + "car_passing_by": "Projíždějící auto", + "air_brake": "Vzduchové brzdy", + "air_horn": "Vzduchový klakson", + "bus": "Autobus", + "police_car": "Policejní auto", + "ambulance": "Záchranka", + "fire_engine": "Hasiči", + "motorcycle": "Motorka", + "rail_transport": "Železnice", + "train": "Vlak", + "train_horn": "Troubení vlaku", + "railroad_car": "Železniční vagon", + "subway": "Metro", + "aircraft": "Letadlo", + "aircraft_engine": "Motor letadla", + "bicycle": "Cyklistické kolo", + "jet_engine": "Tryskový motor", + "propeller": "Vrtule", + "helicopter": "Helikoptéra", + "dental_drill's_drill": "Zubní vrtačka", + "lawn_mower": "Sekačka", + "chainsaw": "Motorová pila", + "idling": "Bežící motor", + "accelerating": "Přidávání plynu", + "door": "Dveře", + "doorbell": "Zvonek", + "sliding_door": "Posuvné dveře", + "slam": "Bouchnutí", + "knock": "Klepání", + "dishes": "Nádobí", + "cutlery": "Příbory", + "chopping": "Krájení", + "bathtub": "Vana", + "hair_dryer": "Fén", + "toilet_flush": "Spláchnutí záchodu", + "toothbrush": "Zubní kartáček", + "electric_toothbrush": "Elektrický zubní kartáček", + "vacuum_cleaner": "Vysavač", + "zipper": "Zip", + "keys_jangling": "Cinkání klíčů", + "coin": "Mince", + "scissors": "Nůžky", + "electric_shaver": "Elektrický holící strojek", + "typing": "Psaní na stroji nebo klávesnici", + "typewriter": "Psací stroj", + "computer_keyboard": "Počítačová klávesnice", + "writing": "Psaní", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Zvonění telefonu", + "telephone_dialing": "Vytáčení", + "alarm_clock": "Budík", + "siren": "Siréna", + "smoke_detector": "Detektor kouře", + "fire_alarm": "Požární alarm", + "foghorn": "Mlhovka", + "whistle": "Píšťalka", + "mechanisms": "Mechanismy", + "clock": "Hodiny", + "tick-tock": "Ťikťak", + "tick": "Ťik", + "sewing_machine": "Šicí stroj", + "air_conditioning": "Klimatizace", + "cash_register": "Kasa", + "printer": "Tiskárna", + "camera": "Kamera", + "tools": "Nářadí", + "hammer": "Kladivo", + "jackhammer": "Sbíječka", + "sawing": "Řezání", + "power_tool": "Elektrické nářadí", + "drill": "Vrtačka", + "explosion": "Exploze", + "gunshot": "Výstřel", + "fireworks": "Ohňostroj", + "firecracker": "Petarda", + "eruption": "Erupce", + "boom": "Třesk", + "wood": "Dřevo", + "splinter": "Tříska", + "glass": "Sklo", + "shatter": "Roztříštění", + "silence": "Ticho", + "sound_effect": "Zvukový efekt", + "environmental_noise": "Okolní hluk", + "white_noise": "Bilý šum", + "radio": "Rádio", + "scream": "Výkřik", + "microwave_oven": "Mikrovlnka", + "race_car": "Závodní auto", + "ding-dong": "Cink", + "water_tap": "Vodovodní kohoutek", + "sink": "Dřez", + "pink_noise": "Růžový šum", + "frying": "Smažení", + "television": "Televize", + "blender": "Mixér", + "train_whistle": "Houkání vlaku", + "engine": "Motor", + "engine_starting": "Startující motor", + "truck": "Nákladní auto", + "static": "Šum", + "engine_knocking": "Klepání v motoru", + "skateboard": "Skateboard", + "chant": "Skandování", + "rapping": "Rapování", + "gasp": "Zalapání po dechu", + "heart_murmur": "Srdeční šelest", + "mantra": "Mantra (pozitivní vibrace)", + "grunt": "Zabručení", + "pant": "Oddechávání", + "shuffle": "Míchání (karet)", + "yip": "Jo", + "bow_wow": "Hlasitý protest", + "caterwaul": "Vřeštět", + "clip_clop": "Klapání kopyt", + "patter": "Plácání", + "plucked_string_instrument": "Drnkací strunný nástroj", + "mallet_percussion": "Palička perkuse", + "bowed_string_instrument": "Smyčcový nástroj", + "string_section": "Smyčcová sekce", + "pizzicato": "Pizzicato", + "double_bass": "Kontrabas", + "wind_instrument": "Dechový nástroj", + "jingle_bell": "Rolnička", + "wind_chime": "Zvonkohra", + "singing_bowl": "Singing Bowl", + "beatboxing": "Beatboxing", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock & Roll", + "psychedelic_rock": "Psychadelický Rock", + "rhythm_and_blues": "Rythm & Blues", + "soul_music": "Soulová hudba", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swingová hudba", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folková hudba", + "middle_eastern_music": "Středo-východní hudba", + "disco": "Disco", + "classical_music": "Klasická hudba", + "house_music": "House hudba", + "dubstep": "Dubstep", + "drum_and_bass": "Drum & Bass", + "electronica": "Elektronická hudba", + "electronic_dance_music": "Elektronická taneční hudba", + "ambient_music": "Ambientní hudba", + "trance_music": "Trance hudba", + "music_of_latin_america": "Latinsko-americká hudba", + "flamenco": "Flamengo", + "blues": "Blues", + "new-age_music": "Novodobá hudba", + "vocal_music": "Vokální hudba", + "a_capella": "A Capella", + "music_of_africa": "Africká hudba", + "afrobeat": "Afrobeat", + "christian_music": "Křesťanská hudba", + "gospel_music": "Gospelová hudba", + "music_of_asia": "Asijská hudba", + "carnatic_music": "Karnatická hudba", + "music_of_bollywood": "Hudba z Bollywoodu", + "ska": "SKA", + "traditional_music": "Tradiční hudba", + "independent_music": "Nezávislá hudba", + "background_music": "Hudba na pozadí", + "theme_music": "Tématická hudba", + "jingle": "Jingle", + "soundtrack_music": "Soundtracková hudba", + "lullaby": "Ukolébavka", + "video_game_music": "Herní hudba", + "christmas_music": "Vánoční hudba", + "dance_music": "Taneční hudba", + "wedding_music": "Svatební hudba", + "happy_music": "Veselá hudba", + "sad_music": "Smutná hudba", + "tender_music": "Něžná hudba", + "exciting_music": "Vzrušující hudba", + "angry_music": "Naštvaná hudba", + "scary_music": "Děsivá hudba", + "rain_on_surface": "Déšť na povrch", + "gurgling": "Klokotání", + "toot": "Troubení", + "reversing_beeps": "Parkovací pípání", + "ice_cream_truck": "Auto se zmrzlinou", + "emergency_vehicle": "Záchranářské vozidlo", + "traffic_noise": "Zvuk provozu", + "train_wheels_squealing": "Skřípání kol vlaku", + "fixed-wing_aircraft": "Letadlo s pevnými křídly", + "light_engine": "Lehký motor", + "medium_engine": "Střední motor", + "heavy_engine": "Těžký motor", + "tap": "Poklepání", + "squeak": "Skřípání", + "cupboard_open_or_close": "Otvírání nebo zavírání skříně", + "drawer_open_or_close": "Otvírání nebo zavírání šuplíku", + "shuffling_cards": "Míchání karet", + "ringtone": "Vyzváněcí melodie", + "dial_tone": "Vytáčecí tón", + "busy_signal": "Tón obsazené linky", + "civil_defense_siren": "Siréna civilní obrany", + "salsa_music": "Salsa hudba", + "buzzer": "Bzučák", + "steam_whistle": "Parní píšťala", + "ratchet": "Ráčna", + "gears": "Ozubená kola", + "pulleys": "Kladky", + "mechanical_fan": "Mechanický větrák", + "single-lens_reflex_camera": "Jednooká zrcadlovka", + "filing": "Plnění", + "sanding": "Pískování", + "machine_gun": "Kulomet", + "fusillade": "Salva", + "artillery_fire": "Dělostřelecká palba", + "cap_gun": "Kapslíková pistole", + "burst": "Výbuch", + "chop": "Sekání", + "crack": "Prasknutí", + "chink": "Cinknutí", + "field_recording": "Nahrávka z terénu" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/common.json b/sam2-cpu/frigate-dev/web/public/locales/cs/common.json new file mode 100644 index 0000000..856c88a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/common.json @@ -0,0 +1,274 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Do doby, než se Frigarte restartuje.", + "untilRestart": "Do restartu", + "justNow": "Teď", + "today": "Dnes", + "yesterday": "Včera", + "last7": "Posledních 7 dní", + "last14": "Posledních 14 dní", + "last30": "Posledních 30 dní", + "thisWeek": "Tento týden", + "lastWeek": "Minulý týden", + "thisMonth": "Tento měsíc", + "lastMonth": "Minulý měsíc", + "5minutes": "5 minut", + "10minutes": "10 minut", + "30minutes": "30 minut", + "1hour": "1 hodina", + "12hours": "12 hodin", + "24hours": "24 hodin", + "pm": "odpoledne", + "am": "ráno", + "year_one": "{{time}} rok", + "year_few": "{{time}} let", + "year_other": "{{time}} let", + "month_one": "{{time}} měsíc", + "month_few": "{{time}} měsíce", + "month_other": "{{time}} měsíců", + "day_one": "{{time}} den", + "day_few": "{{time}} dny", + "day_other": "{{time}} dní", + "hour_one": "{{time}} hodina", + "hour_few": "{{time}} hodiny", + "hour_other": "{{time}} hodin", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minuty", + "minute_other": "{{time}} minut", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekundy", + "second_other": "{{time}} sekund", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "ago": "před {{timeAgo}}", + "yr": "{{time}}r", + "d": "{{time}}d", + "h": "{{time}}h", + "mo": "{{time}}měs", + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "h:mm aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDay": "d MMM", + "s": "{{time}}sec", + "m": "{{time}}min", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "d MMM yyyy, HH:mm", + "12hour": "d MMM yyyy, h:mm aaa" + }, + "formattedTimestampFilename": { + "24hour": "dd-MM-yy-HH-mm-ss", + "12hour": "dd-MM.yy-h-mm-ss-a" + } + }, + "button": { + "twoWayTalk": "Obousměrná komunikace", + "enabled": "Zapnuto", + "cameraAudio": "Zvuk kamery", + "apply": "Použij", + "reset": "Reset", + "done": "Hotovo", + "on": "Zapnuto", + "off": "Vypnuto", + "edit": "Upravit", + "enable": "Zapni", + "disabled": "Vypnuto", + "disable": "Vypni", + "save": "Uložit", + "saving": "Ukládám…", + "cancel": "Zrušit", + "close": "Zavři", + "copy": "Zkopíruj", + "back": "Zpět", + "history": "Historie", + "fullscreen": "Celá obrazovka", + "exitFullscreen": "Opustit režim celé obrazovky", + "pictureInPicture": "Obraz v obraze", + "copyCoordinates": "Zkopíruj souřadnice", + "delete": "Odstranit", + "yes": "Ano", + "no": "Ne", + "download": "Stáhnout", + "info": "Informace", + "suspended": "Pozastaveno", + "unsuspended": "Zrušit pozastavení", + "play": "Hrát", + "unselect": "Zrušit výběr", + "deleteNow": "Smazat hned", + "next": "Další", + "export": "Exportovat" + }, + "label": { + "back": "Jdi zpět" + }, + "unit": { + "speed": { + "kph": "Km/h", + "mph": "míle/h" + }, + "length": { + "feet": "stopa", + "meters": "metry" + } + }, + "selectItem": "Vybrat {{item}}", + "menu": { + "documentation": { + "label": "Dokumentace Frigate", + "title": "Dokumentace" + }, + "live": { + "allCameras": "Všechny kamery", + "title": "Živě", + "cameras": { + "count_one": "{{count}} kamera", + "count_few": "{{count}} kamery", + "count_other": "{{count}} kamer", + "title": "Kamery" + } + }, + "review": "Revize", + "explore": "Prozkoumat", + "system": "Systém", + "systemMetrics": "Systémové metriky", + "configuration": "Konfigurace", + "language": { + "yue": "粵語 (kantonština)", + "en": "English (Angličtina)", + "da": "Dansk (Dánština)", + "fi": "Suomi (Finština)", + "sk": "Slovenčina (Slovenština)", + "withSystem": { + "label": "Použít systémové nastavení pro jazyk" + }, + "zhCN": "简体中文 (Zjednodušená čínština)", + "es": "Español (Španělština)", + "hi": "हिन्दी (Hindština)", + "fr": "Français (Francouzština)", + "ar": "العربية (Arabština)", + "pt": "Português (Portugalština)", + "ru": "Русский (Ruština)", + "de": "Deutsch (Němčina)", + "it": "Italiano (Italština)", + "ja": "日本語 (Japonština)", + "tr": "Türkçe (Turečtina)", + "nl": "Nederlands (Holandština)", + "sv": "Svenska (Švédština)", + "cs": "Čeština", + "nb": "Norsk Bokmål (norský Bokmål)", + "uk": "Українська (Ukrainština)", + "ko": "한국어 (Korejština)", + "vi": "Tiếng Việt (Vietnamština)", + "he": "עברית (Hebrejština)", + "el": "Ελληνικά (Řečtina)", + "fa": "فارسی (Perština)", + "ro": "Română (Rumunština)", + "hu": "Magyar (Maďarština)", + "pl": "Polski (Polština)", + "th": "ไทย (Thaiština)", + "ca": "Català (Katalánština)", + "sl": "Slovinština (Slovinsko)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "sr": "Српски (Serbian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)" + }, + "theme": { + "highcontrast": "Vysoký kontrast", + "default": "Výchozí", + "label": "Téma", + "blue": "Modrá", + "green": "Zelená", + "nord": "Polární", + "red": "Červená" + }, + "help": "Nápověda", + "restart": "Restartovat Frigate", + "user": { + "logout": "Odhlásit", + "setPassword": "Nastavit heslo", + "current": "Aktuální uživatel: {{user}}", + "title": "Uživatel", + "account": "Účet", + "anonymous": "anonymní" + }, + "systemLogs": "Systémový záznam", + "settings": "Nastavení", + "languages": "Jazyky", + "appearance": "Vzhled", + "darkMode": { + "label": "Tmavý režim", + "light": "Světlý", + "dark": "Tmavý", + "withSystem": { + "label": "Použít systémové nastavení pro světlý a tmavý režim" + } + }, + "export": "Exportovat", + "uiPlayground": "UI hřiště", + "faceLibrary": "Knihovna Obličejů", + "configurationEditor": "Editor Konfigurace", + "withSystem": "Systém" + }, + "pagination": { + "previous": { + "label": "Jít na předchozí stranu", + "title": "Předchozí" + }, + "label": "stránkování", + "next": { + "label": "Jít na další stranu", + "title": "Další" + }, + "more": "Více stran" + }, + "accessDenied": { + "documentTitle": "Přístup odepřen - Frigate", + "title": "Přístup odepřen", + "desc": "Nemáte oprávnění zobrazit tuto stránku." + }, + "notFound": { + "desc": "Stránka nenalezena", + "documentTitle": "Nenalezeno - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "Adresa URL byla zkopírována do schránky.", + "save": { + "title": "Uložit", + "error": { + "title": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "noMessage": "Chyba při ukládání změn konfigurace" + } + } + }, + "role": { + "title": "Role", + "admin": "Správce", + "viewer": "Divák", + "desc": "Správci mají plný přístup ke všem funkcím v uživatelském rozhraní Frigate. Diváci jsou omezeni na sledování kamer, položek přehledu a historických záznamů v UI." + }, + "readTheDocumentation": "Přečtěte si dokumentaci" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/auth.json new file mode 100644 index 0000000..00b0160 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Uživatelské jméno", + "password": "Heslo", + "login": "Přihlášení", + "errors": { + "usernameRequired": "Uživatelské jméno je povinné", + "passwordRequired": "Heslo je povinné", + "loginFailed": "Přihlášení se nezdařilo", + "unknownError": "Neznámá chyba. Zkontrolujte logy.", + "webUnknownError": "Neznámá chuba. Zkontrolujte logy konzoly.", + "rateLimit": "Limit požadavků překročen. Zkuste to znovu později." + }, + "firstTimeLogin": "Přihlašujete se poprvé? Přihlašovací údaje jsou vypsány v logu Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/camera.json new file mode 100644 index 0000000..ef56aa7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Skupiny kamer", + "camera": { + "setting": { + "streamMethod": { + "method": { + "noStreaming": { + "label": "Žádný stream", + "desc": "Obrázky z kamery se aktualizují pouze jednou za minutu a neproběhne žádné živé vysílání." + }, + "smartStreaming": { + "label": "Smart Streaming (doporučeno)", + "desc": "Inteligentní streamování aktualizuje obraz vaší kamery jednou za minutu, když nedochází k žádné detekovatelné aktivitě, aby se šetřila šířka pásma a zdroje. Když je detekována aktivita, obraz se plynule přepne na živý přenos." + }, + "continuousStreaming": { + "label": "Kontinuální streamování", + "desc": { + "title": "Obraz z kamery bude vždy živým přenosem, když je viditelný na dashboardu, i když není detekována žádná aktivita.", + "warning": "Nepřetržité streamování může způsobit velké využití šířky pásma a problémy s výkonem. Používejte opatrně." + } + } + }, + "label": "Metoda streamování", + "placeholder": "Vyberte metodu vysílání" + }, + "label": "Nastavení streamování kamery", + "audioIsAvailable": "Audio je k dispozici pro tento stream", + "audioIsUnavailable": "Audio není k dispozici pro tento stream", + "audio": { + "tips": { + "document": "Přečtěte si dokumentaci ", + "title": "Pro tento stream musí být výstup zvuku z vaší kamery a nakonfigurován v go2rtc." + } + }, + "compatibilityMode": { + "label": "Režim kompatibility", + "desc": "Tuto možnost povolte pouze v případě, že živý přenos vaší kamery zobrazuje barevné artefakty a má na pravé straně obrazu diagonální čáru." + }, + "title": "Nastavení streamování {{cameraName}}", + "desc": "Změní možnosti živého vysílání pro dashboard této skupiny kamer. Tato nastavení jsou specifická pro zařízení/prohlížeč.", + "stream": "Proud", + "placeholder": "Vyberte proud" + }, + "birdseye": "Ptačí oko" + }, + "delete": { + "confirm": { + "title": "Potvrdit odstranění", + "desc": "Skutečně si přejete odstranit skupinu kamer {{name}}?" + }, + "label": "Odstranit skupinu kamer" + }, + "add": "Přidat skupinu kamer", + "name": { + "label": "Jméno", + "placeholder": "Zadejte jméno…", + "errorMessage": { + "exists": "Tento název skupiny kamer již existuje.", + "mustLeastCharacters": "Název skupiny kamer musí mít minimálně 2 znaky.", + "nameMustNotPeriod": "Název skupiny kamer nesmí obsahovat tečku.", + "invalid": "Špatný název skupiny kamer." + } + }, + "edit": "Upravit skupinu kamer", + "cameras": { + "label": "Kamery", + "desc": "Vyberte kamery pro tuto skupinu." + }, + "icon": "Ikona", + "success": "Skupina kamer {{name}} byla uložena." + }, + "debug": { + "options": { + "label": "Nastavení", + "title": "Možnosti", + "showOptions": "Zobrazit možnosti", + "hideOptions": "Skrýt možnosti" + }, + "zones": "Zóny", + "motion": "Pohyb", + "regions": "Kraje", + "timestamp": "Časové razítko", + "boundingBox": "Ohraničení", + "mask": "Maska" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/dialog.json new file mode 100644 index 0000000..8b982ed --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/dialog.json @@ -0,0 +1,121 @@ +{ + "restart": { + "title": "Jste si jistí, že chcete restartovat Frigate?", + "button": "Restartovat", + "restarting": { + "title": "Frigate restartuje", + "content": "Tato stránka bude obnovena za {{countdown}} sekund.", + "button": "Vynutit opětovné načtení" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "Potvrďte tento štítek pro Frigate Plus", + "ask_a": "Je tento objekt {{label}}?", + "ask_an": "Tento objekt je {{label}}?", + "ask_full": "Je tento objekt {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Odesláno" + } + }, + "submitToPlus": { + "label": "Odeslat do Frigate+", + "desc": "Objekty na místech, kterým se chcete vyhnout, nejsou falešné poplachy. Označení takových objektů jako falešných pozitiv může model zmást." + } + }, + "video": { + "viewInHistory": "Zobrazit historii" + } + }, + "recording": { + "confirmDelete": { + "toast": { + "success": "Videozáznam spojený s vybranými položkami přehledu byl úspěšně smazán.", + "error": "Chyba mazání: {{error}}" + }, + "title": "Potvrdit odstranění", + "desc": { + "selected": "Opravdu chcete smazat všechna nahraná videa spojená s touto položkou přehledu?

    Chcete-li tento dialog v budoucnu obejít, podržte klávesu Shift." + } + }, + "button": { + "markAsReviewed": "Označit jako zkontrolované", + "deleteNow": "Smazat hned", + "export": "Exportovat" + } + }, + "export": { + "time": { + "fromTimeline": "Vybrat z Časové osy", + "custom": "Vlastní", + "lastHour_one": "Minulou hodinu", + "lastHour_few": "Minulé {{count}} hodiny", + "lastHour_other": "Minulých {{count}} hodin", + "start": { + "title": "Čas začátku", + "label": "Vybrat čas začátku" + }, + "end": { + "title": "Čas konce", + "label": "Vybrat čas konce" + } + }, + "select": "Vybrat", + "export": "Exportovat", + "selectOrExport": "Vybrat pro Export", + "toast": { + "success": "Export úspěšně spuštěn. Soubor najdete v adresáři /exports.", + "error": { + "failed": "Chyba spuštění exportu: {{error}}", + "endTimeMustAfterStartTime": "Čas konce musí být po čase začátku", + "noVaildTimeSelected": "Není vybráno žádné platné časové období" + } + }, + "fromTimeline": { + "saveExport": "Uložit export", + "previewExport": "Prohlížet export" + }, + "name": { + "placeholder": "Jméno exportu" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming pro tuto kameru není povolen.", + "desc": { + "title": "Nastavte go2rtc pro rozšiřující živé zobrazení a pro zvuk pro tuto kameru.", + "readTheDocumentation": "Přečtěte si dokumentaci" + } + }, + "showStats": { + "label": "Ukázat statistiky streamu", + "desc": "Povolte tuto možnost pro zobrazení překryvných statistik v obraze streamu." + }, + "debugView": "Náhled ladění" + }, + "search": { + "saveSearch": { + "desc": "Zadejte název tohoto uloženého vyhledávání.", + "placeholder": "Zadejte název pro vaše vyhledávání", + "success": "Hledání {{searchName}} bylo uloženo.", + "button": { + "save": { + "label": "Uložit toto hledání" + } + }, + "label": "Uložit vyhledávání", + "overwrite": "{{searchName}} už existuje. Uložení přepíše existující hodnotu." + } + }, + "imagePicker": { + "selectImage": "Vyber náhled sledovaného objektu", + "search": { + "placeholder": "Hledej pomocí štítku nebo podštítku..." + }, + "noImages": "Nebyly nalezeny žádné náhledy pro tuto kameru" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/filter.json new file mode 100644 index 0000000..55ff667 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtrovat", + "labels": { + "label": "Štítky", + "all": { + "title": "Všechny popisky", + "short": "Popisky" + }, + "count_one": "{{count}} Štítek", + "count_other": "{{count}} Štítků" + }, + "dates": { + "selectPreset": "Vyberte předvolbu…", + "all": { + "title": "Všechny datumy", + "short": "Datumy" + } + }, + "more": "Víc filtrů", + "reset": { + "label": "Resetovat filtry do výchozích hodnot" + }, + "timeRange": "Časový rozsah", + "score": "Skóre", + "subLabels": { + "all": "Všechny podružné štítky", + "label": "Podružné Štítky" + }, + "features": { + "label": "Funkce", + "hasSnapshot": "Má snímek", + "hasVideoClip": "Má videoklip", + "submittedToFrigatePlus": { + "label": "Odesláno do Frigate+", + "tips": "Nejprve musíte filtrovat sledované objekty, které mají snímek.

    Sledované objekty bez snímku nelze odeslat do Frigate+." + } + }, + "sort": { + "label": "Seřadit", + "relevance": "Závažnost", + "dateAsc": "Datum (Vzestupně)", + "dateDesc": "Datum (Sestupně)", + "scoreAsc": "Skóre objektu (Vzestupně)", + "scoreDesc": "Skóre objektu (Sestupně)", + "speedAsc": "Odhadovaná rychlost (Vzestupně)", + "speedDesc": "Odhadovaná rychlost (Sestupně)" + }, + "motion": { + "showMotionOnly": "Pouze zpomalené záběry" + }, + "explore": { + "settings": { + "title": "Nastavení", + "searchSource": { + "label": "Hledat zdroj", + "options": { + "thumbnailImage": "Obrázek náhledu", + "description": "Popis" + }, + "desc": "Zvolte, zda chcete prohledávat miniatury nebo popisy sledovaných objektů." + }, + "gridColumns": { + "desc": "Zvolte počet sloupců mřížky náhledu.", + "title": "Sloupce mřížky" + }, + "defaultView": { + "title": "Základní pohled", + "desc": "Pokud nejsou vybrány žádné filtry, zobrazí se přehled nejnovějších sledovaných objektů podle štítků, nebo se zobrazí nefiltrovaná mřížka.", + "summary": "Souhrn", + "unfilteredGrid": "Nefiltrovaná mřížka" + } + }, + "date": { + "selectDateBy": { + "label": "Vyberte datum k filtrování" + } + } + }, + "logSettings": { + "label": "Filtrovat úroveň protokolu", + "filterBySeverity": "Filtrovat logy podle závažnosti", + "loading": { + "title": "Načítání", + "desc": "Když se podokno protokolu posune dolů, nové protokoly se automaticky zobrazují, jakmile jsou přidány." + }, + "disableLogStreaming": "Zakázat živé zobrazování logu", + "allLogs": "Všechny protokoly" + }, + "recognizedLicensePlates": { + "title": "Rozeznané SPZ", + "loadFailed": "Chyba načítání rozeznaných SPZ.", + "loading": "Načítám rozeznané SPZ…", + "placeholder": "Zadejte text pro hledání SPZ…", + "selectPlatesFromList": "Vyberte jednu, nebo více SPZ ze seznamu.", + "noLicensePlatesFound": "Žádné SPZ nebyly nalezeny.", + "selectAll": "Označit vše", + "clearAll": "Vymazat vše" + }, + "zones": { + "all": { + "title": "Všechny zóny", + "short": "Zóny" + }, + "label": "Zóny" + }, + "trackedObjectDelete": { + "toast": { + "success": "Sledované objekty úspěšně vymazány.", + "error": "Chyba při mazání sledovaných objektů: {{errorMessage}}" + }, + "title": "Potvrdit odstranění", + "desc": "Smazáním těchto {{objectLength}} sledovaných objektů dojde k odstranění snímku, všech uložených vektorových reprezentací (embeddingů) a souvisejících záznamů o životním cyklu objektu. Záznamy z kamery v náhledu historie NEBUDOU smazány.

    Přejete si to skutečně udělat?

    Podržte Shift pro přeskočení tohoto dialogu v budoucnosti." + }, + "zoneMask": { + "filterBy": "Filtrovat podle masky zóny" + }, + "estimatedSpeed": "Odhadovaná rychlost ({{unit}})", + "cameras": { + "label": "Filtr kamer", + "all": { + "title": "Všechny kamery", + "short": "Kamery" + } + }, + "review": { + "showReviewed": "Zobrazit zkontrolované" + }, + "classes": { + "label": "Třídy", + "all": { + "title": "Všechny třídy" + }, + "count_one": "Třída {{count}}", + "count_other": "Třídy {{count}}" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/icons.json new file mode 100644 index 0000000..a98f6d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Zvolte ikonu", + "search": { + "placeholder": "Hledejte ikonu…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/input.json new file mode 100644 index 0000000..19574eb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Stáhnout video", + "toast": { + "success": "Vaše video se stahuje." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/cs/components/player.json new file mode 100644 index 0000000..6f32e40 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "V tomto období nebyly nalezeny žádné záznamy", + "noPreviewFound": "Náhled nenalezen", + "noPreviewFoundFor": "Náhled nenalezen pro {{cameraName}}", + "submitFrigatePlus": { + "title": "Odeslat tento snímek do služby Frigate+?", + "submit": "Odeslat" + }, + "livePlayerRequiredIOSVersion": "Pro tento typ živého přenosu je vyžadován systém iOS 17.1 nebo novější.", + "streamOffline": { + "title": "Přenos je offline", + "desc": "Žádné snímky nebyly zaznamenány na {{cameraName}} detectpřenosu, zkontrolujte logy chyb" + }, + "cameraDisabled": "Kamera je zakázaná", + "stats": { + "streamType": { + "title": "Typ přenosu:", + "short": "Typ" + }, + "bandwidth": { + "title": "Šířka pásma:", + "short": "Šířka pásma" + }, + "latency": { + "title": "Latence:", + "value": "{{seconds}} sekund", + "short": { + "title": "Latence", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Celkový počet snímků:", + "droppedFrames": { + "title": "Ztracené snímky:", + "short": { + "title": "Ztracené", + "value": "{{droppedFrames}} snímků" + } + }, + "decodedFrames": "Dekódované snímky:", + "droppedFrameRate": "Frekvence ztracených snímků:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Snimek byl úspěšně odeslán službě Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepodařilo se odeslat snímek službě Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/objects.json b/sam2-cpu/frigate-dev/web/public/locales/cs/objects.json new file mode 100644 index 0000000..ca20920 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Osoba", + "dog": "Pes", + "cat": "Kočka", + "horse": "Kůň", + "bird": "Pták", + "boat": "Člun", + "car": "Auto", + "sheep": "Ovce", + "mouse": "Myš", + "keyboard": "Klávesnice", + "animal": "Zvíře", + "vehicle": "Vozidlo", + "bark": "Štěkot", + "goat": "Koza", + "bus": "Autobus", + "motorcycle": "Motorka", + "train": "Vlak", + "bicycle": "Cyklistické kolo", + "door": "Dveře", + "blender": "Mixér", + "sink": "Dřez", + "scissors": "Nůžky", + "clock": "Hodiny", + "toothbrush": "Zubní kartáček", + "hair_dryer": "Fén", + "skateboard": "Skateboard", + "airplane": "Letadlo", + "traffic_light": "Semafor", + "fire_hydrant": "Požární hydrant", + "street_sign": "Uliční cedule", + "stop_sign": "Stopka", + "parking_meter": "Parkovací hodiny", + "bench": "Lavička", + "cow": "Kráva", + "elephant": "Slon", + "bear": "Medvěd", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Čepice", + "backpack": "Batoh", + "umbrella": "Deštník", + "shoe": "Bota", + "eye_glasses": "Brýle", + "handbag": "Kabelka", + "tie": "Kravata", + "suitcase": "Oblečení", + "frisbee": "Frisbee", + "skis": "Lyže", + "snowboard": "Snowboard", + "sports_ball": "Sportovní míč", + "kite": "Drak", + "baseball_bat": "Baseballová pálka", + "baseball_glove": "Baseballová rukavice", + "surfboard": "Surfovací prkno", + "tennis_racket": "Tenisová raketa", + "bottle": "Lahev", + "plate": "Talíř", + "wine_glass": "Sklenice na víno", + "cup": "Šálek", + "fork": "Vidlička", + "knife": "Nůž", + "spoon": "Lžíce", + "bowl": "Mísa", + "banana": "Banán", + "apple": "Jablko", + "sandwich": "Sendvič", + "orange": "Pomeranč", + "broccoli": "Brokolice", + "carrot": "Mrkev", + "hot_dog": "Párek v rohlíku", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Koláč", + "chair": "Židle", + "couch": "Gauč", + "potted_plant": "Hrnková rostlina", + "bed": "Postel", + "mirror": "Zrcadlo", + "dining_table": "Jídelní stůl", + "window": "Okno", + "desk": "Stůl", + "toilet": "Toaleta", + "tv": "TV", + "laptop": "Laptop", + "remote": "Dálkový ovladač", + "cell_phone": "Mobilní telefon", + "microwave": "Mikrovlnná trouba", + "oven": "Trouba", + "toaster": "Toustovač", + "refrigerator": "Lednice", + "book": "Kniha", + "vase": "Váza", + "teddy_bear": "Medvídek", + "hair_brush": "Hřeben", + "squirrel": "Veverka", + "deer": "Jelen", + "fox": "Liška", + "rabbit": "Králík", + "raccoon": "Mýval", + "robot_lawnmower": "Robotická sekačka na trávu", + "waste_bin": "Odpadkový koš", + "on_demand": "Na požádání", + "face": "Obličej", + "license_plate": "SPZ", + "package": "Balík", + "bbq_grill": "Gril", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Čistič", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/classificationModel.json new file mode 100644 index 0000000..25d14c7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/classificationModel.json @@ -0,0 +1,34 @@ +{ + "documentTitle": "Klasifikační modely", + "button": { + "deleteClassificationAttempts": "Odstrániť Klasifikačné obrazy", + "renameCategory": "Premenovať triedu", + "deleteCategory": "Zmazať triedu", + "deleteImages": "Zmazať obrázok", + "trainModel": "Trenovací model", + "addClassification": "Pridať klasifikáciu", + "deleteModels": "Zmazať modeli", + "editModel": "Upraviť model" + }, + "details": { + "scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu." + }, + "tooltip": { + "trainingInProgress": "Model se práve trénuje", + "noNewImages": "Žiadne nové obrázky na trénovanie. Najskôr klasifikujte viac obrazkov v datasete.", + "noChanges": "Od posledného treningu nedošlo k žiadnym zmenám v datasete.", + "modelNotReady": "Model nieje pripravený na trénovanie." + }, + "toast": { + "success": { + "deletedImage": "Zmazať obrazky", + "deletedModel_one": "Úspešne odstranený {{count}} model", + "deletedModel_few": "Úspešne odstranené {{count}} modely", + "deletedModel_other": "Úspěšne ostranených {{count}} modelov", + "deletedCategory": "Zmazať triedu", + "categorizedImage": "Obrázek úspěšně klasifikován", + "trainedModel": "Úspěšně vytrénovaný model.", + "trainingModel": "Trénování modelu bylo úspěšně zahájeno." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/configEditor.json new file mode 100644 index 0000000..19982f2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor konfigurace - Frigate", + "configEditor": "Editor konfigurace", + "copyConfig": "Kopírovat konfiguraci", + "saveAndRestart": "Uložit a restartovat", + "saveOnly": "Jen uložit", + "toast": { + "success": { + "copyToClipboard": "Konfigurace byla zkopírovaná do schránky." + }, + "error": { + "savingError": "Chyba ukládání konfigurace" + } + }, + "confirm": "Opustit bez uložení?", + "safeConfigEditor": "Editor konfigurace (Nouzový režim)", + "safeModeDescription": "Frigate je v nouzovém režimu kvůli chybě při ověřování konfigurace." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/events.json new file mode 100644 index 0000000..6757c21 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/events.json @@ -0,0 +1,48 @@ +{ + "alerts": "Výstrahy", + "detections": "Detekce", + "motion": { + "label": "Pohyb", + "only": "Jen pohyb" + }, + "allCameras": "Všechny kamery", + "empty": { + "alert": "Nejsou žádné výstrahy na kontrolu", + "detection": "Nejsou žádné detekce na kontrolu", + "motion": "Nenalezena žádná data o pohybu" + }, + "timeline": "Časová osa", + "timeline.aria": "Zvolit časovou osu", + "events": { + "label": "Události", + "aria": "Zvolit události", + "noFoundForTimePeriod": "Pro toto období nebyly nalezeny žádné události." + }, + "documentTitle": "Revize - Frigate", + "camera": "Kamera", + "calendarFilter": { + "last24Hours": "Posledních 24 hodin" + }, + "markAsReviewed": "Označit jako zkontrolované", + "markTheseItemsAsReviewed": "Označit tyto položky jako zkontrolované", + "newReviewItems": { + "label": "Zobrazit nové položky na kontrolu", + "button": "Nové položky na kontrolu" + }, + "recordings": { + "documentTitle": "Záznamy - Frigate" + }, + "detected": "Detekováno", + "selected_one": "{{count}} vybráno", + "selected_other": "{{count}} vybráno", + "suspiciousActivity": "Podezřelá aktivita", + "threateningActivity": "Ohrožující činnost", + "zoomIn": "Přiblížit", + "zoomOut": "Oddálit", + "detail": { + "label": "Detail", + "noDataFound": "Žádná detailní data k prohlédnutí", + "aria": "Přepnout detailní zobrazení", + "trackedObject_other": "{{count}} objektů" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/explore.json new file mode 100644 index 0000000..8acdd23 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/explore.json @@ -0,0 +1,228 @@ +{ + "generativeAI": "Generativní AI", + "documentTitle": "Prozkoumat – Frigate", + "details": { + "timestamp": "Časové razítko", + "snapshotScore": { + "label": "Skóre snímku" + }, + "item": { + "title": "Zkontrolovat detaily položky", + "desc": "Zkontrolovat detaily položky", + "button": { + "share": "Sdílet tuto položku kontroly", + "viewInExplore": "Zobrazit v Průzkumu" + }, + "tips": { + "hasMissingObjects": "Upravte svou konfiguraci, pokud chcete, aby Frigate ukládal sledované objekty pro následující štítky: {{objects}}", + "mismatch_one": "{{count}} nedostupný objekt byl detekován a vložen do této položky kontroly. Tento objekt se buď nekvalifikoval jako výstraha nebo detekce, nebo již byl vyčištěn/smazán.", + "mismatch_few": "{{count}} nedostupné objekty byly detekovány a vloženy do této položky kontroly. Tyto objekty se buď nekvalifikovaly jako výstraha nebo detekce, nebo již byly vyčištěny/smazány.", + "mismatch_other": "{{count}} nedostupných objektů bylo detekováno a vloženo do této položky kontroly. Tyto objekty se buď nekvalifikovaly jako výstraha nebo detekce, nebo již byly vyčištěny/smazány." + }, + "toast": { + "success": { + "regenerate": "Od {{provider}} byl vyžádán nový popis. V závislosti na rychlosti vašeho poskytovatele může obnovení nového popisu nějakou dobu trvat.", + "updatedSublabel": "Úspěšně aktualizovaný podružný štítek.", + "updatedLPR": "Úspěšně aktualizovaná SPZ.", + "audioTranscription": "Požádání o přepis zvuku bylo úspěšné." + }, + "error": { + "regenerate": "Chyba volání {{provider}} pro nový popis: {{errorMessage}}", + "updatedSublabelFailed": "Chyba obnovení podružného štítku: {{errorMessage}}", + "updatedLPRFailed": "Chyba obnovení SPZ: {{errorMessage}}", + "audioTranscription": "Požádání o přepis zvuku bylo neúspěšné: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "descNoLabel": "Vložit nový podružný štítek pro tento sledovaný objekt", + "desc": "Vložit nový podružný štítek pro tento {{label}}", + "title": "Upravit podružný štítek" + }, + "editLPR": { + "title": "Upravit SPZ", + "descNoLabel": "Vložit novou SPZ pro tento sledovaný objekt", + "desc": "Vložit novou SPZ pro tento {{label}}" + }, + "estimatedSpeed": "Odhadovaná rychlost", + "objects": "Objekty", + "camera": "Kamera", + "zones": "Zóny", + "button": { + "regenerate": { + "label": "Regenerovat popis sledovaného objektu", + "title": "Regenerovat" + }, + "findSimilar": "Najít podobné" + }, + "description": { + "label": "Popis", + "placeholder": "Popis sledovaného objektu", + "aiTips": "Frigate nebude vyžadovat popis od vašeho poskytovatele generativní AI, dokud neskončí životní cyklus sledovaného objektu." + }, + "expandRegenerationMenu": "Rozbalte nabídku regenerace", + "regenerateFromSnapshot": "Regenerovat ze snímku", + "regenerateFromThumbnails": "Regenerovat z náhledu", + "tips": { + "descriptionSaved": "Popis úspěšně uložen", + "saveDescriptionFailed": "Aktualizace popisu se nezdařila: {{errorMessage}}" + }, + "topScore": { + "info": "Nejvyšší skóre je nejvyšší střední skóre pro sledovaný objekt, takže se může lišit od skóre zobrazeného na miniatuře výsledku vyhledávání.", + "label": "Nejvyšší skóre" + }, + "label": "Štítek", + "recognizedLicensePlate": "Rozpoznaná SPZ", + "score": { + "label": "Skóre" + } + }, + "exploreIsUnavailable": { + "title": "Prozkoumat je nedostupné", + "embeddingsReindexing": { + "context": "Prozkoumat může být použito až vložené sledované objekty dokončí přeindexování.", + "startingUp": "Spouštění…", + "estimatedTime": "Odhadovaný zbývající čas:", + "finishingShortly": "Brzy bude dokončeno", + "step": { + "thumbnailsEmbedded": "Vložené náhledy: ", + "descriptionsEmbedded": "Vložené popisy: ", + "trackedObjectsProcessed": "Zpracované sledované objekty: " + } + }, + "downloadingModels": { + "context": "Frigate stahuje potřebné modely vložení pro podporu funkce sémantického vyhledávání. To může trvat několik minut v závislosti na rychlosti vašeho síťového připojení.", + "setup": { + "visionModelFeatureExtractor": "Extraktor funkcí Vision modelu", + "visionModel": "Vision model", + "textModel": "Textový model", + "textTokenizer": "Textový tokenizér" + }, + "tips": { + "documentation": "Přečtěte si dokumentaci", + "context": "Můžete reindexovat vložení vašich sledovaných oběktů hned jak budou modely staženy." + }, + "error": "Nastala chyba. Zkontrolujte protokoly Frigate." + } + }, + "trackedObjectsCount_one": "{{count}} sledovaný objekt ", + "trackedObjectsCount_few": "{{count}} sledované objekty ", + "trackedObjectsCount_other": "{{count}} sledovaných objektů ", + "searchResult": { + "tooltip": "Shoda s {{type}} na {{confidence}} %", + "deleteTrackedObject": { + "toast": { + "error": "Sledovaný objekt se nepodařilo smazat: {{errorMessage}}", + "success": "Sledovaný objekt úspěšně smazán." + } + } + }, + "objectLifecycle": { + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "external": "{{label}} detekován", + "header": { + "zones": "Zóny", + "ratio": "Poměr", + "area": "Oblast" + }, + "entered_zone": "{{label}} vstoupil do {{zones}}", + "active": "{{label}} začal být aktivní", + "stationary": "{{label}} se stal statickým", + "visible": "{{label}} detekován", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detekován pro {{label}}", + "other": "{{label}} rozpoznán jako {{attribute}}" + }, + "heard": "{{label}} slyšen", + "gone": "{{label}} opuštěn" + }, + "annotationSettings": { + "showAllZones": { + "title": "Zobrazit všechny zóny", + "desc": "Vždy zobrazit zóny na snímcích, kde objekty vstoupily do zóny." + }, + "offset": { + "label": "Odsazení popisku", + "documentation": "Přečtěte si dokumentaci ", + "millisecondsToOffset": "Milisekundy pro posun zobrazení popisku detekce. Výchozí: 0", + "desc": "Tato data pocházejí z detekčního zdroje vaší kamery, ale jsou překryta na snímcích ze záznamu. Je nepravděpodobné, že by tyto dva proudy byly dokonale synchronizované. Výsledkem je, že ohraničující rámeček a stopáž nebudou dokonale zarovnány. K nastavení však lze použít pole annotation_offset.", + "tips": "TIP: Představte si, že existuje klip události s osobou, která jde zleva doprava. Pokud je ohraničovací rámeček na časové ose události konzistentně vlevo od osoby, měla by být hodnota snížena. Podobně, pokud osoba chodí zleva doprava a ohraničující rámeček je trvale před osobou, měla by být hodnota zvýšena.", + "toast": { + "success": "Posun anotace pro {{camera}} byl uložen do konfiguračního souboru. Restartujte Frigate, aby se změny projevily." + } + }, + "title": "Nastavení popisků" + }, + "carousel": { + "previous": "Předchozí snímek", + "next": "Následující snímek" + }, + "title": "Životní cyklus Objektu", + "noImageFound": "Žádný obrázek pro toto časové razítko.", + "createObjectMask": "Vytvořit masku objektu", + "adjustAnnotationSettings": "Upravit nastavení poznámek", + "scrollViewTips": "Posouváním zobrazíte významné okamžiky životního cyklu tohoto objektu.", + "autoTrackingTips": "Pozice ohraničení bude nepřesná pro kamery s automatickým sledováním." + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Stáhnout snímek", + "aria": "Stáhnout snímek" + }, + "viewInHistory": { + "aria": "Zobrazit v historii", + "label": "Zobrazit historii" + }, + "deleteTrackedObject": { + "label": "Smazat tento sledovaný objekt" + }, + "downloadVideo": { + "label": "Stáhnout video", + "aria": "Stáhnout video" + }, + "findSimilar": { + "aria": "Najít podobný sledovaný objekt", + "label": "Najít podobné" + }, + "submitToPlus": { + "aria": "Odeslat do Frigate Plus", + "label": "Odeslat do Frigate+" + }, + "viewObjectLifecycle": { + "label": "Zobrazit životní cyklus objektu", + "aria": "Ukázat životní cyklus objektu" + }, + "addTrigger": { + "label": "Přidat spouštěč", + "aria": "Přidat spouštěč pro tento sledovaný objekt" + }, + "audioTranscription": { + "label": "Přepsat", + "aria": "Požádat o přepis zvukového záznamu" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdit smazání", + "desc": "Odstraněním tohoto sledovaného objektu se odstraní snímek, všechna uložená vložení a všechny související položky životního cyklu objektu. Zaznamenaný záznam tohoto sledovaného objektu v zobrazení Historie NEBUDE smazán.

    Opravdu chcete pokračovat?" + } + }, + "trackedObjectDetails": "Detaily sledovaných objektů", + "type": { + "details": "detaily", + "snapshot": "snímek", + "video": "video", + "object_lifecycle": "životní cyklus objektu" + }, + "noTrackedObjects": "Žádné sledované objekty nebyly nalezeny", + "fetchingTrackedObjectsFailed": "Chyba při načítání sledovaných objektů: {{errorMessage}}", + "exploreMore": "Prozkoumat více {{label}} objektů", + "aiAnalysis": { + "title": "Analýza AI" + }, + "concerns": { + "label": "Obavy" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/exports.json new file mode 100644 index 0000000..5fb25d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Hledat", + "documentTitle": "Exportovat - Frigate", + "noExports": "Žádné exporty nenalezeny", + "deleteExport": "Smazat export", + "deleteExport.desc": "Opravdu chcete smazat {{exportName}}?", + "editExport": { + "title": "Přejmenovat export", + "desc": "Zadejte nové jméno pro tento export.", + "saveExport": "Uložit export" + }, + "toast": { + "error": { + "renameExportFailed": "Nepodařilo se přejmenovat export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Sdílet export", + "downloadVideo": "Stáhnout video", + "deleteExport": "Smazat export", + "editName": "Upravit jméno" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/faceLibrary.json new file mode 100644 index 0000000..73b4c56 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "imageEntry": { + "dropInstructions": "Přetáhněte obrázek zde, nebo klikněte na výběr", + "maxSize": "Maximální velikost: {{size}}MB", + "dropActive": "Přetáhněte obrázek zde…", + "validation": { + "selectImage": "Prosím vyberte soubor obrázků." + } + }, + "createFaceLibrary": { + "new": "Vytvořit nový obličej", + "desc": "Vytvořit novou kolekci", + "nextSteps": "Chcete-li vybudovat pevný základ:
  • Použijte kartu Trénování k výběru a trénování na snímcích pro každou detekovanou osobu.
  • Pro nejlepší výsledky se zaměřte na přímé snímky; vyhněte se trénování snímků, které zachycují obličeje pod úhlem.
  • ", + "title": "Vytvořit kolekci" + }, + "details": { + "unknown": "Neznámý", + "person": "Osoba", + "face": "Detaily tváře", + "subLabelScore": "Skóre dílčího popisku", + "scoreInfo": "Skóre dílčího popisku je vážené skóre všech jistot rozpoznání obličejů, ale to může být odlišné od skóre zobrazeného na snímku.", + "faceDesc": "Podrobnosti o sledovaném objektu, který vygeneroval tuto tvář", + "timestamp": "Časové razítko" + }, + "selectFace": "Vyberte tvář", + "deleteFaceAttempts": { + "title": "Odstranit obličeje", + "desc_one": "Skutečně chcete vymazat {{count}} obličej? Tato akce je nevratná.", + "desc_few": "Skutečně chcete vymazat {{count}} obličeje? Tato akce je nevratná.", + "desc_other": "Skutečně chcete vymazat {{count}} obličejů? Tato akce je nevratná." + }, + "nofaces": "Žádné tváře", + "pixels": "{{area}}px", + "deleteFaceLibrary": { + "title": "Odstranit jméno", + "desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje." + }, + "train": { + "title": "Nedávná rozpoznání", + "empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje", + "aria": "Vybrat trénink" + }, + "description": { + "addFace": "Přidejte novou kolekci do Knihovny obličejů nahráním prvního obrázku.", + "placeholder": "Zadejte název pro tuto kolekci", + "invalidName": "Neplatný název. Názvy mohou obsahovat pouze písmena, čísla, mezery, apostrofy, podtržítka a pomlčky." + }, + "documentTitle": "Knihovna obličejů - Frigate", + "uploadFaceImage": { + "title": "Nahrát obrázek obličeje", + "desc": "Nahrajte obrázek pro skenování tváří a zahrňte jej pro {{pageToggle}}" + }, + "button": { + "deleteFaceAttempts": "Odstranění obličeje", + "addFace": "Přidat obličej", + "renameFace": "Přejmenovat obličej", + "deleteFace": "Odstranit obličej", + "uploadImage": "Nahrát obrázek", + "reprocessFace": "Přepracovat Obličej" + }, + "trainFace": "Trénovat obličej", + "selectItem": "Vyberte {{item}}", + "renameFace": { + "title": "Přejmenovat obličej", + "desc": "Zadejte nový název pro {{name}}" + }, + "readTheDocs": "Přečtěte si dokumentaci", + "toast": { + "success": { + "renamedFace": "Úspěšně přejmenovaný obličej na {{name}}", + "trainedFace": "Úspěšně vytrénovaný obličej.", + "uploadedImage": "Úspěšně nahraný obrázek.", + "deletedFace_one": "Úspěšně odstraněna {{count}} tvář.", + "deletedFace_few": "Úspěšně odstraněny {{count}} tváře.", + "deletedFace_other": "Úspěšně odstraněny {{count}} tváře.", + "deletedName_one": "{{count}} obličej byl úspěšně odstraněn.", + "deletedName_few": "{{count}} tváře byly úspěšně odstraněny.", + "deletedName_other": "{{count}} tváře byly úspěšně odstraněny.", + "updatedFaceScore": "Úspěšně aktualizováno skóre obličeje.", + "addFaceLibrary": "{{name}} byl(a) úspěšně přidán(a) do Knihovny obličejů!" + }, + "error": { + "renameFaceFailed": "Chyba při přejmenování obličeje: {{errorMessage}}", + "trainFailed": "Chyba trénování: {{errorMessage}}", + "updateFaceScoreFailed": "Chyba aktualizace skóre obličeje: {{errorMessage}}", + "deleteFaceFailed": "Chyba při mazání: {{errorMessage}}", + "uploadingImageFailed": "Chyba při nahrávání obrázku: {{errorMessage}}", + "addFaceLibraryFailed": "Nepodařilo se nastavit jméno obličeje: {{errorMessage}}", + "deleteNameFailed": "Chyba při mazání jména: {{errorMessage}}" + } + }, + "steps": { + "nextSteps": "Další kroky", + "faceName": "Zadejte název obličeje", + "uploadFace": "Nahrát obrázek obličeje", + "description": { + "uploadFace": "Pro {{name}} nahrajte obrázek, který zobrazuje jeho obličej zepředu. Obrázek nemusí být oříznut pouze na jeho obličej." + } + }, + "collections": "Kolekce", + "trainFaceAs": "Trénovat Obličej jako:" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/live.json new file mode 100644 index 0000000..f8e77f6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/live.json @@ -0,0 +1,171 @@ +{ + "documentTitle": "Živě - Frigate", + "documentTitle.withCamera": "{{camera}}-Živě-Frigate", + "lowBandwidthMode": "Režim nízké šířky pásma", + "twoWayTalk": { + "enable": "Povolit obousměrný hovor", + "disable": "Zakázat obousměrný hovor" + }, + "cameraAudio": { + "enable": "Povolit zvuk kamery", + "disable": "Zakázat zvuk kamery" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klikněte do snímku pro vycentrování kamery", + "enable": "Povolit pohyb kliknutím", + "disable": "Zakázat pohyb kliknutím" + }, + "left": { + "label": "Posunot PTZ kameru do leva" + }, + "up": { + "label": "Posunot PTZ kameru nahoru" + }, + "down": { + "label": "Posunot PTZ kameru dolů" + }, + "right": { + "label": "Posunot PTZ kameru do prava" + } + }, + "zoom": { + "in": { + "label": "Přiblížit PTZ kameru" + }, + "out": { + "label": "Oddálit PTZ kameru" + } + }, + "frame": { + "center": { + "label": "Klikněte do snímku pro vycentrování PTZ kamery" + } + }, + "presets": "Předvolby PTZ kamery", + "focus": { + "in": { + "label": "Zaostření PTZ kamery" + }, + "out": { + "label": "Rozostření PTZ kamery" + } + } + }, + "camera": { + "enable": "Povolit kameru", + "disable": "Zakázat kameru" + }, + "muteCameras": { + "enable": "Ztlumit všechny kamery", + "disable": "Zrušit ztlumení u všech kamer" + }, + "detect": { + "enable": "Povolit detekci", + "disable": "Zakázat detekci" + }, + "recording": { + "enable": "Povolit nahrávání", + "disable": "Zakázat nahrávání" + }, + "snapshots": { + "enable": "Povolit vytváření snímků", + "disable": "Zakázat vytváření snímků" + }, + "audioDetect": { + "enable": "Povolit detekci zvuků", + "disable": "Zakázat detekci zvuků" + }, + "autotracking": { + "enable": "Povolit automatické sledování", + "disable": "Zakázat automatické sledování" + }, + "streamStats": { + "disable": "Skrýt statistiky streamu", + "enable": "Ukázat statistiky streamu" + }, + "manualRecording": { + "title": "Nahrávání na vyžádání", + "playInBackground": { + "label": "Přehrát na pozadí", + "desc": "Povolte tuto volbu pro pokračování streamování i když je přehrávač skrytý." + }, + "showStats": { + "label": "Ukázat statistiky", + "desc": "Povolte tuto možnost pro zobrazení překryvných statistik v obraze streamu." + }, + "debugView": "Náhled ladění", + "start": "Spustit nahrávání na vyžádání", + "failedToStart": "Chyba manuálního spuštění nahrávání na požádání.", + "end": "Konec nahrávání na vyžádání", + "failedToEnd": "Chyba ukončení manuálního nahrávání na vyžádání.", + "started": "Manuálně spuštěno nahrávání na požádání.", + "ended": "Ukončeno manuální nahrávání na vyžádání.", + "recordDisabledTips": "Protože je v konfiguraci této kamery nahrávání zakázáno nebo omezeno, bude uložen pouze snímek.", + "tips": "Spustit ruční událost na základě nastavení uchovávání záznamů této kamery." + }, + "streamingSettings": "Nastavení Streamování", + "audio": "Zvuk", + "suspend": { + "forTime": "Pozastavení na: " + }, + "stream": { + "title": "Proud", + "audio": { + "tips": { + "title": "Zvuk musí být kamerou vysílán a nakonfigurován v go2rtc pro tento stream.", + "documentation": "Přečtěte si dokumentaci " + }, + "available": "Zvuk je dostupný pro tento stream", + "unavailable": "Zvuk není dostupný pro tento stream" + }, + "twoWayTalk": { + "tips.documentation": "Přečtěte si dokumentaci ", + "available": "Obousměrný hovor je dostupný pro tento stream", + "unavailable": "Obousměrný hovor není dostupný pro tento stream", + "tips": "Vaše zařízení musí tuto funkci podporovat a WebRTC musí být nakonfigurováno pro obousměrnou komunikaci." + }, + "lowBandwidth": { + "resetStream": "Resetovat stream", + "tips": "Živý náhled je v režimu nízké přenosové rychlosti kvůli vyrovnávací paměti nebo chybám ve streamu." + }, + "playInBackground": { + "label": "Přehrát na pozadí", + "tips": "Povolte tuto volbu pro pokračování streamování i když je přehrávač skrytý." + } + }, + "cameraSettings": { + "title": "{{camera}} Nastavení", + "cameraEnabled": "Kamera Povolena", + "objectDetection": "Detekce Objektu", + "snapshots": "Snímky", + "audioDetection": "Detekce Zvuku", + "autotracking": "Automatické sledování", + "recording": "Nahrávání", + "transcription": "Zvukový přepis" + }, + "history": { + "label": "Zobrazit historické záznamy" + }, + "effectiveRetainMode": { + "modes": { + "all": "Vše", + "motion": "Pohyb", + "active_objects": "Aktivní Objekty" + }, + "notAllTips": "Vaše nastavení uchovávání záznamů pro zdroj {{source}} je nastaveno na režim: {{effectiveRetainMode}}, takže tento záznam na vyžádání bude uchovávat pouze segmenty s režimem {{effectiveRetainModeName}}." + }, + "editLayout": { + "exitEdit": "Ukončit Úpravu", + "label": "Upravit Rozložení", + "group": { + "label": "Upravit Skupinu Kamer" + } + }, + "notifications": "Notifikace", + "transcription": { + "enable": "Povolit živý přepis zvuku", + "disable": "Zakázat živý přepis zvuku" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/recording.json new file mode 100644 index 0000000..80fa1f1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportovat", + "calendar": "Kalendář", + "filter": "Filtrovat", + "filters": "Filtry", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Čas konce musí být po čase začátku", + "noValidTimeSelected": "Nebylo zvoleno platné časové období" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/search.json new file mode 100644 index 0000000..e828a67 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Hledat", + "savedSearches": "Uložená vyhledávání", + "searchFor": "Hledat {{inputValue}}", + "button": { + "clear": "Vymazat vyhledávaní", + "save": "Uložit vyhledávání", + "delete": "Vymazat uložená vyhledávání", + "filterInformation": "Filtrovat informace", + "filterActive": "Aktivní filtry" + }, + "trackedObjectId": "ID sledovaného objektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Štítky", + "zones": "Zóny", + "sub_labels": "Podružné Štítky", + "max_speed": "Max rychlost", + "min_speed": "Min rychlost", + "search_type": "Typ Hledání", + "time_range": "Časový Rozsah", + "before": "Před", + "after": "Po", + "max_score": "Maximální Skóre", + "min_score": "Minimální Skóre", + "recognized_license_plate": "Rozpoznaná SPZ", + "has_clip": "Má Klip", + "has_snapshot": "Má Snímek" + }, + "tips": { + "desc": { + "step1": "Zadejte název filtru následovaný dvojtečkou (např. „kamery:“).", + "step2": "Vyberte hodnotu z nabízených možností nebo zadejte vlastní.", + "step3": "Použijte více filtrů tak, že je přidáte jeden po druhém s mezerou mezi nimi.", + "step4": "Datumové filtry (before: a after:) používají formát {{DateFormat}}.", + "step6": "Odstraňte filtr kliknutím na 'x' vedle něj.", + "exampleLabel": "Příklad:", + "text": "Filtry vám pomohou zúžit výsledky hledání. Zde je návod, jak je používat ve vstupním poli:", + "step5": "Pro časový rozsah použijte formát {{exampleTime}}." + }, + "title": "Jak používat textové filtry" + }, + "searchType": { + "thumbnail": "Náhled", + "description": "Popis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum 'před' musí být pozdější než datum 'po'.", + "afterDatebeEarlierBefore": "Datum 'po' musí být dřívější než datum 'před'.", + "minScoreMustBeLessOrEqualMaxScore": "'min_score' musí být menší nebo rovno 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max_score' musí být větší nebo rovno 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min_speed' musí být menší nebo rovno 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max_speed' musí být větší nebo rovno 'min_speed'." + } + }, + "header": { + "currentFilterType": "Hodnoty Filtru", + "noFilters": "Filtry", + "activeFilters": "Aktivní Filtry" + } + }, + "similaritySearch": { + "title": "Hledání Podle Podobnosti", + "active": "Hledání podobností aktivní", + "clear": "Vymazat hledání podobností" + }, + "placeholder": { + "search": "Hledat…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/settings.json new file mode 100644 index 0000000..1a73650 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/settings.json @@ -0,0 +1,900 @@ +{ + "documentTitle": { + "default": "Nastavení - Frigate", + "authentication": "Nastavení ověřování - Frigate", + "camera": "Nastavení kamery - Frigate", + "classification": "Nastavení klasifikace - Frigate", + "notifications": "Nastavení notifikací - Frigate", + "masksAndZones": "Editor masky a zón - Frigate", + "motionTuner": "Ladění detekce pohybu - Frigate", + "object": "Ladění - Frigate", + "general": "Nastavení rozhraní- Frigate", + "frigatePlus": "Frigate+ nastavení - Frigate", + "enrichments": "Nastavení obohacení - Frigate", + "cameraManagement": "Správa kamer - Frigate", + "cameraReview": "Nastavení kontroly kamery - Frigate" + }, + "frigatePlus": { + "toast": { + "error": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "success": "Nastavení Frigate+ byla uložena. Restartujte Frigate+ pro aplikování změn." + }, + "modelInfo": { + "cameras": "Kamery", + "modelSelect": "Zde můžete vybrat dostupné modely ze služby Frigate+. Upozorňujeme, že lze zvolit pouze modely kompatibilní s aktuální konfigurací detektoru.", + "loadingAvailableModels": "Načítám dostupné modely…", + "plusModelType": { + "baseModel": "Základní Model", + "userModel": "Doladěno" + }, + "loading": "Načítám informace o modelu…", + "error": "Chyba načítání informací o modelu", + "availableModels": "Dostupné Moduly", + "supportedDetectors": "Podporované Detektory", + "title": "Informace o Modelu", + "modelType": "Typ Modelu", + "trainDate": "Datum Tréninku", + "baseModel": "Základní Model" + }, + "snapshotConfig": { + "documentation": "Přečtěte si dokumentaci", + "desc": "Odesílání do Frigate+ vyžaduje, aby byly ve vaší konfiguraci povoleny jak běžné snímky, tak snímky typu clean_copy.", + "title": "Nastavení Snímku", + "table": { + "cleanCopySnapshots": "clean_copy Snímky", + "snapshots": "Snímky", + "camera": "Kamera" + }, + "cleanCopyWarning": "Některé kamery mají povolené snímky, ale volba clean_copy je zakázaná. Pro možnost odesílání snímků z těchto kamer do služby Frigate+ je nutné tuto volbu povolit v konfiguraci snímků." + }, + "apiKey": { + "notValidated": "API klíč Frigate+ nebyl rozpoznán nebo nebyl ověřen", + "plusLink": "Přečtěte si více o Frigate+", + "validated": "API klíč Frigate+ byl rozpoznán a ověřen", + "desc": "API klíč Frigate+ umožňuje integraci se službou Frigate+.", + "title": "Frigate+ API Klíč" + }, + "unsavedChanges": "Neuložené změny nastavení Frigate+", + "title": "Nastavení Frigate+", + "restart_required": "Vyžadován restart (model Frigate+ změněn)" + }, + "classification": { + "unsavedChanges": "Neuložené změny nastavení klasifikací", + "semanticSearch": { + "readTheDocumentation": "Přečtěte si dokumentaci", + "reindexNow": { + "alreadyInProgress": "Reindexování je už spuštěno.", + "label": "Reindexovat Teď", + "confirmTitle": "Potvrdit Reindexaci", + "error": "Chyba spouštění reindexování: {{errorMessage}}", + "desc": "Přeindexování znovu vygeneruje vektorové reprezentace (embeddingy) pro všechny sledované objekty. Tento proces probíhá na pozadí, může plně zatížit váš procesor a v závislosti na počtu sledovaných objektů může trvat delší dobu.", + "confirmDesc": "Opravdu chcete přeindexovat embeddingy všech sledovaných objektů? Tento proces poběží na pozadí, ale může plně vytížit procesor a trvat delší dobu. Průběh můžete sledovat na stránce Prozkoumat.", + "confirmButton": "Reindexovat", + "success": "Reindexování úspěšně spuštěno." + }, + "title": "Sémantické Hledání", + "desc": "Sémantické vyhledávání ve Frigate umožňuje najít sledované objekty ve vašich záznamech pomocí samotného obrázku, uživatelem zadaného textového popisu nebo automaticky generovaného popisu.", + "modelSize": { + "small": { + "title": "malý", + "desc": "Použití malý znamená využití kvantizované verze modelu, která spotřebovává méně paměti RAM a běží rychleji na procesoru, přičemž rozdíl v kvalitě embeddingů je zanedbatelný." + }, + "large": { + "title": "velký", + "desc": "Volba velký využívá plný model Jina a automaticky se provádí na GPU, pokud je k dispozici." + }, + "label": "Velikost Modelu", + "desc": "Velikost modelu pro sémantické vyhledávání pomocí embeddingů." + } + }, + "title": "Nastavení Klasifikací", + "birdClassification": { + "title": "Klasifikace Ptáka", + "desc": "Klasifikace ptáků rozpoznává známé druhy pomocí kvantizovaného modelu TensorFlow. Když je rozpoznán známý pták, jeho běžný název bude přidán jako podružný štítek (sub_label). Tato informace se zobrazí v uživatelském rozhraní, je dostupná ve filtrech a zahrnuta i v notifikacích." + }, + "toast": { + "error": "Chyba při ukládání změn konfigurace: {{errorMessage}}", + "success": "Nastavení klasifikací uloženo. Restartujte Frigate pro aplikování změn." + }, + "restart_required": "Vyžadován restart (Nastavení klasifikací se změnilo)", + "licensePlateRecognition": { + "title": "Rozpoznávání SPZ", + "desc": "Frigate dokáže rozpoznávat SPZ na vozidlech a automaticky přidávat rozpoznané znaky do pole recognized_license_plate nebo známý název jako podružný štítek k objektům typu auto. Běžným případem použití je čtení SPZ aut vjíždějících na příjezdovou cestu nebo projíždějících kolem na ulici.", + "readTheDocumentation": "Přečtěte si dokumentaci" + }, + "faceRecognition": { + "title": "Rozpoznávání obličeje", + "readTheDocumentation": "Přečtěte si dokumentaci", + "desc": "Rozpoznávání obličeje umožňuje přiřadit lidem jména, a když je jejich obličej rozpoznán, Frigate přiřadí dané jméno jako podružný štítek (sub_label). Tato informace se zobrazuje v uživatelském rozhraní, je dostupná ve filtrech a je také součástí notifikací.", + "modelSize": { + "label": "Velikost Modelu", + "desc": "Velikost modelu použitého pro rozpoznávání obličeje.", + "small": { + "title": "malý", + "desc": "Použití malý znamená využití FaceNet modelu pro embedding obličejů, který běží efektivně na většině procesorů (CPU)." + }, + "large": { + "title": "velký", + "desc": "Použití velký znamená využití modelu ArcFace pro embedding obličejů, který se v případě dostupnosti automaticky spustí na GPU." + } + } + } + }, + "masksAndZones": { + "zones": { + "speedEstimation": { + "docs": "Přečtěte si dokumentaci", + "title": "Odhad rychlosti", + "desc": "Povolit odhad rychlosti pro objekty v této zóně. Zóna musí mít přesně 4 body.", + "lineADistance": "Vzdálenost linky A ({{unit}})", + "lineBDistance": "Vzdálenost linky B ({{unit}})", + "lineCDistance": "Vzdálenost linky C ({{unit}})", + "lineDDistance": "Vzdálenost linky D ({{unit}})" + }, + "name": { + "inputPlaceHolder": "Zadejte jméno…", + "title": "Jméno", + "tips": "Název musí mít alespoň 2 znaky a nesmí být shodný s názvem kamery nebo jiné zóny." + }, + "inertia": { + "title": "Setrvačnost", + "desc": "Určuje, po kolika snímcích strávených v zóně je objekt považován za přítomný v této zóně.Výchozí hodnota: 3" + }, + "loiteringTime": { + "title": "Doba setrvání", + "desc": "Nastavuje minimální dobu v sekundách, po kterou musí být objekt v zóně, aby došlo k aktivaci.Výchozí hodnota: 0" + }, + "objects": { + "title": "Objekty", + "desc": "Seznam objektů, na které se tato zóna vztahuje." + }, + "allObjects": "Všechny Objekty", + "speedThreshold": { + "title": "Práh rychlosti ({{unit}})", + "desc": "Určuje minimální rychlost, při které jsou objekty v této zóně zohledněny.", + "toast": { + "error": { + "pointLengthError": "Odhad rychlosti byl pro tuto zónu deaktivován. Zóny s odhadem rychlosti musí mít přesně 4 body.", + "loiteringTimeError": "Pokud má zóna nastavenou dobu setrvání větší než 0, nedoporučuje se používat odhad rychlosti." + } + } + }, + "toast": { + "success": "Zóna {{zoneName}} byla uložena. Restartujte Frigate pro aplikování změn." + }, + "label": "Zóny", + "desc": { + "title": "Zóny umožňují definovat konkrétní oblast v záběru, díky čemuž lze určit, zda se objekt nachází v dané oblasti či nikoliv.", + "documentation": "Dokumentace" + }, + "add": "Přidat Zónu", + "edit": "Upravit Zónu", + "documentTitle": "Upravit Zónu - Frigate", + "clickDrawPolygon": "Klikněte pro kreslení polygonu na obrázku.", + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů" + }, + "motionMasks": { + "context": { + "documentation": "Přečtěte si dokumentaci", + "title": "Masky detekce pohybu slouží k zabránění tomu, aby nežádoucí typy pohybu spouštěly detekci (například větve stromů nebo časové značky kamery). Masky detekce pohybu by se měly používat velmi střídmě – příliš rozsáhlé maskování může ztížit sledování objektů." + }, + "polygonAreaTooLarge": { + "documentation": "Přečtěte si dokumentaci", + "title": "Maska detekce pohybu pokrývá {{polygonArea}}% záběru kamery. Příliš velké masky detekce pohybu nejsou doporučovány.", + "tips": "Masky detekce pohybu nebrání detekci objektů. Místo toho byste měli použít požadovanou zónu." + }, + "documentTitle": "Editovat Masku Detekce pohybu - Frigate", + "desc": { + "title": "Masky detekce pohybu slouží k zabránění nežádoucím typům pohybu ve spuštění detekce. Příliš rozsáhlé maskování však může ztížit sledování objektů.", + "documentation": "Dokumentace" + }, + "label": "Maska Detekce pohybu", + "add": "Nová Maska Detekce pohybu", + "edit": "Upravit Masku Detekce pohybu", + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů", + "clickDrawPolygon": "Kliknutím nakreslíte polygon do obrázku.", + "toast": { + "success": { + "title": "{{polygonName}} byl uložen. Restartujte Frigate pro aplikování změn.", + "noName": "Maska Detekce pohybu byla uložena. Restartujte Frigate pro aplikování změn." + } + } + }, + "filter": { + "all": "Všechny Masky a Zóny" + }, + "restart_required": "Vyžadován restart (masky/zóny byly změněny)", + "toast": { + "error": { + "copyCoordinatesFailed": "Nemohu zkopírovat souřadnice do schránky." + }, + "success": { + "copyCoordinates": "Souřadnice pro {{polyName}} zkopírovány do schránky." + } + }, + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Název zóny obsahuje zakázané znaky.", + "mustNotBeSameWithCamera": "Název Zóny nesmí být stejný jako název kamery.", + "mustNotContainPeriod": "Název zóny nesmí obsahovat tečky.", + "alreadyExists": "Zóna se stejným názvem u této kamery již existuje.", + "mustBeAtLeastTwoCharacters": "Název Zóny musí mít minimálně 2 znaky." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Setrvačnost musí být větší než 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "true": "Přichytávat body", + "false": "Nepřichytávat body" + }, + "delete": { + "title": "Potvrdit Smazání", + "desc": "Opravdu chcete smazat {{type}}{{name}}?", + "success": "{{name}} bylo smazáno." + }, + "error": { + "mustBeFinished": "Kreslení polygonu musí být před uložením dokončeno." + }, + "reset": { + "label": "Vymazat všechny body" + }, + "removeLastPoint": "Odebrat poslední bod" + }, + "distance": { + "error": { + "mustBeFilled": "Pro použití odhadu rychlosti musí být vyplněna všechna pole pro vzdálenost.", + "text": "Vzdálenost musí být větší nebo rovna 0.1." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Doba setrvání musí být větší nebo rovna nule." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prahová hodnota rychlosti musí být větší nebo rovna 0,1." + } + } + }, + "objectMasks": { + "label": "Masky Objektu", + "documentTitle": "Upravit Masku Objektu - Frigate", + "desc": { + "documentation": "Dokumentace", + "title": "Masky filtrování objektů slouží k odfiltrování falešných detekcí daného typu objektu na základě jeho umístění." + }, + "add": "Přidat Masku Objektu", + "edit": "Upravit Masku Objektu", + "objects": { + "title": "Objekty", + "allObjectTypes": "Všechny typy objektů", + "desc": "Typ objektu, na který se tato maska objektu vztahuje." + }, + "context": "Masky filtrování objektů slouží k odfiltrování falešných poplachů konkrétního typu objektu na základě jeho umístění.", + "clickDrawPolygon": "Kliknutím nakreslete polygon do obrázku.", + "toast": { + "success": { + "title": "{{polygonName}} byl uložen. Restartujte Frigate pro aplikování změn.", + "noName": "Maska Objektu byla uložena. Restartujte Frigate pro aplikování změn." + } + }, + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodů" + }, + "motionMaskLabel": "Maska Detekce pohybu {{number}}", + "objectMaskLabel": "Maska Objektu {{number}} {{label}}" + }, + "menu": { + "ui": "Uživatelské rozhraní", + "classification": "Klasifikace", + "cameras": "Nastavení kamery", + "masksAndZones": "Masky / Zóny", + "motionTuner": "Ladění detekce pohybu", + "debug": "Ladění", + "users": "Uživatelé", + "notifications": "Notifikace", + "frigateplus": "Frigate+", + "enrichments": "Obohacení", + "triggers": "Spouštěče", + "cameraManagement": "Správa", + "cameraReview": "Kontrola", + "roles": "Role" + }, + "dialog": { + "unsavedChanges": { + "title": "Máte neuložené změny.", + "desc": "Přejete si uložit změny před pokračováním?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Žádná Kamera" + }, + "general": { + "title": "Hlavní nastavení", + "liveDashboard": { + "title": "Živý dashboard", + "automaticLiveView": { + "desc": "Při detekci aktivity se automaticky přepne na živý náhled kamery. Vypnutí této možnosti způsobí, že se statické snímky z kamery na ovládacím panelu Live aktualizují pouze jednou za minutu.", + "label": "Automatický živý náhled" + }, + "playAlertVideos": { + "label": "Přehrát videa s výstrahou", + "desc": "Ve výchozím nastavení se nedávná upozornění na ovládacím panelu Živě přehrávají jako malá opakující se videa. Vypněte tuto možnost, chcete-li na tomto zařízení/prohlížeči zobrazovat pouze statický obrázek nedávných výstrah." + } + }, + "storedLayouts": { + "title": "Uložené rozložení", + "desc": "Rozložení kamer ve skupině kamer lze přetáhnout nebo jim změnit velikost. Pozice jsou uloženy v místním úložišti vašeho prohlížeče.", + "clearAll": "Smazat všechna rozložení" + }, + "cameraGroupStreaming": { + "title": "Nastavení streamování skupiny kamer", + "desc": "Nastavení streamování pro každou kameru je uloženo v místním uložišti vašeho prohlížeče.", + "clearAll": "Vymazat všechna nastavení streamování" + }, + "recordingsViewer": { + "title": "Prohlížeč nahrávek", + "defaultPlaybackRate": { + "label": "Výchozí rychlost přehrávání", + "desc": "Výchozí rychlost přehrávání pro nahrávky." + } + }, + "calendar": { + "title": "Kalendář", + "firstWeekday": { + "label": "První den týdne", + "desc": "Den, kterým bude začínat týden v kalendáři kontrol.", + "sunday": "Neděle", + "monday": "Pondělí" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Vymazáno uložené rozvržení pro kameru {{cameraName}}", + "clearStreamingSettings": "Vymazány nastavení streamování pro všechny skupiny kamer." + }, + "error": { + "clearStoredLayoutFailed": "Chyba mazání uloženého rozvržení: {{errorMessage}}", + "clearStreamingSettingsFailed": "Chyba mazání nastavení streamování: {{errorMessage}}" + } + } + }, + "debug": { + "timestamp": { + "title": "Časové razítko", + "desc": "Překrýt obrázek časovým razítkem" + }, + "regions": { + "title": "Regiony", + "desc": "Zobrazit rámeček oblasti zájmu odesílané detektoru objektů", + "tips": "

    Boxy oblastí zájmu


    Jasně zelené boxy budou překryty na oblastech zájmu ve snímku, které jsou odesílány detektoru objektů.

    " + }, + "title": "Ladění", + "detectorDesc": "Frigate používá vaše detektory {{detectors}} k detekci objektů ve streamu vašich kamer.", + "objectList": "Seznam objektů", + "boundingBoxes": { + "title": "Ohraničující rámečky", + "desc": "Zobrazit ohraničující rámečky okolo sledovaných objektů", + "colors": { + "label": "Barvy Ohraničujících Rámečků Objektů", + "info": "
  • Při spuštění bude každému objektovému štítku přiřazena jiná barva.
  • Tenká tmavě modrá čára označuje, že objekt není v daném okamžiku detekován.
  • Tenká šedá čára znamená, že objekt je detekován jako nehybný.
  • Silná čára označuje, že objekt je aktuálně sledován pomocí automatického sledování (pokud je aktivováno).
  • " + } + }, + "zones": { + "title": "Zóny", + "desc": "Zobrazit obrys všech definovaných zón" + }, + "mask": { + "title": "Masky detekce pohybu", + "desc": "Zobrazit polygony masek detekce pohybu" + }, + "debugging": "Ladění", + "desc": "Ladicí zobrazení ukazuje sledované objekty a jejich statistiky v reálném čase. Seznam objektů zobrazuje časově zpožděný přehled detekovaných objektů.", + "motion": { + "title": "Rámečky detekce pohybu", + "desc": "Zobrazit rámečky okolo oblastí, kde byl detekován pohyb", + "tips": "

    Boxy pohybu


    Červené boxy budou překryty na místech snímku, kde je právě detekován pohyb.

    " + }, + "noObjects": "Žádné objekty", + "objectShapeFilterDrawing": { + "title": "Vykreslení filtru tvaru objektu", + "desc": "Nakreslete na obrázek obdélník pro zobrazení informací o ploše a poměru stran", + "tips": "Povolte tuto možnost pro nakreslení obdélníku na obraz kamery, který zobrazí jeho plochu a poměr stran. Tyto hodnoty pak můžete použít pro nastavení parametrů tvarového filtru objektu ve vaší konfiguraci.", + "document": "Přečtěte si dokumentaci ", + "score": "Skóre", + "ratio": "Poměr", + "area": "Oblast" + }, + "openCameraWebUI": "Otevřít webové rozhraní {{camera}}", + "audio": { + "title": "Zvuk", + "noAudioDetections": "Žádné detekce zvuku", + "score": "skóre", + "currentRMS": "Aktuální RMS", + "currentdbFS": "Aktuální dbFS" + }, + "paths": { + "title": "Cesty", + "desc": "Zobrazit významné body trasy sledovaného objektu", + "tips": "

    Cesty


    Čáry a kruhy označují významné body, kterými se sledovaný objekt během svého životního cyklu pohyboval.

    " + } + }, + "camera": { + "streams": { + "title": "Streamy", + "desc": "Dočasně zakáže kameru dokud Frigate nerestartuje. Deaktivace kamery zcela zastaví zpracování jejích streamů ve Frigate. Detekce, nahrávání ani ladění nebudou dostupné.
    Poznámka: Tímto nedojde k deaktivaci restreamů v go2rtc." + }, + "review": { + "desc": "Dočasně povolte nebo zakažte upozornění a detekce pro tuto kameru dokud Frigate nerestartuje. Pokud jsou vypnuté, nebudou se vytvářet žádné nové položky k přezkoumání. ", + "detections": "Detekce ", + "title": "Revize", + "alerts": "Výstrahy " + }, + "reviewClassification": { + "readTheDocumentation": "Přečtěte si dokumentaci", + "desc": "Frigate rozděluje položky k přezkoumání na Upozornění a Detekce. Ve výchozím nastavení jsou všechny objekty typu osoba a auto považovány za Upozornění. Kategorizaci těchto položek můžete upřesnit nastavením požadovaných zón.", + "title": "Přehled klasifikace", + "objectAlertsTips": "Všechny {{alertsLabels}} objekty na {{cameraName}} budou zobrazeny ve Výstrahách.", + "objectDetectionsTips": "Všechny objekty typu {{detectionsLabels}}, které nejsou na kameře {{cameraName}} zařazeny do kategorie, budou zobrazeny jako Detekce bez ohledu na to, ve které zóně se nacházejí.", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Všechny objekty typu {{detectionsLabels}} detekované v zóně {{zone}} na kameře {{cameraName}}, které nejsou zařazeny jako Upozornění, budou zobrazeny jako Detekce – bez ohledu na to, ve které zóně se nacházejí.", + "text": "Všechny objekty typu {{detectionsLabels}}, které nejsou v zóně {{zone}} na kameře {{cameraName}} zařazeny do kategorie, budou zobrazeny jako Detekce.", + "regardlessOfZoneObjectDetectionsTips": "Všechny objekty {{detectionsLabels}}, které nejsou na kameře {{cameraName}} zařazeny do žádné kategorie, budou zobrazeny jako Detekce – bez ohledu na zónu, ve které se nacházejí." + }, + "noDefinedZones": "Nejsou nastaveny žádné zóny pro tuto kameru.", + "zoneObjectAlertsTips": "Všechny {{alertsLabels}} objekty detekované v {{zone}} na {{cameraName}} budou zobrazeny ve Výstrahách.", + "unsavedChanges": "Neuložené nastavení revize klasifikace pro {{camera}}", + "selectAlertsZones": "Vybrat zóny pro Výstrahy", + "selectDetectionsZones": "Vybrat zóny pro Detekce", + "toast": { + "success": "Konfigurace Revizí Klasifikací byla uložena. Restartujte Frigate pro aplikování změn." + }, + "limitDetections": "Omezit detekce pro specifické zóny" + }, + "title": "Nastavení Kamery", + "object_descriptions": { + "title": "AI generované popisy objektů", + "desc": "Dočasně povolit/zakázat generativní popisy objektů AI pro tuto kameru. Pokud je tato funkce zakázána, nebudou pro sledované objekty na této kameře vyžadovány popisy generované AI." + }, + "review_descriptions": { + "title": "Popisy generativní AI", + "desc": "Dočasně povolit/zakázat generativní AI recenze popisů pro tuto kameru. Pokud je tato funkce zakázána, nebudou pro položky recenzí na této kameře vyžadovány popisy generované AI." + }, + "addCamera": "Přidat novou kameru", + "editCamera": "Upravit kameru:", + "selectCamera": "Vybrat kameru", + "backToSettings": "Zpět k nastavení kamery", + "cameraConfig": { + "add": "Přidat kameru", + "edit": "Upravit kameru", + "description": "Konfigurovat nastavení kamery, včetně vstupů streamu a rolí.", + "name": "Název kamery", + "nameRequired": "Název kamery je povinný", + "nameLength": "Název kamery musí mít méně než 24 znaků.", + "namePlaceholder": "např. přední dveře", + "enabled": "Povolit", + "ffmpeg": { + "inputs": "Vstupní streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta ke streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Je vyžadována alespoň jedna role", + "rolesUnique": "Každá role (audio, detekce, záznam) může být přiřazena pouze k jednomu streamu", + "addInput": "Přidat vstupní stream", + "removeInput": "Odebrat vstupní stream", + "inputsRequired": "Je vyžadován alespoň jeden vstupní stream" + }, + "toast": { + "success": "Kamera {{cameraName}} byla úspěšně uložena" + } + } + }, + "notification": { + "notificationSettings": { + "documentation": "Přečtěte si dokumentaci", + "title": "Nastavení notifikací", + "desc": "Frigate může nativně odesílat push notifikace do vašeho zařízení, pokud běží v prohlížeči nebo je nainstalován jako PWA (progresivní webová aplikace)." + }, + "notificationUnavailable": { + "documentation": "Přečtěte si dokumentaci", + "title": "Notifikace Nedostupné", + "desc": "Webové push notifikace vyžadují zabezpečený kontext (https://…). Jedná se o omezení prohlížeče. Pro použití notifikací přistupujte k Frigate přes zabezpečené připojení." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, pro které chcete povolit notifikace.", + "noCameras": "Žádné dostupné kamery" + }, + "title": "Notifikace", + "email": { + "placeholder": "např. example@email.com", + "title": "Email", + "desc": "Je vyžadována platná e-mailová adresa, která bude použita k upozornění v případě problémů se službou push notifikací." + }, + "registerDevice": "Registrovat Toto Zařízení", + "deviceSpecific": "Nastavení specifická pro zařízení", + "unregisterDevice": "Odregistrovat Toto Zařízení", + "sendTestNotification": "Poslat testovací notifikaci", + "unsavedRegistrations": "Neuložené přihlášky k Notifikacím", + "unsavedChanges": "Neuložené změny Notifikací", + "globalSettings": { + "desc": "Dočasně pozastavit notifikace pro vybrané kamery na všech registrovaných zařízeních.", + "title": "Globální nastavení" + }, + "active": "Notifikace Aktivní", + "suspendTime": { + "suspend": "Pozastavit", + "12hours": "Pozastavit na 12 hodin", + "24hours": "Pozastavit na 24 hodin", + "untilRestart": "Pozastavit do restartu", + "5minutes": "Pozastavit na 5 minut", + "10minutes": "Pozastavit na 10 minut", + "30minutes": "Pozastavit na 30 minut", + "1hour": "Pozastavit na 1 hodinu" + }, + "toast": { + "error": { + "registerFailed": "Nepodařilo se uložit registraci notifikací." + }, + "success": { + "registered": "Notifikace byly úspěšně zaregistrovány. Pro odesílání notifikací (včetně testovací) je nutné Frigate restartovat.", + "settingSaved": "Nastavení notifikací bylo uloženo." + } + }, + "cancelSuspension": "Zrušit Pozastavení", + "suspended": "Notifikace pozastaveny {{time}}" + }, + "users": { + "dialog": { + "form": { + "password": { + "strength": { + "weak": "Slabé", + "medium": "Střední", + "strong": "Silné", + "veryStrong": "Velmi Silné", + "title": "Síla hesla: " + }, + "match": "Hesla souhlasí", + "notMatch": "Hesla nesouhlasí", + "title": "Heslo", + "placeholder": "Vložit heslo", + "confirm": { + "title": "Potvrdit heslo", + "placeholder": "Potvrdit heslo" + } + }, + "newPassword": { + "placeholder": "Vložte nové heslo", + "title": "Nové Heslo", + "confirm": { + "placeholder": "Zopakujte nové heslo" + } + }, + "usernameIsRequired": "Uživatelské jméno je nutné", + "user": { + "title": "Uživatelské jméno", + "desc": "Povolena jsou pouze písmena, čísla, tečky a podtržítka.", + "placeholder": "Vložte uživatelské jméno" + }, + "passwordIsRequired": "Je vyžadováno heslo" + }, + "createUser": { + "title": "Vytvořit nového uživatele", + "desc": "Přidejte nový uživatelský účet a zadejte roli pro určení přístupu k jednotlivým částem uživatelského rozhraní Frigate.", + "usernameOnlyInclude": "Uživatelské jméno smí obsahovat pouze písmena, čísla, . nebo _", + "confirmPassword": "Potvrďte prosím heslo" + }, + "deleteUser": { + "title": "Smazat Uživatele", + "desc": "Tuto akci nelze vrátit zpět. Uživatelský účet bude trvale smazán a veškerá s ním spojená data budou odstraněna.", + "warn": "Opravdu chcete smazat {{username}}?" + }, + "changeRole": { + "roleInfo": { + "intro": "Vyberte odpovídající roli pro tohoto uživatele:", + "admin": "Správce", + "adminDesc": "Plný přístup ke všem funkcím.", + "viewer": "Divák", + "viewerDesc": "Omezení pouze na Živé dashboardy, Revize, Průzkumníka a Exporty.", + "customDesc": "Vlastní role s konkrétním přístupem ke kameře." + }, + "title": "Změnit Roli Uživatele", + "desc": "Aktualizovat oprávnění pro {{username}}", + "select": "Vyberte roli" + }, + "passwordSetting": { + "updatePassword": "Aktualizovat heslo pro uživatele {{username}}", + "setPassword": "Nastavit Heslo", + "desc": "Vytvořte silné heslo pro zabezpečení tohoto účtu.", + "cannotBeEmpty": "Heslo nemůže být prázdné", + "doNotMatch": "Hesla nesouhlasí" + } + }, + "table": { + "username": "Uživatelské jméno", + "actions": "Akce", + "noUsers": "Žádní uživatelé nebyli nalezeni.", + "changeRole": "Změnit roli uživatele", + "password": "Heslo", + "deleteUser": "Smazat uživatele", + "role": "Role" + }, + "updatePassword": "Aktualizovat heslo", + "toast": { + "success": { + "createUser": "Uživatel {{user}} úspěšně vytvořen", + "deleteUser": "Uživatel {{user}} úspěšně odebrán", + "updatePassword": "Heslo úspěšně aktualizováno.", + "roleUpdated": "Role pro {{user}} aktualizována" + }, + "error": { + "setPasswordFailed": "Chyba uložení hesla: {{errorMessage}}", + "createUserFailed": "Chyba vytvoření uživatele: {{errorMessage}}", + "deleteUserFailed": "Chyba při mazání uživatele: {{errorMessage}}", + "roleUpdateFailed": "Chyba při aktualizaci role: {{errorMessage}}" + } + }, + "management": { + "desc": "Spravujte uživatelské účty této instance Frigate.", + "title": "Správa uživatelů" + }, + "addUser": "Přidat uživatele", + "title": "Uživatelé" + }, + "motionDetectionTuner": { + "unsavedChanges": "Neuložené změny ladění detekce pohybu {{camera}}", + "improveContrast": { + "title": "Zlepšit Kontrast", + "desc": "Zlepšit kontrast pro tmavé scény Výchozí: ON" + }, + "toast": { + "success": "Nastavení detekce pohybu bylo uloženo." + }, + "title": "Ladění detekce pohybu", + "desc": { + "documentation": "Přečtěte si příručku Ladění detekce pohybu", + "title": "Frigate používá detekci pohybu jako první kontrolu k ověření, zda se ve snímku děje něco, co stojí za další analýzu pomocí detekce objektů." + }, + "Threshold": { + "title": "Práh", + "desc": "Prahová hodnota určuje, jak velká změna jasu pixelu je nutná, aby byl považován za pohyb. Výchozí: 30" + }, + "contourArea": { + "title": "Obrysová Oblast", + "desc": "Hodnota plochy obrysu se používá k rozhodnutí, které skupiny změněných pixelů se kvalifikují jako pohyb. Výchozí: 10" + } + }, + "enrichments": { + "title": "Nastavení obohacení", + "faceRecognition": { + "title": "Rozpoznání obličeje", + "desc": "Rozpoznávání obličeje umožňuje přiřadit lidem jména a po rozpoznání jejich obličeje. Frigate přiřadí jméno osoby jako podštítek. Tyto informace jsou zahrnuty v uživatelském rozhraní, filtrech a také v oznámeních.", + "readTheDocumentation": "Přečtěte si Dokumentaci", + "modelSize": { + "label": "Velikost Modelu", + "desc": "Velikost modelu použitého pro rozpoznání obličeje.", + "small": { + "title": "malý", + "desc": "Použití metody malý využívá model vkládání obličejů FaceNet, který efektivně běží na většině procesorů." + }, + "large": { + "title": "velký", + "desc": "Použití metody velký využívá model vkládání obličejů ArcFace a v případě potřeby se automaticky spustí na GPU." + } + } + }, + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Jste si jisti, že chcete znovu indexovat všechny vložené sledované objekty? Tento proces poběží na pozadí, ale může maximálně zatížit váš procesor a trvat poměrně dlouho. Průběh můžete sledovat na stránce Prozkoumat.", + "confirmTitle": "Potvrdit Reindexování", + "label": "Přeindexovat nyní", + "desc": "Reindexování regeneruje vložení pro všechny sledované objekty. Tento proces běží na pozadí a může maximálně zatížit váš procesor a trvat poměrně dlouho v závislosti na počtu sledovaných objektů.", + "confirmButton": "Přeindexovat", + "success": "Přeindexování úspěšně spuštěno.", + "alreadyInProgress": "Přeindexování je již spuštěno.", + "error": "Chyba spuštění přeindexování: {{errorMessage}}" + }, + "title": "Sémantické vyhledávání", + "desc": "Sémantické vyhledávání ve Frigate umožňuje najít sledované objekty v rámci vašich zkontrolovaných položek pomocí samotného obrázku, uživatelem definovaného textového popisu nebo automaticky generovaného popisu.", + "readTheDocumentation": "Přečtěte si Dokumentaci", + "modelSize": { + "label": "Velikost modelu", + "desc": "Velikost modelu použitého pro vkládání sémantického vyhledávání.", + "small": { + "title": "malý", + "desc": "Použitím malého modelu se využívá kvantizovaná verze modelu, která spotřebovává méně RAM a běží rychleji na CPU s velmi zanedbatelným rozdílem v kvalitě vkládání." + }, + "large": { + "title": "velký", + "desc": "Použití parametru velký využívá celý model Jina a v případě potřeby se automaticky spustí na GPU." + } + } + }, + "birdClassification": { + "desc": "Klasifikace ptáků identifikuje známé ptáky pomocí kvantovaného modelu Tensorflow. Po rozpoznání známého ptáka se jeho běžný název přidá jako sub_label. Tato informace je zahrnuta v uživatelském rozhraní, filtrech a také v oznámeních.", + "title": "Klasifikace ptáků" + }, + "unsavedChanges": "Neuložené změny nastavení Obohacení", + "licensePlateRecognition": { + "title": "Rozpoznání SPZ", + "desc": "Frigate dokáže rozpoznávat SPZ vozidel a automaticky přidávat detekované znaky do pole recognized_license_plate nebo název známé SPZ jako sub_label k objektům typu auto. Běžným případem použití může být čtení SPZ aut vjíždějících na příjezdovou cestu nebo aut projíždějících po ulici.", + "readTheDocumentation": "Přečtěte si Dokumentaci" + }, + "restart_required": "Nutný restart (nastavení Obohacení změněno)", + "toast": { + "success": "Nastavení Obohacení uloženo. Restartujte Frigate aby se změny aplikovaly.", + "error": "Chyba ukládání změn konfigurace: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Spouštěče", + "management": { + "title": "Správa spouštěčů", + "desc": "Spravovat spouštěče pro {{camera}}. Použít typ miniatury ke spuštění u miniatur podobných vybranému sledovanému objektu a typ popisu ke spuštění u popisů podobných zadanému textu." + }, + "addTrigger": "Přidat spouštěč", + "table": { + "name": "Jméno", + "type": "Typ", + "content": "Obsah", + "threshold": "Prahová hodnota", + "actions": "Akce", + "noTriggers": "Pro tuto kameru nejsou nakonfigurovány žádné spouštěče.", + "edit": "Upravit", + "deleteTrigger": "Smazat spouštěč", + "lastTriggered": "Naposledy spuštěno" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Popis" + }, + "actions": { + "alert": "Označit jako upozornění", + "notification": "Odeslat oznámení" + }, + "dialog": { + "createTrigger": { + "title": "Vytvořit spouštěč", + "desc": "Vytvořit spouštěč pro kameru {{camera}}" + }, + "editTrigger": { + "title": "Upravit spouštěč", + "desc": "Upravit nastavení spouštěče na kameře {{camera}}" + }, + "deleteTrigger": { + "title": "Odstranit spouštěč", + "desc": "Opravdu chcete odstranit spouštěč {{triggerName}}? Tuto akci nelze vrátit zpět." + }, + "form": { + "name": { + "title": "Název", + "placeholder": "Zadejte název spouštěče", + "error": { + "minLength": "Název musí mít alespoň 2 znaky.", + "invalidCharacters": "Jméno může obsahovat pouze písmena, číslice, podtržítka a pomlčky.", + "alreadyExists": "Spouštěč s tímto názvem již pro tuto kameru existuje." + } + }, + "enabled": { + "description": "Povolit nebo zakázat tento spouštěč" + }, + "type": { + "title": "Typ", + "placeholder": "Vybrat typ spouštěče" + }, + "content": { + "title": "Obsah", + "imagePlaceholder": "Vybrat obrázek", + "textPlaceholder": "Zadat textový obsah", + "imageDesc": "Vybrat obrázek, který spustí tuto akci, když bude detekován podobný obrázek.", + "textDesc": "Zadejte text, který spustí tuto akci, když bude zjištěn podobný popis sledovaného objektu.", + "error": { + "required": "Obsah je povinný." + } + }, + "actions": { + "title": "Akce", + "desc": "Ve výchozím nastavení Frigate odesílá MQTT zprávu pro všechny spouštěče. Zvolte dodatečnou akci, která se má provést, když se tento spouštěč aktivuje.", + "error": { + "min": "Musí být vybrána alespoň jedna akce." + } + }, + "threshold": { + "title": "Práh", + "error": { + "min": "Práh musí být alespoň 0", + "max": "Práh musí být nanejvýš 1" + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Spouštěč {{name}} byl úspěšně vytvořen.", + "updateTrigger": "Spouštěč {{name}} byl úspěšně aktualizován.", + "deleteTrigger": "Spouštěč {{name}} byl úspěšně smazán." + }, + "error": { + "createTriggerFailed": "Nepodařilo se vytvořit spouštěč: {{errorMessage}}", + "updateTriggerFailed": "Nepodařilo se aktualizovat spouštěč: {{errorMessage}}", + "deleteTriggerFailed": "Nepodařilo se smazat spouštěč: {{errorMessage}}" + } + } + }, + "roles": { + "addRole": "Přidat roli", + "table": { + "role": "Role", + "cameras": "Kamery", + "actions": "Akce", + "noRoles": "Nebyly nalezeny žádné vlastní role.", + "editCameras": "Upravit kamery", + "deleteRole": "Smazat roli" + }, + "toast": { + "success": { + "createRole": "Role {{role}} byla úspěšně vytvořena", + "updateCameras": "Kamery byly aktualizovány pro roli {{role}}", + "deleteRole": "Role {{role}} byla úspěšně smazána", + "userRolesUpdated_one": "{{count}} uživatel(ů) přiřazených k této roli bylo aktualizováno na „Divák“, který má přístup ke všem kamerám.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Nepodařilo se vytvořit roli: {{errorMessage}}", + "updateCamerasFailed": "Nepodařilo se aktualizovat kamery: {{errorMessage}}", + "deleteRoleFailed": "Nepodařilo se smazat roli: {{errorMessage}}", + "userUpdateFailed": "Nepodařilo se aktualizovat role uživatele: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Vytvořit novou roli", + "desc": "Přidejte novou roli a určete oprávnění k přístupu ke kamerám." + }, + "deleteRole": { + "title": "Smazat roli", + "warn": "Opravdu chcete smazat roli {{role}}?", + "deleting": "Mazání...", + "desc": "Tuto akci nelze vrátit zpět. Role bude trvale smazána a všichni uživatelé s touto rolí budou přeřazeni do role „Divák“, která poskytne přístup ke všem kamerám." + }, + "form": { + "role": { + "title": "Název role", + "placeholder": "Zadejte název role", + "desc": "Povolena jsou pouze písmena, čísla, tečky a podtržítka.", + "roleIsRequired": "Název role je povinný", + "roleOnlyInclude": "Název role smí obsahovat pouze písmena, čísla, . nebo _", + "roleExists": "Role s tímto názvem již existuje." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, ke kterým má tato role přístup. Je vyžadována alespoň jedna kamera.", + "required": "Musí být vybrána alespoň jedna kamera." + } + }, + "editCameras": { + "desc": "Aktualizujte přístup ke kamerám pro roli {{role}}.", + "title": "Upravit kamery role" + } + }, + "management": { + "title": "Správa role diváka", + "desc": "Spravujte vlastní role diváků a jejich oprávnění k přístupu ke kamerám pro tuto instanci Frigate." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/cs/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/cs/views/system.json new file mode 100644 index 0000000..f920a21 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/cs/views/system.json @@ -0,0 +1,186 @@ +{ + "cameras": { + "info": { + "stream": "Stream {{idx}}", + "resolution": "Rozlišení:", + "error": "Chyba: {{error}}", + "unknown": "Neznámý", + "fps": "FPS:", + "audio": "Audio:", + "video": "Video:", + "fetching": "Získávám data o kameře", + "codec": "Kodek:", + "tips": { + "title": "Informace o sondování kamery" + }, + "streamDataFromFFPROBE": "Data ze streamu jsou získávána pomocí ffprobe.", + "cameraProbeInfo": "{{camera}} Informace o sondování kamery", + "aspectRatio": "zachovat poměr stran" + }, + "label": { + "camera": "kamera", + "ffmpeg": "FFmpeg", + "cameraFfmpeg": "{{camName}} FFmpeg", + "detect": "detekováno", + "skipped": "přeskočeno", + "capture": "zachyceno", + "overallFramesPerSecond": "celkový počet snímků za sekundu", + "overallDetectionsPerSecond": "celkový počet detekcí za sekundu", + "overallSkippedDetectionsPerSecond": "celkový počet přeskočených detekcí za sekundu", + "cameraCapture": "záznam {{camName}}", + "cameraDetect": "detekce {{camName}}", + "cameraFramesPerSecond": "{{camName}} snímků za sekundu", + "cameraDetectionsPerSecond": "{{camName}} detekcí za sekundu", + "cameraSkippedDetectionsPerSecond": "{{camName}} přeskočených detekcí za sekundu" + }, + "title": "Kamery", + "overview": "Přehled", + "framesAndDetections": "Snímky / Detekce", + "toast": { + "success": { + "copyToClipboard": "Sondovaná data uložena do schránky." + }, + "error": { + "unableToProbeCamera": "Nemohu sondovat kameru: {{errorMessage}}" + } + } + }, + "stats": { + "cameraIsOffline": "{{camera}} je offline", + "healthy": "Systém je zdravý", + "reindexingEmbeddings": "Přeindexování vektorů ({{processed}} % dokončeno)", + "detectIsSlow": "{{detect}} je pomalé ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je velmi pomalé ({{speed}} ms)", + "detectHighCpuUsage": "{{camera}} má vysoké využití CPU detekcemi ({{detectAvg}} %)", + "ffmpegHighCpuUsage": "{{camera}} má vyské využití CPU FFmpegem ({{ffmpegAvg}}%)", + "shmTooLow": "Alokace /dev/shm ({{total}} MB) by měla být zvýšena alespoň na {{min}} MB." + }, + "enrichments": { + "embeddings": { + "face_recognition_speed": "Rychlost rozpoznávání obličeje", + "plate_recognition_speed": "Rychlost rozpoznávání SPZ", + "plate_recognition": "Rozpoznávání SPZ", + "face_recognition": "Rozpoznávání obličeje", + "image_embedding_speed": "Rychlost vkládání obrázku", + "text_embedding_speed": "Rychlost vkládání textu", + "text_embedding": "Vkládání textu", + "image_embedding": "Vkládání obrázku", + "face_embedding_speed": "Rychlost vkládání obličeje", + "yolov9_plate_detection_speed": "YOLOv9 rychlost detekce SPZ", + "yolov9_plate_detection": "YOLOv9 Detekce SPZ" + }, + "infPerSecond": "Inferencí za sekundu", + "title": "Obohacení" + }, + "general": { + "detector": { + "temperature": "Detekční teplota", + "title": "Detektory", + "inferenceSpeed": "Detekční rychlost", + "memoryUsage": "Detektor využití paměti", + "cpuUsage": "Detektor využití CPU", + "cpuUsageInformation": "CPU používané při přípravě vstupních a výstupních dat do/z detekčních modelů. Tato hodnota neměří využití inferenčních operací, ani v případě použití GPU nebo akcelerátoru." + }, + "hardwareInfo": { + "title": "Informace o hardware", + "gpuInfo": { + "vainfoOutput": { + "processError": "Chyba procesu:", + "returnCode": "Návratový kód: {{code}}", + "processOutput": "Výstup procesu:", + "title": "Výstup Vainfo" + }, + "nvidiaSMIOutput": { + "name": "Jméno: {{name}}", + "title": "Výstup Nvidia SMI", + "driver": "Ovladač: {{driver}}", + "cudaComputerCapability": "Výpočetní schopnost CUDA: {{cuda_compute}}", + "vbios": "Informace o VBios: {{vbios}}" + }, + "copyInfo": { + "label": "Kopírovat informace o GPU" + }, + "toast": { + "success": "Informace o GPU zkopírovány do schránky" + }, + "closeInfo": { + "label": "Zavřít informace o GPU" + } + }, + "npuUsage": "Využití NPU", + "npuMemory": "Paměť NPU", + "gpuUsage": "Využití CPU", + "gpuMemory": "Paměť GPU", + "gpuEncoder": "GPU kodér", + "gpuDecoder": "GPU Dekodér" + }, + "otherProcesses": { + "title": "Ostatní procesy", + "processCpuUsage": "Využití CPU procesy", + "processMemoryUsage": "Využití paměti procesy" + }, + "title": "Hlavní" + }, + "storage": { + "cameraStorage": { + "storageUsed": "Úložiště", + "camera": "Kamera", + "title": "Úložiště kamery", + "unused": { + "title": "Nepoužité", + "tips": "Tato hodnota nemusí přesně reprezentovat volné místo dostupné pro Frigate, pokud máte na disku uloženy další soubory kromě nahrávek Frigate. Frigate nesleduje využití úložiště mimo své nahrávky." + }, + "bandwidth": "Šířka pásma", + "unusedStorageInformation": "Informace o nepoužitém úložišti", + "percentageOfTotalUsed": "Procento celkem" + }, + "recordings": { + "title": "Záznamy", + "earliestRecording": "Nejstarší dostupná nahrávka:", + "tips": "Tato hodnota uvádí celkové využití disku záznamy uloženými v databázi Frigate. Frigate nesleduje využití disku ostatními soubory na vašem disku." + }, + "title": "Úložiště", + "overview": "Přehled", + "shm": { + "title": "přiřazení SHM (sdílené paměti)", + "warning": "Nynější velikost SHM činící {{total}}MB je příliš malá. Zvyšte ji alespoň na {{min_shm}}MB." + } + }, + "lastRefreshed": "Poslední aktualizace: ", + "documentTitle": { + "cameras": "Statistiky kamer – Frigate", + "storage": "Statistiky uložiště - Frigate", + "general": "Obecné statistiky - Frigate", + "enrichments": "Statistiky obohacení - Frigate", + "logs": { + "frigate": "Protokoly Frigate - Frigate", + "go2rtc": "Protokoly Go2RTC - Frigate", + "nginx": "Protokoly Nginx - Frigate" + } + }, + "title": "Systém", + "logs": { + "copy": { + "label": "Kopírovat do schránky", + "success": "Protokoly zkopírovány do schránky", + "error": "Protokoly se nepodařilo zkopírovat do schránky" + }, + "type": { + "label": "Typ", + "message": "Zpráva", + "timestamp": "Časové razítko", + "tag": "Štítek (Tag)" + }, + "download": { + "label": "Stáhnout záznamy" + }, + "tips": "Protokoly jsou streamovány ze serveru", + "toast": { + "error": { + "fetchingLogsFailed": "Chyba při načítání protokolů: {{errorMessage}}", + "whileStreamingLogs": "Chyba při streamování protokolů: {{errorMessage}}" + } + } + }, + "metrics": "Systémové metriky" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/audio.json b/sam2-cpu/frigate-dev/web/public/locales/da/audio.json new file mode 100644 index 0000000..8148133 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/audio.json @@ -0,0 +1,88 @@ +{ + "clip_clop": "Klepanie kopyt", + "neigh": "Revanie", + "cattle": "Hovädzí dobytok", + "moo": "Bučanie", + "cowbell": "Kravský zvonec", + "pig": "Prasa", + "speech": "Tale", + "bicycle": "Cykel", + "car": "Bil", + "bellow": "Under", + "motorcycle": "Motorcykel", + "whispering": "Hvisker", + "bus": "Bus", + "laughter": "Latter", + "train": "Tog", + "boat": "Båd", + "crying": "Græder", + "tambourine": "Tambourin", + "marimba": "Marimba", + "trumpet": "Trumpet", + "trombone": "Trombone", + "violin": "Violin", + "flute": "Fløjte", + "saxophone": "Saxofon", + "clarinet": "Klarinet", + "harp": "Harpe", + "bell": "Klokke", + "harmonica": "Harmonika", + "bagpipes": "Sækkepibe", + "didgeridoo": "Didgeridoo", + "jazz": "Jazz", + "opera": "Opera", + "dubstep": "Dubstep", + "blues": "Blues", + "song": "Sang", + "lullaby": "Vuggevise", + "wind": "Vind", + "thunderstorm": "Tordenvejr", + "thunder": "Torden", + "water": "Vand", + "rain": "Regn", + "raindrop": "Regndråbe", + "waterfall": "Vandfald", + "waves": "Bølger", + "fire": "Ild", + "vehicle": "Køretøj", + "sailboat": "Sejlbåd", + "rowboat": "Robåd", + "motorboat": "Motorbåd", + "ship": "Skib", + "ambulance": "Ambulance", + "helicopter": "Helikopter", + "skateboard": "Skateboard", + "chainsaw": "Motorsav", + "door": "Dør", + "doorbell": "Dørklokke", + "slam": "Smæk", + "knock": "Bank", + "squeak": "Knirke", + "dishes": "Tallerkener", + "cutlery": "Bestik", + "sink": "Håndvask", + "bathtub": "Badekar", + "toothbrush": "Tandbørste", + "zipper": "Lynlås", + "coin": "Mønt", + "scissors": "Saks", + "typewriter": "Skrivemaskine", + "alarm": "Alarm", + "telephone": "Telefon", + "ringtone": "Ringetone", + "siren": "Sirene", + "foghorn": "Tågehorn", + "whistle": "Fløjte", + "clock": "Ur", + "printer": "Printer", + "camera": "Kamera", + "tools": "Værktøj", + "hammer": "Hammer", + "drill": "Bore", + "explosion": "Eksplosion", + "fireworks": "Nytårskrudt", + "babbling": "Pludren", + "yell": "Råb", + "whoop": "Jubel", + "snicker": "Smålatter" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/common.json b/sam2-cpu/frigate-dev/web/public/locales/da/common.json new file mode 100644 index 0000000..dbf6ff2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/common.json @@ -0,0 +1,259 @@ +{ + "time": { + "untilForTime": "Indtil {{time}}", + "untilForRestart": "Indtil Frigate genstarter.", + "untilRestart": "Indtil genstart", + "ago": "{{timeAgo}} siden", + "justNow": "Lige nu", + "today": "I dag", + "yesterday": "I går", + "last7": "Sidste 7 dage", + "last14": "Sidste 14 dage", + "last30": "Sidste 30 dage", + "thisWeek": "Denne uge", + "lastWeek": "Sidste uge", + "thisMonth": "Denne måned", + "lastMonth": "Sidste måned", + "5minutes": "5 minutter", + "10minutes": "10 minutter", + "30minutes": "30 minutter", + "1hour": "1 time", + "12hours": "12 timer", + "24hours": "24 timer", + "pm": "pm", + "am": "am", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "mo": "{{time}}mo", + "month_one": "{{time}} måned", + "month_other": "{{time}} måneder", + "d": "{{time}}d", + "day_one": "{{time}} dag", + "day_other": "{{time}} dage", + "h": "{{time}}h", + "yr": "{{time}}yr", + "hour_one": "{{time}} time", + "hour_other": "{{time}} timer", + "m": "{{time}}m", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minutter", + "s": "{{time}}s", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/t" + }, + "length": { + "feet": "fod", + "meters": "meter" + } + }, + "label": { + "back": "Gå tilbage" + }, + "button": { + "apply": "Anvend", + "reset": "Reset", + "done": "Udført", + "enabled": "Aktiveret", + "enable": "Aktiver", + "disabled": "Deaktiveret", + "disable": "Deaktiver", + "save": "Gem", + "saving": "Gemmer…", + "cancel": "Fortryd", + "close": "Luk", + "copy": "Kopier", + "back": "Tilbage", + "history": "Historik", + "fullscreen": "Fuldskærm", + "exitFullscreen": "Afslut Fludskærm", + "pictureInPicture": "Billede i Billede", + "twoWayTalk": "2 vejs samtale", + "cameraAudio": "Kamera Lyd", + "on": "ON", + "off": "OFF", + "edit": "Rediger", + "copyCoordinates": "Kopier koordinater", + "delete": "Slet", + "yes": "Ja", + "no": "Nej", + "download": "Download", + "info": "Info", + "suspended": "Suspenderet", + "unsuspended": "Ophæv suspendering", + "play": "Afspil", + "unselect": "Fravælg", + "export": "Eksporter", + "deleteNow": "Slet nu", + "next": "Næste" + }, + "menu": { + "system": "System", + "systemMetrics": "System metrics", + "configuration": "Konfiguration", + "systemLogs": "System logs", + "settings": "Indstillinger", + "configurationEditor": "Konfiguratons Editor", + "languages": "Sprog", + "language": { + "en": "English (Engelsk)", + "es": "Español (Spansk)", + "zhCN": "简体中文 (Forsimplet Kinesisk)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Fransk)", + "ar": "العربية (Arabisk)", + "pt": "Português (Portugisisk)", + "ru": "Русский (Russisk)", + "de": "Deutsch (Tysk)", + "ja": "日本語 (Japansk)", + "tr": "Türkçe (Tyrkisk)", + "it": "Italiano (Italiensk)", + "nl": "Nederlands (Hollandsk)", + "sv": "Svenska (Svensk)", + "cs": "Čeština (Tjekkisk)", + "nb": "Norsk Bokmål (Norsk Bokmål)", + "ko": "한국어 (Koreansk)", + "vi": "Tiếng Việt (Vietnamesisk)", + "fa": "فارسی (Persisk)", + "pl": "Polski (Polsk)", + "uk": "Українська (Ukrainsk)", + "he": "עברית (Hebraisk)", + "el": "Ελληνικά (Græsk)", + "ro": "Română (Rumænsk)", + "hu": "Magyar (Ungarsk)", + "fi": "Suomi (Finsk)", + "da": "Dansk (Dansk)", + "sk": "Slovenčina (Slovakisk)", + "yue": "粵語 (Kantonesisk)", + "th": "ไทย (Thai)", + "ca": "Català (Katalansk)", + "withSystem": { + "label": "Brug system indstillinger for sprog" + } + }, + "appearance": "Udseende", + "darkMode": { + "label": "Mørk tilstand", + "light": "Lys", + "dark": "Mørk", + "withSystem": { + "label": "Brug system indstillinger for mørk tilstand" + } + }, + "withSystem": "System", + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grøn", + "nord": "Nord", + "red": "Rød", + "highcontrast": "Høj Kontrast", + "default": "Default" + }, + "help": "Hjælp", + "documentation": { + "title": "Dokumentation", + "label": "Frigate dokumentation" + }, + "restart": "Genstart Frigate", + "live": { + "title": "Live", + "allCameras": "Alle kameraer", + "cameras": { + "title": "Kameraer", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameraer" + } + }, + "review": "Review", + "explore": "Udforsk", + "export": "Eksporter", + "uiPlayground": "UI sandkasse", + "faceLibrary": "Face Library", + "user": { + "title": "Bruger", + "account": "Konto", + "current": "Aktiv bruger: {{user}}", + "anonymous": "anonym", + "logout": "Logout", + "setPassword": "Set Password" + } + }, + "toast": { + "copyUrlToClipboard": "Kopieret URL til klippebord.", + "save": { + "title": "Gem", + "error": { + "title": "Ændringer kan ikke gemmes: {{errorMessage}}", + "noMessage": "Kan ikke gemme konfigurationsændringer" + } + } + }, + "role": { + "title": "Rolle", + "admin": "Admin", + "viewer": "Viewer", + "desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI." + }, + "pagination": { + "label": "paginering", + "previous": { + "title": "Forrige", + "label": "Gå til forrige side" + }, + "next": { + "title": "Næste", + "label": "Gå til næste side" + }, + "more": "Flere sider" + }, + "accessDenied": { + "documentTitle": "Adgang forbudt - Frigate", + "title": "Adgang forbudt", + "desc": "Du har ikke tiiladelse til at se denne side." + }, + "notFound": { + "documentTitle": "Ikke fundet - Frigate", + "title": "404", + "desc": "Side ikke fundet" + }, + "selectItem": "Vælg {{item}}", + "readTheDocumentation": "Læs dokumentationen" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/auth.json new file mode 100644 index 0000000..ee10182 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "Brugernavn", + "password": "Kodeord", + "login": "Log ind", + "errors": { + "usernameRequired": "Brugernavn kræves", + "passwordRequired": "Kodeord kræves", + "loginFailed": "Login fejlede", + "unknownError": "Ukendt fejl. Tjek logs.", + "rateLimit": "Grænsen for forespørgsler er overskredet. Prøv igen senere." + }, + "firstTimeLogin": "Forsøger du at logge ind for første gang? Loginoplysningerne står i Frigate-loggene." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/camera.json new file mode 100644 index 0000000..769c6cc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/camera.json @@ -0,0 +1,21 @@ +{ + "group": { + "label": "Kamera Grupper", + "add": "Tilføj Kameragruppe", + "edit": "Rediger Kamera Gruppe", + "delete": { + "label": "Slet kamera gruppe", + "confirm": { + "title": "Bekræft sletning", + "desc": "Er du sikker på at du vil slette kamera gruppen {{name}}?" + } + }, + "name": { + "label": "Navn", + "placeholder": "Indtast et navn…", + "errorMessage": { + "mustLeastCharacters": "Kameragruppens navn skal være mindst 2 tegn." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/dialog.json new file mode 100644 index 0000000..4d4a851 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/dialog.json @@ -0,0 +1,25 @@ +{ + "restart": { + "title": "Er du sikker på at du vil genstarte Frigate?", + "button": "Genstart", + "restarting": { + "title": "Frigate genstarter", + "button": "Gennemtving genindlæsning nu", + "content": "Denne side genindlæses om {{countdown}} sekunder." + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Indsend til Frigate+", + "desc": "Objekter på steder, du ønsker at undgå, er ikke falske positiver. Hvis du indsender dem som falske positiver, vil det forvirre modellen." + }, + "review": { + "question": { + "label": "Bekræft denne etiket til Frigate Plus", + "ask_a": "Er dette objekt et {{label}}?" + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/filter.json new file mode 100644 index 0000000..3d16c1e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/filter.json @@ -0,0 +1,50 @@ +{ + "filter": "Filter", + "classes": { + "label": "Klasser", + "all": { + "title": "Alle klasser" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klasser" + }, + "labels": { + "all": { + "short": "Labels", + "title": "Alle etiketter" + }, + "count_one": "{{count}} Label", + "label": "Etiketter" + }, + "zones": { + "label": "Zoner", + "all": { + "title": "Alle zoner", + "short": "Zoner" + } + }, + "more": "Flere filtre", + "sort": { + "label": "Sortér", + "dateAsc": "Dato (Stigende)", + "dateDesc": "Dato (Faldende)", + "speedAsc": "Anslået hastighed (Stigende)", + "speedDesc": "Anslået hastighed (Faldende)", + "relevance": "Relevans" + }, + "dates": { + "selectPreset": "Vælg en forudindstilling…", + "all": { + "title": "Alle datoer", + "short": "Datoer" + } + }, + "reset": { + "label": "Nulstille filtre til standardværdier" + }, + "timeRange": "Tidsinterval", + "estimatedSpeed": "Anslået hastighed ({{unit}})", + "features": { + "hasVideoClip": "Har et videoklip" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/icons.json new file mode 100644 index 0000000..44d71db --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Vælg et ikon", + "search": { + "placeholder": "Søg efter ikoner…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/input.json new file mode 100644 index 0000000..0a8c897 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Download Video", + "toast": { + "success": "Din video til gennemgang er begyndt at blive downloadet." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/da/components/player.json new file mode 100644 index 0000000..8a89b29 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/components/player.json @@ -0,0 +1,15 @@ +{ + "noRecordingsFoundForThisTime": "Ingen optagelser fundet i det angivet tidsrum", + "noPreviewFound": "Ingen forhåndsvisning fundet", + "cameraDisabled": "Kamera er deaktiveret", + "noPreviewFoundFor": "Ingen forhåndsvisning fundet for {{cameraName}}", + "submitFrigatePlus": { + "title": "Indsend denne frame til Frigate+?", + "submit": "Indsend" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller nyere kræves for denne type livestream.", + "streamOffline": { + "title": "Stream offline", + "desc": "Der er ikke modtaget nogen frames på {{cameraName}}-detect-streamen, tjek fejlloggene." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/objects.json b/sam2-cpu/frigate-dev/web/public/locales/da/objects.json new file mode 100644 index 0000000..e055dcf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/objects.json @@ -0,0 +1,18 @@ +{ + "person": "Person", + "bicycle": "Cykel", + "car": "Bil", + "motorcycle": "Motorcykel", + "airplane": "Flyvemaskine", + "bus": "Bus", + "train": "Tog", + "boat": "Båd", + "traffic_light": "Trafiklys", + "vehicle": "Køretøj", + "skateboard": "Skateboard", + "door": "Dør", + "sink": "Håndvask", + "toothbrush": "Tandbørste", + "scissors": "Saks", + "clock": "Ur" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/classificationModel.json new file mode 100644 index 0000000..b30e65d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/classificationModel.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Klassifikationsmodeller", + "details": { + "scoreInfo": "Scoren repræsenterer den gennemsnitlige klassifikationssikkerhed på tværs af alle registreringer af dette objekt." + }, + "description": { + "invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger." + }, + "button": { + "deleteClassificationAttempts": "Slet klassifikationsbilleder", + "renameCategory": "Omdøb klasse", + "deleteCategory": "Slet klasse", + "deleteImages": "Slet billeder", + "trainModel": "Træn model", + "addClassification": "Tilføj klassifikation" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/configEditor.json new file mode 100644 index 0000000..ba1d6a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/configEditor.json @@ -0,0 +1,10 @@ +{ + "documentTitle": "Konfigurationsstyring - Frigate", + "copyConfig": "Kopiér konfiguration", + "saveAndRestart": "Gem & Genstart", + "saveOnly": "Kun gem", + "configEditor": "Konfigurationseditor", + "safeConfigEditor": "Konfigurationseditor (Sikker tilstand)", + "safeModeDescription": "Frigate er i sikker tilstand på grund af en fejl ved validering af konfigurationen.", + "confirm": "Afslut uden at gemme?" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/events.json new file mode 100644 index 0000000..f59b2f3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/events.json @@ -0,0 +1,16 @@ +{ + "alerts": "Alarmer", + "detections": "Detekteringer", + "motion": { + "label": "Bevægelse", + "only": "Kun bevægelse" + }, + "allCameras": "Alle kameraer", + "timeline": "Tidslinje", + "camera": "Kamera", + "empty": { + "alert": "Der er ingen advarsler at gennemgå", + "detection": "Der er ingen registreringer at gennemgå", + "motion": "Ingen bevægelsesdata fundet" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/explore.json new file mode 100644 index 0000000..afe962a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/explore.json @@ -0,0 +1,29 @@ +{ + "documentTitle": "Udforsk - Frigate", + "generativeAI": "Generativ AI", + "type": { + "details": "detaljer", + "video": "video" + }, + "objectLifecycle": { + "lifecycleItemDesc": { + "active": "{{label}} blev aktiv" + } + }, + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Starter…", + "estimatedTime": "Estimeret tid tilbage:", + "context": "Udforsk kan bruges, når genindekseringen af de sporede objektindlejringer er fuldført.", + "finishingShortly": "Afsluttes om lidt", + "step": { + "thumbnailsEmbedded": "Miniaturer indlejret: " + } + }, + "title": "Udforsk er ikke tilgængelig" + }, + "exploreMore": "Udforsk flere {{label}}-objekter", + "details": { + "timestamp": "Tidsstempel" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/exports.json new file mode 100644 index 0000000..8c5f119 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/exports.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Eksporter - Frigate", + "search": "Søg", + "deleteExport.desc": "Er du sikker på at du vil slette {{exportName}}?", + "editExport": { + "title": "Omdøb Eksport", + "saveExport": "Gem Eksport", + "desc": "Indtast et nyt navn for denne eksport." + }, + "noExports": "Ingen eksporter fundet", + "deleteExport": "Slet eksport" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/faceLibrary.json new file mode 100644 index 0000000..f309e6f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/faceLibrary.json @@ -0,0 +1,19 @@ +{ + "selectItem": "Vælg {{item}}", + "description": { + "addFace": "Tilføj en ny samling til ansigtsbiblioteket ved at uploade dit første billede.", + "placeholder": "Angiv et navn for bibliotek", + "invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger." + }, + "details": { + "person": "Person", + "timestamp": "Tidsstempel", + "unknown": "Ukendt", + "scoreInfo": "Scoren er et vægtet gennemsnit af alle ansigtsscorer, vægtet efter ansigtets størrelse på hvert billede." + }, + "documentTitle": "Ansigtsbibliotek - Frigate", + "uploadFaceImage": { + "title": "Upload ansigtsbillede", + "desc": "Upload et billede for at scanne efter ansigter og inkludere det for {{pageToggle}}" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/live.json new file mode 100644 index 0000000..254539b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/live.json @@ -0,0 +1,21 @@ +{ + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "twoWayTalk": { + "enable": "Aktivér tovejskommunikation", + "disable": "Deaktiver tovejskommunikation" + }, + "cameraAudio": { + "enable": "Aktivér kameralyd", + "disable": "Deaktivér kamera lyd" + }, + "lowBandwidthMode": "Lavbåndbredde-tilstand", + "ptz": { + "move": { + "clickMove": { + "label": "Klik i billedrammen for at centrere kameraet", + "enable": "Aktivér klik for at flytte" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/recording.json new file mode 100644 index 0000000..4028727 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Eksporter", + "calendar": "Kalender", + "filters": "Filtere", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Sluttidspunkt skal være efter starttidspunkt", + "noValidTimeSelected": "Intet gyldigt tidsinterval valgt" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/search.json new file mode 100644 index 0000000..1cdc146 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/search.json @@ -0,0 +1,12 @@ +{ + "search": "Søg", + "savedSearches": "Gemte Søgninger", + "searchFor": "Søg efter {{inputValue}}", + "button": { + "save": "Gem søgning", + "delete": "Slet gemt søgning", + "filterInformation": "Filter information", + "filterActive": "Filtre aktiv", + "clear": "Ryd søgning" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/settings.json new file mode 100644 index 0000000..61fce33 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/settings.json @@ -0,0 +1,14 @@ +{ + "documentTitle": { + "default": "Indstillinger - Frigate", + "authentication": "Bruger Indstillinger - Frigate", + "camera": "Kamera indstillinger - Frigate", + "object": "Debug - Frigate", + "cameraManagement": "Administrér kameraer - Frigate", + "cameraReview": "Indstillinger for kameragennemgang - Frigate", + "enrichments": "Indstillinger for berigelser - Frigate", + "masksAndZones": "Maske- og zoneeditor - Frigate", + "motionTuner": "Bevægelsesjustering - Frigate", + "general": "Brugergrænsefladeindstillinger - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/da/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/da/views/system.json new file mode 100644 index 0000000..31d7ac9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/da/views/system.json @@ -0,0 +1,103 @@ +{ + "documentTitle": { + "cameras": "Kamera Statistik - Frigate", + "storage": "Lagrings Statistik - Frigate", + "logs": { + "frigate": "Frigate Logs - Frigate", + "go2rtc": "Go2RTC Logs - Frigate", + "nginx": "Nginx Logs - Frigate" + }, + "general": "Generelle statistikker - Frigate", + "enrichments": "Beredningsstatistikker - Frigate" + }, + "title": "System", + "logs": { + "copy": { + "label": "Kopier til udklipsholder", + "success": "Logs er kopieret til udklipsholder", + "error": "Kunne ikke kopiere logs til udklipsholder" + }, + "type": { + "label": "Type", + "timestamp": "Tidsstempel", + "message": "Besked", + "tag": "Tag" + }, + "tips": "Logs bliver streamet fra serveren", + "toast": { + "error": { + "fetchingLogsFailed": "Fejl ved indhentning af logs: {{errorMessage}}", + "whileStreamingLogs": "Fejl ved streaming af logs: {{errorMessage}}" + } + }, + "download": { + "label": "Download logs" + } + }, + "general": { + "title": "Generelt", + "hardwareInfo": { + "gpuUsage": "GPU forbrug", + "gpuMemory": "GPU hukommelse", + "gpuEncoder": "GPU indkoder", + "gpuDecoder": "GPU afkoder", + "title": "Hardware information", + "gpuInfo": { + "closeInfo": { + "label": "Luk GPU information" + }, + "copyInfo": { + "label": "Kopier GPU information" + }, + "toast": { + "success": "Kopierede GPU information til udklipsholder" + } + }, + "npuUsage": "NPU forbrug", + "npuMemory": "NPU hukommelse" + }, + "detector": { + "title": "Detektorer", + "inferenceSpeed": "Detektorinferenshastighed", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektor CPU forbrug", + "cpuUsageInformation": "CPU brugt til at forberede input- og outputdata til/fra detektionsmodeller. Denne værdi måler ikke inferensforbrug, selvom der bruges en GPU eller accelerator.", + "memoryUsage": "Detektorhummelsesforbrug" + }, + "otherProcesses": { + "title": "Andre processer", + "processCpuUsage": "Proces CPU forbrug", + "processMemoryUsage": "Proceshukommelsesforbrug" + } + }, + "metrics": "System metrikker", + "storage": { + "title": "Lagring", + "overview": "Overblik", + "recordings": { + "title": "Optagelser", + "tips": "Denne værdi repræsenterer den samlede lagerplads, der bruges af optagelserne i Frigates database. Frigate sporer ikke lagerpladsforbruget for alle filer på din disk.", + "earliestRecording": "Tidligste optagelse til rådighed:" + }, + "shm": { + "title": "SHM (delt hukommelse) tildeling", + "warning": "Den nuværende SHM størrelse af {{total}}MB er for lille. Øg den til minimum {{min_shm}}MB." + }, + "cameraStorage": { + "title": "Kamera lagring", + "camera": "Kamera", + "unusedStorageInformation": "Ubrugt lagringsinformation", + "storageUsed": "Lagring", + "percentageOfTotalUsed": "Procentandel af total", + "bandwidth": "Båndbredde", + "unused": { + "title": "Ubrugt", + "tips": "Denne værdi repræsenterer muligvis ikke nøjagtigt den ledige plads, der er tilgængelig for Frigate, hvis du har andre filer gemt på dit drev ud over Frigates optagelser. Frigate sporer ikke lagerforbrug ud over sine optagelser." + } + } + }, + "cameras": { + "title": "Kameraer", + "overview": "Overblik" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/audio.json b/sam2-cpu/frigate-dev/web/public/locales/de/audio.json new file mode 100644 index 0000000..4b18775 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Sprache", + "babbling": "Plappern", + "laughter": "Gelächter", + "bellow": "Gebrüll", + "whoop": "Jubel", + "whispering": "Flüstern", + "crying": "Weinen", + "bark": "Bellen", + "goat": "Ziege", + "car": "Auto", + "skateboard": "Skateboard", + "hair_dryer": "Haartrockner", + "animal": "Tier", + "boat": "Boot", + "blender": "Mixer", + "sink": "Waschbecken", + "scissors": "Schere", + "train": "Zug", + "clock": "Uhr", + "bird": "Vogel", + "motorcycle": "Motorrad", + "toothbrush": "Zahnbürste", + "bicycle": "Fahrrad", + "door": "Tür", + "keyboard": "Klaviatur", + "bus": "Bus", + "horse": "Pferd", + "cat": "Katze", + "dog": "Hund", + "sheep": "Schaf", + "mouse": "Maus", + "vehicle": "Fahrzeug", + "yell": "Schrei", + "snicker": "Gekicher", + "sigh": "Seufzer", + "choir": "Chor", + "yodeling": "Gejodel", + "chant": "Choral", + "mantra": "Mantra", + "child_singing": "Kindergesang", + "rapping": "Rappen", + "humming": "Summen", + "groan": "Stöhnen", + "grunt": "Grunzen", + "whistling": "Pfeifen", + "breathing": "Atmen", + "wheeze": "Keuchen", + "gasp": "nach Luft schnappen", + "pant": "Hecheln", + "snort": "Schnauben", + "cough": "Husten", + "sneeze": "Niesen", + "sniff": "Schnüffeln", + "run": "Laufen", + "shuffle": "Schlurfen", + "biting": "Beißen", + "gargling": "Gurgeln", + "stomach_rumble": "Magenknurren", + "burping": "Rülpsen", + "hiccup": "Schluckauf", + "fart": "Furz", + "hands": "Hände", + "finger_snapping": "Fingerschnippen", + "heartbeat": "Herzschlag", + "heart_murmur": "Herzgeräusch", + "cheering": "Gejubel", + "applause": "Beifall", + "chatter": "Geschwätz", + "crowd": "Menge", + "children_playing": "Kinderspiel", + "pets": "Haustiere", + "yip": "Aufjaulen", + "howl": "Heulen", + "growling": "Knurren", + "whimper_dog": "Hundegewimmer", + "purr": "Schnurren", + "meow": "Miauen", + "hiss": "Zischen", + "caterwaul": "Gejaule", + "livestock": "Vieh", + "clip_clop": "Klippklapp", + "neigh": "Wiehern", + "cattle": "Rinder", + "moo": "Muhen", + "cowbell": "Kuhglocke", + "oink": "Grunz", + "bleat": "Blöken", + "cluck": "Gackern", + "cock_a_doodle_doo": "Kikeriki", + "gobble": "Kollern", + "goose": "Gans", + "honk": "Hupen", + "coo": "Gurren", + "crow": "Krähe", + "dogs": "Hunde", + "rats": "Ratten", + "insect": "Insekt", + "fly": "Fliege", + "buzz": "Surren", + "frog": "Frosch", + "snake": "Schlange", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Probennehmer", + "drum_kit": "Schlagzeug", + "drum_machine": "Trommelsynthesizer", + "snare_drum": "Kleine Trommel", + "rimshot": "Rimshot", + "drum_roll": "Trommelwirbel", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Becken", + "hi_hat": "Hi-Hat", + "wood_block": "Holzblock", + "tambourine": "Tamburin", + "tubular_bells": "Glockenspiel", + "camera": "Kamera", + "roar": "Brüllen", + "owl": "Eule", + "whale_vocalization": "Walgesang", + "mandolin": "Mandoline", + "chicken": "Huhn", + "sitar": "Sitar", + "ukulele": "Ukulele", + "tapping": "Klopfen", + "flapping_wings": "Flügelschlagen", + "strum": "Herumklimpern", + "electronic_organ": "Elektrische Orgel", + "duck": "Ente", + "quack": "Quaken", + "wild_animals": "Wildtiere", + "rattle": "Klappern", + "music": "Musik", + "pig": "Schwein", + "chirp": "Zwitschern", + "guitar": "Gitarre", + "plucked_string_instrument": "Zupfinstrument", + "hoot": "Heulen", + "acoustic_guitar": "Akustikgitarre", + "electric_piano": "Elektrisches Klavier", + "cricket": "Grille", + "mosquito": "Mücke", + "musical_instrument": "Musikinstrument", + "steel_guitar": "Hawaiigitarre", + "organ": "Orgel", + "drum": "Trommel", + "roaring_cats": "Katzengeschrei", + "footsteps": "Schritte", + "chewing": "Kauen", + "caw": "Krächzen", + "piano": "Klavier", + "clapping": "Klatschen", + "patter": "Trippeln", + "percussion": "Percussion", + "singing": "Gesang", + "bass_guitar": "Bassgitarre", + "fowl": "Geflügel", + "squawk": "Kreischen", + "pigeon": "Taube", + "snoring": "Schnarchen", + "synthetic_singing": "Synthetischer Gesang", + "bow_wow": "Wau-Wau", + "turkey": "Truthahn", + "croak": "Krächzen", + "electric_guitar": "Elektrische Gitarre", + "throat_clearing": "Räuspern", + "gong": "Gong", + "banjo": "Banjo", + "zither": "Zitter", + "harpsichord": "Cembalo", + "bass_drum": "Basstrommel", + "maraca": "Maraca", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Stahlpfanne", + "brass_instrument": "Blechblasinstrument", + "french_horn": "Waldhorn", + "string_section": "Streicher", + "violin": "Geige", + "pizzicato": "Pizzikato", + "saxophone": "Saxophon", + "clarinet": "Klarinette", + "jingle_bell": "Jingle Bell", + "chime": "Glockenspiel", + "bagpipes": "Dudelsack", + "theremin": "Theremin", + "pop_music": "Popmusik", + "bowed_string_instrument": "Streichinstrument", + "didgeridoo": "Didgeridoo", + "wind_chime": "Windspiel", + "flute": "Flöte", + "church_bell": "Kirchenglocke", + "bell": "Glocke", + "orchestra": "Orchester", + "wind_instrument": "Blasinstrument", + "trombone": "Posaune", + "bicycle_bell": "Fahrradklingel", + "trumpet": "Trompete", + "harmonica": "Mundharmonika", + "double_bass": "Kontrabass", + "cello": "Cello", + "harp": "Harfe", + "tuning_fork": "Stimmgabel", + "accordion": "Akkordeon", + "singing_bowl": "Klangschale", + "mallet_percussion": "Mallet-Schlagzeug", + "hip_hop_music": "Hip-Hop-Musik", + "beatboxing": "Beatboxen", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiver Rock", + "psychedelic_rock": "Psychedelischer Rock", + "rhythm_and_blues": "Rythm and Blues", + "soul_music": "Soulmusik", + "country": "Country", + "swing_music": "Swingmusik", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkmusik", + "disco": "Disco", + "classical_music": "Klassische Musik", + "opera": "Oper", + "electronic_music": "Elektronische Musik", + "house_music": "House Musik", + "dubstep": "Dubstep", + "electronica": "Elektronische Medien", + "electronic_dance_music": "Elektronische Tanzmusik", + "ambient_music": "Hintergrundmusik", + "trance_music": "Trance-Musik", + "music_of_latin_america": "Lateinamerikanische Musik", + "salsa_music": "Salsa-Musik", + "blues": "Blues", + "vocal_music": "Vokalmusik", + "a_capella": "A Capella", + "music_of_africa": "Afrikanische Musik", + "gospel_music": "Gospel-Musik", + "music_of_asia": "Asiatische Musik", + "carnatic_music": "Karnatische Musik", + "music_of_bollywood": "Bollywood-Musik", + "traditional_music": "Traditionelle Musik", + "independent_music": "Eigenständige Musik", + "song": "Lied", + "background_music": "Hintergrundmusik", + "theme_music": "Titelmusik", + "lullaby": "Schlaflied", + "christmas_music": "Weihnachtsmusik", + "dance_music": "Tanzmusik", + "happy_music": "Fröhliche Musik", + "tender_music": "Sanfte Musik", + "exciting_music": "Spannende Musik", + "scary_music": "Gruselige Musik", + "wind": "Wind", + "wind_noise": "Windrauschen", + "rain_on_surface": "Regen auf einer Oberfläche", + "stream": "Stream", + "waterfall": "Wasserfall", + "steam": "Dampf", + "fire": "Feuer", + "crackle": "Knistern", + "sailboat": "Segelboot", + "ship": "Schiff", + "motor_vehicle": "Kraftfahrzeug", + "toot": "tuten", + "car_alarm": "Autoalarm", + "power_windows": "Elektrische Fensterheber", + "tire_squeal": "Reifenquietschen", + "car_passing_by": "Vorbeifahrendes Auto", + "air_brake": "Druckluftbremse", + "air_horn": "Autohupe", + "reversing_beeps": "Rückfahrpiepser", + "ice_cream_truck": "Eiswagen", + "emergency_vehicle": "Einsatzfahrzeug", + "police_car": "Polizeiwagen", + "ambulance": "Krankenwagen", + "fire_engine": "Feuerwehrauto", + "traffic_noise": "Verkehrslärm", + "rail_transport": "Schienentransport", + "train_whistle": "Zugpfeife", + "train_horn": "Zugsignalhorn", + "train_wheels_squealing": "Quietschende Eisenbahnräder", + "aircraft": "Flugzeug", + "aircraft_engine": "Flugzeugmotor", + "jet_engine": "Strahltriebwerk", + "propeller": "Propeller", + "helicopter": "Hubschrauber", + "engine": "Motor", + "dental_drill's_drill": "Zahnbohrer", + "lawn_mower": "Rasenmäher", + "medium_engine": "Mittlerer Motor", + "heavy_engine": "Schwerer Motor", + "engine_knocking": "Motorklopfen", + "engine_starting": "Motorstart", + "idling": "Leerlauf", + "doorbell": "Türklingel", + "ding-dong": "BimBam", + "sliding_door": "Schiebetür", + "slam": "zuknallen", + "knock": "Klopfen", + "tap": "Schlag", + "squeak": "Quietschen", + "drawer_open_or_close": "Schublade Öffnen oder Schließen", + "dishes": "Geschirr", + "chopping": "Kleinhacken", + "frying": "Braten", + "microwave_oven": "Mikrowelle", + "water_tap": "Wasserhahn", + "bathtub": "Badewanne", + "toilet_flush": "Toilettenspülung", + "vacuum_cleaner": "Staubsauger", + "zipper": "Reißverschluss", + "keys_jangling": "Schlüsselanhänger", + "coin": "Münze", + "electric_shaver": "Rasierapparat", + "typing": "Tippen", + "typewriter": "Schreibmaschine", + "computer_keyboard": "Computertastatur", + "telephone": "Telefon", + "telephone_bell_ringing": "Telefonklingeln", + "telephone_dialing": "Telefonwahl", + "dial_tone": "Wählton", + "alarm_clock": "Wecker", + "siren": "Sirene", + "civil_defense_siren": "Zivilschutzsirene", + "smoke_detector": "Rauchmelder", + "foghorn": "Nebelhorn", + "whistle": "Pfeife", + "steam_whistle": "Dampfpfeife", + "mechanisms": "Mechanismen", + "ratchet": "Ratsche", + "tick": "Ticken", + "gears": "Getriebe", + "mechanical_fan": "Mechanischer Lüfter", + "printer": "Drucker", + "tools": "Werkzeuge", + "hammer": "Hammer", + "jackhammer": "Presslufthammer", + "sawing": "Sägen", + "power_tool": "Elektrowerkzeug", + "drill": "Bohrer", + "explosion": "Explosion", + "gunshot": "Schuss", + "fusillade": "Gewehrfeuer", + "artillery_fire": "Artilleriefeuer", + "cap_gun": "Maschinenpistole", + "fireworks": "Feuerwerk", + "firecracker": "Feuerwerkskörper", + "eruption": "Ausbruch", + "wood": "Holz", + "splinter": "Splittern", + "crack": "Knacken", + "glass": "Glas", + "chink": "Klirren", + "shatter": "Zerspringen", + "silence": "Stille", + "environmental_noise": "Umgebungsgeräusch", + "static": "Statisch", + "pink_noise": "Rosa Rauschen", + "television": "Fernsehgerät", + "radio": "Radio", + "scream": "Schrei", + "heavy_metal": "Heavy Metal", + "rock_music": "Rockmusik", + "techno": "Techno", + "reggae": "Reggae", + "rain": "Regen", + "gurgling": "Plätschern", + "jazz": "Jazz", + "video_game_music": "Videospielmusik", + "rock_and_roll": "Rock and Roll", + "scratching": "Scratching", + "thunderstorm": "Gewitter", + "christian_music": "Christliche Musik", + "ska": "Ska", + "rustling_leaves": "Blätterrascheln", + "jingle": "Jingle", + "middle_eastern_music": "Orientalische Musik", + "drum_and_bass": "Trommel und Bass", + "flamenco": "Flamenco", + "music_for_children": "Kindermusik", + "new-age_music": "New-Age-Musik", + "afrobeat": "Afrobeat", + "wedding_music": "Hochzeitsmusik", + "soundtrack_music": "Soundtrack Musik", + "raindrop": "Regentropfen", + "sad_music": "Traurige Musik", + "angry_music": "Wütende Musik", + "ocean": "Ozean", + "thunder": "Donner", + "water": "Wasser", + "waves": "Wellen", + "race_car": "Rennwagen", + "rowboat": "Ruderboot", + "truck": "LKW", + "motorboat": "Motorboot", + "chainsaw": "Kettensäge", + "railroad_car": "Eisenbahnwaggon", + "cupboard_open_or_close": "Schrank Öffnen oder Schließen", + "alarm": "Alarm", + "filing": "Feilen", + "chop": "Hacken", + "single-lens_reflex_camera": "Spiegelreflexkamera", + "light_engine": "Lichtmaschine", + "buzzer": "Summer", + "sound_effect": "Geräuscheffekt", + "accelerating": "Beschleunigen", + "electric_toothbrush": "Elektrische Zahnbürste", + "busy_signal": "Besetztzeichen", + "pulleys": "Riemenscheiben", + "sewing_machine": "Nähmaschine", + "air_conditioning": "Klimaanlage", + "burst": "Platzen", + "skidding": "Schleudern", + "subway": "U-Bahn", + "tick-tock": "Ticktack", + "shuffling_cards": "Karten mischen", + "cutlery": "Besteck", + "cash_register": "Kasse", + "ringtone": "Klingelton", + "writing": "Schreiben", + "fixed-wing_aircraft": "Starrflügler", + "fire_alarm": "Feueralarm", + "white_noise": "Weißes Rauschen", + "sanding": "Schleifen", + "machine_gun": "Maschinengewehr", + "boom": "Dröhnen", + "field_recording": "Außenaufnahme", + "liquid": "Flüssigkeit", + "splash": "Spritzer", + "slosh": "Schwenken", + "squish": "Quetschen", + "drip": "Tropfen", + "pour": "Gießen", + "trickle": "Tröpfeln", + "fill": "Füllen", + "spray": "Sprühen", + "pump": "Pumpen", + "stir": "Umrühren", + "boiling": "Köchelnd", + "arrow": "Pfeil", + "electronic_tuner": "Elektronischer Tuner", + "effects_unit": "Effekteinheit", + "chorus_effect": "Chorus-Effekt", + "sodeling": "Verfilzen", + "chird": "Akkord", + "change_ringing": "Wechsle RingRing", + "shofar": "Schofar", + "gush": "sprudeln", + "sonar": "Sonar", + "whoosh": "Rauschen", + "thump": "Ruck", + "basketball_bounce": "Basketball Abbraller", + "bang": "Knall", + "slap": "Ohrfeige", + "whack": "verhauen", + "smash": "zerschlagen", + "breaking": "zerbrechen", + "bouncing": "Abbraller", + "whip": "Peitsche", + "flap": "Lasche", + "scratch": "Kratzer", + "scrape": "Abfall", + "rub": "scheuern", + "roll": "rollen", + "crushing": "Stauchen", + "crumpling": "zerknüllen", + "tearing": "Reißen", + "beep": "Piep", + "ping": "Ping", + "ding": "klingeln", + "thunk": "dumpfes Geräusch", + "clang": "Geklirr", + "squeal": "Ausruf", + "creak": "Knarren", + "rustle": "Geknister", + "whir": "schwirren", + "clatter": "Geratter", + "sizzle": "brutzeln", + "clicking": "Klicken", + "clickety_clack": "Klappergeräuschen", + "rumble": "Grollen", + "plop": "plumpsen", + "hum": "Brummen", + "zing": "Schwung", + "boing": "ferderndes Geräusch", + "crunch": "knirschendes", + "sine_wave": "Sinus Kurve", + "harmonic": "harmonisch", + "chirp_tone": "Frequenzwobbelung", + "pulse": "Takt", + "inside": "drinnen", + "outside": "draußen", + "reverberation": "Widerhall", + "echo": "Echo", + "noise": "Lärm", + "mains_hum": "Netzbrummen", + "distortion": "Verzerrung", + "sidetone": "Nebengeräusch", + "cacophony": "Dissonanz", + "throbbing": "Pochen", + "vibration": "Vibration" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/common.json b/sam2-cpu/frigate-dev/web/public/locales/de/common.json new file mode 100644 index 0000000..16af90f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "untilForTime": "Bis {{time}}", + "last7": "Letzte 7 Tage", + "untilForRestart": "Bis Frigate neu startet.", + "today": "Heute", + "yesterday": "Gestern", + "thisWeek": "Diese Woche", + "lastMonth": "Letzter Monat", + "5minutes": "5 Minuten", + "12hours": "12 Stunden", + "24hours": "24 Stunden", + "month_one": "{{time}} Monat", + "month_other": "{{time}} Monate", + "d": "{{time}} Tag", + "day_one": "{{time}} Tag", + "day_other": "{{time}} Tage", + "m": "{{time}} Min", + "minute_one": "{{time}} Minute", + "minute_other": "{{time}} Minuten", + "s": "{{time}}s", + "second_one": "{{time}} Sekunde", + "second_other": "{{time}} Sekunden", + "formattedTimestamp2": { + "24hour": "dd. MMM HH:mm:ss", + "12hour": "dd.MM hh:mm:ss" + }, + "last30": "Letzte 30 Tage", + "10minutes": "10 Minuten", + "thisMonth": "Dieser Monat", + "yr": "{{time}}Jahr", + "year_one": "{{time}}Jahr", + "year_other": "{{time}}Jahre", + "hour_one": "{{time}} Stunde", + "hour_other": "{{time}} Stunden", + "last14": "Letzte 14 Tage", + "30minutes": "30 Minuten", + "1hour": "1 Stunde", + "lastWeek": "Letzte Woche", + "h": "{{time}} Stunde", + "ago": "vor {{timeAgo}}", + "untilRestart": "Bis zum Neustart", + "justNow": "Gerade", + "pm": "nachmittags", + "mo": "{{time}}Monat", + "formattedTimestamp": { + "12hour": "d. MMM, hh:mm:ss aaa", + "24hour": "dd. MMM, hh:mm:ss aaa" + }, + "formattedTimestampWithYear": { + "24hour": "%-d %b %Y, %H:%M", + "12hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "am": "vormittags", + "formattedTimestampExcludeSeconds": { + "24hour": "%-d %b, %H:%M", + "12hour": "%-d %b, %H:%M" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d. MMM yyyy", + "24hour": "d. MMM yyyy" + }, + "inProgress": "In Bearbeitung", + "invalidStartTime": "Ungültige Startzeit", + "invalidEndTime": "Ungültige Endzeit" + }, + "button": { + "save": "Speichern", + "delete": "Entfernen", + "apply": "Anwenden", + "enabled": "Aktiviert", + "enable": "Aktivieren", + "disabled": "deaktiviert", + "disable": "deaktivieren", + "saving": "Speichere…", + "close": "Schließen", + "back": "Zurück", + "history": "Historie", + "cameraAudio": "Kamera Ton", + "yes": "JA", + "info": "Info", + "play": "Abspielen", + "export": "Exportieren", + "deleteNow": "Jetzt löschen", + "next": "Nächster", + "fullscreen": "Vollbild", + "no": "Nein", + "off": "AUS", + "reset": "Zurücksetzen", + "copy": "Kopieren", + "twoWayTalk": "Zwei-Wege-Kommunikation", + "exitFullscreen": "Vollbild verlassen", + "unselect": "Selektion aufheben", + "copyCoordinates": "Kopiere Koordinaten", + "done": "Fertig", + "edit": "Bearbeiten", + "download": "Herunterladen", + "cancel": "Abbrechen", + "pictureInPicture": "Bild in Bild", + "on": "AN", + "suspended": "Pausierte", + "unsuspended": "fortsetzen", + "continue": "Weiter" + }, + "label": { + "back": "Zurück", + "hide": "Verstecke {{item}}", + "show": "Zeige {{item}}", + "ID": "ID", + "none": "Nichts", + "all": "Alle" + }, + "menu": { + "configurationEditor": "Konfigurationseditor", + "languages": "Sprachen", + "language": { + "withSystem": { + "label": "Sprache der Systemeinstellungen verwenden" + }, + "en": "Englisch", + "zhCN": "简体中文 (Vereinfachtes Chinesisch)", + "fr": "Französisch", + "es": "Spanisch", + "ar": "Arabisch", + "pt": "Portugiesisch", + "de": "Deutsch", + "it": "Italienisch", + "nl": "Niederländisch", + "sv": "Schwedisch", + "cs": "Tschechisch", + "ko": "Koreanisch", + "pl": "Polnisch", + "el": "Griechisch", + "ro": "Rumänisch", + "hu": "Ungarisch", + "fi": "Finnisch", + "ru": "Russisch", + "ja": "Japanisch", + "tr": "Türkisch", + "da": "Dänisch", + "hi": "Hindi", + "nb": "Norwegisch", + "vi": "Vietnamesisch", + "fa": "Persisch", + "uk": "Ukrainisch", + "he": "Hebräisch", + "sk": "Slowakisch", + "yue": "粵語 (Kantonesisch)", + "th": "ไทย (Thailändisch)", + "ca": "Català (Katalanisch)", + "ur": "اردو (Urdu)", + "ptBR": "Portugiesisch (Brasilianisch)", + "sr": "Српски (Serbisch)", + "sl": "Slovenščina (Slowenisch)", + "lt": "Lietuvių (Litauisch)", + "bg": "Български (bulgarisch)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)" + }, + "appearance": "Erscheinung", + "theme": { + "label": "Design-Thema", + "blue": "Blau", + "green": "Grün", + "default": "Standard", + "nord": "Nord", + "red": "Rot", + "contrast": "Hoher Kontrast", + "highcontrast": "Hoher Kontrast" + }, + "help": "Hilfe", + "documentation": { + "title": "Dokumentation", + "label": "Frigate Dokumentation" + }, + "live": { + "allCameras": "Alle Kameras", + "cameras": { + "title": "Kameras", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameras" + }, + "title": "Live" + }, + "review": "Überprüfen", + "restart": "Frigate neu starten", + "darkMode": { + "light": "Hell", + "label": "Dunkler Modus", + "dark": "Dunkel", + "withSystem": { + "label": "Verwende Systemeinstellungen fuer hell oder dunkel Modus" + } + }, + "system": "System", + "configuration": "Konfigurieren", + "withSystem": "System", + "settings": "Einstellungen", + "systemLogs": "Systemprotokoll", + "systemMetrics": "Systemstatistiken", + "explore": "Erkunden", + "faceLibrary": "Gesichterbibliothek", + "user": { + "title": "Benutzer", + "account": "Benutzerkonto", + "current": "Aktueller Benutzer: {{user}}", + "setPassword": "Passwort setzen", + "anonymous": "anonym", + "logout": "Abmelden" + }, + "uiPlayground": "Testgebiet für Benutzeroberfläche", + "export": "Exportieren", + "classification": "Klassifizierung" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "Fuß", + "meters": "Meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/Stunde", + "mbph": "MB/Stunde", + "gbph": "GB/Stunde" + } + }, + "toast": { + "copyUrlToClipboard": "URL in Zwischenablage kopiert.", + "save": { + "error": { + "title": "Speichern der Konfigurationsänderungen gescheitert: {{errorMessage}}", + "noMessage": "Speichern der Konfigurationsänderungen gescheitert" + }, + "title": "Speichern" + } + }, + "role": { + "title": "Rolle", + "admin": "Administrator", + "viewer": "Zuschauer", + "desc": "Administratoren haben vollen Zugang zu allen Funktionen der Frigate Benutzeroberfläche. Zuschauer können nur Kameras betrachten, erkannte Objekte überprüfen und historische Aufnahmen durchsehen." + }, + "pagination": { + "previous": { + "title": "Voherige", + "label": "Zur voherigen Seite wechseln" + }, + "next": { + "title": "Nächste", + "label": "Zur nächsten Seite wechseln" + }, + "more": "Weitere Seiten", + "label": "Seitennummerierung" + }, + "notFound": { + "title": "404", + "desc": "Seite nicht gefunden", + "documentTitle": "Nicht gefunden - Frigate" + }, + "selectItem": "Wähle {{item}}", + "readTheDocumentation": "Dokumentation lesen", + "accessDenied": { + "desc": "Du hast keine Berechtigung diese Seite anzuzeigen.", + "documentTitle": "Zugang verweigert - Frigate", + "title": "Zugang verweigert" + }, + "information": { + "pixels": "{{area}}px" + }, + "field": { + "optional": "Optional", + "internalID": "Die interne ID, die Frigate in der Konfiguration und Datenbank verwendet" + }, + "list": { + "two": "{{0}} und {{1}}", + "many": "{{items}}, und {{last}}", + "separatorWithSpace": ", " + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/auth.json new file mode 100644 index 0000000..2c48866 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "login": "Anmeldung", + "errors": { + "passwordRequired": "Kennwort ist erforderlich", + "loginFailed": "Anmeldung gescheitert", + "webUnknownError": "Unbekannter Fehler. Prüfe Konsolenlogs.", + "usernameRequired": "Benutzername ist erforderlich", + "rateLimit": "Anmeldelimit überschritten. Bitte später erneut versuchen.", + "unknownError": "Unbekannter Fehler. Prüfe Logs." + }, + "user": "Benutzername", + "password": "Kennwort", + "firstTimeLogin": "Ist dies der erste Loginversuch? Die Zugangsdaten werden in den Frigate Logs angezeigt." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/camera.json new file mode 100644 index 0000000..32874ba --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "delete": { + "label": "Kameragruppe löschen", + "confirm": { + "title": "Löschen bestätigen", + "desc": "Willst Du die Kameragruppe {{name}} wirklich löschen?" + } + }, + "name": { + "label": "Name", + "placeholder": "Gib einen Namen ein…", + "errorMessage": { + "exists": "Name der Kameragruppe bereits vorhanden.", + "nameMustNotPeriod": "Name einer Kameragruppe darf keinen Punkt enthalten.", + "mustLeastCharacters": "Name einer Kameragruppe muss mindestens 2 Zeichen haben.", + "invalid": "Ungültiger Name für eine Kameragruppe." + } + }, + "icon": "Icon", + "camera": { + "setting": { + "label": "Kamera Streaming Einstellungen", + "audioIsAvailable": "Audio ist für diesen Stream verfügbar", + "audioIsUnavailable": "Audio ist für diesen Stream nicht verfügbar", + "streamMethod": { + "label": "Streaming-Methode", + "method": { + "noStreaming": { + "label": "Kein Streaming", + "desc": "Kamerabilder werden nur einmal pro Minute aktualisiert und es wird kein Live Streaming geben." + }, + "smartStreaming": { + "label": "Smart Streaming (empfohlen)", + "desc": "Smart Streaming wird Deine Kamera einmal in der Minute aktualisieren, wenn sich keine erkennbare Aktivität ereignet, um Bandbreite und Ressourcen zu schonen. Sobald eine Aktivität erkannt wird, wechselt das Standbild sofort zu einem Live Stream." + }, + "continuousStreaming": { + "label": "Kontinuierliches Streaming", + "desc": { + "title": "Das auf einem Dashboard sichtbare Kamerabild ist immer ein Live Stream, selbst wenn keine Aktivität erkannt wird.", + "warning": "Kontinuierliches Streaming kann zu hoher Bandbreitenausnutzung und zu Performanceproblemen führen. Bitte behutsam nutzen." + } + } + }, + "placeholder": "Wähle eine streaming Methode" + }, + "title": "{{cameraName}} Streaming Einstellungen", + "compatibilityMode": { + "desc": "Aktiviere diese Option nur, falls der Live Stream Deiner Kamera Farbstörungen zeigt und eine Diagonale Linie auf der rechten Seite des Bildes hat.", + "label": "Kompatibilitätsmodus" + }, + "audio": { + "tips": { + "title": "Audio muss in der Kamera verfügbar und in go2rtc für diesen Stream konfiguriert sein.", + "document": "Lies die Dokumentation. " + } + }, + "desc": "Ändere die Live Stream Optionen für das Dashboard dieser Kameragruppe. Diese Einstellungen sind geräte-/browserspezifisch.", + "stream": "Stream", + "placeholder": "Wähle einen Stream" + }, + "birdseye": "Vogelperspektive" + }, + "add": "Kameragruppe hinzufügen", + "cameras": { + "label": "Kameras", + "desc": "Wähle Kameras für diese Gruppe aus." + }, + "label": "Kameragruppen", + "edit": "Kameragruppe bearbeiten", + "success": "Kameragruppe {{name}} wurde gespeichert." + }, + "debug": { + "options": { + "title": "Optionen", + "hideOptions": "Verberge Optionen", + "label": "Einstellungen", + "showOptions": "Zeige Optionen" + }, + "timestamp": "Zeitstempel", + "zones": "Zonen", + "mask": "Maske", + "motion": "Bewegung", + "regions": "Regionen", + "boundingBox": "Begrenzungsrechteck" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/dialog.json new file mode 100644 index 0000000..464db5a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/dialog.json @@ -0,0 +1,133 @@ +{ + "restart": { + "title": "Sind Sie sicher, dass Sie Frigate neustarten wollen?", + "restarting": { + "title": "Frigate startet neu", + "content": "Diese Seite wird in {{countdown}} Sekunde(n) aktualisiert.", + "button": "Neuladen erzwingen" + }, + "button": "Neustarten" + }, + "explore": { + "plus": { + "review": { + "true": { + "label": "Bestätigen Sie das Label für Frigate Plus", + "true_one": "Das ist ein/eine {{label}}", + "true_other": "Dies sind {{label}}" + }, + "state": { + "submitted": "Übermittelt" + }, + "false": { + "false_one": "Das ist kein(e) {{label}}", + "false_other": "Das sind kein(e) {{label}}", + "label": "Bestätige dieses Label nicht für Frigate Plus" + }, + "question": { + "label": "Bestätige diese Beschriftung für Frigate Plus", + "ask_a": "Ist dieses Objekt ein {{label}}?", + "ask_an": "Ist dieses Objekt ein {{label}}?", + "ask_full": "Ist dieses Objekt ein {{untranslatedLabel}} ({{translatedLabel}})?" + } + }, + "submitToPlus": { + "label": "An Frigate+ übermitteln", + "desc": "Objekte an Orten die du vermeiden möchtest, sind keine Fehlalarme. Wenn du sie als Fehlalarme meldest, verwirrst du das Modell." + } + }, + "video": { + "viewInHistory": "Im Verlauf ansehen" + } + }, + "export": { + "time": { + "fromTimeline": "Aus der Zeitleiste auswählen", + "start": { + "title": "Startzeit", + "label": "Startzeit auswählen" + }, + "end": { + "label": "Endzeit auswählen", + "title": "Endzeit" + }, + "lastHour_one": "Letzte Stunde", + "lastHour_other": "Letzte {{count}} Stunden", + "custom": "Benutzerdefiniert" + }, + "name": { + "placeholder": "Export benennen" + }, + "select": "Auswählen", + "selectOrExport": "Auswählen oder Exportieren", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen", + "failed": "Fehler beim Starten des Exports: {{error}}", + "noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt" + }, + "success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.", + "view": "Ansicht" + }, + "fromTimeline": { + "saveExport": "Export speichern", + "previewExport": "Exportvorschau" + }, + "export": "Exportieren" + }, + "streaming": { + "restreaming": { + "disabled": "Für diese Kamera ist das Restreaming nicht aktiviert.", + "desc": { + "readTheDocumentation": "Weitere Informationen in der Dokumentation", + "title": "Konfiguriere go2rtc, um erweiterte Live-Ansichtsoptionen und Audio für diese Kamera zu nutzen." + } + }, + "showStats": { + "label": "Stream-Statistiken anzeigen", + "desc": "Stream-Statistiken werden bei aktivierter Option als Overlay im Kamera-Feed eingeblendet." + }, + "debugView": "Debug-Ansicht", + "label": "Stream" + }, + "search": { + "saveSearch": { + "label": "Suche speichern", + "desc": "Gib einen Namen für diese gespeicherte Suche an.", + "placeholder": "Gib einen Namen für die Suche ein", + "overwrite": "{{searchName}} existiert bereits. Beim Speichern wird der vorhandene Wert überschrieben.", + "button": { + "save": { + "label": "Diese Suche speichern" + } + }, + "success": "Die Suche {{searchName}} wurde gespeichert." + } + }, + "recording": { + "confirmDelete": { + "title": "Bestätige Löschung", + "desc": { + "selected": "Bist du sicher, dass du alle aufgezeichneten Videos, die mit diesem Beitrag verbunden sind, löschen möchtest?

    Halte Shift-Taste gedrückt, um diesen Dialog in Zukunft zu umgehen." + }, + "toast": { + "success": "Das Videomaterial des gewählten Eintrags wurde erfolgreich gelöscht.", + "error": "Löschen is fehlgeschlagen: {{error}}" + } + }, + "button": { + "export": "Exportieren", + "markAsReviewed": "Als geprüft markieren", + "deleteNow": "Jetzt löschen", + "markAsUnreviewed": "Als ungeprüft markieren" + } + }, + "imagePicker": { + "selectImage": "Vorschaubild eines verfolgten Objekts selektieren", + "search": { + "placeholder": "Nach Label oder Unterlabel suchen..." + }, + "noImages": "Kein Vorschaubild für diese Kamera gefunden", + "unknownLabel": "Gespeichertes Triggerbild" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/filter.json new file mode 100644 index 0000000..1938776 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filter", + "labels": { + "all": { + "short": "Labels", + "title": "Alle Labels" + }, + "label": "Labels", + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "all": { + "title": "Alle Zonen", + "short": "Zonen" + }, + "label": "Zonen" + }, + "dates": { + "all": { + "title": "Alle Zeiträume", + "short": "Daten" + }, + "selectPreset": "Wähle eine Voreinstellung aus…" + }, + "reset": { + "label": "Filter auf Standardwerte zurücksetzen" + }, + "more": "Mehr Filter", + "timeRange": "Zeitraum", + "subLabels": { + "all": "Alle Unterkategorien", + "label": "Unterkategorie" + }, + "features": { + "label": "Eigenschaften", + "hasSnapshot": "Hat einen Schnappschuss", + "hasVideoClip": "Hat einen Video-Clip", + "submittedToFrigatePlus": { + "label": "Eingereicht bei Frigate+", + "tips": "Du musst zuerst nach deine erkannten Objekten, die einen Schnappschuss haben, filtern.

    Erkante Objekte ohne Schnappschuss können nicht zu Frigate+ übermittelt werden." + } + }, + "score": "Ergebnis", + "estimatedSpeed": "Geschätzte Geschwindigkeit ({{unit}})", + "sort": { + "label": "Sortieren", + "dateAsc": "Datum (Aufsteigend)", + "dateDesc": "Datum (Absteigend)", + "scoreAsc": "Objekt Wertung (Aufsteigend)", + "scoreDesc": "Objekt Wertung (Absteigend)", + "speedAsc": "Geschätzte Geschwindigkeit (Aufsteigend)", + "relevance": "Relevanz", + "speedDesc": "Geschätzte Geschwindigkeit (absteigend)" + }, + "cameras": { + "all": { + "title": "Alle Kameras", + "short": "Kameras" + }, + "label": "Kamera Filter" + }, + "motion": { + "showMotionOnly": "Zeige nur Bewegung" + }, + "review": { + "showReviewed": "Geprüfte anzeigen" + }, + "explore": { + "settings": { + "defaultView": { + "title": "Standardansicht", + "desc": "Wenn keine Filter ausgewählt sind, wird eine Zusammenfassung der zuletzt verfolgten Objekte pro Kategorie oder ein ungefiltertes Raster angezeigt.", + "summary": "Zusammenfassung", + "unfilteredGrid": "Ungefiltertes Raster" + }, + "title": "Einstellungen", + "gridColumns": { + "title": "Rasterspalten", + "desc": "Wähle die Anzahl der Spalten in der Rasteransicht." + }, + "searchSource": { + "options": { + "description": "Beschreibung", + "thumbnailImage": "Vorschaubild" + }, + "label": "Quelle der Suche", + "desc": "Wähle, ob die Miniaturansichten oder die Beschreibungen der erkannten Objekte durchsucht werden sollen." + } + }, + "date": { + "selectDateBy": { + "label": "Wähle ein Datum zum Filtern" + } + } + }, + "logSettings": { + "label": "Log-Ebene filtern", + "filterBySeverity": "Protokolle nach Schweregrad filtern", + "loading": { + "title": "Lade", + "desc": "Wenn das Protokollfenster nach unten gescrollt wird, werden neue Protokolle automatisch geladen, sobald sie hinzugefügt werden." + }, + "disableLogStreaming": "Log des Streams deaktivieren", + "allLogs": "Alle Logs" + }, + "trackedObjectDelete": { + "title": "Bestätige Löschung", + "toast": { + "success": "Erkannte Objekte erfolgreich gelöscht.", + "error": "Das Löschen von verfolgten Objekten ist fehlgeschlagen: {{errorMessage}}" + }, + "desc": "Beim Löschen dieser {{objectLength}} verfolgten Objekte werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Objektlebenszykluseinträge entfernt. Aufgezeichnetes Filmmaterial dieser verfolgten Objekte in der Verlaufsansicht wird NICHT gelöscht.

    Bist du sicher, dass du fortfahren möchtest?

    Halte die Shift-Taste gedrückt, um diesen Dialog in Zukunft zu umgehen." + }, + "zoneMask": { + "filterBy": "Nach Zonenmaskierung filtern" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Keine Kennzeichen gefunden.", + "title": "Bekannte Kennzeichen", + "loadFailed": "Bekannte Nummernschilder konnten nicht geladen werden.", + "loading": "Lade bekannte Nummernschilder…", + "placeholder": "Tippe, um Kennzeichen zu suchen…", + "selectPlatesFromList": "Wählen eine oder mehrere Kennzeichen aus der Liste aus.", + "selectAll": "Alle wählen", + "clearAll": "Alle löschen" + }, + "classes": { + "label": "Klassen", + "all": { + "title": "Alle Klassen" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klassen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/icons.json new file mode 100644 index 0000000..41d608b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Suche nach einem Icon…" + }, + "selectIcon": "Wähle ein Icon" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/input.json new file mode 100644 index 0000000..fcee21c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Video herunterladen", + "toast": { + "success": "Das Herunterladen des überprüften Videos wurde gestartet." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/de/components/player.json new file mode 100644 index 0000000..a6b251f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Keine Aufnahmen für diesen Zeitpunkt gefunden", + "noPreviewFound": "Keine Vorschau gefunden", + "submitFrigatePlus": { + "title": "Dieses Bild an Frigate+ senden?", + "submit": "Senden" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 oder höher ist für diesen Typ eines Live-Streams erforderlich.", + "streamOffline": { + "title": "Stream ist offline", + "desc": "Es wurden keine Bilder vom Erkennungsstream der Kamera {{cameraName}} empfangen, bitte Logs überprüfen" + }, + "cameraDisabled": "Kamera ist deaktiviert", + "stats": { + "streamType": { + "title": "Stream Typ:", + "short": "Typ" + }, + "bandwidth": { + "title": "Bandbreite:", + "short": "Bandbreite" + }, + "latency": { + "title": "Latenz:", + "value": "{{seconds}} Sekunden", + "short": { + "title": "Lazenz", + "value": "{{seconds}} s" + } + }, + "droppedFrames": { + "short": { + "title": "Ausgelassen", + "value": "{{droppedFrames}} Bilder" + }, + "title": "Ausgelassene Bilder:" + }, + "decodedFrames": "Dekodierte Bilder:", + "droppedFrameRate": "Verlorene Bildrate:", + "totalFrames": "Bilder insgesamt:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Bild an Frigate+ senden gescheitert" + }, + "success": { + "submittedFrigatePlus": "Bild erfolgreich an Frigate+ gesendet" + } + }, + "noPreviewFoundFor": "Keine Vorschau für {{cameraName}} gefunden" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/objects.json b/sam2-cpu/frigate-dev/web/public/locales/de/objects.json new file mode 100644 index 0000000..f3fdbd3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/objects.json @@ -0,0 +1,120 @@ +{ + "boat": "Boot", + "traffic_light": "Ampel", + "fire_hydrant": "Hydrant", + "stop_sign": "Stoppschild", + "bench": "Bank", + "bird": "Vogel", + "cow": "Kuh", + "elephant": "Elefant", + "bear": "Bär", + "zebra": "Zebra", + "giraffe": "Giraffe", + "shoe": "Schuh", + "tie": "Krawatte", + "frisbee": "Frisbee", + "skis": "Skier", + "kite": "Drachen", + "skateboard": "Skateboard", + "surfboard": "Surfbrett", + "plate": "Platte", + "cup": "Tasse", + "spoon": "Löffel", + "sandwich": "Sandwich", + "broccoli": "Brokkoli", + "carrot": "Karotte", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Kuchen", + "chair": "Stuhl", + "couch": "Sofa", + "bed": "Bett", + "dining_table": "Esstisch", + "toilet": "Toilette", + "door": "Tür", + "sink": "Waschbecken", + "refrigerator": "Kühlschrank", + "book": "Buch", + "bbq_grill": "BBQ Grill", + "amazon": "Amazon", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "Post", + "postnl": "PostNL", + "nzpost": "NZPost", + "purolator": "Purolator", + "postnord": "PostNord", + "dpd": "DPD", + "snowboard": "Snowboard", + "baseball_bat": "Baseballschläger", + "knife": "Messer", + "squirrel": "Eichhörnchen", + "animal": "Tier", + "blender": "Mixer", + "vase": "Vase", + "orange": "Orange", + "teddy_bear": "Teddybär", + "on_demand": "Auf Anfrage", + "scissors": "Schere", + "ups": "UPS", + "train": "Zug", + "toaster": "Toaster", + "clock": "Uhr", + "mirror": "Spiegel", + "backpack": "Rucksack", + "motorcycle": "Motorrad", + "window": "Fenster", + "toothbrush": "Zahnbürste", + "package": "Paket", + "hair_brush": "Haarbürste", + "apple": "Apfel", + "banana": "Banane", + "parking_meter": "Parkuhr", + "oven": "Ofen", + "umbrella": "Regenschirm", + "eye_glasses": "Brillen", + "robot_lawnmower": "Mähroboter", + "potted_plant": "Topfpflanze", + "waste_bin": "Abfallbehälter", + "license_plate": "Kennzeichen", + "bottle": "Flasche", + "deer": "Reh", + "usps": "USPS", + "person": "Person", + "bowl": "Schüssel", + "microwave": "Mikrowelle", + "bicycle": "Fahrrad", + "car": "Auto", + "fork": "Gabel", + "tv": "Fernseher", + "laptop": "Laptop", + "mouse": "Maus", + "goat": "Ziege", + "keyboard": "Klaviatur", + "cell_phone": "Handy", + "remote": "Fernbedienung", + "airplane": "Flugzeug", + "tennis_racket": "Tennisschläger", + "bus": "Bus", + "street_sign": "Straßenschild", + "horse": "Pferd", + "bark": "Bellen", + "cat": "Katze", + "wine_glass": "Weinglas", + "dog": "Hund", + "sheep": "Schaf", + "hat": "Hut", + "hot_dog": "Hot Dog", + "baseball_glove": "Baseballhandschuh", + "suitcase": "Koffer", + "handbag": "Handtasche", + "sports_ball": "Sportball", + "hair_dryer": "Haartrockner", + "vehicle": "Fahrzeug", + "face": "Gesicht", + "fox": "Fuchs", + "desk": "Schreibtisch", + "raccoon": "Waschbär", + "rabbit": "Kaninchen", + "gls": "GLS" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/classificationModel.json new file mode 100644 index 0000000..2b58dfb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Klassifizierungsmodelle - Fregatte", + "details": { + "scoreInfo": "Die Punktzahl gibt die durchschnittliche Konfidenz aller Erkennungen dieses Objekts wieder." + }, + "button": { + "deleteClassificationAttempts": "Lösche klassifizierte Bilder", + "renameCategory": "Klasse umbenennen", + "deleteCategory": "Klasse löschen", + "deleteImages": "Bilder löschen", + "trainModel": "Modell trainieren", + "addClassification": "Klassifizierung hinzufügen", + "deleteModels": "Modell löschen", + "editModel": "Modell bearbeiten" + }, + "tooltip": { + "trainingInProgress": "Modell wird gerade trainiert", + "noNewImages": "Keine weiteren Bilder zum trainieren. Bitte klassifiziere weitere Bilder im Datensatz.", + "noChanges": "Keine Veränderungen des Datensatzes seit dem letzten Training.", + "modelNotReady": "Modell ist nicht bereit zum Training" + }, + "toast": { + "success": { + "deletedCategory": "Klasse gelöscht", + "deletedImage": "Bilder gelöscht", + "deletedModel_one": "{{count}} Modell erfolgreich gelöscht", + "deletedModel_other": "{{count}} Modelle erfolgreich gelöscht", + "categorizedImage": "Erfolgreich klassifizierte Bilder", + "trainedModel": "Modell erfolgreich trainiert.", + "trainingModel": "Modelltraining erfolgreich gestartet.", + "updatedModel": "Modellkonfiguration erfolgreich aktualisiert", + "renamedCategory": "Klasse erfolgreich in {{name}} umbenannt" + }, + "error": { + "deleteImageFailed": "Löschen fehlgeschlagen: {{errorMessage}}", + "deleteCategoryFailed": "Löschen der Klasse fehlgeschlagen: {{errorMessage}}", + "deleteModelFailed": "Model konnte nicht gelöscht werden: {{errorMessage}}", + "trainingFailedToStart": "Modelltraining konnte nicht gestartet werden: {{errorMessage}}", + "updateModelFailed": "Aktualisierung des Modells fehlgeschlagen: {{errorMessage}}", + "renameCategoryFailed": "Umbenennung der Klasse fehlgeschlagen: {{errorMessage}}", + "categorizeFailed": "Bildkategorisierung fehlgeschlagen: {{errorMessage}}", + "trainingFailed": "Modelltraining fehlgeschlagen. Details sind in den Frigate-Protokollen zu finden." + } + }, + "deleteCategory": { + "title": "Klasse löschen", + "desc": "Möchten Sie die Klasse {{name}} wirklich löschen? Dadurch werden alle zugehörigen Bilder dauerhaft gelöscht und das Modell muss neu trainiert werden.", + "minClassesTitle": "Klasse kann nicht gelöscht werden", + "minClassesDesc": "Ein Klassifizierungsmodell benötigt mindestens zwei Klassen. Fügen Sie eine weitere Klasse hinzu, bevor Sie diese löschen." + }, + "deleteModel": { + "title": "Klassifizierungsmodell löschen", + "single": "Möchten Sie {{name}} wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_one": "Möchtest du {{count}} Modell wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Möchtest du {{count}} Modelle wirklich löschen? Dadurch werden alle zugehörigen Daten, einschließlich Bilder und Trainingsdaten, dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden." + }, + "edit": { + "title": "Klassifikationsmodell bearbeiten", + "descriptionState": "Bearbeite die Klassen für dieses Zustandsklassifikationsmodell. Änderungen erfordern erneutes Trainieren des Modells.", + "descriptionObject": "Bearbeite den Objekttyp und Klassifizierungstyp für dieses Objektklassifikationsmodell.", + "stateClassesInfo": "Hinweis: Die Änderung der Statusklassen erfordert ein erneutes Trainieren des Modells mit den aktualisierten Klassen." + }, + "deleteDatasetImages": { + "title": "Datensatz Bilder löschen", + "desc_one": "Bist du sicher, dass {{count}} Bild von {{dataset}} gelöscht werden sollen? Diese Aktion kann nicht rückgängig gemacht werden und erfordert ein erneutes Trainieren des Modells.", + "desc_other": "Bist du sicher, dass {{count}} Bilder von {{dataset}} gelöscht werden sollen? Diese Aktion kann nicht rückgängig gemacht werden und erfordert ein erneutes Trainieren des Modells." + }, + "deleteTrainImages": { + "title": "Trainingsbilder löschen", + "desc_one": "Bist du sicher, dass du {{count}} Bild löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Bist du sicher, dass du {{count}} Bilder löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." + }, + "renameCategory": { + "title": "Klasse umbenennen", + "desc": "Neuen Namen für {{name}} eingeben. Das Modell muss neu trainiert werden, damit die Änderungen wirksam werden." + }, + "description": { + "invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten." + }, + "train": { + "title": "Neue Klassifizierungen", + "titleShort": "frisch", + "aria": "Neue Klassifizierungen auswählen" + }, + "categories": "Klassen", + "createCategory": { + "new": "Neue Klasse erstellen" + }, + "categorizeImageAs": "Bild klassifizieren als:", + "categorizeImage": "Bild klassifizieren", + "menu": { + "objects": "Objekte", + "states": "Zustände" + }, + "noModels": { + "object": { + "title": "Keine Objektklassifikationsmodelle", + "description": "Erstelle ein benutzerdefiniertes Modell, um erkannte Objekte zu klassifizieren.", + "buttonText": "Objektmodell erstellen" + }, + "state": { + "title": "Keine Statusklassifizierungsmodelle", + "description": "Erstellen Sie ein benutzerdefiniertes Modell, um Zustandsänderungen in bestimmten Kamerabereichen zu überwachen und zu klassifizieren.", + "buttonText": "Zustandsmodell erstellen" + } + }, + "wizard": { + "title": "Neue Klassifizierung erstellen", + "steps": { + "nameAndDefine": "Benennen und definieren", + "stateArea": "Gebiet", + "chooseExamples": "Beispiel auswählen" + }, + "step1": { + "description": "Zustandsmodelle überwachen feste Kamerabereiche auf Veränderungen (z. B. Tür offen/geschlossen). Objektmodelle fügen den erkannten Objekten Klassifizierungen hinzu (z. B. bekannte Tiere, Lieferanten usw.).", + "name": "Name", + "namePlaceholder": "Eingeben Modell Name...", + "type": "Typ", + "typeState": "Zustand", + "typeObject": "Objekt", + "objectLabel": "Objekt Bezeichnung", + "objectLabelPlaceholder": "Auswahl Objekt Typ...", + "classificationType": "Klassifizierungstyp", + "classificationTypeTip": "Etwas über Klassifizierungstyp lernen", + "classificationTypeDesc": "Unterbezeichnungen fügen dem Objektnamen zusätzlichen Text hinzu (z. B. „Person: UPS“). Attribute sind durchsuchbare Metadaten, die separat in den Objektmetadaten gespeichert sind.", + "classificationSubLabel": "Unterlabel", + "classificationAttribute": "Merkmal", + "classes": "Klasse", + "states": "Gebiet", + "classesTip": "Über Klassen lernen", + "classesStateDesc": "Definieren Sie die verschiedenen Zustände, in denen sich Ihr Kamerabereich befinden kann. Beispiel: „offen” und „geschlossen” für ein Garagentor.", + "classesObjectDesc": "Definieren Sie die verschiedenen Kategorien, in die erkannte Objekte klassifiziert werden sollen. Beispiel: „Lieferant“, „Bewohner“, „Fremder“ für die Klassifizierung von Personen.", + "classPlaceholder": "Eingabe Klassenbezeichnung...", + "errors": { + "nameRequired": "Modellname ist erforderlich", + "nameLength": "Der Modellname darf maximal 64 Zeichen lang sein", + "nameOnlyNumbers": "Der Modellname darf nicht nur aus Zahlen bestehen", + "classRequired": "Mindestens eine Klasse ist erforderlich", + "classesUnique": "Klassenname muss eindeutig sein", + "stateRequiresTwoClasses": "Gebietsmodelle erfordern mindestens zwei Klassen", + "objectLabelRequired": "Bitte wähle eine Objektbeschriftung", + "objectTypeRequired": "Bitte wählen Sie einen Klassifizierungstyp aus" + } + }, + "step2": { + "description": "Wählen Sie Kameras aus und legen Sie für jede Kamera den zu überwachenden Bereich fest. Das Modell klassifiziert den Zustand dieser Bereiche.", + "cameras": "Kameras", + "selectCamera": "Kamera auswählen", + "noCameras": "Klick + zum hinzufügen der Kameras", + "selectCameraPrompt": "Wählen Sie eine Kamera aus der Liste aus, um ihren Überwachungsbereich festzulegen" + }, + "step3": { + "selectImagesPrompt": "Wählen sie alle Bilder mit: {{className}}", + "selectImagesDescription": "Klicken Sie auf die Bilder, um sie auszuwählen. Klicken Sie auf „Weiter“, wenn Sie mit diesem Kurs fertig sind.", + "allImagesRequired_one": "Bitte klassifizieren Sie alle Bilder. {{count}} Bild verbleibend.", + "allImagesRequired_other": "Bitte klassifizieren Sie alle Bilder. {{count}} Bilder verbleiben.", + "generating": { + "title": "Beispielbilder generieren", + "description": "Frigate extrahiert repräsentative Bilder aus Ihren Aufnahmen. Dies kann einen Moment dauern..." + }, + "training": { + "title": "Trainingsmodell", + "description": "Ihr Modell wird im Hintergrund trainiert. Schließen Sie diesen Dialog, und Ihr Modell wird ausgeführt, sobald das Training abgeschlossen ist." + }, + "retryGenerate": "Generierung wiederholen", + "noImages": "Keine Bilder generiert", + "classifying": "Klassifizieren und Trainieren...", + "trainingStarted": "Training wurde erfolgreich gestartet", + "errors": { + "noCameras": "Keine Kameras konfiguriert", + "noObjectLabel": "Kein Objektlabel ausgewählt", + "generateFailed": "Beispiele konnten nicht generiert werden: {{error}}", + "generationFailed": "Generierung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "classifyFailed": "Bilder konnten nicht klassifiziert werden: {{error}}" + }, + "generateSuccess": "Erfolgreich generierte Beispielbilder", + "modelCreated": "Modell erfolgreich erstellt. Verwenden Sie die Ansicht „Aktuelle Klassifizierungen“, um Bilder für fehlende Zustände hinzuzufügen, und trainieren Sie dann das Modell.", + "missingStatesWarning": { + "title": "Beispiele für fehlende Zustände", + "description": "Es wird empfohlen für alle Zustände Beispiele auszuwählen. Das Modell wird erst trainiert, wenn für alle Zustände Bilder vorhanden sind. Fahren Sie fort und verwenden Sie die Ansicht „Aktuelle Klassifizierungen“, um Bilder für die fehlenden Zustände zu klassifizieren. Trainieren Sie anschließend das Modell." + } + } + }, + "none": "Keiner" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/configEditor.json new file mode 100644 index 0000000..86959e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Konfigurationseditor", + "copyConfig": "Konfiguration kopieren", + "saveAndRestart": "Sichern und Neustarten", + "saveOnly": "Nur Sichern", + "toast": { + "error": { + "savingError": "Fehler beim Speichern der Konfiguration" + }, + "success": { + "copyToClipboard": "Konfiguration in Zwischenablage kopiert." + } + }, + "documentTitle": "Konfigurationseditor – Frigate", + "confirm": "Verlassen ohne zu Speichern?", + "safeConfigEditor": "Konfiguration Editor (abgesicherter Modus)", + "safeModeDescription": "Frigate ist aufgrund eines Konfigurationsvalidierungsfehlers im abgesicherten Modus." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/events.json new file mode 100644 index 0000000..1b031af --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/events.json @@ -0,0 +1,63 @@ +{ + "alerts": "Alarme", + "detections": "Erkennungen", + "motion": { + "label": "Bewegung", + "only": "nur Bewegung" + }, + "empty": { + "alert": "Es gibt keine zu prüfenden Alarme", + "detection": "Es gibt keine zu prüfenden Erkennungen", + "motion": "Keine Bewegungsdaten gefunden" + }, + "timeline": "Zeitleiste", + "timeline.aria": "Zeitleiste auswählen", + "events": { + "label": "Ereignisse", + "noFoundForTimePeriod": "Keine Ereignisse für diesen Zeitraum gefunden.", + "aria": "Wähle Ereignisse aus" + }, + "documentTitle": "Überprüfung - Frigate", + "recordings": { + "documentTitle": "Aufnahmen - Frigate" + }, + "calendarFilter": { + "last24Hours": "Letzte 24 Stunden" + }, + "newReviewItems": { + "label": "Neue zu prüfende Objekte anschauen", + "button": "Neue zu prüfende Objekte" + }, + "markTheseItemsAsReviewed": "Diese Objekte als geprüft kennzeichnen", + "camera": "Kamera", + "allCameras": "Alle Kameras", + "markAsReviewed": "Als geprüft kennzeichnen", + "selected_one": "{{count}} ausgewählt", + "selected_other": "{{count}} ausgewählt", + "detected": "erkannt", + "suspiciousActivity": "Verdächtige Aktivität", + "threateningActivity": "Bedrohliche Aktivität", + "zoomIn": "Hereinzoomen", + "zoomOut": "Herauszoomen", + "detail": { + "label": "Detail", + "aria": "Detailansicht umschalten", + "trackedObject_one": "{{count}} Objekt", + "trackedObject_other": "{{count}} Objekte", + "noObjectDetailData": "Keine detaillierten Daten des Objekt verfügbar.", + "noDataFound": "Keine Detaildaten zur Überprüfung", + "settings": "Detailansicht Einstellungen", + "alwaysExpandActive": { + "desc": "Immer die Objektdetails vom aktivem Überprüfungselement erweitern, sofern verfügbar.", + "title": "Immer aktiv erweitern" + } + }, + "objectTrack": { + "trackedPoint": "Verfolgter Punkt", + "clickToSeek": "Klicke, um zu dieser Zeit zu springen" + }, + "normalActivity": "normal", + "needsReview": "benötigt Überprüfung", + "securityConcern": "Sicherheitsbedenken", + "select_all": "alle" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/explore.json new file mode 100644 index 0000000..1068bfd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/explore.json @@ -0,0 +1,293 @@ +{ + "details": { + "timestamp": "Zeitstempel", + "item": { + "title": "Item-Details begutachten", + "desc": "Item-Details begutachten", + "button": { + "share": "Diese Aufnahme teilen", + "viewInExplore": "Ansicht in Erkunden" + }, + "tips": { + "hasMissingObjects": "Passe die Konfiguration an, so dass Frigate verfolgte Objekte für die folgenden Kategorien speichert: {{objects}}", + "mismatch_one": "{{count}} nicht verfügbares Objekt wurde entdeckt und in diese Überprüfung einbezogen. Dieses Objekt hat sich entweder nicht für einen Alarm oder eine Erkennung qualifiziert oder wurde bereits bereinigt/gelöscht.", + "mismatch_other": "{{count}} nicht verfügbare Objekte wurden entdeckt und in diese Überprüfung einbezogen. Diese Objekte haben sich entweder nicht für einen Alarm oder eine Erkennung qualifiziert oder wurden bereits bereinigt/gelöscht." + }, + "toast": { + "success": { + "updatedSublabel": "Unterkategorie erfolgreich aktualisiert.", + "updatedLPR": "Nummernschild erfolgreich aktualisiert.", + "regenerate": "Eine neue Beschreibung wurde von {{provider}} angefordert. Je nach Geschwindigkeit des Anbieters kann es einige Zeit dauern, bis die neue Beschreibung generiert ist.", + "audioTranscription": "Die Audio-Transkription wurde erfolgreich angefordert. Je nach Geschwindigkeit Ihres Frigate-Servers kann die Transkription einige Zeit in Anspruch nehmen." + }, + "error": { + "regenerate": "Der Aufruf von {{provider}} für eine neue Beschreibung ist fehlgeschlagen: {{errorMessage}}", + "updatedSublabelFailed": "Untekategorie konnte nicht aktualisiert werden: {{errorMessage}}", + "updatedLPRFailed": "Aktualisierung des Kennzeichens fehlgeschlagen: {{errorMessage}}", + "audioTranscription": "Die Anforderung der Audio Transkription ist fehlgeschlagen: {{errorMessage}}" + } + } + }, + "label": "Label", + "zones": "Zonen", + "editSubLabel": { + "title": "Unterkategorie bearbeiten", + "desc": "Geben Sie eine neue Unterkategorie für dieses {{label}} ein", + "descNoLabel": "Geben Sie eine neue Unterkategorie für dieses verfolgte Objekt ein" + }, + "editLPR": { + "title": "Kennzeichen bearbeiten", + "desc": "Gib einen neuen Kennzeichenwert für dieses {{label}} ein", + "descNoLabel": "Gib einen neuen Kennzeichenwert für dieses verfolgte Objekt ein" + }, + "topScore": { + "label": "Beste Ergebnisse", + "info": "Die höchste Punktzahl ist der höchste Medianwert für das verfolgte Objekt und kann daher von der auf der Miniaturansicht des Suchergebnisses angezeigten Punktzahl abweichen." + }, + "recognizedLicensePlate": "Erkanntes Kennzeichen", + "estimatedSpeed": "Geschätzte Geschwindigkeit", + "objects": "Objekte", + "camera": "Kamera", + "button": { + "findSimilar": "Finde ähnliche", + "regenerate": { + "title": "Erneuern", + "label": "Beschreibung des verfolgten Objekts neu generieren" + } + }, + "description": { + "label": "Beschreibung", + "placeholder": "Beschreibung des verfolgten Objekts", + "aiTips": "Frigate wird erst dann eine Beschreibung vom generativen KI-Anbieter anfordern, wenn der Lebenszyklus des verfolgten Objekts beendet ist." + }, + "expandRegenerationMenu": "Erneuerungsmenü erweitern", + "regenerateFromSnapshot": "Aus Snapshot neu generieren", + "regenerateFromThumbnails": "Aus Vorschaubild neu generieren", + "tips": { + "descriptionSaved": "Erfolgreich gespeicherte Beschreibung", + "saveDescriptionFailed": "Die Aktualisierung der Beschreibung ist fehlgeschlagen: {{errorMessage}}" + }, + "snapshotScore": { + "label": "Schnappschuss Bewertung" + }, + "score": { + "label": "Ergebnis" + } + }, + "documentTitle": "Erkunde - Frigate", + "generativeAI": "Generative KI", + "exploreIsUnavailable": { + "title": "Erkunden ist nicht Verfügbar", + "embeddingsReindexing": { + "context": "Erkunden kann nach der Re-Indexierung der verfolgten Objekte verwendet werden.", + "startingUp": "Startet…", + "estimatedTime": "Voraussichtlich verbleibende Zeit:", + "finishingShortly": "Bald erledigt", + "step": { + "thumbnailsEmbedded": "Vorschaubilder eingebettet: ", + "descriptionsEmbedded": "Beschreibungen eingebettet: ", + "trackedObjectsProcessed": "Verfolgte Objekte bearbeitet: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "Vision Model", + "visionModelFeatureExtractor": "Vision Model Feature Extraktor", + "textModel": "Text Model", + "textTokenizer": "Text Tokenizer" + }, + "tips": { + "context": "Sie sollten eine Re-Indexierung der verfolgten Objekte durchführen, sobald die Modelle heruntergeladen sind.", + "documentation": "Lesen Sie die Dokumentation" + }, + "error": "Ein Fehler ist aufgetreten. Bitte prüfen Sie die Frigate Logs.", + "context": "Frigate lädt derzeit benötigte Modelle für den Support des \"Semantic Search\"-Features. Je nach der Geschwindigkeit der Netzwerkverbindung kann dies einige Minuten in Anspruch nehmen." + } + }, + "trackedObjectDetails": "Details zu verfolgtem Objekt", + "objectLifecycle": { + "noImageFound": "Kein Bild für diesen Zeitstempel gefunden.", + "createObjectMask": "Objekt-Maske erstellen", + "lifecycleItemDesc": { + "entered_zone": "{{label}} hat {{zones}} betreten", + "visible": "{{label}} erkannt", + "attribute": { + "other": "{{label}} erkannt als {{attribute}}", + "faceOrLicense_plate": "{{attribute}} erkannt für {{label}}" + }, + "external": "{{label}} erkannt", + "active": "{{label}} wurde aktiv", + "gone": "{{label}} hat verlassen", + "stationary": "{{label}} wurde stationär", + "heard": "{{label}} gehört", + "header": { + "ratio": "Verhältnis", + "area": "Bereich", + "zones": "Zonen" + } + }, + "annotationSettings": { + "offset": { + "documentation": "Lesen Sie die Dokumentation ", + "label": "Anmerkungen Versatz", + "desc": "Diese Daten stammen aus dem Erkennungs-Feed der Kamera, werden aber mit Bildern aus dem Aufnahme-Feed überlagert. Es ist unwahrscheinlich, dass die beiden Streams perfekt synchronisiert sind. Daher stimmen die Bounding Box und das Filmmaterial nicht perfekt überein. Das Feld annotation_offset kann jedoch verwendet werden, um dies anzupassen.", + "millisecondsToOffset": "Millisekunden, um die Erkennungen verschoben werden soll. Standard: 0", + "tips": "TIPP: Stelle dir einen Ereignisclip vor, in dem eine Person von links nach rechts läuft. Wenn die Bounding Box der Ereigniszeitleiste durchgehend links von der Person liegt, sollte der Wert verringert werden. Ähnlich verhält es sich, wenn eine Person von links nach rechts geht und die Bounding Box durchgängig vor der Person liegt, dann sollte der Wert erhöht werden.", + "toast": { + "success": "Versatz für {{camera}} wurde in der Konfigurationsdatei gespeichert. Starten Sie Frigate neu, um Ihre Änderungen zu übernehmen." + } + }, + "showAllZones": { + "title": "Zeige alle Zonen", + "desc": "Immer Zonen auf Rahmen anzeigen, in die Objekte eingetreten sind." + }, + "title": "Anmerkungseinstellungen" + }, + "adjustAnnotationSettings": "Anmerkungseinstellungen anpassen", + "title": "Objekt-Lebenszyklus", + "carousel": { + "next": "Nächste Anzeige", + "previous": "Vorherige Anzeige" + }, + "scrollViewTips": "Scrolle um die wichtigsten Momente dieses Objekts anzuzeigen.", + "autoTrackingTips": "Die Positionen der Bounding Box sind bei Kameras mit automatischer Verfolgung ungenau.", + "count": "{{first}} von {{second}}", + "trackedPoint": "Verfolgter Punkt" + }, + "type": { + "details": "Details", + "video": "Video", + "object_lifecycle": "Objekt-Lebenszyklus", + "snapshot": "Snapshot", + "thumbnail": "Vorschaubild", + "tracking_details": "Nachverfolgungs-Details" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Schnappschuss herunterladen", + "aria": "Schnappschuss herunterladen" + }, + "downloadVideo": { + "label": "Video herunterladen", + "aria": "Video herunterladen" + }, + "viewObjectLifecycle": { + "label": "Lebenszyklus von Objekten anzeigen", + "aria": "Den Lebenszyklus des Objekts anzeigen" + }, + "findSimilar": { + "label": "Ähnliches finden", + "aria": "Ähnliche verfolgte Objekte finden" + }, + "submitToPlus": { + "label": "Bei Frigate+ einreichen", + "aria": "Bei Frigate+ einreichen" + }, + "viewInHistory": { + "label": "Ansicht im Verlauf", + "aria": "Ansicht im Verlauf" + }, + "deleteTrackedObject": { + "label": "Dieses verfolgte Objekt löschen" + }, + "audioTranscription": { + "aria": "Audio Transkription anfordern", + "label": "Transkribieren" + }, + "addTrigger": { + "aria": "Einen Trigger für dieses verfolgte Objekt hinzufügen", + "label": "Trigger hinzufügen" + }, + "viewTrackingDetails": { + "label": "Details zum Verfolgen anzeigen", + "aria": "Details zum Verfolgen anzeigen" + }, + "showObjectDetails": { + "label": "Objektpfad anzeigen" + }, + "hideObjectDetails": { + "label": "Objektpfad verbergen" + }, + "downloadCleanSnapshot": { + "label": "Bereinigte Momentaufnahme herunterladen", + "aria": "Bereinigte Momentaufnahme herunterladen" + } + }, + "dialog": { + "confirmDelete": { + "title": "Löschen bestätigen", + "desc": "Beim Löschen dieses verfolgten Objekts werden der Schnappschuss, alle gespeicherten Einbettungen und alle zugehörigen Verfolgungsdetails entfernt. Aufgezeichnetes Filmmaterial dieses verfolgten Objekts in der Verlaufsansicht wird NICHT gelöscht.

    Sind Sie sicher, dass Sie fortfahren möchten?" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Verfolgtes Objekt erfolgreich gelöscht.", + "error": "Das verfolgte Objekt konnte nicht gelöscht werden: {{errorMessage}}" + } + }, + "tooltip": "Entspricht {{type}} bei {{confidence}}%", + "previousTrackedObject": "Vorheriges verfolgtes Objekt", + "nextTrackedObject": "Nächstes verfolgtes Objekt" + }, + "noTrackedObjects": "Keine verfolgten Objekte gefunden", + "fetchingTrackedObjectsFailed": "Fehler beim Abrufen von verfolgten Objekten: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} verfolgtes Objekt ", + "trackedObjectsCount_other": "{{count}} verfolgte Objekte ", + "exploreMore": "Erkunde mehr {{label}} Objekte", + "aiAnalysis": { + "title": "KI-Analyse" + }, + "concerns": { + "label": "Bedenken" + }, + "trackingDetails": { + "noImageFound": "Kein Bild mit diesem Zeitstempel gefunden.", + "createObjectMask": "Objekt-Maske erstellen", + "scrollViewTips": "Klicke, um die relevanten Momente aus dem Lebenszyklus dieses Objektes zu sehen.", + "lifecycleItemDesc": { + "visible": "{{label}} erkannt", + "entered_zone": "{{label}} betrat {{zones}}", + "active": "{{label}} wurde aktiv", + "stationary": "{{label}} wurde stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} erkannt für {{label}}", + "other": "{{label}} erkannt als {{attribute}}" + }, + "gone": "{{label}} hat verlassen", + "heard": "{{label}} wurde gehört", + "external": "{{label}} erkannt", + "header": { + "zones": "Zonen", + "ratio": "Verhältnis", + "area": "Bereich", + "score": "Bewertung" + } + }, + "annotationSettings": { + "title": "Anmerkungseinstellungen", + "showAllZones": { + "title": "Zeige alle Zonen", + "desc": "Immer Zonen auf Rahmen anzeigen, in die Objekte eingetreten sind." + }, + "offset": { + "label": "Anmerkungen Versatz", + "desc": "Diese Daten stammen aus dem Erkennungsfeed der Kamera, werden jedoch über Bilder aus dem Aufzeichnungsfeed gelegt. Es ist unwahrscheinlich, dass beide Streams perfekt synchron sind. Daher stimmen der Begrenzungsrahmen und das Filmmaterial nicht vollständig überein. Mit dieser Einstellung lassen sich die Anmerkungen zeitlich nach vorne oder hinten verschieben, um sie besser an das aufgezeichnete Filmmaterial anzupassen.", + "millisecondsToOffset": "Millisekunden, um Erkennungs-Anmerkungen zu verschieben. Standard: 0", + "tips": "Verringere den Wert, wenn die Videowiedergabe den Boxen und Wegpunkten voraus ist, und erhöhe den Wert, wenn die Videowiedergabe hinter ihnen zurückbleibt. Dieser Wert kann negativ sein.", + "toast": { + "success": "Der Anmerkungs-Offset für {{camera}} wurde in der Konfigurationsdatei gespeichert." + } + } + }, + "carousel": { + "previous": "Vorherige Anzeige", + "next": "Nächste Anzeige" + }, + "title": "Verfolgungsdetails", + "adjustAnnotationSettings": "Anmerkungseinstellungen anpassen", + "autoTrackingTips": "Die Positionen der Begrenzungsrahmen sind bei Kameras mit automatischer Verfolgung ungenau.", + "count": "{{first}} von {{second}}", + "trackedPoint": "Verfolgter Punkt" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/exports.json new file mode 100644 index 0000000..c3bae12 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/exports.json @@ -0,0 +1,23 @@ +{ + "deleteExport": "Export löschen", + "editExport": { + "title": "Export umbenennen", + "desc": "Gib einen neuen Namen für diesen Export an.", + "saveExport": "Export speichern" + }, + "documentTitle": "Exportieren - Frigate", + "deleteExport.desc": "Soll {{exportName}} wirklich gelöscht werden?", + "search": "Suche", + "noExports": "Keine Exporte gefunden", + "toast": { + "error": { + "renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Export teilen", + "downloadVideo": "Video herunterladen", + "editName": "Name ändern", + "deleteExport": "Export löschen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/faceLibrary.json new file mode 100644 index 0000000..bdbd448 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "description": { + "placeholder": "Gib einen Name für diese Kollektion ein", + "addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem ein Bild hinzufügst.", + "invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten." + }, + "details": { + "person": "Person", + "confidence": "Vertrauen", + "timestamp": "Zeitstempel", + "faceDesc": "Details des verfolgten Objekts, das dieses Gesicht erzeugt hat", + "face": "Gesichtsdetails", + "subLabelScore": "Sub Label Score", + "scoreInfo": "Der Sub Label Score ist der gewichtete Score für alle erkannten Gesichter und kann daher vom Score abweichen, der auf dem Schnappschuss angezeigt wird.", + "unknown": "Unbekannt" + }, + "uploadFaceImage": { + "title": "Lade Gesichtsbild hoch", + "desc": "Lade ein Bild zur Gesichtserkennung hoch und füge es für {{pageToggle}} hinzu" + }, + "createFaceLibrary": { + "title": "Kollektion erstellen", + "new": "Lege ein neues Gesicht an", + "desc": "Erstelle eine neue Kollektion", + "nextSteps": "Um eine solide Grundlage zu bilden:
  • Benutze den \"Aktuelle Erkennungen\" Tab, um Bilder für jede erkannte Person auszuwählen und zu trainieren.
  • Konzentriere dich für gute Ergebnisse auf Frontalfotos; vermeide Bilder zu Trainingszwecken, bei denen Gesichter aus einem Winkel erfasst wurden.
  • " + }, + "documentTitle": "Gesichtsbibliothek - Frigate", + "selectItem": "Wähle {{item}}", + "selectFace": "Wähle Gesicht", + "imageEntry": { + "dropActive": "Ziehe das Bild hierher…", + "dropInstructions": "Ziehe ein Bild hier her, füge es ein oder klicke um eines auszuwählen", + "maxSize": "Maximale Größe: {{size}} MB", + "validation": { + "selectImage": "Bitte wähle ein Bild aus." + } + }, + "button": { + "addFace": "Gesicht hinzufügen", + "uploadImage": "Bild hochladen", + "deleteFaceAttempts": "Lösche Gesichter", + "reprocessFace": "Gesichter erneut verarbeiten", + "renameFace": "Gesicht umbenennen", + "deleteFace": "Lösche Gesicht" + }, + "train": { + "title": "Kürzliche Erkennungen", + "aria": "Wähle aktuelle Erkennungen", + "empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung", + "titleShort": "frisch" + }, + "deleteFaceLibrary": { + "title": "Lösche Name", + "desc": "Möchtest du die Sammlung {{name}} löschen? Alle zugehörigen Gesichter werden gelöscht." + }, + "readTheDocs": "Lies die Dokumentation", + "trainFaceAs": "Trainiere Gesicht als:", + "trainFace": "Trainiere Gesicht", + "toast": { + "success": { + "uploadedImage": "Das Bild wurde erfolgreich hochgeladen.", + "deletedFace_one": "Erfolgreich {{count}} Gesicht gelöscht.", + "deletedFace_other": "Erfolgreich {{count}} Gesichter gelöscht.", + "deletedName_one": "{{count}} Gesicht wurde erfolgreich gelöscht.", + "deletedName_other": "{{count}} Gesichter wurden erfolgreich gelöscht.", + "addFaceLibrary": "{{name}} wurde erfolgreich in die Gesichtsbibliothek aufgenommen!", + "trainedFace": "Gesicht erfolgreich trainiert.", + "updatedFaceScore": "Gesichtsbewertung erfolgreich auf {{name}} ({{score}}) aktualisiert.", + "renamedFace": "Gesicht erfolgreich in {{name}} umbenannt" + }, + "error": { + "deleteFaceFailed": "Das Löschen ist fehlgeschlagen: {{errorMessage}}", + "uploadingImageFailed": "Bild kann nicht hochgeladen werden: {{errorMessage}}", + "addFaceLibraryFailed": "Der Gesichtsname konnte nicht gesetzt werden: {{errorMessage}}", + "trainFailed": "Ausbildung fehlgeschlagen: {{errorMessage}}", + "updateFaceScoreFailed": "Aktualisierung der Gesichtsbewertung fehlgeschlagen: {{errorMessage}}", + "deleteNameFailed": "Name kann nicht gelöscht werden: {{errorMessage}}", + "renameFaceFailed": "Gesicht konnte nicht umbenannt werden: {{errorMessage}}" + } + }, + "steps": { + "uploadFace": "Lade Bild des Gesichts hoch", + "nextSteps": "Nächste Schritte", + "faceName": "Gebe Gesichtsname ein", + "description": { + "uploadFace": "Lade ein Bild von {{name}} hoch, das ihr/sein Gesicht aus einer frontalen Perspektive zeigt. Das Bild muss nicht auf das Gesicht zugeschnitten sein." + } + }, + "renameFace": { + "title": "Gesicht umbenennen", + "desc": "Gib den neuen Namen für {{name}} ein" + }, + "collections": "Sammlungen", + "deleteFaceAttempts": { + "title": "Lösche Gesichter", + "desc_one": "Bist du sicher, dass du {{count}} Gesicht löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "desc_other": "Bist du sicher, dass du {{count}} Gesichter löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden." + }, + "nofaces": "Keine Gesichter verfügbar", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/live.json new file mode 100644 index 0000000..e0bf995 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/live.json @@ -0,0 +1,189 @@ +{ + "lowBandwidthMode": "Modus für geringe Bandbreite", + "twoWayTalk": { + "enable": "Gegensprechfunktion aktivieren", + "disable": "Gegensprechfunktion ausschalten" + }, + "cameraAudio": { + "enable": "Kamera-Audio aktivieren", + "disable": "Kamera-Audio deaktivieren" + }, + "ptz": { + "move": { + "clickMove": { + "disable": "Bewegen per Klick deaktivieren", + "enable": "Bewegen per Klick aktivieren", + "label": "Zum Zentrieren der Kamera ins Bild klicken" + }, + "up": { + "label": "PTZ-Kamera nach oben bewegen" + }, + "left": { + "label": "PTZ-Kamera nach links bewegen" + }, + "down": { + "label": "PTZ-Kamera nach unten bewegen" + }, + "right": { + "label": "PTZ-Kamera nach rechts bewegen" + } + }, + "zoom": { + "in": { + "label": "PTZ-Kamera rein zoomen" + }, + "out": { + "label": "PTZ-Kamera heraus zoomen" + } + }, + "presets": "PTZ-Kamera Voreinstellungen", + "frame": { + "center": { + "label": "Klicke in den Rahmen, um die PTZ-Kamera zu zentrieren" + } + }, + "focus": { + "in": { + "label": "PTZ Kamera hinein fokussieren" + }, + "out": { + "label": "PTZ Kamera hinaus fokussieren" + } + } + }, + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "muteCameras": { + "disable": "Stumm aller Kameras aufheben", + "enable": "Alle Kameras auf stumm" + }, + "recording": { + "disable": "Aufzeichnung deaktivieren", + "enable": "Aufzeichnung aktivieren" + }, + "snapshots": { + "enable": "Schnappschüsse aktivieren", + "disable": "Schnappschüsse deaktivieren" + }, + "autotracking": { + "disable": "Autotracking deaktivieren", + "enable": "Autotracking aktivieren" + }, + "streamStats": { + "enable": "Stream Statistiken anzeigen", + "disable": "Stream-Statistiken ausblenden" + }, + "manualRecording": { + "title": "On-Demand", + "showStats": { + "label": "Statistiken anzeigen", + "desc": "Aktivieren Sie diese Option, um Stream-Statistiken als Overlay über dem Kamera-Feed anzuzeigen." + }, + "started": "Manuelle On-Demand Aufzeichnung gestartet.", + "failedToStart": "Manuelle On-Demand Aufzeichnung konnte nicht gestartet werden.", + "recordDisabledTips": "Da die Aufzeichnung in der Konfiguration für diese Kamera deaktiviert oder eingeschränkt ist, wird nur ein Schnappschuss gespeichert.", + "end": "On-Demand Aufzeichnung beenden", + "ended": "Manuelle On-Demand Aufzeichnung beendet.", + "playInBackground": { + "desc": "Aktivieren Sie diese Option, um das Streaming fortzusetzen, wenn der Player ausgeblendet ist.", + "label": "Im Hintergrund abspielen" + }, + "tips": "Lade einen Sofort-Schnappschuss herunter oder starte ein manuelles Ereignis basierend auf den Aufbewahrungseinstellungen für Aufzeichnungen dieser Kamera.", + "debugView": "Debug-Ansicht", + "start": "On-Demand Aufzeichnung starten", + "failedToEnd": "Die manuelle On-Demand Aufzeichnung konnte nicht beendet werden." + }, + "streamingSettings": "Streaming Einstellungen", + "notifications": "Benachrichtigungen", + "stream": { + "audio": { + "available": "Audio ist für diesen Stream verfügbar", + "tips": { + "title": "Audio muss von deiner Kamera ausgegeben und für diesen Stream in go2rtc konfiguriert werden.", + "documentation": "Dokumentation lesen " + }, + "unavailable": "Für diesen Stream ist kein Audio verfügbar" + }, + "twoWayTalk": { + "tips": "Ihr Gerät muss die Funktion unterstützen und WebRTC muss für die bidirektionale Kommunikation konfiguriert sein.", + "tips.documentation": "Dokumentation lesen ", + "available": "Für diesen Stream ist eine Zwei-Wege-Sprechfunktion verfügbar", + "unavailable": "Zwei-Wege-Kommunikation für diesen Stream nicht verfügbar" + }, + "lowBandwidth": { + "tips": "Die Live-Ansicht befindet sich aufgrund von Puffer- oder Stream-Fehlern im Modus mit geringer Bandbreite.", + "resetStream": "Stream zurücksetzen" + }, + "title": "Stream", + "playInBackground": { + "tips": "Aktivieren Sie diese Option, um das Streaming fortzusetzen, wenn der Player ausgeblendet ist.", + "label": "Im Hintergrund abspielen" + }, + "debug": { + "picker": "Stream Auswahl nicht verfügbar im Debug Modus. Die Debug Ansicht nutzt immer den Stream, welcher der Rolle zugewiesen ist." + } + }, + "effectiveRetainMode": { + "modes": { + "motion": "Bewegung", + "active_objects": "Aktive Objekte", + "all": "Alle" + }, + "notAllTips": "Dein Konfiguration zur Aufzeichnungsaufbewahrung von {{source}} ist eingestellt auf -Modus:{{effectiveRetainMode}} , daher werden in dieser On-Demand Aufzeichnung nur Segmente gespeichert mit{{effectiveRetainModeName}} ." + }, + "editLayout": { + "group": { + "label": "Kameragruppe bearbeiten" + }, + "exitEdit": "Bearbeitung beenden", + "label": "Layout bearbeiten" + }, + "camera": { + "enable": "Kamera aktivieren", + "disable": "Kamera deaktivieren" + }, + "audioDetect": { + "enable": "Audioerkennung aktivieren", + "disable": "Audioerkennung deaktivieren" + }, + "detect": { + "enable": "Erkennung aktivieren", + "disable": "Erkennung deaktivieren" + }, + "cameraSettings": { + "objectDetection": "Objekterkennung", + "recording": "Aufnahme", + "snapshots": "Schnappschüsse", + "cameraEnabled": "Kamera aktiviert", + "autotracking": "Autotracking", + "audioDetection": "Audioerkennung", + "title": "{{camera}} Einstellungen", + "transcription": "Audio Transkription" + }, + "history": { + "label": "Historisches Filmmaterial zeigen" + }, + "audio": "Audio", + "suspend": { + "forTime": "Aussetzen für: " + }, + "transcription": { + "enable": "Live Audio Transkription einschalten", + "disable": "Live Audio Transkription ausschalten" + }, + "noCameras": { + "title": "Keine Kameras konfiguriert", + "description": "Beginne indem du eine Kamera anschließt.", + "buttonText": "Kamera hinzufügen", + "restricted": { + "title": "Keine Kamera verfügbar", + "description": "Sie haben keine Berechtigung, Kameras in dieser Gruppe anzuzeigen." + } + }, + "snapshot": { + "takeSnapshot": "Sofort-Schnappschuss herunterladen", + "noVideoSource": "Keine Video-Quelle für Schnappschuss verfügbar.", + "captureFailed": "Die Aufnahme des Schnappschusses ist fehlgeschlagen.", + "downloadStarted": "Schnappschuss Download gestartet." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/recording.json new file mode 100644 index 0000000..354cd40 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "calendar": "Kalender", + "filters": "Filter", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Endzeit muss nach Startzeit liegen", + "noValidTimeSelected": "Gewählter Zeitraum ist ungültig" + } + }, + "filter": "Filter" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/search.json new file mode 100644 index 0000000..5729716 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/search.json @@ -0,0 +1,74 @@ +{ + "savedSearches": "Gespeicherte Suchen", + "searchFor": "Suche nach {{inputValue}}", + "button": { + "save": "Suche speichern", + "filterActive": "Filter aktiv", + "delete": "Gespeicherte Suche löschen", + "filterInformation": "Information filtern", + "clear": "Suche löschen" + }, + "trackedObjectId": "ID verfolgtes Objekt", + "filter": { + "label": { + "cameras": "Kameras", + "zones": "Zonen", + "search_type": "Suchtyp", + "before": "Vor", + "after": "Nach", + "min_score": "Minimalwert", + "max_score": "Maximalwert", + "recognized_license_plate": "Erkanntes Autokennzeichen", + "has_clip": "Clip vorhanden", + "has_snapshot": "Schnappschuss vorhanden", + "min_speed": "Minimalgeschwindigkeit", + "max_speed": "Maximalgeschwindigkeit", + "time_range": "Zeitraum", + "labels": "Labels", + "sub_labels": "Unterlabels" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Das \"Vor\" Datum muss später als das \"Nach\" Datum sein.", + "minScoreMustBeLessOrEqualMaxScore": "Der \"Minimalwert\" muss kleiner oder gleich dem \"Maximalwert\" sein.", + "afterDatebeEarlierBefore": "Das \"Nach\" Datum muss früher als das \"Vor\" Datum sein.", + "maxScoreMustBeGreaterOrEqualMinScore": "Der \"Maximalwert\" muss größer oder gleich dem \"Minimalwert\" sein.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Der \"Minimalgeschwindigkeit\" muss kleiner oder gleich der \"Maximalgeschwindigkeit\" sein.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Der \"Maximalgeschwindigkeit\" muss größer oder gleich der \"Maximalgeschwindigkeit\" sein." + } + }, + "header": { + "currentFilterType": "Filterwerte", + "noFilters": "Filter", + "activeFilters": "Aktive Filter" + }, + "tips": { + "desc": { + "step": "
    • Gib einen Filternamen gefolgt von einem Doppelpunkt ein (z.B. \"Kameras:\").
    • Wähle einen Wert aus den Vorschlägen aus oder tippe einen individuellen ein.
    • Verwende mehrere Filter, indem Du sie nacheinander mit Leerzeichen getrennt eingibst.
    • Datumfilter (Vor: und Nach:) im Format {{DateFormat}}.
    • Zeitraumfilter im Format {{exampleTime}}.
    • Lösche Filter durch Drücken des \"x\" daneben.
    ", + "text": "Mit Filtern kannst Du Suchergebnisse eingrenzen. Hier erfährst Du, wie diese im Eingabefeld verwendet werden können:", + "example": "Beispiel: Kameras:Tor Label:Person Vor:01012024 Zeitraum:15:00-16:00", + "step3": "Verwende mehrere Filter, indem du sie nacheinander mit einem Leerzeichen dazwischen hinzufügst.", + "step2": "Wähle einen Wert aus den Vorschlägen aus oder gib einen eigenen ein.", + "step1": "Gib einen Filter-Schlüssel ein, gefolgt von einem Doppelpunkt (z.B. „kameras:“).", + "exampleLabel": "Beispiel:", + "step6": "Entferne Filter, indem du auf das „x“ daneben klickst.", + "step4": "Datumsfilter (bevor: und nach:) verwenden das Format {{DateFormat}}.", + "step5": "Der Zeitbereichsfilter verwendet das Format {{exampleTime}}." + }, + "title": "Wie man Textfilter verwendet" + }, + "searchType": { + "thumbnail": "Vorschaubild", + "description": "Beschreibung" + } + }, + "similaritySearch": { + "title": "Ähnlichkeitssuche", + "clear": "Ähnlichkeitssuche löschen", + "active": "Aktive Ähnlichkeitssuche" + }, + "search": "Suche", + "placeholder": { + "search": "Suchen…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/settings.json new file mode 100644 index 0000000..b43173f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/settings.json @@ -0,0 +1,1299 @@ +{ + "documentTitle": { + "default": "Einstellungen - Frigate", + "authentication": "Authentifizierungseinstellungen – Frigate", + "camera": "Kameraeinstellungen - Frigate", + "masksAndZones": "Masken- und Zoneneditor – Frigate", + "object": "Debug - Frigate", + "general": "UI-Einstellungen – Frigate", + "frigatePlus": "Frigate+ Einstellungen – Frigate", + "classification": "Klassifizierungseinstellungen – Frigate", + "motionTuner": "Bewegungserkennungs-Optimierer – Frigate", + "notifications": "Benachrichtigungseinstellungen", + "enrichments": "Erweiterte Statistiken - Frigate", + "cameraManagement": "Kameras verwalten - Frigate", + "cameraReview": "Kameraeinstellungen prüfen - Frigate" + }, + "menu": { + "ui": "Benutzeroberfläche", + "cameras": "Kameraeinstellungen", + "classification": "Klassifizierung", + "masksAndZones": "Maskierungen / Zonen", + "motionTuner": "Bewegungserkennungs-Optimierer", + "debug": "Debug", + "frigateplus": "Frigate+", + "users": "Benutzer", + "notifications": "Benachrichtigungen", + "enrichments": "Erkennungsfunktionen", + "triggers": "Auslöser", + "roles": "Rollen", + "cameraManagement": "Verwaltung", + "cameraReview": "Überprüfung" + }, + "dialog": { + "unsavedChanges": { + "title": "Du hast nicht gespeicherte Änderungen.", + "desc": "Möchtest Du deine Änderungen speichern, bevor du fortfährst?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Keine Kamera" + }, + "general": { + "title": "Einstellungen der Benutzeroberfläche", + "liveDashboard": { + "title": "Live Übersicht", + "playAlertVideos": { + "label": "Spiele Videos mit Alarmierung", + "desc": "Standardmäßig werden die letzten Warnmeldungen auf dem Live-Dashboard als kurze Videoschleifen abgespielt. Deaktiviere diese Option, um nur ein statisches Bild der letzten Warnungen auf diesem Gerät/Browser anzuzeigen." + }, + "automaticLiveView": { + "desc": "Wechsle automatisch zur Live Ansicht der Kamera, wenn einen Aktivität erkannt wurde. Wenn du diese Option deaktivierst, werden die statischen Kamerabilder auf der Liveübersicht nur einmal pro Minute aktualisiert.", + "label": "Automatische Live Ansicht" + }, + "displayCameraNames": { + "label": "Immer Namen der Kamera anzeigen", + "desc": "Kamerabezeichnung immer im einem Chip im Live-View-Dashboard für mehrere Kameras anzeigen." + }, + "liveFallbackTimeout": { + "label": "Live Player Ausfallzeitlimit", + "desc": "Wenn der hochwertige Live-Stream einer Kamera nicht verfügbar ist, wechsle nach dieser Anzahl von Sekunden in den Modus für geringe Bandbreite. Standard: 3." + } + }, + "storedLayouts": { + "title": "Gespeicherte Ansichten", + "clearAll": "Lösche alle Ansichten", + "desc": "Das Layout der Kameras in einer Kameragruppe kann verschoben/geändert werden. Die Positionen werden im lokalen Cache des Browsers gespeichert." + }, + "cameraGroupStreaming": { + "title": "Einstellungen für Kamera-Gruppen-Streaming", + "clearAll": "Alle Streamingeinstellungen löschen", + "desc": "Die Streaming-Einstellungen für jede Kameragruppe werden im lokalen Cache des Browsers gespeichert." + }, + "recordingsViewer": { + "title": "Aufzeichnungsbetrachter", + "defaultPlaybackRate": { + "desc": "Standard-Wiedergabegeschwindigkeit für die Wiedergabe von Aufnahmen.", + "label": "Standard-Wiedergabegeschwindigkeit" + } + }, + "calendar": { + "title": "Kalender", + "firstWeekday": { + "label": "Erster Wochentag", + "desc": "Der Tag, an dem die Wochen des Überprüfungs-Kalenders beginnen.", + "sunday": "Sonntag", + "monday": "Montag" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Gespeichertes Layout für {{cameraName}} gelöscht", + "clearStreamingSettings": "Streaming Einstellungen aller Kameragruppen bereinigt." + }, + "error": { + "clearStoredLayoutFailed": "Das gespeicherte Layout konnte nicht gelöscht werden: {{errorMessage}}", + "clearStreamingSettingsFailed": "Die Streaming-Einstellungen konnten nicht gelöscht werden: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Klassifizierungseinstellungen", + "semanticSearch": { + "title": "Semantische Suche", + "desc": "Die semantische Suche in Frigate ermöglicht es, verfolgte Objekte innerhalb der Überprüfungselemente zu finden, indem entweder das Bild selbst, eine benutzerdefinierte Textbeschreibung oder eine automatisch generierte Beschreibung verwendet wird.", + "readTheDocumentation": "Lesen Sie die Dokumentation", + "reindexNow": { + "alreadyInProgress": "Neu-Indizierung läufts bereits.", + "label": "Neuindizieren", + "confirmTitle": "Bestätige Neu-Indizierung", + "confirmButton": "Neu-Indizieren", + "success": "Neuindizierung erfolgreich gestartet.", + "error": "Starten der Neuindizierung fehlgeschlagen: {{errorMessage}}", + "desc": "Durch die Neuindizierung werden die Einbettungen für alle verfolgten Objekte neu generiert. Dieser Prozess läuft im Hintergrund und kann Ihre CPU überlasten. Je nach Anzahl der verfolgten Objekte kann er einige Zeit in Anspruch nehmen.", + "confirmDesc": "Möchten Sie alle verfolgten Objekteinbettungen wirklich neu indizieren? Dieser Vorgang läuft im Hintergrund, kann aber Ihre CPU überlasten und einige Zeit in Anspruch nehmen. Sie können den Fortschritt auf der Explore-Seite verfolgen." + }, + "modelSize": { + "large": { + "title": "groß", + "desc": "Bei Verwendung von large wird das vollständige Jina-Modell verwendet und ggf. automatisch auf der GPU ausgeführt." + }, + "label": "Model Größe", + "small": { + "title": "klein", + "desc": "Durch die Verwendung von small wird eine quantisierte Version des Modells eingesetzt, die weniger RAM verwendet und schneller auf der CPU läuft, wobei der Unterschied in der Einbettungsqualität sehr gering ist." + }, + "desc": "Die Größe des Modells, das für semantische Sucheinbettungen verwendet wird." + } + }, + "birdClassification": { + "desc": "Die Vogelklassifizierung identifiziert bekannte Vögel mithilfe eines quantisierten Tensorflow-Modells. Wenn ein bekannter Vogel erkannt wird, wird sein allgemeiner Name als sub_label hinzugefügt. Diese Informationen sind in der Benutzeroberfläche, in Filtern und in Benachrichtigungen enthalten.", + "title": "Vogel-Klassifizierung" + }, + "licensePlateRecognition": { + "readTheDocumentation": "Lies die Dokumentation", + "title": "Nummernschilderkennung", + "desc": "Frigate kann Nummernschilder an Fahrzeugen erkennen und die erkannten Zeichen automatisch dem Feld „recognized_license_plate“ oder einem bekannten Namen als Unterbezeichnung für Objekte vom Typ „Auto“ hinzufügen. Ein häufiger Anwendungsfall ist das Lesen der Nummernschilder von Autos, die in eine Einfahrt einfahren oder auf einer Straße vorbeifahren." + }, + "faceRecognition": { + "readTheDocumentation": "Lies die Dokumentation", + "modelSize": { + "small": { + "title": "klein", + "desc": "ei der Verwendung von small wird ein FaceNet-Gesichtseinbettungsmodell eingesetzt, das auf den meisten CPUs effizient läuft." + }, + "label": "Model Größe", + "large": { + "title": "groß", + "desc": "Bei der Verwendung von large wird ein ArcFace-Gesichtseinbettungsmodell verwendet und ggf. automatisch auf der GPU ausgeführt." + }, + "desc": "Die Größe des für die Gesichtserkennung verwendeten Modells." + }, + "title": "Gesichtserkennung", + "desc": "Mithilfe der Gesichtserkennung können Personen Namen zugewiesen werden. Sobald das Gesicht erkannt wird, weist Frigate den Namen der Person als Unterbezeichnung zu. Diese Informationen werden in die Benutzeroberfläche, Filter und Benachrichtigungen integriert." + }, + "toast": { + "error": "Sichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}", + "success": "Die Klassifizierungseinstellungen wurden gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen." + }, + "restart_required": "Neustart erforderlich (Klassifizierungseinstellungen geändert)", + "unsavedChanges": "Nicht gespeicherte Änderungen der Klassifizierungseinstellungen" + }, + "camera": { + "reviewClassification": { + "toast": { + "success": "Die Konfiguration der Klassifizierung wurde gespeichert. Starte Frigate neu, um die Änderungen zu übernehmen." + }, + "title": "Überprüfung der Klassifikation", + "selectAlertsZones": "Zonen für Warnungen auswählen", + "limitDetections": "Begrenzung der Erkennungen auf bestimmte Zonen", + "readTheDocumentation": "Lies die Dokumentation", + "noDefinedZones": "Für diese Kamera sind keine Zonen definiert.", + "selectDetectionsZones": "Zonen für Erkennungen auswählen", + "objectAlertsTips": "Alle {{alertsLabels}} -Objekte auf {{cameraName}} werden als Warnungen angezeigt.", + "desc": "Frigate kategorisiert Überprüfungselemente als Warnungen und Erkennungen. Standardmäßig werden alle Objekte vom Typ Person und Auto als Warnungen betrachtet. Sie können die Kategorisierung Ihrer Überprüfungselemente verfeinern, indem Sie die erforderlichen Zonen dafür konfigurieren.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}} Objekte, die in {{zone}} auf {{cameraName}} erkannt werden , werden als Warnungen angezeigt.", + "objectDetectionsTips": "Alle {{detectionsLabels}} Objekte, die auf {{cameraName}} nicht kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}} Objekte, die in {{zone}} auf {{cameraName}} nicht kategorisiert sind, werden als Erkennungen angezeigt.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}} Objekte, die auf {{cameraName}} nicht kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt.", + "notSelectDetections": "Alle {{detectionsLabels}} Objekte, die in {{zone}} auf {{cameraName}} erkannt wurden und nicht als Warnungen kategorisiert sind, werden unabhängig von der Zone, in der sie sich befinden, als Erkennungen angezeigt." + }, + "unsavedChanges": "Nicht gespeicherte Überprüfung der Klassifizierungseinstellungen für {{camera}}" + }, + "streams": { + "title": "Streams", + "desc": "Deaktiviere eine Kamera vorübergehend, bis Frigate neu gestartet wird. Das Deaktivieren einer Kamera stoppt die Verarbeitung der Streams dieser Kamera durch Frigate vollständig. Erkennung, Aufzeichnung und Debugging sind dann nicht mehr möglich.
    Hinweis: Go2RTC-Restreams werden dadurch nicht deaktiviert." + }, + "review": { + "title": "Überprüfung", + "alerts": "Warnungen ", + "detections": "Erkennungen ", + "desc": "Aktiviere/deaktiviere Benachrichtigungen und Erkennungen für diese Kamera vorübergehend, bis Frigate neu gestartet wird. Wenn deaktiviert, werden keine neuen Überprüfungseinträge erstellt. " + }, + "title": "Kameraeinstellungen", + "object_descriptions": { + "title": "Generative KI-Objektbeschreibungen", + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für verfolgte Objekte auf dieser Kamera angefordert." + }, + "cameraConfig": { + "ffmpeg": { + "roles": "Rollen", + "pathRequired": "Stream-Pfad ist erforderlich", + "path": "Stream-Pfad", + "inputs": "Eingabe Streams", + "pathPlaceholder": "rtsp://...", + "rolesRequired": "Mindestens eine Rolle ist erforderlich", + "rolesUnique": "Jede Rolle (Audio, Erkennung, Aufzeichnung) kann nur einem Stream zugewiesen werden", + "addInput": "Eingabe-Stream hinzufügen", + "removeInput": "Eingabe-Stream entfernen", + "inputsRequired": "Mindestens ein Eingabe-Stream ist erforderlich" + }, + "enabled": "Aktiviert", + "namePlaceholder": "z. B., Vorder_Türe", + "nameInvalid": "Der Name der Kamera darf nur Buchstaben, Zahlen, Unterstriche oder Bindestriche enthalten", + "name": "Kamera Name", + "edit": "Kamera bearbeiten", + "add": "Kamera hinzufügen", + "description": "Kameraeinstellungen einschließlich Stream-Eingänge und Rollen konfigurieren.", + "nameRequired": "Kameraname ist erforderlich", + "toast": { + "success": "Kamera {{cameraName}} erfolgreich gespeichert" + }, + "nameLength": "Der Name der Kamera darf maximal 24 Zeichen lang sein." + }, + "backToSettings": "Zurück zu den Kamera Einstellungen", + "selectCamera": "Kamera wählen", + "editCamera": "Kamera bearbeiten:", + "addCamera": "Neue Kamera hinzufügen", + "review_descriptions": { + "desc": "Generativen KI-Objektbeschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Funktion deaktiviert ist, werden keine KI-generierten Beschreibungen für Überprüfungselemente auf dieser Kamera angefordert.", + "title": "Beschreibungen zur generativen KI-Überprüfung" + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Zonenname enthält unzulässige Zeichen.", + "alreadyExists": "Für diese Kamera existiert bereits eine Zone mit diesem Namen.", + "mustBeAtLeastTwoCharacters": "Der Zonenname muss aus mindestens 2 Zeichen bestehen.", + "mustNotBeSameWithCamera": "Der Zonenname darf nicht mit dem Kameranamen identisch sein.", + "mustNotContainPeriod": "Der Zonenname darf keine Punkte enthalten.", + "mustHaveAtLeastOneLetter": "Der Name der Zone muss mindestens einen Buchstaben enthalten." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Die Verweildauer muss größer oder gleich 0 sein." + } + }, + "distance": { + "error": { + "text": "Der Abstand muss größer als oder gleich 0.1 sein.", + "mustBeFilled": "Alle Entfernungsfelder müssen ausgefüllt werden, um die Geschwindigkeitsschätzung zu verwenden." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Die Trägheit muss über 0 liegen." + } + }, + "polygonDrawing": { + "removeLastPoint": "Letzten Punkt entfernen", + "reset": { + "label": "Alle Punkte löschen" + }, + "snapPoints": { + "true": "Fangpunkte", + "false": "Punkte nicht einrasten" + }, + "delete": { + "title": "Löschen bestätigen", + "success": "{{name}} wurde gelöscht.", + "desc": "Bist du sicher, dass du die {{type}} {{name}} löschen möchtest?" + }, + "error": { + "mustBeFinished": "Polygonzeichnung muss vor dem Speichern abgeschlossen sein." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Der Geschwindigkeitsschwellwert muss größer oder gleich 0,1 sein." + } + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Die Koordinaten konnten nicht in die Zwischenablage kopiert werden." + }, + "success": { + "copyCoordinates": "Koordinaten von {{polyName}} wurden in die Zwischenablage kopiert." + } + }, + "filter": { + "all": "Alle Maskierungen und Zonen" + }, + "zones": { + "edit": "Zone bearbeiten", + "toast": { + "success": "Die Zone ({{zoneName}}) wurde gespeichert." + }, + "desc": { + "documentation": "Dokumentation", + "title": "Zonen ermöglichen es dir, einen bestimmten Bereich des Bildes festzulegen, damit du bestimmen kannst, ob sich ein Objekt in einem bestimmten Bereich befindet oder nicht." + }, + "allObjects": "Alle Objekte", + "speedEstimation": { + "title": "Geschwindigkeitsschätzung", + "desc": "Aktiviere die Geschwindigkeitsabschätzung für Objekte in diesem Bereich. Der Bereich muss genau 4 Punkte haben.", + "docs": "Bitte lies die Dokumentation", + "lineADistance": "Spur A Distanz ({{unit}})", + "lineDDistance": "Spur D Distanz ({{unit}})", + "lineBDistance": "Spur B Distanz ({{unit}})", + "lineCDistance": "Spur C Distanz ({{unit}})" + }, + "label": "Zonen", + "documentTitle": "Zone bearbeiten - Frigate", + "add": "Zone hinzufügen", + "name": { + "title": "Name", + "inputPlaceHolder": "Geben Sie einen Namen ein…", + "tips": "Die Bezeichnung muss mindestens 2 Zeichen lang sein, mindestens einen Buchstaben enthalten und darf nicht die Bezeichnung einer Kamera oder einer anderen Zone sein." + }, + "objects": { + "title": "Objekte", + "desc": "Liste der Objekte, die zu diesem Bereich gehören." + }, + "speedThreshold": { + "title": "Geschwindigkeitsschwelle ({{unit}})", + "desc": "Legt eine Mindestgeschwindigkeit für Objekte fest, damit sie als \"in diesem Bereich\" betrachtet werden können.", + "toast": { + "error": { + "loiteringTimeError": "Zonen mit einer Verweilzeit größer als 0 sollten nicht für die Geschwindigkeitsmessung verwendet werden.", + "pointLengthError": "Die Geschwindigkeitsabschätzung wurde für diesen Bereich deaktiviert. Bereiche mit Geschwindigkeitsabschätzung müssen genau 4 Punkte haben." + } + } + }, + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "clickDrawPolygon": "Klicke, um ein Polygon auf dem Bild zu zeichnen.", + "inertia": { + "desc": "Legt fest, wie viele Bilder ein Objekt in einem Bereich sein muss, bevor es \"als in dem Bereich befindlich\" betrachtet wird. Standard: 3", + "title": "Trägheit" + }, + "loiteringTime": { + "desc": "Legt eine Mindestzeit in Sekunden fest, die das Objekt in dem Bereich sein muss, damit es aktiviert wird. Standard: 0", + "title": "Verweilzeit" + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentation", + "title": "Bewegungsmasken werden verwendet, um zu verhindern, dass unerwünschte Bewegungsarten eine Erkennung auslösen. Zu starkes Maskieren erschwert das Nachverfolgen von Objekten." + }, + "documentTitle": "Bewegungsmaske bearbeiten – Frigate", + "context": { + "documentation": "Lies die Dokumentation", + "title": "Bewegungsmasken werden verwendet, um unerwünschte Bewegungen (z. B. Baumäste, Kamerazeitstempel) daran zu hindern, eine Erkennung auszulösen. Bewegungsmasken sollten sehr sparsam eingesetzt werden – zu starkes Maskieren erschwert das Nachverfolgen von Objekten." + }, + "clickDrawPolygon": "Klicke, um ein Polygon auf dem Bild zu zeichnen.", + "toast": { + "success": { + "noName": "Bewegungsmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." + } + }, + "add": "Neue Bewegungsmaske", + "edit": "Bewegungsmaske bearbeiten", + "polygonAreaTooLarge": { + "title": "Die Bewegungsmaske deckt {{polygonArea}} % des Kamerabildes ab. Große Bewegungsmasken werden nicht empfohlen.", + "tips": "Bewegungsmasken verhindern nicht, dass Objekte erkannt werden. Du solltest stattdessen eine erforderliche Zone verwenden.", + "documentation": "Lies die Dokumentation" + }, + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "label": "Bewegungsmaske" + }, + "restart_required": "Neustart erforderlich (Maske/Zone hat sich geändert)", + "objectMasks": { + "label": "Objektmasken", + "documentTitle": "Objektmaske bearbeiten – Frigate", + "toast": { + "success": { + "noName": "Objektmaske wurde gespeichert.", + "title": "{{polygonName}} wurde gespeichert." + } + }, + "desc": { + "title": "Objekt-Filtermasken werden verwendet, um Fehlalarme für einen bestimmten Objekttyp basierend auf dem Standort herauszufiltern.", + "documentation": "Dokumentation" + }, + "add": "Objektmaske hinzufügen", + "clickDrawPolygon": "Klicken Sie, um ein Polygon auf dem Bild zu zeichnen.", + "edit": "Objektmaske bearbeiten", + "context": "Objekt-Filtermasken werden verwendet, um Fehlalarme für einen bestimmten Objekttyp anhand des Standorts herauszufiltern.", + "point_one": "{{count}} Punkt", + "point_other": "{{count}} Punkte", + "objects": { + "title": "Objekte", + "desc": "Der Objekttyp, für den diese Objektmaske gilt.", + "allObjectTypes": "Alle Objekttypen" + } + }, + "motionMaskLabel": "Bewegungsmaske {{number}}", + "objectMaskLabel": "Objektmaske {{number}} ({{label}})" + }, + "debug": { + "objectShapeFilterDrawing": { + "score": "Bewertung", + "document": "Lies die Dokumentation. ", + "tips": "Aktiviere diese Option, um ein Rechteck auf dem Kamerabild zu zeichnen, das dessen Bereich und Seitenverhältnis anzeigt. Diese Werte können anschließend verwendet werden, um Parameter für den Objektform-Filter in deiner Konfiguration festzulegen.", + "area": "Bereich", + "ratio": "Verhältnis", + "title": "Objektform-Filter-Zeichnung", + "desc": "Zeichne ein Rechteck auf das Bild, um Flächen- und Verhältnisdetails anzuzeigen" + }, + "motion": { + "tips": "

    Bewegungsrahmen


    Rote Rahmen werden über die Bereiche des Bildes gelegt, in denen aktuell Bewegung erkannt wird.

    ", + "title": "Bewegungsrahmen", + "desc": "Rahmen um Bereiche anzeigen, in denen Bewegung erkannt wird" + }, + "boundingBoxes": { + "title": "Begrenzungsrahmen", + "desc": "Begrenzungsrahmen um verfolgte Objekte anzeigen", + "colors": { + "info": "
  • Beim Start werden jedem Objektlabel unterschiedliche Farben zugewiesen.
  • Eine dünne dunkelblaue Linie zeigt an, dass das Objekt zum aktuellen Zeitpunkt nicht erkannt wird.
  • Eine dünne graue Linie zeigt an, dass das Objekt als stationär erkannt wurde.
  • Eine dicke Linie zeigt an, dass das Objekt aktuell vom Autotracking verfolgt wird (wenn aktiviert).
  • ", + "label": "Farben der Objekt-Begrenzungsrahmen" + } + }, + "zones": { + "title": "Zonen", + "desc": "Umrisse aller definierten Zonen anzeigen" + }, + "timestamp": { + "desc": "Einen Zeitstempel auf dem Bild einblenden", + "title": "Zeitstempel" + }, + "mask": { + "desc": "Bewegungsmasken-Polygone anzeigen", + "title": "Bewegungsmasken" + }, + "detectorDesc": "Frigate verwendet deine Detektoren ({{detectors}}), um Objekte im Videostream deiner Kamera zu erkennen.", + "debugging": "Fehlersuche", + "objectList": "Objektliste", + "noObjects": "Keine Objekte", + "regions": { + "title": "Regionen", + "tips": "

    Regionsrahmen


    Leuchtend grüne Rahmen werden über die Interessensbereiche im Bild gelegt, die an den Objektdetektor übermittelt werden.

    ", + "desc": "Einen Rahmen für den an den Objektdetektor übermittelten Interessensbereich anzeigen" + }, + "title": "Debug", + "desc": "Die Debug-Ansicht zeigt eine Echtzeitansicht der verfolgten Objekte und ihrer Statistiken. Die Objektliste zeigt eine zeitverzögerte Zusammenfassung der erkannten Objekte.", + "paths": { + "title": "Pfade", + "desc": "Wichtige Punkte des Pfads des verfolgten Objekts anzeigen", + "tips": "

    Pfade


    Linien und Kreise zeigen wichtige Punkte an, an denen sich das verfolgte Objekt während seines Lebenszyklus bewegt hat.

    " + }, + "openCameraWebUI": "Web-Benutzeroberfläche von {{camera}} öffnen", + "audio": { + "title": "Audio", + "noAudioDetections": "Keine Audioerkennungen", + "score": "Punktzahl", + "currentRMS": "Aktueller Effektivwert", + "currentdbFS": "Aktuelle dbFS" + } + }, + "motionDetectionTuner": { + "Threshold": { + "title": "Schwellenwert", + "desc": "Der Schwellenwert legt fest, wie stark sich die Helligkeit eines Pixels ändern muss, damit dies als Bewegung erkannt wird. Standard: 30" + }, + "improveContrast": { + "title": "Kontrast verbessern", + "desc": "Den Kontrast für dunklere Szenen verbessern. Standard: EIN" + }, + "toast": { + "success": "Bewegungseinstellungen wurden gespeichert." + }, + "desc": { + "documentation": "Lies die Anleitung zur Bewegungsoptimierung", + "title": "Frigate verwendet die Bewegungserkennung als erste Überprüfung, um festzustellen, ob im Bildausschnitt etwas passiert, das eine Objekterkennung rechtfertigt." + }, + "contourArea": { + "title": "Konturfläche", + "desc": "Der Wert für die Konturfläche wird verwendet, um zu bestimmen, welche Gruppen von veränderten Pixeln als Bewegung gelten. Standard: 10" + }, + "title": "Bewegungserkennungs-Optimierer", + "unsavedChanges": "Nicht gespeicherte Änderungen im Bewegungserkennungs-Optimierer ({{camera}})" + }, + "users": { + "addUser": "Benutzer hinzufügen", + "updatePassword": "Passwort aktualisieren", + "toast": { + "success": { + "deleteUser": "Benutzer {{user}} wurde erfolgreich gelöscht", + "createUser": "Benutzer {{user}} wurde erfolgreich erstellt", + "updatePassword": "Passwort erfolgreich aktualisiert.", + "roleUpdated": "Rolle für {{user}} aktualisiert" + }, + "error": { + "setPasswordFailed": "Speichern des Passworts fehlgeschlagen: {{errorMessage}}", + "createUserFailed": "Benutzer konnte nicht erstellt werden: {{errorMessage}}", + "deleteUserFailed": "Benutzer konnte nicht gelöscht werden: {{errorMessage}}", + "roleUpdateFailed": "Aktualisierung der Rolle fehlgeschlagen: {{errorMessage}}" + } + }, + "title": "Benutzer", + "management": { + "title": "Benutzerverwaltung", + "desc": "Verwalte die Benutzerkonten dieser Frigate-Instanz." + }, + "table": { + "changeRole": "Benutzerrolle ändern", + "deleteUser": "Benutzer löschen", + "noUsers": "Keine Benutzer gefunden.", + "password": "Passwort", + "username": "Benutzername", + "actions": "Aktionen", + "role": "Rolle" + }, + "dialog": { + "form": { + "user": { + "title": "Benutzername", + "desc": "Nur Buchstaben, Zahlen, Punkte und Unterstriche sind erlaubt.", + "placeholder": "Benutzernamen eingeben" + }, + "password": { + "notMatch": "Passwörter stimmen nicht überein", + "strength": { + "weak": "Schwach", + "title": "Passwortstärke: ", + "medium": "Mittel", + "strong": "Stark", + "veryStrong": "Sehr stark" + }, + "confirm": { + "placeholder": "Bestätige Passwort", + "title": "Bestätige Passwort" + }, + "match": "Passwörter stimmen überein", + "title": "Passwort", + "placeholder": "Passwort eingeben", + "requirements": { + "title": "Passwort Anforderungen:", + "length": "Mindestens 8 Zeichen", + "uppercase": "Mindestens ein Großbuchstabe", + "digit": "Mindestens eine Ziffer", + "special": "Mindestens ein Sonderzeichen (!@#$%^&*(),.?\":{}|<>)" + }, + "show": "Passwort anzeigen", + "hide": "Verberge Passwort" + }, + "newPassword": { + "title": "Neues Passwort", + "placeholder": "Neues Passwort eingeben", + "confirm": { + "placeholder": "Neues Passwort erneut eingeben" + } + }, + "usernameIsRequired": "Benutzername ist erforderlich", + "passwordIsRequired": "Passwort benötigt", + "currentPassword": { + "title": "Aktuelles Passwort", + "placeholder": "Gib Dein aktuelles Passwort ein" + } + }, + "changeRole": { + "desc": "Berechtigungen für {{username}} aktualisieren", + "roleInfo": { + "intro": "Wähle die entsprechende Rolle für diesen Benutzer:", + "admin": "Admin", + "adminDesc": "Voller Zugang zu allen Funktionen.", + "viewer": "Betrachter", + "viewerDesc": "Nur auf Live-Dashboards, Überprüfung, Erkundung und Exporte beschränkt.", + "customDesc": "Benutzerdefinierte Rolle mit spezifischem Kamerazugriff." + }, + "title": "Benutzerrolle ändern", + "select": "Wähle eine Rolle" + }, + "deleteUser": { + "desc": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird das Benutzerkonto dauerhaft gelöscht und alle zugehörigen Daten werden entfernt.", + "warn": "Bist du sicher, dass du {{username}} löschen willst?", + "title": "Benutzer löschen" + }, + "createUser": { + "title": "Neuen Benutzer anlegen", + "desc": "Füge ein neues Benutzerkonto hinzu und lege eine Rolle für den Zugriff auf Bereiche der Frigate-Benutzeroberfläche fest.", + "usernameOnlyInclude": "Der Benutzername darf nur Buchstaben, Zahlen, . oder _ enthalten", + "confirmPassword": "Bitte bestätige dein Passwort" + }, + "passwordSetting": { + "updatePassword": "Passwort für {{username}} aktualisieren", + "setPassword": "Passwort festlegen", + "desc": "Erstelle ein sicheres Passwort, um dieses Konto zu schützen.", + "cannotBeEmpty": "Das Passwort darf nicht leer sein", + "doNotMatch": "Die Passwörter sind nicht identisch", + "currentPasswordRequired": "Aktuelles Passwort wird benötigt", + "incorrectCurrentPassword": "Aktuelles Passwort ist falsch", + "passwordVerificationFailed": "Passwort konnte nicht überprüft werden", + "multiDeviceWarning": "Alle anderen Geräte, auf denen Sie angemeldet sind, müssen sich innerhalb von {{refresh_time}} erneut anmelden. Sie können auch alle Benutzer dazu zwingen, sich sofort erneut zu authentifizieren, indem Sie Ihr JWT-Geheimnis rotieren." + } + } + }, + "notification": { + "email": { + "desc": "Eine gültige E-Mail-Adresse ist erforderlich und wird verwendet, um Sie zu benachrichtigen, falls es Probleme mit dem Push-Dienst gibt.", + "placeholder": "z. B. example@email.com", + "title": "Email" + }, + "notificationSettings": { + "title": "Einstellungen für Benachrichtigungen", + "desc": "Frigate kann von Haus aus Push-Benachrichtigungen an ein Gerät senden, wenn es im Browser läuft oder als PWA installiert ist.", + "documentation": "Lese die Dokumentation" + }, + "title": "Benachrichtigungen", + "notificationUnavailable": { + "title": "Benachrichtigungen nicht verfügbar", + "desc": "Web Push Benachrichtigungen erfordern einen sicheren Kontext (https://…). Das ist eine Vorgabe des Browsers. Greife auf Frigate gesichert zu um Benachrichtigungen zu nutzen.", + "documentation": "Dokumentation lesen" + }, + "cameras": { + "desc": "Wähle aus für welche Kameras Benachrichtigungen aktiviert werden sollen.", + "noCameras": "Keine Kameras verfügbar", + "title": "Kameras" + }, + "sendTestNotification": "Test Benachrichtigung senden", + "globalSettings": { + "desc": "Benachrichtigungen für bestimmte Kameras auf allen registrierten Geräten vorübergehend aussetzen.", + "title": "Globale Einstellungen" + }, + "deviceSpecific": "Geräte spezifische Einstellungen", + "active": "Benachrichtigungen aktiv", + "registerDevice": "Dieses Gerät registrieren", + "unregisterDevice": "Dieses Gerät abmelden", + "toast": { + "error": { + "registerFailed": "Speichern der Benachrichtigungsregistrierung fehlgeschlagen." + }, + "success": { + "registered": "Erfolgreich für Benachrichtigungen registriert. Starte Frigate neu bevor Benachrichtigungen (inklusive Testbenachrichtigung) gesendet werden können.", + "settingSaved": "Benachrichtigungseinstellungen wurden gespeichert." + } + }, + "suspendTime": { + "30minutes": "für 30 Minuten pausieren", + "1hour": "für 1 Stunde pausieren", + "12hours": "für 12 Stunden pausieren", + "untilRestart": "bis Neustart pausieren", + "24hours": "für 24 Stunden pausieren", + "5minutes": "Für 5 Minuten pausieren", + "10minutes": "Für 10 Minuten pausieren", + "suspend": "Pausieren" + }, + "cancelSuspension": "Pausieren abbrechen", + "suspended": "Benachrichtigungen für {{time}} pausiert", + "unsavedChanges": "Nicht gespeicherte Änderungen an den Benachrichtigungen", + "unsavedRegistrations": "Nicht gespeicherte Benachrichtigungsanmeldungen" + }, + "frigatePlus": { + "title": "Frigate+ Einstellungen", + "apiKey": { + "title": "Frigate+ API Key", + "desc": "Der Frigate+ API Key aktiviert die Integration des Frigate+ Dienstes.", + "validated": "Frigate+ API Key erkannt und validiert", + "notValidated": "Frigate+ API Key nicht erkannt und validiert", + "plusLink": "Lese mehr zu Frigate+" + }, + "snapshotConfig": { + "desc": "Für die Übermittlung an Frigate+ muss in der Konfiguration sowohl Snapshots als auch clean_copy-Snapshots aktiviert sein.", + "cleanCopyWarning": "Einige Kameras haben Snapshots aktiviert aber clean copy deaktiviert. Aktiviere clean_copy in der Snapshot Konfiguration um Bilder an Frigate+ zu senden.", + "documentation": "die Dokumentation lesen", + "table": { + "camera": "Kamera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "title": "Snapshot Einstellungen" + }, + "modelInfo": { + "modelType": "Model Typ", + "trainDate": "Trainings Datum", + "supportedDetectors": "Unterstützte Detektoren", + "modelSelect": "Die verfügbaren Modelle auf Frigate+ können hier ausgewählt werden. Beachte, dass nur Modelle kompatibel mit deiner aktuellen Detektorkonfiguration zur Auswahl stehen.", + "plusModelType": { + "baseModel": "Basis Model", + "userModel": "Feinabgestimmt" + }, + "cameras": "Kameras", + "loading": "Lade Model Informationen…", + "error": "Model Informationen laden fehlgeschlagen", + "availableModels": "Verfügbare Modelle", + "loadingAvailableModels": "Lade verfügbare Modelle…", + "baseModel": "Basis Model", + "title": "Model Informationen" + }, + "toast": { + "error": "Speichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}", + "success": "Frigate+ Einstellungen wurden gespeichert. Starte Frigate neu um Änderungen anzuwenden." + }, + "restart_required": "Neustart erforderlich (Frigate+ Model geändert)", + "unsavedChanges": "Nicht gespeicherte Änderungen an den Frigate+-Einstellungen" + }, + "enrichments": { + "birdClassification": { + "title": "Vogel Klassifizierung", + "desc": "Die Vogelklassifizierung identifiziert bekannte Vögel mithilfe eines quantisierten Tensorflow-Modells. Wenn ein bekannter Vogel erkannt wird, wird sein allgemeiner Name als sub_label hinzugefügt. Diese Informationen sind in der Benutzeroberfläche, in Filtern und in Benachrichtigungen enthalten." + }, + "title": "Anreicherungseinstellungen", + "unsavedChanges": "Ungesicherte geänderte Verbesserungseinstellungen", + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Sind Sie sicher, dass Sie alle verfolgten Objekteinbettungen neu indizieren wollen? Dieser Prozess läuft im Hintergrund, kann aber Ihre CPU auslasten und eine gewisse Zeit in Anspruch nehmen. Sie können den Fortschritt auf der Seite Explore verfolgen.", + "label": "Jetzt neu indizieren", + "desc": "Bei der Neuindizierung werden die Einbettungen für alle verfolgten Objekte neu generiert. Dieser Prozess läuft im Hintergrund und kann je nach Anzahl der verfolgten Objekte Ihre CPU auslasten und eine gewisse Zeit in Anspruch nehmen.", + "confirmTitle": "Neuindizierung bestätigen", + "confirmButton": "Neuindizierung", + "success": "Die Neuindizierung wurde erfolgreich gestartet.", + "alreadyInProgress": "Die Neuindizierung ist bereits im Gange.", + "error": "Die Neuindizierung konnte nicht gestartet werden: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Bei der Verwendung von klein wird eine quantisierte Version des Modells verwendet, die weniger Arbeitsspeicher verbraucht und schneller auf der CPU läuft, wobei der Unterschied in der Einbettungsqualität sehr gering ist.", + "title": "klein" + }, + "label": "Modell Größe", + "desc": "Die Größe des für die Einbettung der semantischen Suche verwendeten Modells.", + "large": { + "title": "groß", + "desc": "Bei der Verwendung von groß wird das gesamte Jina-Modell verwendet und automatisch auf der GPU ausgeführt, falls zutreffend." + } + }, + "title": "Semantische Suche", + "desc": "Die semantische Suche in Frigate ermöglicht es Ihnen, verfolgte Objekte innerhalb Ihrer Überprüfungselemente zu finden, indem Sie entweder das Bild selbst, eine benutzerdefinierte Textbeschreibung oder eine automatisch generierte Beschreibung verwenden.", + "readTheDocumentation": "Lies die Dokumentation" + }, + "faceRecognition": { + "title": "Gesichtserkennung", + "desc": "Die Gesichtserkennung ermöglicht es, Personen Namen zuzuweisen, und wenn ihr Gesicht erkannt wird, ordnet Frigate den Namen der Person als Untertitel zu. Diese Informationen sind in der Benutzeroberfläche, den Filtern und in den Benachrichtigungen enthalten.", + "readTheDocumentation": "Lies die Dokumentation", + "modelSize": { + "label": "Modellgröße", + "desc": "Die Größe des für die Gesichtserkennung verwendeten Modells.", + "small": { + "title": "klein", + "desc": "Mit klein wird ein FaceNet-Gesichtseinbettungsmodell verwendet, das auf den meisten CPUs effizient läuft." + }, + "large": { + "title": "groß", + "desc": "Die Verwendung von groß verwendet ein ArcFace-Gesichtseinbettungsmodell und läuft automatisch auf der GPU, falls zutreffend." + } + } + }, + "licensePlateRecognition": { + "title": "Kennzeichenerkennung", + "desc": "Frigate kann Kennzeichen an Fahrzeugen erkennen und die erkannten Zeichen automatisch in das Feld recognized_license_plate oder einen bekannten Namen als sub_label zu Objekten vom Typ car hinzufügen. Ein häufiger Anwendungsfall ist das Lesen der Kennzeichen von Autos, die in eine Einfahrt einfahren oder auf einer Straße vorbeifahren.", + "readTheDocumentation": "Lies die Dokumentation" + }, + "restart_required": "Neustart erforderlich (Verbesserungseinstellungen geändert)", + "toast": { + "success": "Die Einstellungen für die Verbesserungen wurden gespeichert. Starten Sie Frigate neu, um Ihre Änderungen zu übernehmen.", + "error": "Konfigurationsänderungen konnten nicht gespeichert werden: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Auslöser", + "management": { + "title": "Auslöser", + "desc": "Auslöser für {{camera}} verwalten. Verwenden Sie den Vorschaubild Typ, um ähnliche Vorschaubilder wie das ausgewählte verfolgte Objekt auszulösen, und den Beschreibungstyp, um ähnliche Beschreibungen wie den von Ihnen angegebenen Text auszulösen." + }, + "addTrigger": "Auslöser hinzufügen", + "table": { + "name": "Name", + "type": "Typ", + "content": "Inhalt", + "threshold": "Schwellenwert", + "actions": "Aktionen", + "noTriggers": "Für diese Kamera sind keine Auslöser konfiguriert.", + "edit": "Bearbeiten", + "deleteTrigger": "Auslöser löschen", + "lastTriggered": "Zuletzt ausgelöst" + }, + "type": { + "thumbnail": "Vorschaubild", + "description": "Beschreibung" + }, + "actions": { + "alert": "Als Alarm markieren", + "notification": "Benachrichtigung senden", + "sub_label": "Unterlabel hinzufügen", + "attribute": "Attribut hinzufügen" + }, + "dialog": { + "createTrigger": { + "title": "Auslöser erstellen", + "desc": "Auslöser für Kamera {{camera}} erstellen" + }, + "editTrigger": { + "title": "Auslöser bearbeiten", + "desc": "Einstellungen für Kamera {{camera}} bearbeiten" + }, + "deleteTrigger": { + "title": "Auslöser löschen", + "desc": "Sind Sie sicher, dass Sie den Auslöser {{triggerName}} löschen wollen? Dies kann nicht Rückgängig gemacht werden." + }, + "form": { + "name": { + "title": "Name", + "placeholder": "Benennen Sie diesen Auslöser", + "error": { + "minLength": "Der Name muss mindestens 2 Zeichen lang sein.", + "invalidCharacters": "Der Name darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.", + "alreadyExists": "Ein Auslöser mit diesem Namen existiert bereits für diese Kamera." + }, + "description": "Geben Sie einen eindeutigen Namen oder eine Beschreibung ein, um diesen Auslöser zu identifizieren" + }, + "enabled": { + "description": "Diesen Auslöser aktivieren oder deaktivieren" + }, + "type": { + "title": "Typ", + "placeholder": "Auslöser Typ wählen", + "description": "Auslösen, wenn eine ähnliche Beschreibung eines verfolgten Objekts erkannt wird", + "thumbnail": "Auslösen, wenn eine ähnliche Miniaturansicht eines verfolgten Objekts erkannt wird" + }, + "content": { + "title": "Inhalt", + "imagePlaceholder": "Miniaturansicht auswählen", + "textPlaceholder": "Inhaltstext eingeben", + "imageDesc": "Es werden nur die letzten 100 Miniaturansichten angezeigt. Wenn Sie die gewünschte Miniaturansicht nicht finden können, überprüfen Sie bitte frühere Objekte in „Explore“ und richten Sie dort über das Menü einen Trigger ein.", + "textDesc": "Einen Text eingeben, um diese Aktion auszulösen, wenn eine ähnliche Beschreibung eines verfolgten Objekts erkannt wird.", + "error": { + "required": "Inhalt ist erforderlich." + } + }, + "threshold": { + "title": "Schwellenwert", + "error": { + "min": "Schwellenwert muss mindestens 0 sein", + "max": "Schwellenwert darf höchstens 1 sein" + }, + "desc": "Legen Sie den Ähnlichkeitsschwellenwert für diesen Trigger fest. Ein höherer Schwellenwert bedeutet, dass eine größere Übereinstimmung erforderlich ist, um den Trigger auszulösen." + }, + "actions": { + "title": "Aktionen", + "desc": "Standardmäßig sendet Frigate für alle Trigger eine MQTT-Nachricht. Unterbezeichnungen fügen den Triggernamen zur Objektbezeichnung hinzu. Attribute sind durchsuchbare Metadaten, die separat in den Metadaten des verfolgten Objekts gespeichert werden.", + "error": { + "min": "Mindesten eine Aktion muss ausgewählt sein." + } + }, + "friendly_name": { + "title": "Nutzerfreundlicher Name", + "placeholder": "Benenne oder beschreibe diesen Auslöser", + "description": "Ein optionaler nutzerfreundlicher Name oder eine Beschreibung für diesen Auslöser." + } + } + }, + "toast": { + "success": { + "createTrigger": "Auslöser {{name}} erfolgreich erstellt.", + "updateTrigger": "Auslöser {{name}} erfolgreich aktualisiert.", + "deleteTrigger": "Auslöser {{name}} erfolgreich gelöscht." + }, + "error": { + "createTriggerFailed": "Auslöser konnte nicht erstellt werden: {{errorMessage}}", + "updateTriggerFailed": "Auslöser könnte nicht aktualisiert werden: {{errorMessage}}", + "deleteTriggerFailed": "Auslöser konnte nicht gelöscht werden: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantische Suche ist deaktiviert", + "desc": "Semantische Suche muss aktiviert sein um Auslöser nutzen zu können." + }, + "wizard": { + "title": "Auslöser erstellen", + "step1": { + "description": "Konfigurieren Sie die Grundeinstellungen für Ihren Auslöser." + }, + "step2": { + "description": "Legen Sie den Inhalt fest, der diese Aktion auslöst." + }, + "step3": { + "description": "Konfigurieren Sie den Schwellenwert und die Aktionen für diesen Trigger." + }, + "steps": { + "nameAndType": "Name und Typ", + "configureData": "Daten konfigurieren", + "thresholdAndActions": "Schwellenwert und Maßnahmen" + } + } + }, + "roles": { + "dialog": { + "form": { + "cameras": { + "required": "Mindestens eine Kamera muss ausgewählt werden.", + "title": "Kameras", + "desc": "Wählen Sie die Kameras aus, auf die diese Rolle Zugriff hat. Mindestens eine Kamera ist erforderlich." + }, + "role": { + "title": "Rolle Name", + "placeholder": "Rollen Name eingeben", + "desc": "Es sind nur Buchstaben, Zahlen, Punkte und Unterstriche zulässig.", + "roleIsRequired": "Rollen Name ist erforderlich", + "roleOnlyInclude": "Der Rollenname darf nur Buchstaben, Zahlen, . oder _ enthalten", + "roleExists": "Eine Rolle mit diesem Namen existiert bereits." + } + }, + "createRole": { + "title": "Neue Rolle erstellen", + "desc": "Fügen Sie eine neue Rolle hinzu und legen Sie die Berechtigungen für den Kamerazugriff fest." + }, + "editCameras": { + "title": "Rollenkameras bearbeiten", + "desc": "Aktualisieren Sie den Kamerazugriff für die Rolle {{role}}." + }, + "deleteRole": { + "title": "Rolle löschen", + "desc": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird die Rolle dauerhaft gelöscht und allen Benutzern mit dieser Rolle die Rolle „Betrachter“ zugewiesen, die dann Zugriff auf alle Kameras erhält.", + "warn": "Möchten Sie {{role}} wirklich löschen?", + "deleting": "Lösche..." + } + }, + "management": { + "title": "Zuschauer Rollenverwaltung", + "desc": "Verwalten Sie benutzerdefinierte Zuschauerrollen und ihre Kamerazugriffsberechtigungen für diese Frigate-Instanz." + }, + "addRole": "Rolle hinzufügen", + "table": { + "role": "Rolle", + "cameras": "Kameras", + "actions": "Aktionen", + "noRoles": "Keine benutzerdefinierten Rollen gefunden.", + "editCameras": "Kameras bearbeiten", + "deleteRole": "Rolle löschen" + }, + "toast": { + "success": { + "createRole": "Rolle {{role}} erfolgreich erstellt", + "updateCameras": "Kameras für Rolle {{role}} aktualisiert", + "deleteRole": "Rolle {{role}} erfolgreich gelöscht", + "userRolesUpdated_one": "{{count}} Benutzer, denen diese Rolle zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras hat.", + "userRolesUpdated_other": "{{count}} Benutzer, denen diese Rollen zugewiesen wurde, wurden auf „Zuschauer“ aktualisiert, der Zugriff auf alle Kameras habem." + }, + "error": { + "createRoleFailed": "Fehler beim Erstellen der Rolle: {{errorMessage}}", + "updateCamerasFailed": "Aktualisierung der Kameras fehlgeschlagen: {{errorMessage}}", + "deleteRoleFailed": "Rolle konnte nicht gelöscht werden: {{errorMessage}}", + "userUpdateFailed": "Aktualisierung der Benutzerrollen fehlgeschlagen: {{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "Kamera hinzufügen", + "description": "Folge den Anweisungen unten, um eine neue Kamera zu deiner Frigate-Installation hinzuzufügen.", + "steps": { + "nameAndConnection": "Name & Verbindung", + "streamConfiguration": "Stream Konfiguration", + "validationAndTesting": "Überprüfung & Testen", + "probeOrSnapshot": "Test oder Momentaufnahme" + }, + "save": { + "success": "Neue Kamera {{cameraName}} erfolgreich hinzugefügt.", + "failure": "Fehler beim Speichern von {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Auflösung", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Bitte korrekte Stream-URL eingeben", + "testFailed": "Stream Test fehlgeschlagen: {{error}}" + }, + "step1": { + "description": "Geben Sie Ihre Kameradaten ein und wählen Sie, ob Sie die Kamera automatisch erkennen lassen oder die Marke manuell auswählen möchten.", + "cameraName": "Kameraname", + "cameraNamePlaceholder": "z.B. vordere_tür oder Hof Übersicht", + "host": "Host/IP Adresse", + "port": "Port", + "username": "Nutzername", + "usernamePlaceholder": "Optional", + "password": "Passwort", + "passwordPlaceholder": "Optional", + "selectTransport": "Transport-Protokoll auswählen", + "cameraBrand": "Kamerahersteller", + "selectBrand": "Wähle die Kamerahersteller für die URL-Vorlage aus", + "customUrl": "Benutzerdefinierte Stream-URL", + "brandInformation": "Hersteller Information", + "brandUrlFormat": "Für Kameras mit RTSP URL nutze folgendes Format: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nutzername:passwort@host:port/pfad", + "testConnection": "Teste Verbindung", + "testSuccess": "Verbindungstest erfolgreich!", + "testFailed": "Verbindungstest fehlgeschlagen. Bitte prüfe deine Eingaben und versuche es erneut.", + "streamDetails": "Stream Details", + "warnings": { + "noSnapshot": "Es kann kein Snapshot aus dem konfigurierten Stream abgerufen werden." + }, + "errors": { + "brandOrCustomUrlRequired": "Wählen Sie entweder einen Kamerahersteller mit Host/IP aus oder wählen Sie „Andere“ mit einer benutzerdefinierten URL", + "nameRequired": "Der Kameraname wird benötigt", + "nameLength": "Der Kameraname darf höchsten 64 Zeichen lang sein", + "invalidCharacters": "Der Kameraname enthält ungültige Zeichen", + "nameExists": "Der Kameraname existiert bereits", + "brands": { + "reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Es wird empfohlen, http in den Kameraeinstellungen zu aktivieren und den Kamera-Assistenten neu zu starten." + }, + "customUrlRtspRequired": "Benutzerdefinierte URLs müssen mit „rtsp://“ beginnen. Für Nicht-RTSP-Kamerastreams ist eine manuelle Konfiguration erforderlich." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "connectionSettings": "Verbindungseinstellungen", + "detectionMethod": "Stream Erkennungsmethode", + "onvifPort": "ONVIF Port", + "probeMode": "Untersuche Kamera", + "detectionMethodDescription": "Suchen Sie die Kamera mit ONVIF (sofern unterstützt), um die URLs der Kamerastreams zu finden, oder wählen Sie manuell die Kameramarke aus, um vordefinierte URLs zu verwenden. Um eine benutzerdefinierte RTSP-URL einzugeben, wählen Sie die manuelle Methode und dann „Andere“.", + "onvifPortDescription": "Bei Kameras, die ONVIF unterstützen, ist dies in der Regel 80 oder 8080.", + "useDigestAuth": "Digest-Authentifizierung verwenden", + "useDigestAuthDescription": "Verwenden Sie die HTTP-Digest-Authentifizierung für ONVIF. Einige Kameras erfordern möglicherweise einen speziellen ONVIF-Benutzernamen/ein spezielles ONVIF-Passwort anstelle des Standard-Admin-Benutzers.", + "manualMode": "Manuelle Auswahl" + }, + "step2": { + "description": "Suchen Sie in der Kamera nach verfügbaren Streams oder konfigurieren Sie manuelle Einstellungen basierend auf der von Ihnen ausgewählten Erkennungsmethode.", + "streamsTitle": "Kamera Streams", + "addStream": "Stream hinzufügen", + "addAnotherStream": "Weiteren Stream hinzufügen", + "streamTitle": "Stream {{nummer}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://nutzername:passwort@host:port/pfad", + "url": "URL", + "resolution": "Auflösung", + "selectResolution": "Auflösung auswählen", + "quality": "Qualität", + "selectQuality": "Qualität auswählen", + "roles": "Rollen", + "roleLabels": { + "detect": "Objekt-Erkennung", + "record": "Aufzeichnung", + "audio": "Audio" + }, + "testStream": "Verbindung testen", + "testSuccess": "Verbindung erfolgreich getestet!", + "testFailed": "Verbindungstest fehlgeschlagen. Bitte überprüfen Sie ihre Eingaben und versuchen Sie es erneut.", + "testFailedTitle": "Test fehlgeschlagen", + "connected": "Verbunden", + "notConnected": "Nicht verbunden", + "featuresTitle": "Funktionen", + "go2rtc": "Verbindungen zur Kamera reduzieren", + "detectRoleWarning": "Mindestens ein Stream muss die Rolle „detect“ haben, um fortfahren zu können.", + "rolesPopover": { + "title": "Stream Rollen", + "detect": "Haupt-Feed für Objekt-Erkennung.", + "record": "Speichert Segmente des Video-Feeds basierend auf den Konfigurationseinstellungen.", + "audio": "Feed für audiobasierte Erkennung." + }, + "featuresPopover": { + "title": "Stream Funktionen", + "description": "Verwende go2rtc Restreaming, um die Verbindungen zu deiner Kamera zu reduzieren." + }, + "streamDetails": "Verbindungsdetails", + "probing": "Kamera wird geprüft...", + "retry": "Wiederholen", + "testing": { + "probingMetadata": "Metadaten der Kamera werden überprüft...", + "fetchingSnapshot": "Kamera-Schnappschuss wird abgerufen..." + }, + "probeFailed": "Fehler beim Untersuchen der Kamera: {{error}}", + "probingDevice": "Untersuche Gerät...", + "probeSuccessful": "Erkennung erfolgreich", + "probeError": "Erkennungsfehler", + "probeNoSuccess": "Erkennung fehlgeschlagen", + "deviceInfo": "Geräteinformationen", + "manufacturer": "Hersteller", + "model": "Modell", + "firmware": "Firmware", + "profiles": "Profile", + "ptzSupport": "PTZ Unterstützung", + "autotrackingSupport": "Unterstützung für Autoverfolgung", + "presets": "Voreinstellung", + "rtspCandidates": "RTSP Kandidaten", + "rtspCandidatesDescription": "Die folgenden RTSP-URLs wurden bei der Kameraerkennung gefunden. Testen Sie die Verbindung, um die Stream-Metadaten anzuzeigen.", + "noRtspCandidates": "Es wurden keine RTSP-URLs von der Kamera gefunden. Möglicherweise sind Ihre Anmeldedaten falsch oder die Kamera unterstützt ONVIF oder die Methode zum Abrufen von RTSP-URLs nicht. Gehen Sie zurück und geben Sie die RTSP-URL manuell ein.", + "candidateStreamTitle": "Kandidate {{number}}", + "useCandidate": "Verwenden", + "uriCopy": "Kopieren", + "uriCopied": "URI in die Zwischenablage kopiert", + "testConnection": "Test Verbindung", + "toggleUriView": "Klicken Sie hier, um die vollständige URI zu sehen", + "errors": { + "hostRequired": "Host/IP adresse wird benötigt" + } + }, + "step3": { + "description": "Konfigurieren Sie Stream-Rollen und fügen Sie zusätzliche Streams für Ihre Kamera hinzu", + "validationTitle": "Stream Validierung", + "connectAllStreams": "Verbinde alle Streams", + "reconnectionSuccess": "Wiederverbindung erfolgreich.", + "reconnectionPartial": "Einige Streams konnten nicht wieder verbunden werden.", + "streamUnavailable": "Stream-Vorschau nicht verfügbar", + "reload": "Neu laden", + "connecting": "Verbinde...", + "streamTitle": "Stream {{number}}", + "valid": "Gültig", + "failed": "Fehlgeschlagen", + "notTested": "Nicht getestet", + "connectStream": "Verbinden", + "connectingStream": "Verbinde", + "disconnectStream": "Trennen", + "estimatedBandwidth": "Geschätzte Bandbreite", + "roles": "Rollen", + "none": "Keine", + "error": "Fehler", + "streamValidated": "Stream {{number}} wurde erfolgreich validiert", + "streamValidationFailed": "Stream {{number}} Validierung fehlgeschlagen", + "saveAndApply": "Neue Kamera speichern", + "saveError": "Ungültige Konfiguration. Bitte prüfe die Einstellungen.", + "issues": { + "title": "Stream Validierung", + "videoCodecGood": "Video-Codec ist {{codec}}.", + "audioCodecGood": "Audio-Codec ist {{codec}}.", + "noAudioWarning": "Für diesen Stream wurde kein Ton erkannt, die Aufzeichnungen enthalten keinen Ton.", + "audioCodecRecordError": "Der AAC-Audio-Codec ist erforderlich, um Audio in Aufnahmen zu unterstützen.", + "audioCodecRequired": "Ein Audiostream ist erforderlich, um Audioerkennung zu unterstützen.", + "restreamingWarning": "Eine Reduzierung der Verbindungen zur Kamera für den Aufzeichnungsstream kann zu einer etwas höheren CPU-Auslastung führen.", + "dahua": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Kameras von Dahua / Amcrest / EmpireTech unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu nutzen, sofern sie verfügbar sind." + }, + "hikvision": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Hikvision-Kameras unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu nutzen, sofern sie verfügbar sind." + } + }, + "streamsTitle": "Kamera Stream", + "addStream": "Stream hinzufügen", + "addAnotherStream": "weiteren Stream hinzufügen", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://benutzername:passwort@host:port/path", + "selectStream": "Auswahl Stream", + "searchCandidates": "Suche Kandidaten...", + "noStreamFound": "Kein Stream gefunden", + "url": "URL", + "resolution": "Auflösung", + "selectResolution": "Wähle Auflösung", + "quality": "Qualität", + "selectQuality": "Wähle Qualität", + "roleLabels": { + "detect": "Objekterkennung", + "record": "Aufnahme", + "audio": "Ton" + }, + "testStream": "Verbindungstest", + "testSuccess": "Verbindungstest erfolgreich!", + "testFailed": "Verbindungstest fehlgeschlagen", + "testFailedTitle": "Test fehlgeschlagen", + "connected": "Verbunden", + "notConnected": "nicht verbunden", + "featuresTitle": "Funktionen", + "go2rtc": "Verbindungen zur Kamera reduzieren", + "detectRoleWarning": "Mindestens ein Stream muss die Rolle „detect“ haben, um fortfahren zu können.", + "rolesPopover": { + "title": "Stream Rollen", + "detect": "Hauptfeed für die Objekterkennung.", + "record": "Speichert Segmente des Video-Feeds basierend auf den Konfigurationseinstellungen.", + "audio": "Feed für audiobasierte Erkennung." + }, + "featuresPopover": { + "title": "Stream Funktionen", + "description": "Verwenden Sie go2rtc-Restreaming, um die Verbindungen zu Ihrer Kamera zu reduzieren." + } + }, + "step4": { + "description": "Endgültige Validierung und Analyse vor dem Speichern Ihrer neuen Kamera. Verbinden Sie jeden Stream vor dem Speichern.", + "validationTitle": "Stream-Validierung", + "connectAllStreams": "Alle Streams verbinden", + "reconnectionSuccess": "Wiederverbindung erfolgreich.", + "reconnectionPartial": "Einige Streams konnten nicht wieder verbunden werden.", + "streamUnavailable": "Stream Vorschau nicht verfügbar", + "reload": "neu Laden", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "gültig", + "failed": "fehlgeschlagen", + "notTested": "nicht getestet", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "getrennt", + "estimatedBandwidth": "Voraussichtliche Bandbreite", + "roles": "Rollen", + "ffmpegModule": "Stream-Kompatibilitätsmodus verwenden", + "ffmpegModuleDescription": "Wenn der Stream nach mehreren Versuchen nicht geladen wird, versuchen Sie, diese Option zu aktivieren. Wenn diese Option aktiviert ist, verwendet Frigate das ffmpeg-Modul mit go2rtc. Dies kann zu einer besseren Kompatibilität mit einigen Kamerastreams führen.", + "none": "keiner", + "error": "Fehler", + "streamValidated": "Steam {{number}} erfolgreich validiert", + "streamValidationFailed": "Stream {{number}} Validierung fehlgeschlagen", + "saveAndApply": "Neue Kamera speichern", + "saveError": "Ungültige Konfiguration. Bitte überprüfen Sie Ihre Einstellungen.", + "issues": { + "title": "Stream-Validierung", + "videoCodecGood": "Video codec ist {{codec}}.", + "audioCodecGood": "Audio codec ist {{codec}}.", + "resolutionHigh": "Eine Auflösung von {{resolution}} kann zu einem erhöhten Ressourcenverbrauch führen.", + "resolutionLow": "Eine Auflösung von {{resolution}} ist möglicherweise zu gering, um kleine Objekte zuverlässig zu erkennen.", + "noAudioWarning": "Für diesen Stream wurde kein Ton erkannt, die Aufzeichnungen enthalten keinen Ton.", + "audioCodecRecordError": "Der AAC-Audio-Codec ist erforderlich, um Audio in Aufnahmen zu unterstützen.", + "audioCodecRequired": "Ein Audiostream ist erforderlich, um die Audioerkennung zu unterstützen.", + "restreamingWarning": "Die Reduzierung der Verbindungen zur Kamera für den Aufzeichnungsstream kann zu einer geringfügigen Erhöhung der CPU-Auslastung führen.", + "brands": { + "reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Aktivieren Sie HTTP in den Firmware-Einstellungen der Kamera und starten Sie den Assistenten neu." + }, + "dahua": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Kameras von Dahua / Amcrest / EmpireTech unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind." + }, + "hikvision": { + "substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Hikvision-Kameras unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind." + } + } + } + }, + "cameraManagement": { + "title": "Kameras verwalten", + "addCamera": "Neue Kamera hinzufügen", + "editCamera": "Kamera bearbeiten:", + "selectCamera": "Wähle eine Kamera", + "backToSettings": "Zurück zu Kamera-Einstellungen", + "streams": { + "title": "Kameras aktivieren / deaktivieren", + "desc": "Deaktiviere eine Kamera vorübergehend, bis Frigate neu gestartet wird. Deaktivierung einer Kamera stoppt die Verarbeitung der Streams dieser Kamera durch Frigate vollständig. Erkennung, Aufzeichnung und Debugging sind dann nicht mehr verfügbar.
    Hinweis: Dies deaktiviert nicht die go2rtc restreams." + }, + "cameraConfig": { + "add": "Kamera hinzufügen", + "edit": "Kamera bearbeiten", + "description": "Konfiguriere die Kameraeinstellungen, einschließlich Streams und Rollen.", + "name": "Kameraname", + "nameRequired": "Kameraname benötigt", + "nameLength": "Kameraname darf maximal 64 Zeichen lang sein.", + "namePlaceholder": "z.B. vordere_tür oder Hof Übersicht", + "enabled": "Aktiviert", + "ffmpeg": { + "inputs": "Eingang Streams", + "path": "Stream-Pfad", + "pathRequired": "Stream-Pfad benötigt", + "pathPlaceholder": "rtsp://...", + "roles": "Rollen", + "rolesRequired": "Mindestens eine Rolle wird benötigt", + "rolesUnique": "Jede Rolle (audio, detect, record) kann nur einem Stream zugewiesen werden", + "addInput": "Eingangs-Stream hinzufügen", + "removeInput": "Eingangs-Stream entfernen", + "inputsRequired": "Es wird mindestens ein Eingangs-Stream benötigt" + }, + "go2rtcStreams": "go2rtc Streams", + "streamUrls": "Stream URLs", + "addUrl": "URL hinzufügen", + "addGo2rtcStream": "go2rtc Stream hinzufügen", + "toast": { + "success": "Kamera {{cameraName}} erfolgreich gespeichert" + } + } + }, + "cameraReview": { + "title": "Kamera-Einstellungen überprüfen", + "object_descriptions": { + "title": "Generative KI Objektbeschreibungen", + "desc": "Aktiviere/deaktiviere vorübergehend die Objektbeschreibungen durch Generative KI für diese Kamera. Wenn diese Option deaktiviert ist, werden keine KI-generierten Beschreibungen für verfolgte Objekte dieser Kamera erstellt." + }, + "review_descriptions": { + "title": "Generative KI Review Beschreibungen", + "desc": "Generative KI Review Beschreibungen für diese Kamera vorübergehend aktivieren/deaktivieren. Wenn diese Option deaktiviert ist, werden für die Review Elemente dieser Kamera keine KI-generierten Beschreibungen angefordert." + }, + "review": { + "title": "Überprüfung", + "desc": "Aktivieren/deaktivieren Sie vorübergehend Warnmeldungen und Erkennungen für diese Kamera, bis Frigate neu gestartet wird. Wenn diese Funktion deaktiviert ist, werden keine neuen Überprüfungselemente generiert. ", + "alerts": "Warnungen ", + "detections": "Erkennungen " + }, + "reviewClassification": { + "title": "Bewertungsklassifizierung", + "desc": "Frigate kategorisiert zu überprüfende Elemente als Warnmeldungen und Erkennungen. Standardmäßig werden alle Objekte vom Typ Person und Auto als Warnmeldungen betrachtet. Sie können die Kategorisierung der zu überprüfenden Elemente verfeinern, indem Sie die erforderlichen Zonen für sie konfigurieren.", + "noDefinedZones": "Für diese Kamera sind keine Zonen definiert.", + "objectAlertsTips": "Alle {{alertsLabels}}-Objekte auf {{cameraName}} werden als Warnmeldungen angezeigt.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-Objekte, die in {{zone}} auf {{cameraName}} erkannt wurden, werden als Warnmeldungen angezeigt.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-Objekte, die nicht unter {{cameraName}} kategorisiert sind, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-Objekte, die nicht in {{zone}} auf {{cameraName}} kategorisiert sind, werden als Erkennungen angezeigt.", + "notSelectDetections": "Alle {{detectionsLabels}}-Objekte, die in {{zone}} auf {{cameraName}} erkannt und nicht als Warnmeldungen kategorisiert wurden, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-Objekte, die nicht unter {{cameraName}} kategorisiert sind, werden unabhängig davon, in welcher Zone sie sich befinden, als Erkennungen angezeigt." + }, + "unsavedChanges": "Nicht gespeicherte Überprüfung der Klassifizierungseinstellungen für {{camera}}", + "selectAlertsZones": "Zonen für Warnmeldungen auswählen", + "selectDetectionsZones": "Zonen für Erkennungen auswählen", + "limitDetections": "Erkennungen auf bestimmte Zonen beschränken", + "toast": { + "success": "Die Konfiguration der Bewertungsklassifizierung wurde gespeichert. Starten Sie Frigate neu, um die Änderungen zu übernehmen." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/de/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/de/views/system.json new file mode 100644 index 0000000..bd948a0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/de/views/system.json @@ -0,0 +1,198 @@ +{ + "general": { + "hardwareInfo": { + "gpuInfo": { + "vainfoOutput": { + "title": "Ergebnis der Vainfo-Abfrage", + "returnCode": "Rückgabecode: {{code}}", + "processError": "Prozess Fehler:", + "processOutput": "Prozess-Output:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Ausgabe", + "cudaComputerCapability": "CUDA Rechenleistung:{{cuda_compute}}", + "name": "Name: {{name}}", + "driver": "Treiber: {{driver}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Schließe GPU Info" + }, + "copyInfo": { + "label": "Kopiere GPU Info" + }, + "toast": { + "success": "GPU-Infos in die Zwischenablage kopiert" + } + }, + "title": "Hardwareinformationen", + "gpuUsage": "GPU Auslastung", + "gpuMemory": "Grafikspeicher", + "gpuDecoder": "GPU Decoder", + "gpuEncoder": "GPU Encoder", + "npuUsage": "NPU Verwendung", + "npuMemory": "NPU Speicher", + "intelGpuWarning": { + "title": "Intel GPU Statistik Warnung", + "message": "GPU stats nicht verfügbar", + "description": "Dies ist ein bekannter Fehler in den GPU-Statistik-Tools von Intel (intel_gpu_top), bei dem das Tool ausfällt und wiederholt eine GPU-Auslastung von 0 % anzeigt, selbst wenn die Hardwarebeschleunigung und die Objekterkennung auf der (i)GPU korrekt funktionieren. Dies ist kein Fehler von Frigate. Du kannst den Host neu starten, um das Problem vorübergehend zu beheben und zu prüfen, ob die GPU korrekt funktioniert. Dies hat keine Auswirkungen auf die Leistung." + } + }, + "title": "Allgemein", + "detector": { + "title": "Detektoren", + "cpuUsage": "CPU-Auslastung des Detektors", + "memoryUsage": "Arbeitsspeichernutzung des Detektors", + "inferenceSpeed": "Detektoren Inferenzgeschwindigkeit", + "temperature": "Temperatur des Detektors", + "cpuUsageInformation": "CPU, die zur Vorbereitung von Eingabe- und Ausgabedaten für/aus Erkennungsmodellen verwendet wird. Dieser Wert misst nicht die Inferenzauslastung, selbst wenn eine GPU oder ein Beschleuniger verwendet wird." + }, + "otherProcesses": { + "title": "Andere Prozesse", + "processCpuUsage": "CPU Auslastung für Prozess", + "processMemoryUsage": "Prozessspeicherauslastung" + } + }, + "documentTitle": { + "cameras": "Kamerastatistiken – Frigate", + "storage": "Speicherstatistiken - Frigate", + "general": "Allgemeine Statistiken - Frigate", + "logs": { + "frigate": "Frigate Protokolle – Frigate", + "go2rtc": "Go2RTC Protokolle - Frigate", + "nginx": "Nginx Protokolle - Frigate" + }, + "enrichments": "Erweiterte Statistiken - Frigate" + }, + "title": "System", + "logs": { + "download": { + "label": "Protokolldateien herunterladen" + }, + "copy": { + "success": "Protokolle in die Zwischenablage kopiert", + "label": "In die Zwischenablage kopieren", + "error": "Protokolle konnten nicht in die Zwischenablage kopiert werden" + }, + "type": { + "message": "Nachricht", + "timestamp": "Zeitstempel", + "label": "Art", + "tag": "Tag" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Fehler beim Abrufen der Protokolle: {{errorMessage}}", + "whileStreamingLogs": "Beim Übertragen der Protokolle ist ein Fehler aufgetreten: {{errorMessage}}" + } + }, + "tips": "Protokolle werden in Echtzeit vom Server übertragen" + }, + "metrics": "Systemmetriken", + "storage": { + "recordings": { + "earliestRecording": "Älteste verfügbare Aufzeichnung:", + "title": "Aufnahmen", + "tips": "Dieser Wert gibt den Gesamtspeicherplatz an, den die Aufzeichnungen in der Datenbank von Frigate belegen. Frigate erfasst nicht die Speichernutzung für alle Dateien auf Ihrer Festplatte." + }, + "cameraStorage": { + "camera": "Kamera", + "title": "Kamera Speicher", + "unused": { + "title": "Ungenutzt", + "tips": "Dieser Wert gibt möglicherweise nicht genau den freien Speicherplatz an, der Frigate zur Verfügung steht, wenn neben den Aufzeichnungen von Frigate noch andere Dateien auf der Festplatte gespeichert sind. Frigate verfolgt die Speichernutzung außerhalb der Aufzeichnungen nicht." + }, + "unusedStorageInformation": "Info zum ungenutzten Speicher", + "storageUsed": "Speicher", + "percentageOfTotalUsed": "Prozentualer Anteil am Gesamtanteil", + "bandwidth": "Bandbreite" + }, + "title": "Speicher", + "overview": "Übersicht", + "shm": { + "title": "SHM (Shared Memory) Zuweisung", + "warning": "Die aktuelle SHM-Größe von {{total}} MB ist zu klein. Erhöhe sie auf mindestens {{min_shm}} MB." + } + }, + "cameras": { + "info": { + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Codec:", + "fetching": "Lade Kamera Daten", + "resolution": "Auflösung:", + "fps": "FPS:", + "unknown": "Unbekannt", + "audio": "Audio:", + "error": "Fehler: {{error}}", + "cameraProbeInfo": "{{camera}} Kamera-Untersuchungsinfo", + "streamDataFromFFPROBE": "Stream-Daten werden mit ffprobe erhalten.", + "tips": { + "title": "Kamera-Untersuchsungsinfo" + }, + "aspectRatio": "Seitenverhältnis" + }, + "overview": "Übersicht", + "label": { + "detect": "erkennen", + "camera": "Kamera", + "skipped": "übersprungene", + "ffmpeg": "FFmpeg", + "capture": "aufnehmen", + "overallFramesPerSecond": "Bilder pro Sekunde", + "overallDetectionsPerSecond": "Erkennungen pro Sekunde", + "overallSkippedDetectionsPerSecond": "übersprungene Erkennungen pro Sekunde", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} Aufnahme", + "cameraDetect": "{{camName}} Erkennung", + "cameraFramesPerSecond": "{{camName}} Bilder pro Sekunde", + "cameraDetectionsPerSecond": "{{camName}} Erkennungen pro Sekunde", + "cameraSkippedDetectionsPerSecond": "{{camName}} übersprungene Erkennungen pro Sekunde" + }, + "title": "Kameras", + "framesAndDetections": "Bilder / Erkennungen", + "toast": { + "success": { + "copyToClipboard": "Kopiert Untersuchungsdaten in die Zwischenablage." + }, + "error": { + "unableToProbeCamera": "Die Kamera kann nicht getestet werden: {{errorMessage}}" + } + } + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Geschwindigkeit der Bildeinbettung", + "face_embedding_speed": "Geschwindigkeit der Gesichtseinbettung", + "plate_recognition_speed": "Geschwindigkeit der Kennzeichenerkennung", + "text_embedding_speed": "Geschwindigkeit der Texteinbettung", + "plate_recognition": "Kennzeichen Erkennung", + "face_recognition_speed": "Gesichts Erkennungs Geschwindigkeit", + "text_embedding": "Einbettung von Bildern", + "face_recognition": "Gesichts Erkennung", + "image_embedding": "Bild Embedding", + "yolov9_plate_detection_speed": "YOLOv9 Kennzeichenerkennungsgeschwindigkeit", + "yolov9_plate_detection": "YOLOv9 Kennzeichenerkennung", + "review_description": "Bewertung Beschreibung", + "review_description_speed": "Bewertungsbeschreibung Geschwindigkeit", + "review_description_events_per_second": "Bewertungsbeschreibung", + "object_description": "Objekt Beschreibung", + "object_description_speed": "Objektbeschreibung Geschwindigkeit", + "object_description_events_per_second": "Objektbeschreibung" + }, + "title": "Optimierungen", + "infPerSecond": "Rückschlüsse pro Sekunde", + "averageInf": "Durchschnittliche Inferenzzeit" + }, + "stats": { + "healthy": "Das System läuft problemlos", + "ffmpegHighCpuUsage": "{{camera}} hat eine hohe FFmpeg CPU Auslastung ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} hat eine hohe CPU Auslastung bei der Erkennung ({{detectAvg}}%)", + "reindexingEmbeddings": "Neuindizierung von Einbettungen ({{processed}}% erledigt)", + "detectIsSlow": "{{detect}} ist langsam ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} ist sehr langsam ({{speed}} ms)", + "cameraIsOffline": "{{camera}} ist offline", + "shmTooLow": "Die Zuweisung für /dev/shm ({{total}} MB) sollte auf mindestens {{min}} MB erhöht werden." + }, + "lastRefreshed": "Zuletzt aktualisiert: " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/audio.json b/sam2-cpu/frigate-dev/web/public/locales/el/audio.json new file mode 100644 index 0000000..2bd01b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/audio.json @@ -0,0 +1,65 @@ +{ + "speech": "Διάλογος", + "babbling": "Φλυαρία", + "yell": "Φωνές", + "bellow": "Κάτω από", + "whoop": "Κραυγή", + "whispering": "Ψίθυρος", + "laughter": "Γέλια", + "snicker": "Χαχανιτά", + "crying": "Κλάμα", + "dog": "Σκύλος", + "cat": "Γάτα", + "pig": "Γουρούνι", + "oink": "Κραυγή Γουρουνιού", + "moo": "Μουγκανιτό", + "cowbell": "Κουδούνι Αγελάδας", + "horse": "Άλογο", + "goat": "Κατσίκα", + "chicken": "Πρόβατο", + "child_singing": "Τραγούδι Παιδιού", + "sneeze": "Φτέρνισμα", + "sniff": "Όσφρηση", + "run": "Τρέξιμο", + "shuffle": "Ανακάτεμα", + "footsteps": "Βήματα", + "chewing": "Μάσημα", + "biting": "Δάγκωμα", + "bicycle": "Ποδήλατο", + "car": "Αυτοκίνητο", + "motorcycle": "Μηχανή", + "breathing": "Αναπνοή", + "snoring": "Ροχαλιτό", + "honk": "Κόρνα", + "wild_animals": "Άγρια Ζώα", + "roaring_cats": "Κραυγές από Γάτες", + "roar": "Βρυχηθμός", + "bird": "Πουλί", + "pigeon": "Περιστέρι", + "crow": "Κοράκι", + "caw": "Αγελάδα", + "owl": "Κουκουβάγια", + "flapping_wings": "Φτερούγισμα", + "dogs": "Σκυλιά", + "rats": "Ποντίκια", + "guitar": "Κιθάρα", + "electric_guitar": "Ηλεκτρική Κιθάρα", + "bass_guitar": "Μπάσο", + "acoustic_guitar": "Ακουστική Κιθάρα", + "classical_music": "Κλασική Μουσική", + "opera": "Όπερα", + "electronic_music": "Ηλεκτρονική Μουσική", + "bus": "Λεωφορείο", + "train": "Εκπαίδευση", + "boat": "Βάρκα", + "sigh": "Αναστεναγμός", + "singing": "Τραγούδι", + "choir": "Χορωδία", + "whistling": "Σφύριγμα", + "camera": "Κάμερα", + "wheeze": "Ξεφύσημα", + "yodeling": "Λαρυγγισμός", + "chant": "Ύμνος", + "mantra": "Μάντρα", + "synthetic_singing": "Συνθετικό Τραγούδι" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/common.json b/sam2-cpu/frigate-dev/web/public/locales/el/common.json new file mode 100644 index 0000000..5cc5277 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/common.json @@ -0,0 +1,125 @@ +{ + "time": { + "untilForTime": "Ως {{time}}", + "untilForRestart": "Μέχρι να γίνει επανεκίννηση του Frigate.", + "untilRestart": "Μέχρι να γίνει επανεκκίνηση", + "justNow": "Μόλις τώρα", + "ago": "Πριν {{timeAgo}}", + "today": "Σήμερα", + "yesterday": "Εχθές", + "last7": "Τελευταίες 7 ημέρες", + "year_one": "{{time}} χρόνος", + "year_other": "{{time}} χρόνια", + "month_one": "{{time}} μήνας", + "month_other": "{{time}} μήνες", + "day_one": "{{time}} ημέρα", + "day_other": "{{time}} ημέρες", + "hour_one": "{{time}} ώρα", + "hour_other": "{{time}} ώρες", + "minute_one": "{{time}} λεπτό", + "minute_other": "{{time}} λεπτά", + "second_one": "{{time}} δευτερόλεπτο", + "second_other": "{{time}} δευτερόλεπτα", + "last14": "Τελευταίες 14 ημέρες", + "last30": "Τελευταίες 30 ημέρες", + "thisWeek": "Αυτή την εβδομάδα", + "lastWeek": "Προηγούμενη Εβδομάδα", + "am": "π.μ.", + "yr": "{{time}}χρ", + "mo": "{{time}}μη", + "thisMonth": "Αυτό τον Μήνα", + "lastMonth": "Τελευταίος Μήνας", + "5minutes": "5 λεπτά", + "10minutes": "10 λεπτά", + "30minutes": "30 λεπτά", + "1hour": "1 ώρα", + "12hours": "12 ώρες", + "24hours": "24 ώρες", + "pm": "μ.μ.", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + } + }, + "menu": { + "live": { + "cameras": { + "count_one": "{{count}} Κάμερα", + "count_other": "{{count}} Κάμερες" + } + } + }, + "button": { + "save": "Αποθήκευση", + "apply": "Εφαρμογή", + "reset": "Επαναφορά", + "done": "Τέλος", + "enabled": "Ενεργοποιημένο", + "enable": "Ενεργοποίηση", + "disabled": "Απενεργοποιημένο", + "disable": "Απενεργοποίηση", + "saving": "Αποθήκευση…", + "cancel": "Ακύρωση", + "close": "Κλείσιμο", + "copy": "Αντιγραφή", + "back": "Πίσω", + "pictureInPicture": "Εικόνα σε εικόνα", + "cameraAudio": "Ήχος κάμερας", + "edit": "Επεξεργασία", + "copyCoordinates": "Αντιγραφή συντεταγμένων", + "delete": "Διαγραφή", + "yes": "Ναι", + "no": "Όχι", + "download": "Κατέβασμα", + "info": "Πληροφορίες" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "χλμ/ώρα" + }, + "length": { + "meters": "μέτρα" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/ώρα", + "mbph": "MB/ώρα", + "gbph": "GB/ώρα" + } + }, + "label": { + "back": "Επιστροφή" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/auth.json new file mode 100644 index 0000000..c978b36 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Όνομα χρήστη", + "password": "Κωδικός", + "login": "Σύνδεση", + "errors": { + "usernameRequired": "Απαιτείται όνομα χρήστη", + "passwordRequired": "Απαιτείται κωδικός", + "rateLimit": "Το όριο μεταφοράς έχει ξεπεραστεί. Δοκιμάστε ξανά αργότερα.", + "loginFailed": "Αποτυχία σύνδεσης", + "unknownError": "Άγνωστο σφάλμα. Ελέγξτε το αρχείο καταγραφής.", + "webUnknownError": "Άγνωστο σφάλμα. Εξετάστε το αρχείο καταγραφής κονσόλας." + }, + "firstTimeLogin": "Προσπαθείτε να συνδεθείτε για πρώτη φορά; Τα διαπιστευτήρια είναι τυπωμένα στα logs του Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/camera.json new file mode 100644 index 0000000..3de7248 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/camera.json @@ -0,0 +1,42 @@ +{ + "group": { + "add": "Προσθήκη ομάδας καμερών", + "label": "Ομάδες καμερών", + "edit": "Επεξεργασία ομάδας καμερών", + "delete": { + "label": "Διαγραφή ομάδας κάμερας", + "confirm": { + "title": "Επιβεβαίωση Διαγραφής", + "desc": "Είστε σίγουροι για την διαγραφή της ομάδας κάμερας {{name}};" + } + }, + "name": { + "label": "Όνομα", + "placeholder": "Εισάγετε όνομα…", + "errorMessage": { + "mustLeastCharacters": "Το όνομα ομάδας κάμερας πρέπει να περιέχει τουλάχιστον 2 χαρακτήρες.", + "exists": "Το όνομα ομάδας κάμερας υπάρχει ήδη.", + "nameMustNotPeriod": "Το όνομα ομάδας κάμερας δεν μπορεί να περιλαμβάνει κενά.", + "invalid": "Άκυρο όνομα ομάδας κάμερας." + } + }, + "camera": { + "setting": { + "audioIsUnavailable": "Ο ήχος δεν είναι διαθέσιμος για αυτή την μετάδοση", + "audio": { + "tips": { + "title": "Η κάμερα πρέπει να εκπέμπει ήχο και να είναι ρυθμισμένο το go2rtc για αυτή την μετάδοση." + } + }, + "stream": "Μετάδοση", + "placeholder": "Επιλέξτε μια μετάδοση" + } + }, + "cameras": { + "label": "Κάμερες", + "desc": "Διαλέξτε κάμερες για αυτή την ομάδα." + }, + "icon": "Εικονίδιο", + "success": "Η ομάδα κάμερας {{name}} έχει αποθηκευθεί." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/dialog.json new file mode 100644 index 0000000..c482688 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/dialog.json @@ -0,0 +1,50 @@ +{ + "restart": { + "restarting": { + "content": "Αυτή η σελίδα θα φορτώσει ξανά σε {{countdown}} δευτερόλεπτα.", + "title": "Το Frigate κάνει επανεκκίνηση", + "button": "Αναγκαστική επαναφόρτωση τώρα" + }, + "title": "Είστε σίγουροι ότι θέλετε να επανεκκινήσετε το Frigate;", + "button": "Επανεκκίνηση" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Υποβολή σε Frigate+", + "desc": "Τα αντικείμενα σε τοποθεσίες που θέλετε να αποφύγετε δεν είναι ψευδώς θετικά. Η υποβολή τους ως ψευδώς θετικά θα προκαλέσει σύγχυση στο μοντέλο." + }, + "review": { + "question": { + "label": "Επιβεβαιώστε αυτήν την ετικέτα για το Frigate Plus", + "ask_a": "Είναι αυτό το αντικείμενο {{label}};", + "ask_an": "Είναι αυτό το αντικείμενο {{label}};", + "ask_full": "Είναι αυτό το αντικείμενο {{untranslatedLabel}} ({{translatedLabel}});" + }, + "state": { + "submitted": "Υποβλήθηκε" + } + } + }, + "video": { + "viewInHistory": "Προβολή στο Ιστορικό" + } + }, + "export": { + "time": { + "fromTimeline": "Επιλογή από Χρονολόγιο", + "lastHour_one": "Τελευταία ώρα", + "lastHour_other": "Τελευταίες {{count}} Ώρες", + "custom": "Προσαρμοσμένο", + "start": { + "title": "Αρχή Χρόνου" + } + }, + "select": "Επιλογή", + "export": "Εξαγωγή", + "selectOrExport": "Επιλογή ή Εξαγωγή", + "toast": { + "success": "Επιτυχής έναρξη εξαγωγής. Δείτε το αρχείο στον φάκελο /exports." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/filter.json new file mode 100644 index 0000000..da69d7f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/filter.json @@ -0,0 +1,41 @@ +{ + "filter": "Φίλτρο", + "labels": { + "label": "Ετικέτες", + "all": { + "title": "Όλες οι ετικέτες", + "short": "Ετικέτες" + }, + "count_one": "{{count}} Ετικέτα", + "count_other": "{{count}} Ετικέτες" + }, + "classes": { + "all": { + "title": "Όλες οι κλάσεις" + }, + "count_one": "{{count}} Κλάση", + "count_other": "{{count}} Κλάσεις", + "label": "Κλάσεις" + }, + "zones": { + "label": "Ζώνες", + "all": { + "title": "Όλες οι ζώνες", + "short": "Ζώνες" + } + }, + "score": "Σκορ", + "estimatedSpeed": "Εκτιμώμενη Ταχύτητα {{unit}}", + "features": { + "label": "Χαρακτηριστικά", + "hasSnapshot": "Έχει ένα στιγμιότυπο" + }, + "dates": { + "selectPreset": "Διαλέξτε μια Προεπιλογή…", + "all": { + "title": "Όλες οι Ημερομηνίες", + "short": "Ημερομηνίες" + } + }, + "more": "Επιπλέον Φίλτρα" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/icons.json new file mode 100644 index 0000000..5be267a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Επιλέξτε ένα εικονίδιο", + "search": { + "placeholder": "Αναζήτηση εικονιδίου…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/input.json new file mode 100644 index 0000000..87a9318 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Λήψη βίντεο", + "toast": { + "success": "Το βίντεο αξιολόγησης έχει αρχίσει να κατεβαίνει." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/el/components/player.json new file mode 100644 index 0000000..de23a97 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Δεν βρέθηκαν εγγραφές για αυτήν την ώρα", + "noPreviewFound": "Δεν βρέθηκε προεπισκόπηση", + "noPreviewFoundFor": "Δεν βρέθηκε προεπισκόπηση για {{cameraName}}", + "submitFrigatePlus": { + "title": "Να υποβληθεί αυτό το καρέ στο Frigate+;", + "submit": "Υποβολή" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ή μεγαλύτερο χρειάζεται για αυτό τον τύπο του live stream.", + "streamOffline": { + "title": "Η μετάδοση είναι εκτός λειτουργίας", + "desc": "Δεν έχουν ληφθεί καρέ στη ροή {{cameraName}} detect, ελέγξτε τα αρχεία καταγραφής σφαλμάτων" + }, + "cameraDisabled": "Η Κάμερα έχει απενεργοποιηθεί", + "stats": { + "streamType": { + "title": "Τύπος μετάδοσης:", + "short": "Τύπος" + }, + "bandwidth": { + "title": "Ταχύτητα:", + "short": "Ταχύτητα" + }, + "latency": { + "title": "Καθυστέρηση:", + "value": "{{seconds}} δευτερόλεπτα", + "short": { + "title": "Καθυστέρηση", + "value": "{{seconds}} δευτερόλεπτα" + } + }, + "totalFrames": "Συνολικός αριθμός Καρέ:", + "droppedFrames": { + "title": "Απορριφθέντα καρέ:", + "short": { + "title": "Απορριφθέντα", + "value": "{{droppedFrames}} καρέ" + } + }, + "decodedFrames": "Αποκωδικοποιημένα Καρέ:", + "droppedFrameRate": "Ρυθμός Απορριφθέντων Καρέ:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Επιτυχής αποστολή εικόνας στο Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Αποτυχία αποστολής εικόνας στο Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/objects.json b/sam2-cpu/frigate-dev/web/public/locales/el/objects.json new file mode 100644 index 0000000..5cc7e4f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/objects.json @@ -0,0 +1,24 @@ +{ + "person": "Άτομο", + "bicycle": "Ποδήλατο", + "car": "Αυτοκίνητο", + "motorcycle": "Μηχανή", + "airplane": "Αεροπλάνο", + "bird": "Πουλί", + "bus": "Λεωφορείο", + "train": "Εκπαίδευση", + "boat": "Βάρκα", + "traffic_light": "Φανάρι Κυκλοφορίας", + "fire_hydrant": "Πυροσβεστικός Κρουνός", + "horse": "Άλογο", + "street_sign": "Πινακίδα Δρόμου", + "stop_sign": "Πινακίδα Στοπ", + "bear": "Αρκούδα", + "zebra": "Ζέμπρα", + "giraffe": "Καμηλοπάρδαλη", + "hat": "Καπέλο", + "parking_meter": "Παρκόμετρο", + "bench": "Παγκάκι", + "cat": "Γάτα", + "dog": "Σκύλος" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/configEditor.json new file mode 100644 index 0000000..79917bf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Επεξεργαστής ρυθμίσεων - Frigate", + "configEditor": "Επεξεργαστής Ρυθμίσεων", + "saveAndRestart": "Αποθήκευση και επανεκκίνηση", + "safeConfigEditor": "Επεξεργαστής ρυθμίσεων (Ασφαλής Λειτουργία)", + "safeModeDescription": "Το Frigate είναι σε ασφαλή λειτουργία λόγω λάθους εγκυρότητας ρυθμίσεων.", + "copyConfig": "Αντιγραφή Ρυθμίσεων", + "saveOnly": "Μόνο Αποθήκευση", + "confirm": "Έξοδος χωρίς αποθήκευση;", + "toast": { + "success": { + "copyToClipboard": "Οι Ρυθμίσεις αντιγράφτηκαν στο πρόχειρο." + }, + "error": { + "savingError": "Σφάλμα αποθήκευσης ρυθμίσεων" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/events.json new file mode 100644 index 0000000..e2e21a0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/events.json @@ -0,0 +1,32 @@ +{ + "alerts": "Ειδοποιήσεις", + "detections": "Εντοπισμοί", + "motion": { + "label": "Κίνηση", + "only": "Κίνηση μόνο" + }, + "allCameras": "Όλες οι κάμερες", + "empty": { + "alert": "Δεν υπάρχουν ειδοποιήσεις για εξέταση", + "detection": "Δεν υπάρχουν εντοπισμοί για εξέταση", + "motion": "Δεν βρέθηκαν στοιχεία κίνησης" + }, + "timeline": "Χρονολόγιο", + "timeline.aria": "Επιλογή χρονοσειράς", + "events": { + "label": "Γεγονότα", + "aria": "Επιλογή γεγονότων", + "noFoundForTimePeriod": "Δεν βρέθηκαν γεγονότα για αυτή την περίοδο." + }, + "selected_other": "{{count}} επελεγμένα", + "camera": "Κάμερα", + "detected": "ανιχνέυτηκε", + "documentTitle": "Προεσκόπιση - Frigate", + "recordings": { + "documentTitle": "Καταγραφές - Frigate" + }, + "calendarFilter": { + "last24Hours": "Τελευταίες 24 Ώρες" + }, + "markAsReviewed": "Επιβεβαίωση ως Ελεγμένα" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/explore.json new file mode 100644 index 0000000..12390dc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/explore.json @@ -0,0 +1,46 @@ +{ + "documentTitle": "Εξερευνήστε - Frigate", + "generativeAI": "Παραγωγική τεχνητή νοημοσύνη", + "exploreMore": "Εξερευνήστε περισσότερα αντικείμενα {{label}}", + "exploreIsUnavailable": { + "title": "Η εξερεύνηση δεν είναι διαθέσιμη", + "embeddingsReindexing": { + "context": "Η εξερεύνηση μπορεί να πραγματοποιηθεί μετά το πέρας της καταλογράφησης εμπλουτισμών.", + "startingUp": "Εκκίνηση…", + "estimatedTime": "Εκτιμώμενο υπόλοιπο χρόνου:", + "finishingShortly": "Ολοκλήρωση συντόμως", + "step": { + "thumbnailsEmbedded": "Ενσωματωμένες εικόνες: ", + "descriptionsEmbedded": "Ενσωματωμένες περιγραφές: ", + "trackedObjectsProcessed": "Επεξεργασία παρακολουθούμενων αντικειμένων: " + } + }, + "downloadingModels": { + "context": "Το Frigate κατεβάζει τα απαιτούμενα μοντέλα ενσωμάτωσης για να υποστηρίξει την σημασιολογική αναζήτηση. Αυτό μπορεί να διαρκέσει αρκετά λεπτά αναλόγως και της ταχύτητας σύνδεσης με το διαδύκτιο.", + "setup": { + "visionModel": "Οπτικό Μοντέλο", + "visionModelFeatureExtractor": "Εξαγωγή χαρακτηριστικών οπτικού μοντέλου", + "textModel": "Μοντέλο γραφής" + } + } + }, + "details": { + "timestamp": "Χρονοσήμανση", + "item": { + "tips": { + "mismatch_one": "{{count}} μη διαθέσιμο αντικείμενο ανιχνεύτηκε και έχει συνιπολογιστεί στην προεσκόπιση. Αυτό το αντικείμενο είτε δεν πληροί τις προϋποθέσεις ως προειδοποίηση ή ανίχνευση ή έχει ήδη καθαριστεί/διαγραφεί.", + "mismatch_other": "{{count}} μη διαθέσιμα αντικείμενα ανιχνεύτηκαν και έχουν συνιπολογιστεί στην προεσκόπιση. Αυτά τα αντικείμενα είτε δεν πληρούν τις προϋποθέσεις ως προειδοποιήσεις ή ανιχνεύσεις ή έχουν ήδη καθαριστεί/διαγραφεί." + } + } + }, + "type": { + "video": "βίντεο", + "object_lifecycle": "κύκλος ζωής αντικειμένου" + }, + "objectLifecycle": { + "title": "Κύκλος Ζωής Αντικειμένου", + "noImageFound": "Δεν βρέθηκε εικόνα για αυτό το χρονικό σημείο." + }, + "trackedObjectsCount_one": "{{count}} παρακολουθούμενο αντικείμενο ", + "trackedObjectsCount_other": "{{count}} παρακολουθούμενα αντικείμενα " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/exports.json new file mode 100644 index 0000000..8aff542 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Εξαγωγή - Frigate", + "search": "Αναζήτηση", + "deleteExport": "Διαγραφή εξαγωγής", + "noExports": "Δεν βρέθηκαν εξαγωγές", + "deleteExport.desc": "Είστε σίγουροι οτι θέλετε να διαγράψετε {{exportName}};", + "editExport": { + "title": "Μετονομασία Εξαγωγής", + "desc": "Εισάγετε ένα νέο όνομα για την εξαγωγή.", + "saveExport": "Αποθήκευση Εξαγωγής" + }, + "toast": { + "error": { + "renameExportFailed": "Αποτυχία μετονομασίας εξαγωγής:{{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/faceLibrary.json new file mode 100644 index 0000000..f41e89c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/faceLibrary.json @@ -0,0 +1,49 @@ +{ + "description": { + "addFace": "Οδηγός για την προσθήκη μιας νέας συλλογής στη Βιβλιοθήκη Προσώπων.", + "placeholder": "Εισαγάγετε ένα όνομα για αυτήν τη συλλογή", + "invalidName": "Μη έγκυρο όνομα. Τα ονόματα μπορούν να περιλαμβάνουν γράμματα, αριθμούς, κενό διάστημα, απόστροφο, παύλα, κάτω παύλα." + }, + "details": { + "person": "Άτομο", + "subLabelScore": "Σκορ υποετικέτας", + "scoreInfo": "Το σκορ υποετικέτας είναι το σταθμισμένο σκορ όλων των αναγνωρισμένων προσώπων, αυτό μπορεί να διαφέρει από το σκορ που φαίνεται στο στιγμιότυπο.", + "face": "Λεπτομέρειες προσώπου", + "faceDesc": "Λεπτομέρειες του παρακολουθούμενου αντικειμένου που παρήγε αυτό το πρόσωπο", + "timestamp": "Χρονοσήμανση", + "unknown": "Άγνωστο" + }, + "deleteFaceAttempts": { + "desc_one": "Είστε σίγουροι ότι θέλετε να διαγράψετε {{count}} πρόσωπο; Αυτή η πράξη δεν επαναφέρεται.", + "desc_other": "Είστε σίγουροι ότι θέλετε να διαγράψετε {{count}} πρόσωπα; Αυτή η πράξη δεν επαναφέρεται." + }, + "toast": { + "success": { + "deletedFace_one": "Επιτυχής διαγραφή {{count}} προσώπου.", + "deletedFace_other": "Επιτυχής διαγραφή {{count}} προσώπων.", + "deletedName_one": "{{count}} πρόσωπο διεγράφη επιτυχημένα.", + "deletedName_other": "{{count}} πρόσωπα διεγράφη επιτυχημένα." + } + }, + "documentTitle": "Βιβλιοθήκη προσώπων - Frigate", + "uploadFaceImage": { + "title": "Μεταφόρτωση Εικόνας Προσώπου" + }, + "steps": { + "nextSteps": "Επόμενα βήματα", + "description": { + "uploadFace": "Μεταφορτώστε μια εικόνα του/της {{name}} που δείχνει το πρόσωπο τους από μπροστινή λήψη. Η εικόνα δεν χρειάζεται να περιέχει μόνο το πρόσωπο τους." + } + }, + "train": { + "title": "Εκπαίδευση", + "aria": "Επιλογή εκπαίδευσης", + "empty": "Δεν υπάρχουν πρόσφατες προσπάθειες αναγνώρισης προσώπου" + }, + "collections": "Συλλογές", + "createFaceLibrary": { + "title": "Δημιουργία Συλλογής", + "desc": "Δημιουργία νέας συλλογής", + "new": "Δημιουργία Νέου Προσώπου" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/live.json new file mode 100644 index 0000000..b242711 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/live.json @@ -0,0 +1,69 @@ +{ + "documentTitle": "Ζωντανά - Frigate", + "twoWayTalk": { + "enable": "Ενεργοποίηση αμφίδρομης επικοινωνίας", + "disable": "Απενεργοποίηση αμφίδρομης επικοινωνίας" + }, + "documentTitle.withCamera": "{{camera}} - Ζωντανή μετάδοση - Frigate", + "lowBandwidthMode": "Λειτουργία χαμηλής ευρυζωνικότητας", + "cameraAudio": { + "enable": "Ενεργοποίηση ήχου Κάμερας", + "disable": "Απενεργοποίηση Ήχου Κάμερας" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Πατήστε στο πλαίσιο για να κεντράρετε την κάμερα", + "enable": "Ενεργοποίηση κλικ για μεταφορά", + "disable": "Απενεργοποίηση κλικ για μεταφορά" + }, + "left": { + "label": "Κίνηση κάμερας προς τα αριστερά" + }, + "up": { + "label": "Κίνηση κάμερας προς τα πάνω" + }, + "down": { + "label": "Κίνηση κάμερας προς τα κάτω" + }, + "right": { + "label": "Κίνηση κάμερας προς τα δεξιά" + } + }, + "zoom": { + "in": { + "label": "Ζουμάρισμα κάμερας προς τα μέσα" + }, + "out": { + "label": "Ζουμάρισμα κάμερας προς τα έξω" + } + } + }, + "camera": { + "enable": "Ενεργοποίηση Κάμερας", + "disable": "Απενεργοποίηση Κάμερας" + }, + "muteCameras": { + "enable": "Σίγαση Όλων των Καμερών", + "disable": "Απενεργοποίηση Σίγασης Όλων των Καμερών" + }, + "detect": { + "enable": "Ενεργοποίηση Ανίχνευσης", + "disable": "Απενεργοποίηση Ανίχνευσης" + }, + "recording": { + "enable": "Ενεργοποίηση Καταγραφής", + "disable": "Απενεργοποίηση Καταγραφής" + }, + "snapshots": { + "enable": "Ενεργοποίηση Στιγμιοτίπων", + "disable": "Απενεργοποίηση Στιγμιοτίπων" + }, + "audioDetect": { + "enable": "Ενεργοποίηση Ανίχνευσης Ήχου", + "disable": "Απενεργοποίηση Ανίχνευσης Ήχου" + }, + "noCameras": { + "buttonText": "Προσθήκη Κάμερας" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/recording.json new file mode 100644 index 0000000..9681d0e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Φίλτρο", + "export": "Εξαγωγή", + "calendar": "Ημερολόγιο", + "filters": "Φίλτρα", + "toast": { + "error": { + "noValidTimeSelected": "Μη επιλογή έγκυρης περιόδου", + "endTimeMustAfterStartTime": "Το επιλεγμένο τέλος περιόδου πρέπει να είναι μετά την επιλεγμένη αρχή περιόδου" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/search.json new file mode 100644 index 0000000..1281446 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/search.json @@ -0,0 +1,29 @@ +{ + "search": "Αναζήτηση", + "savedSearches": "Αποθηκευμένες Αναζητήσεις", + "button": { + "clear": "Εκαθάρηση αναζήτησης", + "save": "Αποθήκευση αναζήτησης", + "delete": "Διαγραφή αποθηκευμένης αναζήτησης", + "filterInformation": "Πληροφορίες φίλτρου", + "filterActive": "Φίλτρα ενεργά" + }, + "searchFor": "Αναζήτηση {{inputValue}}", + "trackedObjectId": "Σήμανση παρακολουθούμενου αντικειμένου", + "filter": { + "label": { + "cameras": "Κάμερες", + "labels": "Ετικέτες", + "zones": "Ζώνες", + "max_speed": "Ανώτατη Ταχύτητα", + "recognized_license_plate": "Αναγνωρισμένη Πινακίδα Κυκλοφορίας", + "has_clip": "Έχει Κλιπ", + "has_snapshot": "Έχει Στιγμιότυπο", + "sub_labels": "Υποετικέτες", + "search_type": "Τύπος Αναζήτησης", + "time_range": "Χρονική Περίοδος", + "before": "Πριν", + "after": "Μετά" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/settings.json new file mode 100644 index 0000000..909bc57 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/settings.json @@ -0,0 +1,56 @@ +{ + "documentTitle": { + "default": "Ρυθμίσεις - Frigate", + "authentication": "Ρυθμίσεις ελέγχου ταυτοποίησης - Frigate", + "camera": "Ρυθμίσεις Κάμερας - Frigate", + "enrichments": "Ρυθμίσεις εμπλουτισμού - Frigate", + "masksAndZones": "Ρυθμίσεις Μασκών και Ζωνών - Frigate", + "motionTuner": "Ρύθμιση Κίνησης - Frigate", + "object": "Επίλυση σφαλμάτων - Frigate", + "general": "Γενικές ρυθμίσεις - Frigate", + "frigatePlus": "Ρυθμίσεις Frigate+ - Frigate", + "notifications": "Ρυθμίσεις Ειδοποιήσεων" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} σημέιο", + "point_other": "{{count}} σημεία" + }, + "motionMasks": { + "point_one": "{{count}} σημείο", + "point_other": "{{count}} σημεία" + }, + "objectMasks": { + "point_one": "{{count}} σημέιο", + "point_other": "{{count}} σημεία" + } + }, + "menu": { + "ui": "Επιφάνεια Εργασίας", + "enrichments": "Εμπλουτισμοί", + "cameras": "Ρυθμίσεις Κάμερας", + "masksAndZones": "Μάσκες / Ζώνες", + "motionTuner": "Ρυθμιστής Κίνησης", + "debug": "Επίλυση Σφαλμάτων" + }, + "dialog": { + "unsavedChanges": { + "title": "Έχετε μη αποθηκευμένες αλλαγές.", + "desc": "Θέλετε να αποθηκεύσετε τις αλλαγές σας πριν την συνέχεια;" + } + }, + "cameraSetting": { + "camera": "Κάμερα", + "noCamera": "Δεν υπάρχει Κάμερα" + }, + "triggers": { + "dialog": { + "form": { + "friendly_name": { + "placeholder": "Ονομάτισε ή περιέγραψε αυτό το εύνασμα", + "description": "Ένα προαιρετικό φιλικό όνομα, ή ένα περιγραφικό κείμενο για αυτό το εύνασμα." + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/el/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/el/views/system.json new file mode 100644 index 0000000..0ec8ff5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/el/views/system.json @@ -0,0 +1,39 @@ +{ + "documentTitle": { + "cameras": "Στατιστικά Καμερών - Frigate", + "storage": "Στατιστικά αποθήκευσης - Frigate", + "general": "Γενικά στατιστικά - Frigate", + "enrichments": "Στατιστικά Εμπλουτισμού - Frigate", + "logs": { + "frigate": "Frigate αρχέιο καταγραφών - Frigate", + "go2rtc": "Αρχείο καταγραφής Go2RTC - Frigate", + "nginx": "Αρχείο καταγραφών Nginx - Frigate" + } + }, + "title": "Σύστημα", + "metrics": "Μετρήσεις συστήματος", + "logs": { + "download": { + "label": "Λήψη Αρχείων Καταγραφής" + }, + "copy": { + "label": "Αντιγραφή στο πρόχειρο", + "success": "Αρχεία καταγραφής αντιγράφτηκαν στο πρόχειρο", + "error": "Αποτυχία αντιγραφής των αρχείων καταγραφής στο πρόχειρο" + }, + "type": { + "label": "Τύπος", + "timestamp": "Χρονοσήμανση", + "tag": "Λέξη Κλειδί", + "message": "Μήνυμα" + } + }, + "general": { + "title": "Γενικά", + "detector": { + "title": "Ανιχνευτές", + "inferenceSpeed": "Ταχύτητα Συμπεράσματος Ανιχνευτή", + "temperature": "Θερμοκρασία Ανιχνευτή" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/audio.json b/sam2-cpu/frigate-dev/web/public/locales/en/audio.json new file mode 100644 index 0000000..5c197e8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Speech", + "babbling": "Babbling", + "yell": "Yell", + "bellow": "Bellow", + "whoop": "Whoop", + "whispering": "Whispering", + "laughter": "Laughter", + "snicker": "Snicker", + "crying": "Crying", + "sigh": "Sigh", + "singing": "Singing", + "choir": "Choir", + "yodeling": "Yodeling", + "chant": "Chant", + "mantra": "Mantra", + "child_singing": "Child Singing", + "synthetic_singing": "Synthetic Singing", + "rapping": "Rapping", + "humming": "Humming", + "groan": "Groan", + "grunt": "Grunt", + "whistling": "Whistling", + "breathing": "Breathing", + "wheeze": "Wheeze", + "snoring": "Snoring", + "gasp": "Gasp", + "pant": "Pant", + "snort": "Snort", + "cough": "Cough", + "throat_clearing": "Throat Clearing", + "sneeze": "Sneeze", + "sniff": "Sniff", + "run": "Run", + "shuffle": "Shuffle", + "footsteps": "Footsteps", + "chewing": "Chewing", + "biting": "Biting", + "gargling": "Gargling", + "stomach_rumble": "Stomach Rumble", + "burping": "Burping", + "hiccup": "Hiccup", + "fart": "Fart", + "hands": "Hands", + "finger_snapping": "Finger Snapping", + "clapping": "Clapping", + "heartbeat": "Heartbeat", + "heart_murmur": "Heart Murmur", + "cheering": "Cheering", + "applause": "Applause", + "chatter": "Chatter", + "crowd": "Crowd", + "children_playing": "Children Playing", + "animal": "Animal", + "pets": "Pets", + "dog": "Dog", + "bark": "Bark", + "yip": "Yip", + "howl": "Howl", + "bow_wow": "Bow Wow", + "growling": "Growling", + "whimper_dog": "Dog Whimper", + "cat": "Cat", + "purr": "Purr", + "meow": "Meow", + "hiss": "Hiss", + "caterwaul": "Caterwaul", + "livestock": "Livestock", + "horse": "Horse", + "clip_clop": "Clip Clop", + "neigh": "Neigh", + "cattle": "Cattle", + "moo": "Moo", + "cowbell": "Cowbell", + "pig": "Pig", + "oink": "Oink", + "goat": "Goat", + "bleat": "Bleat", + "sheep": "Sheep", + "fowl": "Fowl", + "chicken": "Chicken", + "cluck": "Cluck", + "cock_a_doodle_doo": "Cock-a-Doodle-Doo", + "turkey": "Turkey", + "gobble": "Gobble", + "duck": "Duck", + "quack": "Quack", + "goose": "Goose", + "honk": "Honk", + "wild_animals": "Wild Animals", + "roaring_cats": "Roaring Cats", + "roar": "Roar", + "bird": "Bird", + "chirp": "Chirp", + "squawk": "Squawk", + "pigeon": "Pigeon", + "coo": "Coo", + "crow": "Crow", + "caw": "Caw", + "owl": "Owl", + "hoot": "Hoot", + "flapping_wings": "Flapping Wings", + "dogs": "Dogs", + "rats": "Rats", + "mouse": "Mouse", + "patter": "Patter", + "insect": "Insect", + "cricket": "Cricket", + "mosquito": "Mosquito", + "fly": "Fly", + "buzz": "Buzz", + "frog": "Frog", + "croak": "Croak", + "snake": "Snake", + "rattle": "Rattle", + "whale_vocalization": "Whale Vocalization", + "music": "Music", + "musical_instrument": "Musical Instrument", + "plucked_string_instrument": "Plucked String Instrument", + "guitar": "Guitar", + "electric_guitar": "Electric Guitar", + "bass_guitar": "Bass Guitar", + "acoustic_guitar": "Acoustic Guitar", + "steel_guitar": "Steel Guitar", + "tapping": "Tapping", + "strum": "Strum", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolin", + "zither": "Zither", + "ukulele": "Ukulele", + "keyboard": "Keyboard", + "piano": "Piano", + "electric_piano": "Electric Piano", + "organ": "Organ", + "electronic_organ": "Electronic Organ", + "hammond_organ": "Hammond Organ", + "synthesizer": "Synthesizer", + "sampler": "Sampler", + "harpsichord": "Harpsichord", + "percussion": "Percussion", + "drum_kit": "Drum Kit", + "drum_machine": "Drum Machine", + "drum": "Drum", + "snare_drum": "Snare Drum", + "rimshot": "Rimshot", + "drum_roll": "Drum Roll", + "bass_drum": "Bass Drum", + "timpani": "Timpani", + "tabla": "Tabla", + "cymbal": "Cymbal", + "hi_hat": "Hi-Hat", + "wood_block": "Wood Block", + "tambourine": "Tambourine", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Tubular Bells", + "mallet_percussion": "Mallet Percussion", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibraphone", + "steelpan": "Steelpan", + "orchestra": "Orchestra", + "brass_instrument": "Brass Instrument", + "french_horn": "French Horn", + "trumpet": "Trumpet", + "trombone": "Trombone", + "bowed_string_instrument": "Bowed String Instrument", + "string_section": "String Section", + "violin": "Violin", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Double Bass", + "wind_instrument": "Wind Instrument", + "flute": "Flute", + "saxophone": "Saxophone", + "clarinet": "Clarinet", + "harp": "Harp", + "bell": "Bell", + "church_bell": "Church Bell", + "jingle_bell": "Jingle Bell", + "bicycle_bell": "Bicycle Bell", + "tuning_fork": "Tuning Fork", + "chime": "Chime", + "wind_chime": "Wind Chime", + "harmonica": "Harmonica", + "accordion": "Accordion", + "bagpipes": "Bagpipes", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Singing Bowl", + "scratching": "Scratching", + "pop_music": "Pop Music", + "hip_hop_music": "Hip-Hop Music", + "beatboxing": "Beatboxing", + "rock_music": "Rock Music", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Psychedelic Rock", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Soul Music", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swing Music", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folk Music", + "middle_eastern_music": "Middle Eastern Music", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Classical Music", + "opera": "Opera", + "electronic_music": "Electronic Music", + "house_music": "House Music", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronica", + "electronic_dance_music": "Electronic Dance Music", + "ambient_music": "Ambient Music", + "trance_music": "Trance Music", + "music_of_latin_america": "Music of Latin America", + "salsa_music": "Salsa Music", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Music for Children", + "new-age_music": "New Age Music", + "vocal_music": "Vocal Music", + "a_capella": "A Capella", + "music_of_africa": "Music of Africa", + "afrobeat": "Afrobeat", + "christian_music": "Christian Music", + "gospel_music": "Gospel Music", + "music_of_asia": "Music of Asia", + "carnatic_music": "Carnatic Music", + "music_of_bollywood": "Music of Bollywood", + "ska": "Ska", + "traditional_music": "Traditional Music", + "independent_music": "Independent Music", + "song": "Song", + "background_music": "Background Music", + "theme_music": "Theme Music", + "jingle": "Jingle", + "soundtrack_music": "Soundtrack Music", + "lullaby": "Lullaby", + "video_game_music": "Video Game Music", + "christmas_music": "Christmas Music", + "dance_music": "Dance Music", + "wedding_music": "Wedding Music", + "happy_music": "Happy Music", + "sad_music": "Sad Music", + "tender_music": "Tender Music", + "exciting_music": "Exciting Music", + "angry_music": "Angry Music", + "scary_music": "Scary Music", + "wind": "Wind", + "rustling_leaves": "Rustling Leaves", + "wind_noise": "Wind Noise", + "thunderstorm": "Thunderstorm", + "thunder": "Thunder", + "water": "Water", + "rain": "Rain", + "raindrop": "Raindrop", + "rain_on_surface": "Rain on Surface", + "stream": "Stream", + "waterfall": "Waterfall", + "ocean": "Ocean", + "waves": "Waves", + "steam": "Steam", + "gurgling": "Gurgling", + "fire": "Fire", + "crackle": "Crackle", + "vehicle": "Vehicle", + "boat": "Boat", + "sailboat": "Sailboat", + "rowboat": "Rowboat", + "motorboat": "Motorboat", + "ship": "Ship", + "motor_vehicle": "Motor Vehicle", + "car": "Car", + "toot": "Toot", + "car_alarm": "Car Alarm", + "power_windows": "Power Windows", + "skidding": "Skidding", + "tire_squeal": "Tire Squeal", + "car_passing_by": "Car Passing By", + "race_car": "Race Car", + "truck": "Truck", + "air_brake": "Air Brake", + "air_horn": "Air Horn", + "reversing_beeps": "Reversing Beeps", + "ice_cream_truck": "Ice Cream Truck", + "bus": "Bus", + "emergency_vehicle": "Emergency Vehicle", + "police_car": "Police Car", + "ambulance": "Ambulance", + "fire_engine": "Fire Engine", + "motorcycle": "Motorcycle", + "traffic_noise": "Traffic Noise", + "rail_transport": "Rail Transport", + "train": "Train", + "train_whistle": "Train Whistle", + "train_horn": "Train Horn", + "railroad_car": "Railroad Car", + "train_wheels_squealing": "Train Wheels Squealing", + "subway": "Subway", + "aircraft": "Aircraft", + "aircraft_engine": "Aircraft Engine", + "jet_engine": "Jet Engine", + "propeller": "Propeller", + "helicopter": "Helicopter", + "fixed-wing_aircraft": "Fixed-Wing Aircraft", + "bicycle": "Bicycle", + "skateboard": "Skateboard", + "engine": "Engine", + "light_engine": "Light Engine", + "dental_drill's_drill": "Dental Drill", + "lawn_mower": "Lawn Mower", + "chainsaw": "Chainsaw", + "medium_engine": "Medium Engine", + "heavy_engine": "Heavy Engine", + "engine_knocking": "Engine Knocking", + "engine_starting": "Engine Starting", + "idling": "Idling", + "accelerating": "Accelerating", + "door": "Door", + "doorbell": "Doorbell", + "ding-dong": "Ding-Dong", + "sliding_door": "Sliding Door", + "slam": "Slam", + "knock": "Knock", + "tap": "Tap", + "squeak": "Squeak", + "cupboard_open_or_close": "Cupboard Open or Close", + "drawer_open_or_close": "Drawer Open or Close", + "dishes": "Dishes", + "cutlery": "Cutlery", + "chopping": "Chopping", + "frying": "Frying", + "microwave_oven": "Microwave Oven", + "blender": "Blender", + "water_tap": "Water Tap", + "sink": "Sink", + "bathtub": "Bathtub", + "hair_dryer": "Hair Dryer", + "toilet_flush": "Toilet Flush", + "toothbrush": "Toothbrush", + "electric_toothbrush": "Electric Toothbrush", + "vacuum_cleaner": "Vacuum Cleaner", + "zipper": "Zipper", + "keys_jangling": "Keys Jangling", + "coin": "Coin", + "scissors": "Scissors", + "electric_shaver": "Electric Shaver", + "shuffling_cards": "Shuffling Cards", + "typing": "Typing", + "typewriter": "Typewriter", + "computer_keyboard": "Computer Keyboard", + "writing": "Writing", + "alarm": "Alarm", + "telephone": "Telephone", + "telephone_bell_ringing": "Telephone Bell Ringing", + "ringtone": "Ringtone", + "telephone_dialing": "Telephone Dialing", + "dial_tone": "Dial Tone", + "busy_signal": "Busy Signal", + "alarm_clock": "Alarm Clock", + "siren": "Siren", + "civil_defense_siren": "Civil Defense Siren", + "buzzer": "Buzzer", + "smoke_detector": "Smoke Detector", + "fire_alarm": "Fire Alarm", + "foghorn": "Foghorn", + "whistle": "Whistle", + "steam_whistle": "Steam Whistle", + "mechanisms": "Mechanisms", + "ratchet": "Ratchet", + "clock": "Clock", + "tick": "Tick", + "tick-tock": "Tick-Tock", + "gears": "Gears", + "pulleys": "Pulleys", + "sewing_machine": "Sewing Machine", + "mechanical_fan": "Mechanical Fan", + "air_conditioning": "Air Conditioning", + "cash_register": "Cash Register", + "printer": "Printer", + "camera": "Camera", + "single-lens_reflex_camera": "Single-Lens Reflex Camera", + "tools": "Tools", + "hammer": "Hammer", + "jackhammer": "Jackhammer", + "sawing": "Sawing", + "filing": "Filing", + "sanding": "Sanding", + "power_tool": "Power Tool", + "drill": "Drill", + "explosion": "Explosion", + "gunshot": "Gunshot", + "machine_gun": "Machine Gun", + "fusillade": "Fusillade", + "artillery_fire": "Artillery Fire", + "cap_gun": "Cap Gun", + "fireworks": "Fireworks", + "firecracker": "Firecracker", + "burst": "Burst", + "eruption": "Eruption", + "boom": "Boom", + "wood": "Wood", + "chop": "Chop", + "splinter": "Splinter", + "crack": "Crack", + "glass": "Glass", + "chink": "Chink", + "shatter": "Shatter", + "silence": "Silence", + "sound_effect": "Sound Effect", + "environmental_noise": "Environmental Noise", + "static": "Static", + "white_noise": "White Noise", + "pink_noise": "Pink Noise", + "television": "Television", + "radio": "Radio", + "field_recording": "Field Recording", + "scream": "Scream", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Change Ringing", + "shofar": "Shofar", + "liquid": "Liquid", + "splash": "Splash", + "slosh": "Slosh", + "squish": "Squish", + "drip": "Drip", + "pour": "Pour", + "trickle": "Trickle", + "gush": "Gush", + "fill": "Fill", + "spray": "Spray", + "pump": "Pump", + "stir": "Stir", + "boiling": "Boiling", + "sonar": "Sonar", + "arrow": "Arrow", + "whoosh": "Whoosh", + "thump": "Thump", + "thunk": "Thunk", + "electronic_tuner": "Electronic Tuner", + "effects_unit": "Effects Unit", + "chorus_effect": "Chorus Effect", + "basketball_bounce": "Basketball Bounce", + "bang": "Bang", + "slap": "Slap", + "whack": "Whack", + "smash": "Smash", + "breaking": "Breaking", + "bouncing": "Bouncing", + "whip": "Whip", + "flap": "Flap", + "scratch": "Scratch", + "scrape": "Scrape", + "rub": "Rub", + "roll": "Roll", + "crushing": "Crushing", + "crumpling": "Crumpling", + "tearing": "Tearing", + "beep": "Beep", + "ping": "Ping", + "ding": "Ding", + "clang": "Clang", + "squeal": "Squeal", + "creak": "Creak", + "rustle": "Rustle", + "whir": "Whir", + "clatter": "Clatter", + "sizzle": "Sizzle", + "clicking": "Clicking", + "clickety_clack": "Clickety Clack", + "rumble": "Rumble", + "plop": "Plop", + "hum": "Hum", + "zing": "Zing", + "boing": "Boing", + "crunch": "Crunch", + "sine_wave": "Sine Wave", + "harmonic": "Harmonic", + "chirp_tone": "Chirp Tone", + "pulse": "Pulse", + "inside": "Inside", + "outside": "Outside", + "reverberation": "Reverberation", + "echo": "Echo", + "noise": "Noise", + "mains_hum": "Mains Hum", + "distortion": "Distortion", + "sidetone": "Sidetone", + "cacophony": "Cacophony", + "throbbing": "Throbbing", + "vibration": "Vibration" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/common.json b/sam2-cpu/frigate-dev/web/public/locales/en/common.json new file mode 100644 index 0000000..aa841c3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/common.json @@ -0,0 +1,297 @@ +{ + "time": { + "untilForTime": "Until {{time}}", + "untilForRestart": "Until Frigate restarts.", + "untilRestart": "Until restart", + "ago": "{{timeAgo}} ago", + "justNow": "Just now", + "today": "Today", + "yesterday": "Yesterday", + "last7": "Last 7 days", + "last14": "Last 14 days", + "last30": "Last 30 days", + "thisWeek": "This Week", + "lastWeek": "Last Week", + "thisMonth": "This Month", + "lastMonth": "Last Month", + "5minutes": "5 minutes", + "10minutes": "10 minutes", + "30minutes": "30 minutes", + "1hour": "1 hour", + "12hours": "12 hours", + "24hours": "24 hours", + "pm": "pm", + "am": "am", + "yr": "{{time}}yr", + "year_one": "{{time}} year", + "year_other": "{{time}} years", + "mo": "{{time}}mo", + "month_one": "{{time}} month", + "month_other": "{{time}} months", + "d": "{{time}}d", + "day_one": "{{time}} day", + "day_other": "{{time}} days", + "h": "{{time}}h", + "hour_one": "{{time}} hour", + "hour_other": "{{time}} hours", + "m": "{{time}}m", + "minute_one": "{{time}} minute", + "minute_other": "{{time}} minutes", + "s": "{{time}}s", + "second_one": "{{time}} second", + "second_other": "{{time}} seconds", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "In progress", + "invalidStartTime": "Invalid start time", + "invalidEndTime": "Invalid end time" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "feet": "feet", + "meters": "meters" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "Go back", + "hide": "Hide {{item}}", + "show": "Show {{item}}", + "ID": "ID", + "none": "None", + "all": "All" + }, + "list": { + "two": "{{0}} and {{1}}", + "many": "{{items}}, and {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Optional", + "internalID": "The Internal ID Frigate uses in the configuration and database" + }, + "button": { + "apply": "Apply", + "reset": "Reset", + "done": "Done", + "enabled": "Enabled", + "enable": "Enable", + "disabled": "Disabled", + "disable": "Disable", + "save": "Save", + "saving": "Saving…", + "cancel": "Cancel", + "close": "Close", + "copy": "Copy", + "back": "Back", + "history": "History", + "fullscreen": "Fullscreen", + "exitFullscreen": "Exit Fullscreen", + "pictureInPicture": "Picture in Picture", + "twoWayTalk": "Two Way Talk", + "cameraAudio": "Camera Audio", + "on": "ON", + "off": "OFF", + "edit": "Edit", + "copyCoordinates": "Copy coordinates", + "delete": "Delete", + "yes": "Yes", + "no": "No", + "download": "Download", + "info": "Info", + "suspended": "Suspended", + "unsuspended": "Unsuspend", + "play": "Play", + "unselect": "Unselect", + "export": "Export", + "deleteNow": "Delete Now", + "next": "Next", + "continue": "Continue" + }, + "menu": { + "system": "System", + "systemMetrics": "System metrics", + "configuration": "Configuration", + "systemLogs": "System logs", + "settings": "Settings", + "configurationEditor": "Configuration Editor", + "languages": "Languages", + "language": { + "en": "English (English)", + "es": "Español (Spanish)", + "zhCN": "简体中文 (Simplified Chinese)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (French)", + "ar": "العربية (Arabic)", + "pt": "Português (Portuguese)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "ru": "Русский (Russian)", + "de": "Deutsch (German)", + "ja": "日本語 (Japanese)", + "tr": "Türkçe (Turkish)", + "it": "Italiano (Italian)", + "nl": "Nederlands (Dutch)", + "sv": "Svenska (Swedish)", + "cs": "Čeština (Czech)", + "nb": "Norsk Bokmål (Norwegian Bokmål)", + "ko": "한국어 (Korean)", + "vi": "Tiếng Việt (Vietnamese)", + "fa": "فارسی (Persian)", + "pl": "Polski (Polish)", + "uk": "Українська (Ukrainian)", + "he": "עברית (Hebrew)", + "el": "Ελληνικά (Greek)", + "ro": "Română (Romanian)", + "hu": "Magyar (Hungarian)", + "fi": "Suomi (Finnish)", + "da": "Dansk (Danish)", + "sk": "Slovenčina (Slovak)", + "yue": "粵語 (Cantonese)", + "th": "ไทย (Thai)", + "ca": "Català (Catalan)", + "sr": "Српски (Serbian)", + "sl": "Slovenščina (Slovenian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Use the system settings for language" + } + }, + "appearance": "Appearance", + "darkMode": { + "label": "Dark Mode", + "light": "Light", + "dark": "Dark", + "withSystem": { + "label": "Use the system settings for light or dark mode" + } + }, + "withSystem": "System", + "theme": { + "label": "Theme", + "blue": "Blue", + "green": "Green", + "nord": "Nord", + "red": "Red", + "highcontrast": "High Contrast", + "default": "Default" + }, + "help": "Help", + "documentation": { + "title": "Documentation", + "label": "Frigate documentation" + }, + "restart": "Restart Frigate", + "live": { + "title": "Live", + "allCameras": "All Cameras", + "cameras": { + "title": "Cameras", + "count_one": "{{count}} Camera", + "count_other": "{{count}} Cameras" + } + }, + "review": "Review", + "explore": "Explore", + "export": "Export", + "uiPlayground": "UI Playground", + "faceLibrary": "Face Library", + "classification": "Classification", + "user": { + "title": "User", + "account": "Account", + "current": "Current User: {{user}}", + "anonymous": "anonymous", + "logout": "Logout", + "setPassword": "Set Password" + } + }, + "toast": { + "copyUrlToClipboard": "Copied URL to clipboard.", + "save": { + "title": "Save", + "error": { + "title": "Failed to save config changes: {{errorMessage}}", + "noMessage": "Failed to save config changes" + } + } + }, + "role": { + "title": "Role", + "admin": "Admin", + "viewer": "Viewer", + "desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI." + }, + "pagination": { + "label": "pagination", + "previous": { + "title": "Previous", + "label": "Go to previous page" + }, + "next": { + "title": "Next", + "label": "Go to next page" + }, + "more": "More pages" + }, + "accessDenied": { + "documentTitle": "Access Denied - Frigate", + "title": "Access Denied", + "desc": "You don't have permission to view this page." + }, + "notFound": { + "documentTitle": "Not Found - Frigate", + "title": "404", + "desc": "Page not found" + }, + "selectItem": "Select {{item}}", + "readTheDocumentation": "Read the documentation", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/auth.json new file mode 100644 index 0000000..56b7500 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Username", + "password": "Password", + "login": "Login", + "firstTimeLogin": "Trying to log in for the first time? Credentials are printed in the Frigate logs.", + "errors": { + "usernameRequired": "Username is required", + "passwordRequired": "Password is required", + "rateLimit": "Exceeded rate limit. Try again later.", + "loginFailed": "Login failed", + "unknownError": "Unknown error. Check logs.", + "webUnknownError": "Unknown error. Check console logs." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/camera.json new file mode 100644 index 0000000..864efa6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "Camera Groups", + "add": "Add Camera Group", + "edit": "Edit Camera Group", + "delete": { + "label": "Delete Camera Group", + "confirm": { + "title": "Confirm Delete", + "desc": "Are you sure you want to delete the camera group {{name}}?" + } + }, + "name": { + "label": "Name", + "placeholder": "Enter a name…", + "errorMessage": { + "mustLeastCharacters": "Camera group name must be at least 2 characters.", + "exists": "Camera group name already exists.", + "nameMustNotPeriod": "Camera group name must not contain a period.", + "invalid": "Invalid camera group name." + } + }, + "cameras": { + "label": "Cameras", + "desc": "Select cameras for this group." + }, + "icon": "Icon", + "success": "Camera group ({{name}}) has been saved.", + "camera": { + "birdseye": "Birdseye", + "setting": { + "label": "Camera Streaming Settings", + "title": "{{cameraName}} Streaming Settings", + "desc": "Change the live streaming options for this camera group's dashboard. These settings are device/browser-specific.", + "audioIsAvailable": "Audio is available for this stream", + "audioIsUnavailable": "Audio is unavailable for this stream", + "audio": { + "tips": { + "title": "Audio must be output from your camera and configured in go2rtc for this stream." + } + }, + "stream": "Stream", + "placeholder": "Choose a stream", + "streamMethod": { + "label": "Streaming Method", + "placeholder": "Choose a streaming method", + "method": { + "noStreaming": { + "label": "No Streaming", + "desc": "Camera images will only update once per minute and no live streaming will occur." + }, + "smartStreaming": { + "label": "Smart Streaming (recommended)", + "desc": "Smart streaming will update your camera image once per minute when no detectable activity is occurring to conserve bandwidth and resources. When activity is detected, the image seamlessly switches to a live stream." + }, + "continuousStreaming": { + "label": "Continuous Streaming", + "desc": { + "title": "Camera image will always be a live stream when visible on the dashboard, even if no activity is being detected.", + "warning": "Continuous streaming may cause high bandwidth usage and performance issues. Use with caution." + } + } + } + }, + "compatibilityMode": { + "label": "Compatibility mode", + "desc": "Enable this option only if your camera's live stream is displaying color artifacts and has a diagonal line on the right side of the image." + } + } + } + }, + "debug": { + "options": { + "label": "Settings", + "title": "Options", + "showOptions": "Show Options", + "hideOptions": "Hide Options" + }, + "boundingBox": "Bounding Box", + "timestamp": "Timestamp", + "zones": "Zones", + "mask": "Mask", + "motion": "Motion", + "regions": "Regions" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/dialog.json new file mode 100644 index 0000000..a56c2b1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/dialog.json @@ -0,0 +1,122 @@ +{ + "restart": { + "title": "Are you sure you want to restart Frigate?", + "button": "Restart", + "restarting": { + "title": "Frigate is Restarting", + "content": "This page will reload in {{countdown}} seconds.", + "button": "Force Reload Now" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Submit To Frigate+", + "desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model." + }, + "review": { + "question": { + "label": "Confirm this label for Frigate Plus", + "ask_a": "Is this object a {{label}}?", + "ask_an": "Is this object an {{label}}?", + "ask_full": "Is this object a {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Submitted" + } + } + }, + "video": { + "viewInHistory": "View in History" + } + }, + "export": { + "time": { + "fromTimeline": "Select from Timeline", + "lastHour_one": "Last Hour", + "lastHour_other": "Last {{count}} Hours", + "custom": "Custom", + "start": { + "title": "Start Time", + "label": "Select Start Time" + }, + "end": { + "title": "End Time", + "label": "Select End Time" + } + }, + "name": { + "placeholder": "Name the Export" + }, + "select": "Select", + "export": "Export", + "selectOrExport": "Select or Export", + "toast": { + "success": "Successfully started export. View the file in the exports page.", + "view": "View", + "error": { + "failed": "Failed to start export: {{error}}", + "endTimeMustAfterStartTime": "End time must be after start time", + "noVaildTimeSelected": "No valid time range selected" + } + }, + "fromTimeline": { + "saveExport": "Save Export", + "previewExport": "Preview Export" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming is not enabled for this camera.", + "desc": { + "title": "Set up go2rtc for additional live view options and audio for this camera." + } + }, + "showStats": { + "label": "Show stream stats", + "desc": "Enable this option to show stream statistics as an overlay on the camera feed." + }, + "debugView": "Debug View" + }, + "search": { + "saveSearch": { + "label": "Save Search", + "desc": "Provide a name for this saved search.", + "placeholder": "Enter a name for your search", + "overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.", + "success": "Search ({{searchName}}) has been saved.", + "button": { + "save": { + "label": "Save this search" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Confirm Delete", + "desc": { + "selected": "Are you sure you want to delete all recorded video associated with this review item?

    Hold the Shift key to bypass this dialog in the future." + }, + "toast": { + "success": "Video footage associated with the selected review items has been deleted successfully.", + "error": "Failed to delete: {{error}}" + } + }, + "button": { + "export": "Export", + "markAsReviewed": "Mark as reviewed", + "markAsUnreviewed": "Mark as unreviewed", + "deleteNow": "Delete Now" + } + }, + "imagePicker": { + "selectImage": "Select a tracked object's thumbnail", + "unknownLabel": "Saved Trigger Image", + "search": { + "placeholder": "Search by label or sub label..." + }, + "noImages": "No thumbnails found for this camera" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/filter.json new file mode 100644 index 0000000..177234b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/filter.json @@ -0,0 +1,134 @@ +{ + "filter": "Filter", + "classes": { + "label": "Classes", + "all": { "title": "All Classes" }, + "count_one": "{{count}} Class", + "count_other": "{{count}} Classes" + }, + "labels": { + "label": "Labels", + "all": { + "title": "All Labels", + "short": "Labels" + }, + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "label": "Zones", + "all": { + "title": "All Zones", + "short": "Zones" + } + }, + "dates": { + "selectPreset": "Select a Preset…", + "all": { + "title": "All Dates", + "short": "Dates" + } + }, + "more": "More Filters", + "reset": { + "label": "Reset filters to default values" + }, + "timeRange": "Time Range", + "subLabels": { + "label": "Sub Labels", + "all": "All Sub Labels" + }, + "score": "Score", + "estimatedSpeed": "Estimated Speed ({{unit}})", + "features": { + "label": "Features", + "hasSnapshot": "Has a snapshot", + "hasVideoClip": "Has a video clip", + "submittedToFrigatePlus": { + "label": "Submitted to Frigate+", + "tips": "You must first filter on tracked objects that have a snapshot.

    Tracked objects without a snapshot cannot be submitted to Frigate+." + } + }, + "sort": { + "label": "Sort", + "dateAsc": "Date (Ascending)", + "dateDesc": "Date (Descending)", + "scoreAsc": "Object Score (Ascending)", + "scoreDesc": "Object Score (Descending)", + "speedAsc": "Estimated Speed (Ascending)", + "speedDesc": "Estimated Speed (Descending)", + "relevance": "Relevance" + }, + "cameras": { + "label": "Cameras Filter", + "all": { + "title": "All Cameras", + "short": "Cameras" + } + }, + "review": { + "showReviewed": "Show Reviewed" + }, + "motion": { + "showMotionOnly": "Show Motion Only" + }, + "explore": { + "settings": { + "title": "Settings", + "defaultView": { + "title": "Default View", + "desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.", + "summary": "Summary", + "unfilteredGrid": "Unfiltered Grid" + }, + "gridColumns": { + "title": "Grid Columns", + "desc": "Select the number of columns in the grid view." + }, + "searchSource": { + "label": "Search Source", + "desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.", + "options": { + "thumbnailImage": "Thumbnail Image", + "description": "Description" + } + } + }, + "date": { + "selectDateBy": { + "label": "Select a date to filter by" + } + } + }, + "logSettings": { + "label": "Filter log level", + "filterBySeverity": "Filter logs by severity", + "loading": { + "title": "Loading", + "desc": "When the log pane is scrolled to the bottom, new logs automatically stream as they are added." + }, + "disableLogStreaming": "Disable log streaming", + "allLogs": "All logs" + }, + "trackedObjectDelete": { + "title": "Confirm Delete", + "desc": "Deleting these {{objectLength}} tracked objects removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of these tracked objects in History view will NOT be deleted.

    Are you sure you want to proceed?

    Hold the Shift key to bypass this dialog in the future.", + "toast": { + "success": "Tracked objects deleted successfully.", + "error": "Failed to delete tracked objects: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filter by zone mask" + }, + "recognizedLicensePlates": { + "title": "Recognized License Plates", + "loadFailed": "Failed to load recognized license plates.", + "loading": "Loading recognized license plates…", + "placeholder": "Type to search license plates…", + "noLicensePlatesFound": "No license plates found.", + "selectPlatesFromList": "Select one or more plates from the list.", + "selectAll": "Select all", + "clearAll": "Clear all" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/icons.json new file mode 100644 index 0000000..e7a35a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Select an icon", + "search": { + "placeholder": "Search for an icon…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/input.json new file mode 100644 index 0000000..7a9e359 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Download Video", + "toast": { + "success": "Your review item video has started downloading." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/en/components/player.json new file mode 100644 index 0000000..3b50ff5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "No recordings found for this time", + "noPreviewFound": "No Preview Found", + "noPreviewFoundFor": "No Preview Found for {{cameraName}}", + "submitFrigatePlus": { + "title": "Submit this frame to Frigate+?", + "submit": "Submit" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 or greater is required for this live stream type.", + "streamOffline": { + "title": "Stream Offline", + "desc": "No frames have been received on the {{cameraName}} detect stream, check error logs" + }, + "cameraDisabled": "Camera is disabled", + "stats": { + "streamType": { + "title": "Stream Type:", + "short": "Type" + }, + "bandwidth": { + "title": "Bandwidth:", + "short": "Bandwidth" + }, + "latency": { + "title": "Latency:", + "value": "{{seconds}} seconds", + "short": { + "title": "Latency", + "value": "{{seconds}} sec" + } + }, + "totalFrames": "Total Frames:", + "droppedFrames": { + "title": "Dropped Frames:", + "short": { + "title": "Dropped", + "value": "{{droppedFrames}} frames" + } + }, + "decodedFrames": "Decoded Frames:", + "droppedFrameRate": "Dropped Frame Rate:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Successfully submitted frame to Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Failed to submit frame to Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/audio.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/audio.json new file mode 100644 index 0000000..f9aaffa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/audio.json @@ -0,0 +1,26 @@ +{ + "label": "Global Audio events configuration.", + "properties": { + "enabled": { + "label": "Enable audio events." + }, + "max_not_heard": { + "label": "Seconds of not hearing the type of audio to end the event." + }, + "min_volume": { + "label": "Min volume required to run audio detection." + }, + "listen": { + "label": "Audio to listen for." + }, + "filters": { + "label": "Audio filters." + }, + "enabled_in_config": { + "label": "Keep track of original state of audio detection." + }, + "num_threads": { + "label": "Number of detection threads" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/audio_transcription.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/audio_transcription.json new file mode 100644 index 0000000..6922b9d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/audio_transcription.json @@ -0,0 +1,23 @@ +{ + "label": "Audio transcription config.", + "properties": { + "enabled": { + "label": "Enable audio transcription." + }, + "language": { + "label": "Language abbreviation to use for audio event transcription/translation." + }, + "device": { + "label": "The device used for license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + }, + "live_enabled": { + "label": "Enable live transcriptions." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/auth.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/auth.json new file mode 100644 index 0000000..a524d8d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/auth.json @@ -0,0 +1,35 @@ +{ + "label": "Auth configuration.", + "properties": { + "enabled": { + "label": "Enable authentication" + }, + "reset_admin_password": { + "label": "Reset the admin password on startup" + }, + "cookie_name": { + "label": "Name for jwt token cookie" + }, + "cookie_secure": { + "label": "Set secure flag on cookie" + }, + "session_length": { + "label": "Session length for jwt session tokens" + }, + "refresh_time": { + "label": "Refresh the session if it is going to expire in this many seconds" + }, + "failed_login_rate_limit": { + "label": "Rate limits for failed login attempts." + }, + "trusted_proxies": { + "label": "Trusted proxies for determining IP address to rate limit" + }, + "hash_iterations": { + "label": "Password hash iterations" + }, + "roles": { + "label": "Role to camera mappings. Empty list grants access to all cameras." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/birdseye.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/birdseye.json new file mode 100644 index 0000000..f122f31 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/birdseye.json @@ -0,0 +1,37 @@ +{ + "label": "Birdseye configuration.", + "properties": { + "enabled": { + "label": "Enable birdseye view." + }, + "mode": { + "label": "Tracking mode." + }, + "restream": { + "label": "Restream birdseye via RTSP." + }, + "width": { + "label": "Birdseye width." + }, + "height": { + "label": "Birdseye height." + }, + "quality": { + "label": "Encoding quality." + }, + "inactivity_threshold": { + "label": "Birdseye Inactivity Threshold" + }, + "layout": { + "label": "Birdseye Layout Config", + "properties": { + "scaling_factor": { + "label": "Birdseye Scaling Factor" + }, + "max_cameras": { + "label": "Max cameras" + } + } + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/camera_groups.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/camera_groups.json new file mode 100644 index 0000000..2900e9c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/camera_groups.json @@ -0,0 +1,14 @@ +{ + "label": "Camera group configuration", + "properties": { + "cameras": { + "label": "List of cameras in this group." + }, + "icon": { + "label": "Icon that represents camera group." + }, + "order": { + "label": "Sort order for group." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/cameras.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/cameras.json new file mode 100644 index 0000000..67015bd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/cameras.json @@ -0,0 +1,761 @@ +{ + "label": "Camera configuration.", + "properties": { + "name": { + "label": "Camera name." + }, + "friendly_name": { + "label": "Camera friendly name used in the Frigate UI." + }, + "enabled": { + "label": "Enable camera." + }, + "audio": { + "label": "Audio events configuration.", + "properties": { + "enabled": { + "label": "Enable audio events." + }, + "max_not_heard": { + "label": "Seconds of not hearing the type of audio to end the event." + }, + "min_volume": { + "label": "Min volume required to run audio detection." + }, + "listen": { + "label": "Audio to listen for." + }, + "filters": { + "label": "Audio filters." + }, + "enabled_in_config": { + "label": "Keep track of original state of audio detection." + }, + "num_threads": { + "label": "Number of detection threads" + } + } + }, + "audio_transcription": { + "label": "Audio transcription config.", + "properties": { + "enabled": { + "label": "Enable audio transcription." + }, + "language": { + "label": "Language abbreviation to use for audio event transcription/translation." + }, + "device": { + "label": "The device used for license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + }, + "live_enabled": { + "label": "Enable live transcriptions." + } + } + }, + "birdseye": { + "label": "Birdseye camera configuration.", + "properties": { + "enabled": { + "label": "Enable birdseye view for camera." + }, + "mode": { + "label": "Tracking mode for camera." + }, + "order": { + "label": "Position of the camera in the birdseye view." + } + } + }, + "detect": { + "label": "Object detection configuration.", + "properties": { + "enabled": { + "label": "Detection Enabled." + }, + "height": { + "label": "Height of the stream for the detect role." + }, + "width": { + "label": "Width of the stream for the detect role." + }, + "fps": { + "label": "Number of frames per second to process through detection." + }, + "min_initialized": { + "label": "Minimum number of consecutive hits for an object to be initialized by the tracker." + }, + "max_disappeared": { + "label": "Maximum number of frames the object can disappear before detection ends." + }, + "stationary": { + "label": "Stationary objects config.", + "properties": { + "interval": { + "label": "Frame interval for checking stationary objects." + }, + "threshold": { + "label": "Number of frames without a position change for an object to be considered stationary" + }, + "max_frames": { + "label": "Max frames for stationary objects.", + "properties": { + "default": { + "label": "Default max frames." + }, + "objects": { + "label": "Object specific max frames." + } + } + }, + "classifier": { + "label": "Enable visual classifier for determing if objects with jittery bounding boxes are stationary." + } + } + }, + "annotation_offset": { + "label": "Milliseconds to offset detect annotations by." + } + } + }, + "face_recognition": { + "label": "Face recognition config.", + "properties": { + "enabled": { + "label": "Enable face recognition." + }, + "min_area": { + "label": "Min area of face box to consider running face recognition." + } + } + }, + "ffmpeg": { + "label": "FFmpeg configuration for the camera.", + "properties": { + "path": { + "label": "FFmpeg path" + }, + "global_args": { + "label": "Global FFmpeg arguments." + }, + "hwaccel_args": { + "label": "FFmpeg hardware acceleration arguments." + }, + "input_args": { + "label": "FFmpeg input arguments." + }, + "output_args": { + "label": "FFmpeg output arguments per role.", + "properties": { + "detect": { + "label": "Detect role FFmpeg output arguments." + }, + "record": { + "label": "Record role FFmpeg output arguments." + } + } + }, + "retry_interval": { + "label": "Time in seconds to wait before FFmpeg retries connecting to the camera." + }, + "apple_compatibility": { + "label": "Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players." + }, + "inputs": { + "label": "Camera inputs." + } + } + }, + "live": { + "label": "Live playback settings.", + "properties": { + "streams": { + "label": "Friendly names and restream names to use for live view." + }, + "height": { + "label": "Live camera view height" + }, + "quality": { + "label": "Live camera view quality" + } + } + }, + "lpr": { + "label": "LPR config.", + "properties": { + "enabled": { + "label": "Enable license plate recognition." + }, + "expire_time": { + "label": "Expire plates not seen after number of seconds (for dedicated LPR cameras only)." + }, + "min_area": { + "label": "Minimum area of license plate to begin running recognition." + }, + "enhancement": { + "label": "Amount of contrast adjustment and denoising to apply to license plate images before recognition." + } + } + }, + "motion": { + "label": "Motion detection configuration.", + "properties": { + "enabled": { + "label": "Enable motion on all cameras." + }, + "threshold": { + "label": "Motion detection threshold (1-255)." + }, + "lightning_threshold": { + "label": "Lightning detection threshold (0.3-1.0)." + }, + "improve_contrast": { + "label": "Improve Contrast" + }, + "contour_area": { + "label": "Contour Area" + }, + "delta_alpha": { + "label": "Delta Alpha" + }, + "frame_alpha": { + "label": "Frame Alpha" + }, + "frame_height": { + "label": "Frame Height" + }, + "mask": { + "label": "Coordinates polygon for the motion mask." + }, + "mqtt_off_delay": { + "label": "Delay for updating MQTT with no motion detected." + }, + "enabled_in_config": { + "label": "Keep track of original state of motion detection." + } + } + }, + "objects": { + "label": "Object configuration.", + "properties": { + "track": { + "label": "Objects to track." + }, + "filters": { + "label": "Object filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "mask": { + "label": "Object mask." + }, + "genai": { + "label": "Config for using genai to analyze objects.", + "properties": { + "enabled": { + "label": "Enable GenAI for camera." + }, + "use_snapshot": { + "label": "Use snapshots for generating descriptions." + }, + "prompt": { + "label": "Default caption prompt." + }, + "object_prompts": { + "label": "Object specific prompts." + }, + "objects": { + "label": "List of objects to run generative AI for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to run generative AI." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "send_triggers": { + "label": "What triggers to use to send frames to generative AI for a tracked object.", + "properties": { + "tracked_object_end": { + "label": "Send once the object is no longer tracked." + }, + "after_significant_updates": { + "label": "Send an early request to generative AI when X frames accumulated." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + } + } + } + } + }, + "record": { + "label": "Record configuration.", + "properties": { + "enabled": { + "label": "Enable record on all cameras." + }, + "sync_recordings": { + "label": "Sync recordings with disk on startup and once a day." + }, + "expire_interval": { + "label": "Number of minutes to wait between cleanup runs." + }, + "continuous": { + "label": "Continuous recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "motion": { + "label": "Motion recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "detections": { + "label": "Detection specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "alerts": { + "label": "Alert specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "export": { + "label": "Recording Export Config", + "properties": { + "timelapse_args": { + "label": "Timelapse Args" + } + } + }, + "preview": { + "label": "Recording Preview Config", + "properties": { + "quality": { + "label": "Quality of recording preview." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of recording." + } + } + }, + "review": { + "label": "Review configuration.", + "properties": { + "alerts": { + "label": "Review alerts config.", + "properties": { + "enabled": { + "label": "Enable alerts." + }, + "labels": { + "label": "Labels to create alerts for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as an alert." + }, + "enabled_in_config": { + "label": "Keep track of original state of alerts." + }, + "cutoff_time": { + "label": "Time to cutoff alerts after no alert-causing activity has occurred." + } + } + }, + "detections": { + "label": "Review detections config.", + "properties": { + "enabled": { + "label": "Enable detections." + }, + "labels": { + "label": "Labels to create detections for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as a detection." + }, + "cutoff_time": { + "label": "Time to cutoff detection after no detection-causing activity has occurred." + }, + "enabled_in_config": { + "label": "Keep track of original state of detections." + } + } + }, + "genai": { + "label": "Review description genai config.", + "properties": { + "enabled": { + "label": "Enable GenAI descriptions for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts." + }, + "detections": { + "label": "Enable GenAI for detections." + }, + "additional_concerns": { + "label": "Additional concerns that GenAI should make note of on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + }, + "preferred_language": { + "label": "Preferred language for GenAI Response" + }, + "activity_context_prompt": { + "label": "Custom activity context prompt defining normal activity patterns for this property." + } + } + } + } + }, + "semantic_search": { + "label": "Semantic search configuration.", + "properties": { + "triggers": { + "label": "Trigger actions on tracked objects that match existing thumbnails or descriptions", + "properties": { + "enabled": { + "label": "Enable this trigger" + }, + "type": { + "label": "Type of trigger" + }, + "data": { + "label": "Trigger content (text phrase or image ID)" + }, + "threshold": { + "label": "Confidence score required to run the trigger" + }, + "actions": { + "label": "Actions to perform when trigger is matched" + } + } + } + } + }, + "snapshots": { + "label": "Snapshot configuration.", + "properties": { + "enabled": { + "label": "Snapshots enabled." + }, + "clean_copy": { + "label": "Create a clean copy of the snapshot image." + }, + "timestamp": { + "label": "Add a timestamp overlay on the snapshot." + }, + "bounding_box": { + "label": "Add a bounding box overlay on the snapshot." + }, + "crop": { + "label": "Crop the snapshot to the detected object." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save a snapshot." + }, + "height": { + "label": "Snapshot image height." + }, + "retain": { + "label": "Snapshot retention.", + "properties": { + "default": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + }, + "objects": { + "label": "Object retention period." + } + } + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } + }, + "timestamp_style": { + "label": "Timestamp style configuration.", + "properties": { + "position": { + "label": "Timestamp position." + }, + "format": { + "label": "Timestamp format." + }, + "color": { + "label": "Timestamp color.", + "properties": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + }, + "blue": { + "label": "Blue" + } + } + }, + "thickness": { + "label": "Timestamp thickness." + }, + "effect": { + "label": "Timestamp effect." + } + } + }, + "best_image_timeout": { + "label": "How long to wait for the image with the highest confidence score." + }, + "mqtt": { + "label": "MQTT configuration.", + "properties": { + "enabled": { + "label": "Send image over MQTT." + }, + "timestamp": { + "label": "Add timestamp to MQTT image." + }, + "bounding_box": { + "label": "Add bounding box to MQTT image." + }, + "crop": { + "label": "Crop MQTT image to detected object." + }, + "height": { + "label": "MQTT image height." + }, + "required_zones": { + "label": "List of required zones to be entered in order to send the image." + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } + }, + "notifications": { + "label": "Notifications configuration.", + "properties": { + "enabled": { + "label": "Enable notifications" + }, + "email": { + "label": "Email required for push." + }, + "cooldown": { + "label": "Cooldown period for notifications (time in seconds)." + }, + "enabled_in_config": { + "label": "Keep track of original state of notifications." + } + } + }, + "onvif": { + "label": "Camera Onvif Configuration.", + "properties": { + "host": { + "label": "Onvif Host" + }, + "port": { + "label": "Onvif Port" + }, + "user": { + "label": "Onvif Username" + }, + "password": { + "label": "Onvif Password" + }, + "tls_insecure": { + "label": "Onvif Disable TLS verification" + }, + "autotracking": { + "label": "PTZ auto tracking config.", + "properties": { + "enabled": { + "label": "Enable PTZ object autotracking." + }, + "calibrate_on_startup": { + "label": "Perform a camera calibration when Frigate starts." + }, + "zooming": { + "label": "Autotracker zooming mode." + }, + "zoom_factor": { + "label": "Zooming factor (0.1-0.75)." + }, + "track": { + "label": "Objects to track." + }, + "required_zones": { + "label": "List of required zones to be entered in order to begin autotracking." + }, + "return_preset": { + "label": "Name of camera preset to return to when object tracking is over." + }, + "timeout": { + "label": "Seconds to delay before returning to preset." + }, + "movement_weights": { + "label": "Internal value used for PTZ movements based on the speed of your camera's motor." + }, + "enabled_in_config": { + "label": "Keep track of original state of autotracking." + } + } + }, + "ignore_time_mismatch": { + "label": "Onvif Ignore Time Synchronization Mismatch Between Camera and Server" + } + } + }, + "type": { + "label": "Camera Type" + }, + "ui": { + "label": "Camera UI Modifications.", + "properties": { + "order": { + "label": "Order of camera in UI." + }, + "dashboard": { + "label": "Show this camera in Frigate dashboard UI." + } + } + }, + "webui_url": { + "label": "URL to visit the camera directly from system page" + }, + "zones": { + "label": "Zone configuration.", + "properties": { + "filters": { + "label": "Zone filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "coordinates": { + "label": "Coordinates polygon for the defined zone." + }, + "distances": { + "label": "Real-world distances for the sides of quadrilateral for the defined zone." + }, + "inertia": { + "label": "Number of consecutive frames required for object to be considered present in the zone." + }, + "loitering_time": { + "label": "Number of seconds that an object must loiter to be considered in the zone." + }, + "speed_threshold": { + "label": "Minimum speed value for an object to be considered in the zone." + }, + "objects": { + "label": "List of objects that can trigger the zone." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of camera." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/classification.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/classification.json new file mode 100644 index 0000000..e8014b2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/classification.json @@ -0,0 +1,58 @@ +{ + "label": "Object classification config.", + "properties": { + "bird": { + "label": "Bird classification config.", + "properties": { + "enabled": { + "label": "Enable bird classification." + }, + "threshold": { + "label": "Minimum classification score required to be considered a match." + } + } + }, + "custom": { + "label": "Custom Classification Model Configs.", + "properties": { + "enabled": { + "label": "Enable running the model." + }, + "name": { + "label": "Name of classification model." + }, + "threshold": { + "label": "Classification score threshold to change the state." + }, + "object_config": { + "properties": { + "objects": { + "label": "Object types to classify." + }, + "classification_type": { + "label": "Type of classification that is applied." + } + } + }, + "state_config": { + "properties": { + "cameras": { + "label": "Cameras to run classification on.", + "properties": { + "crop": { + "label": "Crop of image frame on this camera to run classification on." + } + } + }, + "motion": { + "label": "If classification should be run when motion is detected in the crop." + }, + "interval": { + "label": "Interval to run classification on in seconds." + } + } + } + } + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/database.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/database.json new file mode 100644 index 0000000..ece7ccb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/database.json @@ -0,0 +1,8 @@ +{ + "label": "Database configuration.", + "properties": { + "path": { + "label": "Database path." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/detect.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/detect.json new file mode 100644 index 0000000..9e1b593 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/detect.json @@ -0,0 +1,51 @@ +{ + "label": "Global object tracking configuration.", + "properties": { + "enabled": { + "label": "Detection Enabled." + }, + "height": { + "label": "Height of the stream for the detect role." + }, + "width": { + "label": "Width of the stream for the detect role." + }, + "fps": { + "label": "Number of frames per second to process through detection." + }, + "min_initialized": { + "label": "Minimum number of consecutive hits for an object to be initialized by the tracker." + }, + "max_disappeared": { + "label": "Maximum number of frames the object can disappear before detection ends." + }, + "stationary": { + "label": "Stationary objects config.", + "properties": { + "interval": { + "label": "Frame interval for checking stationary objects." + }, + "threshold": { + "label": "Number of frames without a position change for an object to be considered stationary" + }, + "max_frames": { + "label": "Max frames for stationary objects.", + "properties": { + "default": { + "label": "Default max frames." + }, + "objects": { + "label": "Object specific max frames." + } + } + }, + "classifier": { + "label": "Enable visual classifier for determing if objects with jittery bounding boxes are stationary." + } + } + }, + "annotation_offset": { + "label": "Milliseconds to offset detect annotations by." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/detectors.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/detectors.json new file mode 100644 index 0000000..1bd6fec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/detectors.json @@ -0,0 +1,14 @@ +{ + "label": "Detector hardware configuration.", + "properties": { + "type": { + "label": "Detector Type" + }, + "model": { + "label": "Detector specific model configuration." + }, + "model_path": { + "label": "Detector specific model path." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/environment_vars.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/environment_vars.json new file mode 100644 index 0000000..ce97ce4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/environment_vars.json @@ -0,0 +1,3 @@ +{ + "label": "Frigate environment variables." +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/face_recognition.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/face_recognition.json new file mode 100644 index 0000000..705d754 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/face_recognition.json @@ -0,0 +1,36 @@ +{ + "label": "Face recognition config.", + "properties": { + "enabled": { + "label": "Enable face recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "unknown_score": { + "label": "Minimum face distance score required to be marked as a potential match." + }, + "detection_threshold": { + "label": "Minimum face detection score required to be considered a face." + }, + "recognition_threshold": { + "label": "Minimum face distance score required to be considered a match." + }, + "min_area": { + "label": "Min area of face box to consider running face recognition." + }, + "min_faces": { + "label": "Min face recognitions for the sub label to be applied to the person object." + }, + "save_attempts": { + "label": "Number of face attempts to save in the recent recognitions tab." + }, + "blur_confidence_filter": { + "label": "Apply blur quality filter to face confidence." + }, + "device": { + "label": "The device key to use for face recognition.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/ffmpeg.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/ffmpeg.json new file mode 100644 index 0000000..570da5a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/ffmpeg.json @@ -0,0 +1,34 @@ +{ + "label": "Global FFmpeg configuration.", + "properties": { + "path": { + "label": "FFmpeg path" + }, + "global_args": { + "label": "Global FFmpeg arguments." + }, + "hwaccel_args": { + "label": "FFmpeg hardware acceleration arguments." + }, + "input_args": { + "label": "FFmpeg input arguments." + }, + "output_args": { + "label": "FFmpeg output arguments per role.", + "properties": { + "detect": { + "label": "Detect role FFmpeg output arguments." + }, + "record": { + "label": "Record role FFmpeg output arguments." + } + } + }, + "retry_interval": { + "label": "Time in seconds to wait before FFmpeg retries connecting to the camera." + }, + "apple_compatibility": { + "label": "Set tag on HEVC (H.265) recording stream to improve compatibility with Apple players." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/genai.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/genai.json new file mode 100644 index 0000000..084b921 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/genai.json @@ -0,0 +1,20 @@ +{ + "label": "Generative AI configuration.", + "properties": { + "api_key": { + "label": "Provider API key." + }, + "base_url": { + "label": "Provider base url." + }, + "model": { + "label": "GenAI model." + }, + "provider": { + "label": "GenAI provider." + }, + "provider_options": { + "label": "GenAI Provider extra options." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/go2rtc.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/go2rtc.json new file mode 100644 index 0000000..76ec330 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/go2rtc.json @@ -0,0 +1,3 @@ +{ + "label": "Global restream configuration." +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/live.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/live.json new file mode 100644 index 0000000..3621701 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/live.json @@ -0,0 +1,14 @@ +{ + "label": "Live playback settings.", + "properties": { + "streams": { + "label": "Friendly names and restream names to use for live view." + }, + "height": { + "label": "Live camera view height" + }, + "quality": { + "label": "Live camera view quality" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/logger.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/logger.json new file mode 100644 index 0000000..3d51786 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/logger.json @@ -0,0 +1,11 @@ +{ + "label": "Logging configuration.", + "properties": { + "default": { + "label": "Default logging level." + }, + "logs": { + "label": "Log level for specified processes." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/lpr.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/lpr.json new file mode 100644 index 0000000..951d1f8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/lpr.json @@ -0,0 +1,45 @@ +{ + "label": "License Plate recognition config.", + "properties": { + "enabled": { + "label": "Enable license plate recognition." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "detection_threshold": { + "label": "License plate object confidence score required to begin running recognition." + }, + "min_area": { + "label": "Minimum area of license plate to begin running recognition." + }, + "recognition_threshold": { + "label": "Recognition confidence score required to add the plate to the object as a sub label." + }, + "min_plate_length": { + "label": "Minimum number of characters a license plate must have to be added to the object as a sub label." + }, + "format": { + "label": "Regular expression for the expected format of license plate." + }, + "match_distance": { + "label": "Allow this number of missing/incorrect characters to still cause a detected plate to match a known plate." + }, + "known_plates": { + "label": "Known plates to track (strings or regular expressions)." + }, + "enhancement": { + "label": "Amount of contrast adjustment and denoising to apply to license plate images before recognition." + }, + "debug_save_plates": { + "label": "Save plates captured for LPR for debugging purposes." + }, + "device": { + "label": "The device key to use for LPR.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + }, + "replace_rules": { + "label": "List of regex replacement rules for normalizing detected plates. Each rule has 'pattern' and 'replacement'." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/model.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/model.json new file mode 100644 index 0000000..0bc2c1d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/model.json @@ -0,0 +1,35 @@ +{ + "label": "Detection model configuration.", + "properties": { + "path": { + "label": "Custom Object detection model path." + }, + "labelmap_path": { + "label": "Label map for custom object detector." + }, + "width": { + "label": "Object detection model input width." + }, + "height": { + "label": "Object detection model input height." + }, + "labelmap": { + "label": "Labelmap customization." + }, + "attributes_map": { + "label": "Map of object labels to their attribute labels." + }, + "input_tensor": { + "label": "Model Input Tensor Shape" + }, + "input_pixel_format": { + "label": "Model Input Pixel Color Format" + }, + "input_dtype": { + "label": "Model Input D Type" + }, + "model_type": { + "label": "Object Detection Model Type" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/motion.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/motion.json new file mode 100644 index 0000000..183bfdf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/motion.json @@ -0,0 +1,3 @@ +{ + "label": "Global motion detection configuration." +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/mqtt.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/mqtt.json new file mode 100644 index 0000000..d2625ac --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/mqtt.json @@ -0,0 +1,44 @@ +{ + "label": "MQTT configuration.", + "properties": { + "enabled": { + "label": "Enable MQTT Communication." + }, + "host": { + "label": "MQTT Host" + }, + "port": { + "label": "MQTT Port" + }, + "topic_prefix": { + "label": "MQTT Topic Prefix" + }, + "client_id": { + "label": "MQTT Client ID" + }, + "stats_interval": { + "label": "MQTT Camera Stats Interval" + }, + "user": { + "label": "MQTT Username" + }, + "password": { + "label": "MQTT Password" + }, + "tls_ca_certs": { + "label": "MQTT TLS CA Certificates" + }, + "tls_client_cert": { + "label": "MQTT TLS Client Certificate" + }, + "tls_client_key": { + "label": "MQTT TLS Client Key" + }, + "tls_insecure": { + "label": "MQTT TLS Insecure" + }, + "qos": { + "label": "MQTT QoS" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/networking.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/networking.json new file mode 100644 index 0000000..0f8d9cc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/networking.json @@ -0,0 +1,13 @@ +{ + "label": "Networking configuration", + "properties": { + "ipv6": { + "label": "Network configuration", + "properties": { + "enabled": { + "label": "Enable IPv6 for port 5000 and/or 8971" + } + } + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/notifications.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/notifications.json new file mode 100644 index 0000000..b529f10 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/notifications.json @@ -0,0 +1,17 @@ +{ + "label": "Global notification configuration.", + "properties": { + "enabled": { + "label": "Enable notifications" + }, + "email": { + "label": "Email required for push." + }, + "cooldown": { + "label": "Cooldown period for notifications (time in seconds)." + }, + "enabled_in_config": { + "label": "Keep track of original state of notifications." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/objects.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/objects.json new file mode 100644 index 0000000..f041672 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/objects.json @@ -0,0 +1,77 @@ +{ + "label": "Global object configuration.", + "properties": { + "track": { + "label": "Objects to track." + }, + "filters": { + "label": "Object filters.", + "properties": { + "min_area": { + "label": "Minimum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum area of bounding box for object to be counted. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum ratio of bounding box's width/height for object to be counted." + }, + "max_ratio": { + "label": "Maximum ratio of bounding box's width/height for object to be counted." + }, + "threshold": { + "label": "Average detection confidence threshold for object to be counted." + }, + "min_score": { + "label": "Minimum detection confidence for object to be counted." + }, + "mask": { + "label": "Detection area polygon mask for this filter configuration." + } + } + }, + "mask": { + "label": "Object mask." + }, + "genai": { + "label": "Config for using genai to analyze objects.", + "properties": { + "enabled": { + "label": "Enable GenAI for camera." + }, + "use_snapshot": { + "label": "Use snapshots for generating descriptions." + }, + "prompt": { + "label": "Default caption prompt." + }, + "object_prompts": { + "label": "Object specific prompts." + }, + "objects": { + "label": "List of objects to run generative AI for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to run generative AI." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "send_triggers": { + "label": "What triggers to use to send frames to generative AI for a tracked object.", + "properties": { + "tracked_object_end": { + "label": "Send once the object is no longer tracked." + }, + "after_significant_updates": { + "label": "Send an early request to generative AI when X frames accumulated." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + } + } + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/proxy.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/proxy.json new file mode 100644 index 0000000..732d6fa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/proxy.json @@ -0,0 +1,31 @@ +{ + "label": "Proxy configuration.", + "properties": { + "header_map": { + "label": "Header mapping definitions for proxy user passing.", + "properties": { + "user": { + "label": "Header name from upstream proxy to identify user." + }, + "role": { + "label": "Header name from upstream proxy to identify user role." + }, + "role_map": { + "label": "Mapping of Frigate roles to upstream group values. " + } + } + }, + "logout_url": { + "label": "Redirect url for logging out with proxy." + }, + "auth_secret": { + "label": "Secret value for proxy authentication." + }, + "default_role": { + "label": "Default role for proxy users." + }, + "separator": { + "label": "The character used to separate values in a mapped header." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/record.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/record.json new file mode 100644 index 0000000..8113908 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/record.json @@ -0,0 +1,93 @@ +{ + "label": "Global record configuration.", + "properties": { + "enabled": { + "label": "Enable record on all cameras." + }, + "sync_recordings": { + "label": "Sync recordings with disk on startup and once a day." + }, + "expire_interval": { + "label": "Number of minutes to wait between cleanup runs." + }, + "continuous": { + "label": "Continuous recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "motion": { + "label": "Motion recording retention settings.", + "properties": { + "days": { + "label": "Default retention period." + } + } + }, + "detections": { + "label": "Detection specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "alerts": { + "label": "Alert specific retention settings.", + "properties": { + "pre_capture": { + "label": "Seconds to retain before event starts." + }, + "post_capture": { + "label": "Seconds to retain after event ends." + }, + "retain": { + "label": "Event retention settings.", + "properties": { + "days": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + } + } + } + } + }, + "export": { + "label": "Recording Export Config", + "properties": { + "timelapse_args": { + "label": "Timelapse Args" + } + } + }, + "preview": { + "label": "Recording Preview Config", + "properties": { + "quality": { + "label": "Quality of recording preview." + } + } + }, + "enabled_in_config": { + "label": "Keep track of original state of recording." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/review.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/review.json new file mode 100644 index 0000000..dba83ee --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/review.json @@ -0,0 +1,74 @@ +{ + "label": "Review configuration.", + "properties": { + "alerts": { + "label": "Review alerts config.", + "properties": { + "enabled": { + "label": "Enable alerts." + }, + "labels": { + "label": "Labels to create alerts for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as an alert." + }, + "enabled_in_config": { + "label": "Keep track of original state of alerts." + }, + "cutoff_time": { + "label": "Time to cutoff alerts after no alert-causing activity has occurred." + } + } + }, + "detections": { + "label": "Review detections config.", + "properties": { + "enabled": { + "label": "Enable detections." + }, + "labels": { + "label": "Labels to create detections for." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save the event as a detection." + }, + "cutoff_time": { + "label": "Time to cutoff detection after no detection-causing activity has occurred." + }, + "enabled_in_config": { + "label": "Keep track of original state of detections." + } + } + }, + "genai": { + "label": "Review description genai config.", + "properties": { + "enabled": { + "label": "Enable GenAI descriptions for review items." + }, + "alerts": { + "label": "Enable GenAI for alerts." + }, + "detections": { + "label": "Enable GenAI for detections." + }, + "additional_concerns": { + "label": "Additional concerns that GenAI should make note of on this camera." + }, + "debug_save_thumbnails": { + "label": "Save thumbnails sent to generative AI for debugging purposes." + }, + "enabled_in_config": { + "label": "Keep track of original state of generative AI." + }, + "preferred_language": { + "label": "Preferred language for GenAI Response" + }, + "activity_context_prompt": { + "label": "Custom activity context prompt defining normal activity patterns for this property." + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/safe_mode.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/safe_mode.json new file mode 100644 index 0000000..352f78b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/safe_mode.json @@ -0,0 +1,3 @@ +{ + "label": "If Frigate should be started in safe mode." +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/semantic_search.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/semantic_search.json new file mode 100644 index 0000000..2c46640 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/semantic_search.json @@ -0,0 +1,21 @@ +{ + "label": "Semantic search configuration.", + "properties": { + "enabled": { + "label": "Enable semantic search." + }, + "reindex": { + "label": "Reindex all tracked objects on startup." + }, + "model": { + "label": "The CLIP model to use for semantic search." + }, + "model_size": { + "label": "The size of the embeddings model used." + }, + "device": { + "label": "The device key to use for semantic search.", + "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/snapshots.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/snapshots.json new file mode 100644 index 0000000..a633614 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/snapshots.json @@ -0,0 +1,43 @@ +{ + "label": "Global snapshots configuration.", + "properties": { + "enabled": { + "label": "Snapshots enabled." + }, + "clean_copy": { + "label": "Create a clean copy of the snapshot image." + }, + "timestamp": { + "label": "Add a timestamp overlay on the snapshot." + }, + "bounding_box": { + "label": "Add a bounding box overlay on the snapshot." + }, + "crop": { + "label": "Crop the snapshot to the detected object." + }, + "required_zones": { + "label": "List of required zones to be entered in order to save a snapshot." + }, + "height": { + "label": "Snapshot image height." + }, + "retain": { + "label": "Snapshot retention.", + "properties": { + "default": { + "label": "Default retention period." + }, + "mode": { + "label": "Retain mode." + }, + "objects": { + "label": "Object retention period." + } + } + }, + "quality": { + "label": "Quality of the encoded jpeg (0-100)." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/telemetry.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/telemetry.json new file mode 100644 index 0000000..802ced2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/telemetry.json @@ -0,0 +1,28 @@ +{ + "label": "Telemetry configuration.", + "properties": { + "network_interfaces": { + "label": "Enabled network interfaces for bandwidth calculation." + }, + "stats": { + "label": "System Stats Configuration", + "properties": { + "amd_gpu_stats": { + "label": "Enable AMD GPU stats." + }, + "intel_gpu_stats": { + "label": "Enable Intel GPU stats." + }, + "network_bandwidth": { + "label": "Enable network bandwidth for ffmpeg processes." + }, + "intel_gpu_device": { + "label": "Define the device to use when gathering SR-IOV stats." + } + } + }, + "version_check": { + "label": "Enable latest version check." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/timestamp_style.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/timestamp_style.json new file mode 100644 index 0000000..6a31194 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/timestamp_style.json @@ -0,0 +1,31 @@ +{ + "label": "Global timestamp style configuration.", + "properties": { + "position": { + "label": "Timestamp position." + }, + "format": { + "label": "Timestamp format." + }, + "color": { + "label": "Timestamp color.", + "properties": { + "red": { + "label": "Red" + }, + "green": { + "label": "Green" + }, + "blue": { + "label": "Blue" + } + } + }, + "thickness": { + "label": "Timestamp thickness." + }, + "effect": { + "label": "Timestamp effect." + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/tls.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/tls.json new file mode 100644 index 0000000..58493ff --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/tls.json @@ -0,0 +1,8 @@ +{ + "label": "TLS configuration.", + "properties": { + "enabled": { + "label": "Enable TLS for port 8971" + } + } +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/ui.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/ui.json new file mode 100644 index 0000000..cdd91cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/ui.json @@ -0,0 +1,20 @@ +{ + "label": "UI configuration.", + "properties": { + "timezone": { + "label": "Override UI timezone." + }, + "time_format": { + "label": "Override UI time format." + }, + "date_style": { + "label": "Override UI dateStyle." + }, + "time_style": { + "label": "Override UI timeStyle." + }, + "unit_system": { + "label": "The unit system to use for measurements." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/config/version.json b/sam2-cpu/frigate-dev/web/public/locales/en/config/version.json new file mode 100644 index 0000000..e777d75 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/config/version.json @@ -0,0 +1,3 @@ +{ + "label": "Current config version." +} \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/objects.json b/sam2-cpu/frigate-dev/web/public/locales/en/objects.json new file mode 100644 index 0000000..130bfcc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Person", + "bicycle": "Bicycle", + "car": "Car", + "motorcycle": "Motorcycle", + "airplane": "Airplane", + "bus": "Bus", + "train": "Train", + "boat": "Boat", + "traffic_light": "Traffic Light", + "fire_hydrant": "Fire Hydrant", + "street_sign": "Street Sign", + "stop_sign": "Stop Sign", + "parking_meter": "Parking Meter", + "bench": "Bench", + "bird": "Bird", + "cat": "Cat", + "dog": "Dog", + "horse": "Horse", + "sheep": "Sheep", + "cow": "Cow", + "elephant": "Elephant", + "bear": "Bear", + "zebra": "Zebra", + "giraffe": "Giraffe", + "hat": "Hat", + "backpack": "Backpack", + "umbrella": "Umbrella", + "shoe": "Shoe", + "eye_glasses": "Eye Glasses", + "handbag": "Handbag", + "tie": "Tie", + "suitcase": "Suitcase", + "frisbee": "Frisbee", + "skis": "Skis", + "snowboard": "Snowboard", + "sports_ball": "Sports Ball", + "kite": "Kite", + "baseball_bat": "Baseball Bat", + "baseball_glove": "Baseball Glove", + "skateboard": "Skateboard", + "surfboard": "Surfboard", + "tennis_racket": "Tennis Racket", + "bottle": "Bottle", + "plate": "Plate", + "wine_glass": "Wine Glass", + "cup": "Cup", + "fork": "Fork", + "knife": "Knife", + "spoon": "Spoon", + "bowl": "Bowl", + "banana": "Banana", + "apple": "Apple", + "sandwich": "Sandwich", + "orange": "Orange", + "broccoli": "Broccoli", + "carrot": "Carrot", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Cake", + "chair": "Chair", + "couch": "Couch", + "potted_plant": "Potted Plant", + "bed": "Bed", + "mirror": "Mirror", + "dining_table": "Dining Table", + "window": "Window", + "desk": "Desk", + "toilet": "Toilet", + "door": "Door", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Mouse", + "remote": "Remote", + "keyboard": "Keyboard", + "cell_phone": "Cell Phone", + "microwave": "Microwave", + "oven": "Oven", + "toaster": "Toaster", + "sink": "Sink", + "refrigerator": "Refrigerator", + "blender": "Blender", + "book": "Book", + "clock": "Clock", + "vase": "Vase", + "scissors": "Scissors", + "teddy_bear": "Teddy Bear", + "hair_dryer": "Hair Dryer", + "toothbrush": "Toothbrush", + "hair_brush": "Hair Brush", + "vehicle": "Vehicle", + "squirrel": "Squirrel", + "deer": "Deer", + "animal": "Animal", + "bark": "Bark", + "fox": "Fox", + "goat": "Goat", + "rabbit": "Rabbit", + "raccoon": "Raccoon", + "robot_lawnmower": "Robot Lawnmower", + "waste_bin": "Waste Bin", + "on_demand": "On Demand", + "face": "Face", + "license_plate": "License Plate", + "package": "Package", + "bbq_grill": "BBQ Grill", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/classificationModel.json new file mode 100644 index 0000000..0e9095e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Classification Models - Frigate", + "details": { + "scoreInfo": "Score represents the average classification confidence across all detections of this object." + }, + "button": { + "deleteClassificationAttempts": "Delete Classification Images", + "renameCategory": "Rename Class", + "deleteCategory": "Delete Class", + "deleteImages": "Delete Images", + "trainModel": "Train Model", + "addClassification": "Add Classification", + "deleteModels": "Delete Models", + "editModel": "Edit Model" + }, + "tooltip": { + "trainingInProgress": "Model is currently training", + "noNewImages": "No new images to train. Classify more images in the dataset first.", + "noChanges": "No changes to the dataset since last training.", + "modelNotReady": "Model is not ready for training" + }, + "toast": { + "success": { + "deletedCategory": "Deleted Class", + "deletedImage": "Deleted Images", + "deletedModel_one": "Successfully deleted {{count}} model", + "deletedModel_other": "Successfully deleted {{count}} models", + "categorizedImage": "Successfully Classified Image", + "trainedModel": "Successfully trained model.", + "trainingModel": "Successfully started model training.", + "updatedModel": "Successfully updated model configuration", + "renamedCategory": "Successfully renamed class to {{name}}" + }, + "error": { + "deleteImageFailed": "Failed to delete: {{errorMessage}}", + "deleteCategoryFailed": "Failed to delete class: {{errorMessage}}", + "deleteModelFailed": "Failed to delete model: {{errorMessage}}", + "categorizeFailed": "Failed to categorize image: {{errorMessage}}", + "trainingFailed": "Model training failed. Check Frigate logs for details.", + "trainingFailedToStart": "Failed to start model training: {{errorMessage}}", + "updateModelFailed": "Failed to update model: {{errorMessage}}", + "renameCategoryFailed": "Failed to rename class: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Delete Class", + "desc": "Are you sure you want to delete the class {{name}}? This will permanently delete all associated images and require re-training the model.", + "minClassesTitle": "Cannot Delete Class", + "minClassesDesc": "A classification model must have at least 2 classes. Add another class before deleting this one." + }, + "deleteModel": { + "title": "Delete Classification Model", + "single": "Are you sure you want to delete {{name}}? This will permanently delete all associated data including images and training data. This action cannot be undone.", + "desc_one": "Are you sure you want to delete {{count}} model? This will permanently delete all associated data including images and training data. This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} models? This will permanently delete all associated data including images and training data. This action cannot be undone." + }, + "edit": { + "title": "Edit Classification Model", + "descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.", + "descriptionObject": "Edit the object type and classification type for this object classification model.", + "stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes." + }, + "deleteDatasetImages": { + "title": "Delete Dataset Images", + "desc_one": "Are you sure you want to delete {{count}} image from {{dataset}}? This action cannot be undone and will require re-training the model.", + "desc_other": "Are you sure you want to delete {{count}} images from {{dataset}}? This action cannot be undone and will require re-training the model." + }, + "deleteTrainImages": { + "title": "Delete Train Images", + "desc_one": "Are you sure you want to delete {{count}} image? This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} images? This action cannot be undone." + }, + "renameCategory": { + "title": "Rename Class", + "desc": "Enter a new name for {{name}}. You will be required to retrain the model for the name change to take affect." + }, + "description": { + "invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens." + }, + "train": { + "title": "Recent Classifications", + "titleShort": "Recent", + "aria": "Select Recent Classifications" + }, + "categories": "Classes", + "none": "None", + "createCategory": { + "new": "Create New Class" + }, + "categorizeImageAs": "Classify Image As:", + "categorizeImage": "Classify Image", + "menu": { + "objects": "Objects", + "states": "States" + }, + "noModels": { + "object": { + "title": "No Object Classification Models", + "description": "Create a custom model to classify detected objects.", + "buttonText": "Create Object Model" + }, + "state": { + "title": "No State Classification Models", + "description": "Create a custom model to monitor and classify state changes in specific camera areas.", + "buttonText": "Create State Model" + } + }, + "wizard": { + "title": "Create New Classification", + "steps": { + "nameAndDefine": "Name & Define", + "stateArea": "State Area", + "chooseExamples": "Choose Examples" + }, + "step1": { + "description": "State models monitor fixed camera areas for changes (e.g., door open/closed). Object models add classifications to detected objects (e.g., known animals, delivery persons, etc.).", + "name": "Name", + "namePlaceholder": "Enter model name...", + "type": "Type", + "typeState": "State", + "typeObject": "Object", + "objectLabel": "Object Label", + "objectLabelPlaceholder": "Select object type...", + "classificationType": "Classification Type", + "classificationTypeTip": "Learn about classification types", + "classificationTypeDesc": "Sub Labels add additional text to the object label (e.g., 'Person: UPS'). Attributes are searchable metadata stored separately in the object metadata.", + "classificationSubLabel": "Sub Label", + "classificationAttribute": "Attribute", + "classes": "Classes", + "states": "States", + "classesTip": "Learn about classes", + "classesStateDesc": "Define the different states your camera area can be in. For example: 'open' and 'closed' for a garage door.", + "classesObjectDesc": "Define the different categories to classify detected objects into. For example: 'delivery_person', 'resident', 'stranger' for person classification.", + "classPlaceholder": "Enter class name...", + "errors": { + "nameRequired": "Model name is required", + "nameLength": "Model name must be 64 characters or less", + "nameOnlyNumbers": "Model name cannot contain only numbers", + "classRequired": "At least 1 class is required", + "classesUnique": "Class names must be unique", + "stateRequiresTwoClasses": "State models require at least 2 classes", + "objectLabelRequired": "Please select an object label", + "objectTypeRequired": "Please select a classification type" + } + }, + "step2": { + "description": "Select cameras and define the area to monitor for each camera. The model will classify the state of these areas.", + "cameras": "Cameras", + "selectCamera": "Select Camera", + "noCameras": "Click + to add cameras", + "selectCameraPrompt": "Select a camera from the list to define its monitoring area" + }, + "step3": { + "selectImagesPrompt": "Select all images with: {{className}}", + "selectImagesDescription": "Click on images to select them. Click Continue when you're done with this class.", + "allImagesRequired_one": "Please classify all images. {{count}} image remaining.", + "allImagesRequired_other": "Please classify all images. {{count}} images remaining.", + "generating": { + "title": "Generating Sample Images", + "description": "Frigate is pulling representative images from your recordings. This may take a moment..." + }, + "training": { + "title": "Training Model", + "description": "Your model is being trained in the background. Close this dialog, and your model will start running as soon as training is complete." + }, + "retryGenerate": "Retry Generation", + "noImages": "No sample images generated", + "classifying": "Classifying & Training...", + "trainingStarted": "Training started successfully", + "modelCreated": "Model created successfully. Use the Recent Classifications view to add images for missing states, then train the model.", + "errors": { + "noCameras": "No cameras configured", + "noObjectLabel": "No object label selected", + "generateFailed": "Failed to generate examples: {{error}}", + "generationFailed": "Generation failed. Please try again.", + "classifyFailed": "Failed to classify images: {{error}}" + }, + "generateSuccess": "Successfully generated sample images", + "missingStatesWarning": { + "title": "Missing State Examples", + "description": "It's recommended to select examples for all states for best results. You can continue without selecting all states, but the model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/configEditor.json new file mode 100644 index 0000000..614143c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Config Editor - Frigate", + "configEditor": "Config Editor", + "safeConfigEditor": "Config Editor (Safe Mode)", + "safeModeDescription": "Frigate is in safe mode due to a config validation error.", + "copyConfig": "Copy Config", + "saveAndRestart": "Save & Restart", + "saveOnly": "Save Only", + "confirm": "Exit without saving?", + "toast": { + "success": { + "copyToClipboard": "Config copied to clipboard." + }, + "error": { + "savingError": "Error saving config" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/events.json new file mode 100644 index 0000000..5c0f137 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/events.json @@ -0,0 +1,61 @@ +{ + "alerts": "Alerts", + "detections": "Detections", + "motion": { + "label": "Motion", + "only": "Motion only" + }, + "allCameras": "All Cameras", + "empty": { + "alert": "There are no alerts to review", + "detection": "There are no detections to review", + "motion": "No motion data found" + }, + "timeline": "Timeline", + "timeline.aria": "Select timeline", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out", + "events": { + "label": "Events", + "aria": "Select events", + "noFoundForTimePeriod": "No events found for this time period." + }, + "detail": { + "label": "Detail", + "noDataFound": "No detail data to review", + "aria": "Toggle detail view", + "trackedObject_one": "{{count}} object", + "trackedObject_other": "{{count}} objects", + "noObjectDetailData": "No object detail data available.", + "settings": "Detail View Settings", + "alwaysExpandActive": { + "title": "Always expand active", + "desc": "Always expand the active review item's object details when available." + } + }, + "objectTrack": { + "trackedPoint": "Tracked point", + "clickToSeek": "Click to seek to this time" + }, + "documentTitle": "Review - Frigate", + "recordings": { + "documentTitle": "Recordings - Frigate" + }, + "calendarFilter": { + "last24Hours": "Last 24 Hours" + }, + "markAsReviewed": "Mark as Reviewed", + "markTheseItemsAsReviewed": "Mark these items as reviewed", + "newReviewItems": { + "label": "View new review items", + "button": "New Items To Review" + }, + "selected_one": "{{count}} selected", + "selected_other": "{{count}} selected", + "select_all": "All", + "camera": "Camera", + "detected": "detected", + "normalActivity": "Normal", + "needsReview": "Needs review", + "securityConcern": "Security concern" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/explore.json new file mode 100644 index 0000000..6c938c1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/explore.json @@ -0,0 +1,238 @@ +{ + "documentTitle": "Explore - Frigate", + "generativeAI": "Generative AI", + "exploreMore": "Explore more {{label}} objects", + "exploreIsUnavailable": { + "title": "Explore is Unavailable", + "embeddingsReindexing": { + "context": "Explore can be used after tracked object embeddings have finished reindexing.", + "startingUp": "Starting up…", + "estimatedTime": "Estimated time remaining:", + "finishingShortly": "Finishing shortly", + "step": { + "thumbnailsEmbedded": "Thumbnails embedded: ", + "descriptionsEmbedded": "Descriptions embedded: ", + "trackedObjectsProcessed": "Tracked objects processed: " + } + }, + "downloadingModels": { + "context": "Frigate is downloading the necessary embeddings models to support the Semantic Search feature. This may take several minutes depending on the speed of your network connection.", + "setup": { + "visionModel": "Vision model", + "visionModelFeatureExtractor": "Vision model feature extractor", + "textModel": "Text model", + "textTokenizer": "Text tokenizer" + }, + "tips": { + "context": "You may want to reindex the embeddings of your tracked objects once the models are downloaded." + }, + "error": "An error has occurred. Check Frigate logs." + } + }, + "trackedObjectDetails": "Tracked Object Details", + "type": { + "details": "details", + "snapshot": "snapshot", + "thumbnail": "thumbnail", + "video": "video", + "tracking_details": "tracking details" + }, + "trackingDetails": { + "title": "Tracking Details", + "noImageFound": "No image found for this timestamp.", + "createObjectMask": "Create Object Mask", + "adjustAnnotationSettings": "Adjust annotation settings", + "scrollViewTips": "Click to view the significant moments of this object's lifecycle.", + "autoTrackingTips": "Bounding box positions will be inaccurate for autotracking cameras.", + "count": "{{first}} of {{second}}", + "trackedPoint": "Tracked Point", + "lifecycleItemDesc": { + "visible": "{{label}} detected", + "entered_zone": "{{label}} entered {{zones}}", + "active": "{{label}} became active", + "stationary": "{{label}} became stationary", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detected for {{label}}", + "other": "{{label}} recognized as {{attribute}}" + }, + "gone": "{{label}} left", + "heard": "{{label}} heard", + "external": "{{label}} detected", + "header": { + "zones": "Zones", + "ratio": "Ratio", + "area": "Area", + "score": "Score" + } + }, + "annotationSettings": { + "title": "Annotation Settings", + "showAllZones": { + "title": "Show All Zones", + "desc": "Always show zones on frames where objects have entered a zone." + }, + "offset": { + "label": "Annotation Offset", + "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", + "millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0", + "tips": "Lower the value if the video playback is ahead of the boxes and path points, and increase the value if the video playback is behind them. This value can be negative.", + "toast": { + "success": "Annotation offset for {{camera}} has been saved to the config file." + } + } + }, + "carousel": { + "previous": "Previous slide", + "next": "Next slide" + } + }, + "details": { + "item": { + "title": "Review Item Details", + "desc": "Review item details", + "button": { + "share": "Share this review item", + "viewInExplore": "View in Explore" + }, + "tips": { + "mismatch_one": "{{count}} unavailable object was detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", + "mismatch_other": "{{count}} unavailable objects were detected and included in this review item. Those objects either did not qualify as an alert or detection or have already been cleaned up/deleted.", + "hasMissingObjects": "Adjust your configuration if you want Frigate to save tracked objects for the following labels: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "A new description has been requested from {{provider}}. Depending on the speed of your provider, the new description may take some time to regenerate.", + "updatedSublabel": "Successfully updated sub label.", + "updatedLPR": "Successfully updated license plate.", + "audioTranscription": "Successfully requested audio transcription. Depending on the speed of your Frigate server, the transcription may take some time to complete." + }, + "error": { + "regenerate": "Failed to call {{provider}} for a new description: {{errorMessage}}", + "updatedSublabelFailed": "Failed to update sub label: {{errorMessage}}", + "updatedLPRFailed": "Failed to update license plate: {{errorMessage}}", + "audioTranscription": "Failed to request audio transcription: {{errorMessage}}" + } + } + }, + "label": "Label", + "editSubLabel": { + "title": "Edit sub label", + "desc": "Enter a new sub label for this {{label}}", + "descNoLabel": "Enter a new sub label for this tracked object" + }, + "editLPR": { + "title": "Edit license plate", + "desc": "Enter a new license plate value for this {{label}}", + "descNoLabel": "Enter a new license plate value for this tracked object" + }, + "snapshotScore": { + "label": "Snapshot Score" + }, + "topScore": { + "label": "Top Score", + "info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail." + }, + "score": { + "label": "Score" + }, + "recognizedLicensePlate": "Recognized License Plate", + "estimatedSpeed": "Estimated Speed", + "objects": "Objects", + "camera": "Camera", + "zones": "Zones", + "timestamp": "Timestamp", + "button": { + "findSimilar": "Find Similar", + "regenerate": { + "title": "Regenerate", + "label": "Regenerate tracked object description" + } + }, + "description": { + "label": "Description", + "placeholder": "Description of the tracked object", + "aiTips": "Frigate will not request a description from your Generative AI provider until the tracked object's lifecycle has ended." + }, + "expandRegenerationMenu": "Expand regeneration menu", + "regenerateFromSnapshot": "Regenerate from Snapshot", + "regenerateFromThumbnails": "Regenerate from Thumbnails", + "tips": { + "descriptionSaved": "Successfully saved description", + "saveDescriptionFailed": "Failed to update the description: {{errorMessage}}" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Download video", + "aria": "Download video" + }, + "downloadSnapshot": { + "label": "Download snapshot", + "aria": "Download snapshot" + }, + "downloadCleanSnapshot": { + "label": "Download clean snapshot", + "aria": "Download clean snapshot" + }, + "viewTrackingDetails": { + "label": "View tracking details", + "aria": "Show the tracking details" + }, + "findSimilar": { + "label": "Find similar", + "aria": "Find similar tracked objects" + }, + "addTrigger": { + "label": "Add trigger", + "aria": "Add a trigger for this tracked object" + }, + "audioTranscription": { + "label": "Transcribe", + "aria": "Request audio transcription" + }, + "submitToPlus": { + "label": "Submit to Frigate+", + "aria": "Submit to Frigate Plus" + }, + "viewInHistory": { + "label": "View in History", + "aria": "View in History" + }, + "deleteTrackedObject": { + "label": "Delete this tracked object" + }, + "showObjectDetails": { + "label": "Show object path" + }, + "hideObjectDetails": { + "label": "Hide object path" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirm Delete", + "desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated tracking details entries. Recorded footage of this tracked object in History view will NOT be deleted.

    Are you sure you want to proceed?" + } + }, + "noTrackedObjects": "No Tracked Objects Found", + "fetchingTrackedObjectsFailed": "Error fetching tracked objects: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} tracked object ", + "trackedObjectsCount_other": "{{count}} tracked objects ", + "searchResult": { + "tooltip": "Matched {{type}} at {{confidence}}%", + "previousTrackedObject": "Previous tracked object", + "nextTrackedObject": "Next tracked object", + "deleteTrackedObject": { + "toast": { + "success": "Tracked object deleted successfully.", + "error": "Failed to delete tracked object: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "AI Analysis" + }, + "concerns": { + "label": "Concerns" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/exports.json new file mode 100644 index 0000000..4a79d20 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Export - Frigate", + "search": "Search", + "noExports": "No exports found", + "deleteExport": "Delete Export", + "deleteExport.desc": "Are you sure you want to delete {{exportName}}?", + "editExport": { + "title": "Rename Export", + "desc": "Enter a new name for this export.", + "saveExport": "Save Export" + }, + "tooltip": { + "shareExport": "Share export", + "downloadVideo": "Download video", + "editName": "Edit name", + "deleteExport": "Delete export" + }, + "toast": { + "error": { + "renameExportFailed": "Failed to rename export: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/faceLibrary.json new file mode 100644 index 0000000..2dbb1a4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/faceLibrary.json @@ -0,0 +1,91 @@ +{ + "description": { + "addFace": "Add a new collection to the Face Library by uploading your first image.", + "placeholder": "Enter a name for this collection", + "invalidName": "Invalid name. Names can only include letters, numbers, spaces, apostrophes, underscores, and hyphens." + }, + "details": { + "timestamp": "Timestamp", + "unknown": "Unknown", + "scoreInfo": "Score is a weighted average of all face scores, weighted by the size of the face in each image." + }, + "documentTitle": "Face Library - Frigate", + "uploadFaceImage": { + "title": "Upload Face Image", + "desc": "Upload an image to scan for faces and include for {{pageToggle}}" + }, + "collections": "Collections", + "createFaceLibrary": { + "new": "Create New Face", + "nextSteps": "To build a strong foundation:
  • Use the Recent Recognitions tab to select and train on images for each detected person.
  • Focus on straight-on images for best results; avoid training images that capture faces at an angle.
  • " + }, + "steps": { + "faceName": "Enter Face Name", + "uploadFace": "Upload Face Image", + "nextSteps": "Next Steps", + "description": { + "uploadFace": "Upload an image of {{name}} that shows their face from a front-facing angle. The image does not need to be cropped to just their face." + } + }, + "train": { + "title": "Recent Recognitions", + "titleShort": "Recent", + "aria": "Select recent recognitions", + "empty": "There are no recent face recognition attempts" + }, + "deleteFaceLibrary": { + "title": "Delete Name", + "desc": "Are you sure you want to delete the collection {{name}}? This will permanently delete all associated faces." + }, + "deleteFaceAttempts": { + "title": "Delete Faces", + "desc_one": "Are you sure you want to delete {{count}} face? This action cannot be undone.", + "desc_other": "Are you sure you want to delete {{count}} faces? This action cannot be undone." + }, + "renameFace": { + "title": "Rename Face", + "desc": "Enter a new name for {{name}}" + }, + "button": { + "deleteFaceAttempts": "Delete Faces", + "addFace": "Add Face", + "renameFace": "Rename Face", + "deleteFace": "Delete Face", + "uploadImage": "Upload Image", + "reprocessFace": "Reprocess Face" + }, + "imageEntry": { + "validation": { + "selectImage": "Please select an image file." + }, + "dropActive": "Drop the image here…", + "dropInstructions": "Drag and drop or paste an image here, or click to select", + "maxSize": "Max size: {{size}}MB" + }, + "nofaces": "No faces available", + "trainFaceAs": "Train Face as:", + "trainFace": "Train Face", + "toast": { + "success": { + "uploadedImage": "Successfully uploaded image.", + "addFaceLibrary": "{{name}} has successfully been added to the Face Library!", + "deletedFace_one": "Successfully deleted {{count}} face.", + "deletedFace_other": "Successfully deleted {{count}} faces.", + "deletedName_zero": "Empty collection deleted successfully.", + "deletedName_one": "{{count}} face has been successfully deleted.", + "deletedName_other": "{{count}} faces have been successfully deleted.", + "renamedFace": "Successfully renamed face to {{name}}", + "trainedFace": "Successfully trained face.", + "updatedFaceScore": "Successfully updated face score to {{name}} ({{score}})." + }, + "error": { + "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", + "addFaceLibraryFailed": "Failed to set face name: {{errorMessage}}", + "deleteFaceFailed": "Failed to delete: {{errorMessage}}", + "deleteNameFailed": "Failed to delete name: {{errorMessage}}", + "renameFaceFailed": "Failed to rename face: {{errorMessage}}", + "trainFailed": "Failed to train: {{errorMessage}}", + "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/live.json new file mode 100644 index 0000000..21f367e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/live.json @@ -0,0 +1,186 @@ +{ + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "lowBandwidthMode": "Low-bandwidth Mode", + "twoWayTalk": { + "enable": "Enable Two Way Talk", + "disable": "Disable Two Way Talk" + }, + "cameraAudio": { + "enable": "Enable Camera Audio", + "disable": "Disable Camera Audio" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Click in the frame to center the camera", + "enable": "Enable click to move", + "disable": "Disable click to move" + }, + "left": { + "label": "Move PTZ camera to the left" + }, + "up": { + "label": "Move PTZ camera up" + }, + "down": { + "label": "Move PTZ camera down" + }, + "right": { + "label": "Move PTZ camera to the right" + } + }, + "zoom": { + "in": { + "label": "Zoom PTZ camera in" + }, + "out": { + "label": "Zoom PTZ camera out" + } + }, + "focus": { + "in": { + "label": "Focus PTZ camera in" + }, + "out": { + "label": "Focus PTZ camera out" + } + }, + "frame": { + "center": { + "label": "Click in the frame to center the PTZ camera" + } + }, + "presets": "PTZ camera presets" + }, + "camera": { + "enable": "Enable Camera", + "disable": "Disable Camera" + }, + "muteCameras": { + "enable": "Mute All Cameras", + "disable": "Unmute All Cameras" + }, + "detect": { + "enable": "Enable Detect", + "disable": "Disable Detect" + }, + "recording": { + "enable": "Enable Recording", + "disable": "Disable Recording" + }, + "snapshots": { + "enable": "Enable Snapshots", + "disable": "Disable Snapshots" + }, + "snapshot": { + "takeSnapshot": "Download instant snapshot", + "noVideoSource": "No video source available for snapshot.", + "captureFailed": "Failed to capture snapshot.", + "downloadStarted": "Snapshot download started." + }, + "audioDetect": { + "enable": "Enable Audio Detect", + "disable": "Disable Audio Detect" + }, + "transcription": { + "enable": "Enable Live Audio Transcription", + "disable": "Disable Live Audio Transcription" + }, + "autotracking": { + "enable": "Enable Autotracking", + "disable": "Disable Autotracking" + }, + "streamStats": { + "enable": "Show Stream Stats", + "disable": "Hide Stream Stats" + }, + "manualRecording": { + "title": "On-Demand", + "tips": "Download an instant snapshot or start a manual event based on this camera's recording retention settings.", + "playInBackground": { + "label": "Play in background", + "desc": "Enable this option to continue streaming when the player is hidden." + }, + "showStats": { + "label": "Show Stats", + "desc": "Enable this option to show stream statistics as an overlay on the camera feed." + }, + "debugView": "Debug View", + "start": "Start on-demand recording", + "started": "Started manual on-demand recording.", + "failedToStart": "Failed to start manual on-demand recording.", + "recordDisabledTips": "Since recording is disabled or restricted in the config for this camera, only a snapshot will be saved.", + "end": "End on-demand recording", + "ended": "Ended manual on-demand recording.", + "failedToEnd": "Failed to end manual on-demand recording." + }, + "streamingSettings": "Streaming Settings", + "notifications": "Notifications", + "audio": "Audio", + "suspend": { + "forTime": "Suspend for: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Audio must be output from your camera and configured in go2rtc for this stream." + }, + "available": "Audio is available for this stream", + "unavailable": "Audio is not available for this stream" + }, + "debug": { + "picker": "Stream selection unavailable in debug mode. Debug view always uses the stream assigned the detect role." + }, + "twoWayTalk": { + "tips": "Your device must support the feature and WebRTC must be configured for two-way talk.", + "available": "Two-way talk is available for this stream", + "unavailable": "Two-way talk is unavailable for this stream" + }, + "lowBandwidth": { + "tips": "Live view is in low-bandwidth mode due to buffering or stream errors.", + "resetStream": "Reset stream" + }, + "playInBackground": { + "label": "Play in background", + "tips": "Enable this option to continue streaming when the player is hidden." + } + }, + "cameraSettings": { + "title": "{{camera}} Settings", + "cameraEnabled": "Camera Enabled", + "objectDetection": "Object Detection", + "recording": "Recording", + "snapshots": "Snapshots", + "audioDetection": "Audio Detection", + "transcription": "Audio Transcription", + "autotracking": "Autotracking" + }, + "history": { + "label": "Show historical footage" + }, + "effectiveRetainMode": { + "modes": { + "all": "All", + "motion": "Motion", + "active_objects": "Active Objects" + } + }, + "editLayout": { + "label": "Edit Layout", + "group": { + "label": "Edit Camera Group" + }, + "exitEdit": "Exit Editing" + }, + "noCameras": { + "title": "No Cameras Configured", + "description": "Get started by connecting a camera to Frigate.", + "buttonText": "Add Camera", + "restricted": { + "title": "No Cameras Available", + "description": "You don't have permission to view any cameras in this group." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/recording.json new file mode 100644 index 0000000..9ca7c43 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "calendar": "Calendar", + "filter": "Filter", + "filters": "Filters", + "toast": { + "error": { + "noValidTimeSelected": "No valid time range selected", + "endTimeMustAfterStartTime": "End time must be after start time" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/search.json new file mode 100644 index 0000000..22da772 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Search", + "savedSearches": "Saved Searches", + "searchFor": "Search for {{inputValue}}", + "button": { + "clear": "Clear search", + "save": "Save search", + "delete": "Delete saved search", + "filterInformation": "Filter information", + "filterActive": "Filters active" + }, + "trackedObjectId": "Tracked Object ID", + "filter": { + "label": { + "cameras": "Cameras", + "labels": "Labels", + "zones": "Zones", + "sub_labels": "Sub Labels", + "search_type": "Search Type", + "time_range": "Time Range", + "before": "Before", + "after": "After", + "min_score": "Min Score", + "max_score": "Max Score", + "min_speed": "Min Speed", + "max_speed": "Max Speed", + "recognized_license_plate": "Recognized License Plate", + "has_clip": "Has Clip", + "has_snapshot": "Has Snapshot" + }, + "searchType": { + "thumbnail": "Thumbnail", + "description": "Description" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "The 'before' date must be later than the 'after' date.", + "afterDatebeEarlierBefore": "The 'after' date must be earlier than the 'before' date.", + "minScoreMustBeLessOrEqualMaxScore": "The 'min_score' must be less than or equal to the 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "The 'max_score' must be greater than or equal to the 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "The 'min_speed' must be less than or equal to the 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "The 'max_speed' must be greater than or equal to the 'min_speed'." + } + }, + "tips": { + "title": "How to use text filters", + "desc": { + "text": "Filters help you narrow down your search results. Here's how to use them in the input field:", + "step1": "Type a filter key name followed by a colon (e.g., \"cameras:\").", + "step2": "Select a value from the suggestions or type your own.", + "step3": "Use multiple filters by adding them one after another with a space in between.", + "step4": "Date filters (before: and after:) use {{DateFormat}} format.", + "step5": "Time range filter uses {{exampleTime}} format.", + "step6": "Remove filters by clicking the 'x' next to them.", + "exampleLabel": "Example:" + } + }, + "header": { + "currentFilterType": "Filter Values", + "noFilters": "Filters", + "activeFilters": "Active Filters" + } + }, + "similaritySearch": { + "title": "Similarity Search", + "active": "Similarity search active", + "clear": "Clear similarity search" + }, + "placeholder": { + "search": "Search…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/settings.json new file mode 100644 index 0000000..1946a1c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/settings.json @@ -0,0 +1,1069 @@ +{ + "documentTitle": { + "default": "Settings - Frigate", + "authentication": "Authentication Settings - Frigate", + "cameraManagement": "Manage Cameras - Frigate", + "cameraReview": "Camera Review Settings - Frigate", + "enrichments": "Enrichments Settings - Frigate", + "masksAndZones": "Mask and Zone Editor - Frigate", + "motionTuner": "Motion Tuner - Frigate", + "object": "Debug - Frigate", + "general": "UI Settings - Frigate", + "frigatePlus": "Frigate+ Settings - Frigate", + "notifications": "Notification Settings - Frigate" + }, + "menu": { + "ui": "UI", + "enrichments": "Enrichments", + "cameraManagement": "Management", + "cameraReview": "Review", + "masksAndZones": "Masks / Zones", + "motionTuner": "Motion Tuner", + "triggers": "Triggers", + "debug": "Debug", + "users": "Users", + "roles": "Roles", + "notifications": "Notifications", + "frigateplus": "Frigate+" + }, + "dialog": { + "unsavedChanges": { + "title": "You have unsaved changes.", + "desc": "Do you want to save your changes before continuing?" + } + }, + "cameraSetting": { + "camera": "Camera", + "noCamera": "No Camera" + }, + "general": { + "title": "UI Settings", + "liveDashboard": { + "title": "Live Dashboard", + "automaticLiveView": { + "label": "Automatic Live View", + "desc": "Automatically switch to a camera's live view when activity is detected. Disabling this option causes static camera images on the Live dashboard to only update once per minute." + }, + "playAlertVideos": { + "label": "Play Alert Videos", + "desc": "By default, recent alerts on the Live dashboard play as small looping videos. Disable this option to only show a static image of recent alerts on this device/browser." + }, + "displayCameraNames": { + "label": "Always Show Camera Names", + "desc": "Always show the camera names in a chip in the multi-camera live view dashboard." + }, + "liveFallbackTimeout": { + "label": "Live Player Fallback Timeout", + "desc": "When a camera's high quality live stream is unavailable, fall back to low bandwidth mode after this many seconds. Default: 3." + } + }, + "storedLayouts": { + "title": "Stored Layouts", + "desc": "The layout of cameras in a camera group can be dragged/resized. The positions are stored in your browser's local storage.", + "clearAll": "Clear All Layouts" + }, + "cameraGroupStreaming": { + "title": "Camera Group Streaming Settings", + "desc": "Streaming settings for each camera group are stored in your browser's local storage.", + "clearAll": "Clear All Streaming Settings" + }, + "recordingsViewer": { + "title": "Recordings Viewer", + "defaultPlaybackRate": { + "label": "Default Playback Rate", + "desc": "Default playback rate for recordings playback." + } + }, + "calendar": { + "title": "Calendar", + "firstWeekday": { + "label": "First Weekday", + "desc": "The day that the weeks of the review calendar begin on.", + "sunday": "Sunday", + "monday": "Monday" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Cleared stored layout for {{cameraName}}", + "clearStreamingSettings": "Cleared streaming settings for all camera groups." + }, + "error": { + "clearStoredLayoutFailed": "Failed to clear stored layout: {{errorMessage}}", + "clearStreamingSettingsFailed": "Failed to clear streaming settings: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Enrichments Settings", + "unsavedChanges": "Unsaved Enrichments settings changes", + "birdClassification": { + "title": "Bird Classification", + "desc": "Bird classification identifies known birds using a quantized Tensorflow model. When a known bird is recognized, its common name will be added as a sub_label. This information is included in the UI, filters, as well as in notifications." + }, + "semanticSearch": { + "title": "Semantic Search", + "desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.", + "reindexNow": { + "label": "Reindex Now", + "desc": "Reindexing will regenerate embeddings for all tracked object. This process runs in the background and may max out your CPU and take a fair amount of time depending on the number of tracked objects you have.", + "confirmTitle": "Confirm Reindexing", + "confirmDesc": "Are you sure you want to reindex all tracked object embeddings? This process will run in the background but it may max out your CPU and take a fair amount of time. You can watch the progress on the Explore page.", + "confirmButton": "Reindex", + "success": "Reindexing started successfully.", + "alreadyInProgress": "Reindexing is already in progress.", + "error": "Failed to start reindexing: {{errorMessage}}" + }, + "modelSize": { + "label": "Model Size", + "desc": "The size of the model used for semantic search embeddings.", + "small": { + "title": "small", + "desc": "Using small employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality." + }, + "large": { + "title": "large", + "desc": "Using large employs the full Jina model and will automatically run on the GPU if applicable." + } + } + }, + "faceRecognition": { + "title": "Face Recognition", + "desc": "Face recognition allows people to be assigned names and when their face is recognized Frigate will assign the person's name as a sub label. This information is included in the UI, filters, as well as in notifications.", + "modelSize": { + "label": "Model Size", + "desc": "The size of the model used for face recognition.", + "small": { + "title": "small", + "desc": "Using small employs a FaceNet face embedding model that runs efficiently on most CPUs." + }, + "large": { + "title": "large", + "desc": "Using large employs an ArcFace face embedding model and will automatically run on the GPU if applicable." + } + } + }, + "licensePlateRecognition": { + "title": "License Plate Recognition", + "desc": "Frigate can recognize license plates on vehicles and automatically add the detected characters to the recognized_license_plate field or a known name as a sub_label to objects that are of type car. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street." + }, + "restart_required": "Restart required (Enrichments settings changed)", + "toast": { + "success": "Enrichments settings have been saved. Restart Frigate to apply your changes.", + "error": "Failed to save config changes: {{errorMessage}}" + } + }, + "cameraWizard": { + "title": "Add Camera", + "description": "Follow the steps below to add a new camera to your Frigate installation.", + "steps": { + "nameAndConnection": "Name & Connection", + "probeOrSnapshot": "Probe or Snapshot", + "streamConfiguration": "Stream Configuration", + "validationAndTesting": "Validation & Testing" + }, + "save": { + "success": "Successfully saved new camera {{cameraName}}.", + "failure": "Error saving {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolution", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Please provide a valid stream URL", + "testFailed": "Stream test failed: {{error}}" + }, + "step1": { + "description": "Enter your camera details and choose to probe the camera or manually select the brand.", + "cameraName": "Camera Name", + "cameraNamePlaceholder": "e.g., front_door or Back Yard Overview", + "host": "Host/IP Address", + "port": "Port", + "username": "Username", + "usernamePlaceholder": "Optional", + "password": "Password", + "passwordPlaceholder": "Optional", + "selectTransport": "Select transport protocol", + "cameraBrand": "Camera Brand", + "selectBrand": "Select camera brand for URL template", + "customUrl": "Custom Stream URL", + "brandInformation": "Brand information", + "brandUrlFormat": "For cameras with the RTSP URL format as: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "connectionSettings": "Connection Settings", + "detectionMethod": "Stream Detection Method", + "onvifPort": "ONVIF Port", + "probeMode": "Probe camera", + "manualMode": "Manual selection", + "detectionMethodDescription": "Probe the camera with ONVIF (if supported) to find camera stream URLs, or manually select the camera brand to use pre-defined URLs. To enter a custom RTSP URL, choose the manual method and select \"Other\".", + "onvifPortDescription": "For cameras that support ONVIF, this is usually 80 or 8080.", + "useDigestAuth": "Use digest authentication", + "useDigestAuthDescription": "Use HTTP digest authentication for ONVIF. Some cameras may require a dedicated ONVIF username/password instead of the standard admin user.", + "errors": { + "brandOrCustomUrlRequired": "Either select a camera brand with host/IP or choose 'Other' with a custom URL", + "nameRequired": "Camera name is required", + "nameLength": "Camera name must be 64 characters or less", + "invalidCharacters": "Camera name contains invalid characters", + "nameExists": "Camera name already exists", + "customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\". Manual configuration is required for non-RTSP camera streams." + } + }, + "step2": { + "description": "Probe the camera for available streams or configure manual settings based on your selected detection method.", + "testSuccess": "Connection test successful!", + "testFailed": "Connection test failed. Please check your input and try again.", + "testFailedTitle": "Test Failed", + "streamDetails": "Stream Details", + "probing": "Probing camera...", + "retry": "Retry", + "testing": { + "probingMetadata": "Probing camera metadata...", + "fetchingSnapshot": "Fetching camera snapshot..." + }, + "probeFailed": "Failed to probe camera: {{error}}", + "probingDevice": "Probing device...", + "probeSuccessful": "Probe successful", + "probeError": "Probe Error", + "probeNoSuccess": "Probe unsuccessful", + "deviceInfo": "Device Information", + "manufacturer": "Manufacturer", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiles", + "ptzSupport": "PTZ Support", + "autotrackingSupport": "Autotracking Support", + "presets": "Presets", + "rtspCandidates": "RTSP Candidates", + "rtspCandidatesDescription": "The following RTSP URLs were found from the camera probe. Test the connection to view stream metadata.", + "noRtspCandidates": "No RTSP URLs were found from the camera. Your credentials may be incorrect, or the camera may not support ONVIF or the method used to retrieve RTSP URLs. Go back and enter the RTSP URL manually.", + "candidateStreamTitle": "Candidate {{number}}", + "useCandidate": "Use", + "uriCopy": "Copy", + "uriCopied": "URI copied to clipboard", + "testConnection": "Test Connection", + "toggleUriView": "Click to toggle full URI view", + "connected": "Connected", + "notConnected": "Not Connected", + "errors": { + "hostRequired": "Host/IP address is required" + } + }, + "step3": { + "description": "Configure stream roles and add additional streams for your camera.", + "streamsTitle": "Camera Streams", + "addStream": "Add Stream", + "addAnotherStream": "Add Another Stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Select a stream", + "searchCandidates": "Search candidates...", + "noStreamFound": "No stream found", + "url": "URL", + "resolution": "Resolution", + "selectResolution": "Select resolution", + "quality": "Quality", + "selectQuality": "Select quality", + "roles": "Roles", + "roleLabels": { + "detect": "Object Detection", + "record": "Recording", + "audio": "Audio" + }, + "testStream": "Test Connection", + "testSuccess": "Stream test successful!", + "testFailed": "Stream test failed", + "testFailedTitle": "Test Failed", + "connected": "Connected", + "notConnected": "Not Connected", + "featuresTitle": "Features", + "go2rtc": "Reduce connections to camera", + "detectRoleWarning": "At least one stream must have the \"detect\" role to proceed.", + "rolesPopover": { + "title": "Stream Roles", + "detect": "Main feed for object detection.", + "record": "Saves segments of the video feed based on configuration settings.", + "audio": "Feed for audio based detection." + }, + "featuresPopover": { + "title": "Stream Features", + "description": "Use go2rtc restreaming to reduce connections to your camera." + } + }, + "step4": { + "description": "Final validation and analysis before saving your new camera. Connect each stream before saving.", + "validationTitle": "Stream Validation", + "connectAllStreams": "Connect All Streams", + "reconnectionSuccess": "Reconnection successful.", + "reconnectionPartial": "Some streams failed to reconnect.", + "streamUnavailable": "Stream preview unavailable", + "reload": "Reload", + "connecting": "Connecting...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Failed", + "notTested": "Not tested", + "connectStream": "Connect", + "connectingStream": "Connecting", + "disconnectStream": "Disconnect", + "estimatedBandwidth": "Estimated Bandwidth", + "roles": "Roles", + "ffmpegModule": "Use stream compatibility mode", + "ffmpegModuleDescription": "If the stream does not load after several attempts, try enabling this. When enabled, Frigate will use the ffmpeg module with go2rtc. This may provide better compatibility with some camera streams.", + "none": "None", + "error": "Error", + "streamValidated": "Stream {{number}} validated successfully", + "streamValidationFailed": "Stream {{number}} validation failed", + "saveAndApply": "Save New Camera", + "saveError": "Invalid configuration. Please check your settings.", + "issues": { + "title": "Stream Validation", + "videoCodecGood": "Video codec is {{codec}}.", + "audioCodecGood": "Audio codec is {{codec}}.", + "resolutionHigh": "A resolution of {{resolution}} may cause increased resource usage.", + "resolutionLow": "A resolution of {{resolution}} may be too low for reliable detection of small objects.", + "noAudioWarning": "No audio detected for this stream, recordings will not have audio.", + "audioCodecRecordError": "The AAC audio codec is required to support audio in recordings.", + "audioCodecRequired": "An audio stream is required to support audio detection.", + "restreamingWarning": "Reducing connections to the camera for the record stream may increase CPU usage slightly.", + "brands": { + "reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard.", + "reolink-http": "Reolink HTTP streams should use FFmpeg for better compatibility. Enable 'Use stream compatibility mode' for this stream." + }, + "dahua": { + "substreamWarning": "Substream 1 is locked to a low resolution. Many Dahua / Amcrest / EmpireTech cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available." + }, + "hikvision": { + "substreamWarning": "Substream 1 is locked to a low resolution. Many Hikvision cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available." + } + } + } + }, + "cameraManagement": { + "title": "Manage Cameras", + "addCamera": "Add New Camera", + "editCamera": "Edit Camera:", + "selectCamera": "Select a Camera", + "backToSettings": "Back to Camera Settings", + "streams": { + "title": "Enable / Disable Cameras", + "desc": "Temporarily disable a camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
    Note: This does not disable go2rtc restreams." + }, + "cameraConfig": { + "add": "Add Camera", + "edit": "Edit Camera", + "description": "Configure camera settings including stream inputs and roles.", + "name": "Camera Name", + "nameRequired": "Camera name is required", + "nameLength": "Camera name must be less than 64 characters.", + "namePlaceholder": "e.g., front_door or Back Yard Overview", + "enabled": "Enabled", + "ffmpeg": { + "inputs": "Input Streams", + "path": "Stream Path", + "pathRequired": "Stream path is required", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "At least one role is required", + "rolesUnique": "Each role (audio, detect, record) can only be assigned to one stream", + "addInput": "Add Input Stream", + "removeInput": "Remove Input Stream", + "inputsRequired": "At least one input stream is required" + }, + "go2rtcStreams": "go2rtc Streams", + "streamUrls": "Stream URLs", + "addUrl": "Add URL", + "addGo2rtcStream": "Add go2rtc Stream", + "toast": { + "success": "Camera {{cameraName}} saved successfully" + } + } + }, + "cameraReview": { + "title": "Camera Review Settings", + "object_descriptions": { + "title": "Generative AI Object Descriptions", + "desc": "Temporarily enable/disable Generative AI object descriptions for this camera. When disabled, AI generated descriptions will not be requested for tracked objects on this camera." + }, + "review_descriptions": { + "title": "Generative AI Review Descriptions", + "desc": "Temporarily enable/disable Generative AI review descriptions for this camera. When disabled, AI generated descriptions will not be requested for review items on this camera." + }, + "review": { + "title": "Review", + "desc": "Temporarily enable/disable alerts and detections for this camera until Frigate restarts. When disabled, no new review items will be generated. ", + "alerts": "Alerts ", + "detections": "Detections " + }, + "reviewClassification": { + "title": "Review Classification", + "desc": "Frigate categorizes review items as Alerts and Detections. By default, all person and car objects are considered Alerts. You can refine categorization of your review items by configuring required zones for them.", + + "noDefinedZones": "No zones are defined for this camera.", + "objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.", + "zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.", + "objectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.", + "zoneObjectDetectionsTips": { + "text": "All {{detectionsLabels}} objects not categorized in {{zone}} on {{cameraName}} will be shown as Detections.", + "notSelectDetections": "All {{detectionsLabels}} objects detected in {{zone}} on {{cameraName}} not categorized as Alerts will be shown as Detections regardless of which zone they are in.", + "regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in." + }, + "unsavedChanges": "Unsaved Review Classification settings for {{camera}}", + "selectAlertsZones": "Select zones for Alerts", + "selectDetectionsZones": "Select zones for Detections", + "limitDetections": "Limit detections to specific zones", + "toast": { + "success": "Review Classification configuration has been saved. Restart Frigate to apply changes." + } + } + }, + "masksAndZones": { + "filter": { + "all": "All Masks and Zones" + }, + "restart_required": "Restart required (masks/zones changed)", + "toast": { + "success": { + "copyCoordinates": "Copied coordinates for {{polyName}} to clipboard." + }, + "error": { + "copyCoordinatesFailed": "Could not copy coordinates to clipboard." + } + }, + "motionMaskLabel": "Motion Mask {{number}}", + "objectMaskLabel": "Object Mask {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.", + "mustNotBeSameWithCamera": "Zone name must not be the same as camera name.", + "alreadyExists": "A zone with this name already exists for this camera.", + "mustNotContainPeriod": "Zone name must not contain periods.", + "hasIllegalCharacter": "Zone name contains illegal characters.", + "mustHaveAtLeastOneLetter": "Zone name must have at least one letter." + } + }, + "distance": { + "error": { + "text": "Distance must be greater than or equal to 0.1.", + "mustBeFilled": "All distance fields must be filled to use speed estimation." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inertia must be above 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Speed threshold must greater than or equal to 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Remove last point", + "reset": { + "label": "Clear all points" + }, + "snapPoints": { + "true": "Snap points", + "false": "Don't Snap points" + }, + "delete": { + "title": "Confirm Delete", + "desc": "Are you sure you want to delete the {{type}} {{name}}?", + "success": "{{name}} has been deleted." + }, + "error": { + "mustBeFinished": "Polygon drawing must be finished before saving." + } + } + }, + "zones": { + "label": "Zones", + "documentTitle": "Edit Zone - Frigate", + "desc": { + "title": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", + "documentation": "Documentation" + }, + "add": "Add Zone", + "edit": "Edit Zone", + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "name": { + "title": "Name", + "inputPlaceHolder": "Enter a name…", + "tips": "Name must be at least 2 characters, must have at least one letter, and must not be the name of a camera or another zone on this camera." + }, + "inertia": { + "title": "Inertia", + "desc": "Specifies how many frames that an object must be in a zone before they are considered in the zone. Default: 3" + }, + "loiteringTime": { + "title": "Loitering Time", + "desc": "Sets a minimum amount of time in seconds that the object must be in the zone for it to activate. Default: 0" + }, + "objects": { + "title": "Objects", + "desc": "List of objects that apply to this zone." + }, + "allObjects": "All Objects", + "speedEstimation": { + "title": "Speed Estimation", + "desc": "Enable speed estimation for objects in this zone. The zone must have exactly 4 points.", + "lineADistance": "Line A distance ({{unit}})", + "lineBDistance": "Line B distance ({{unit}})", + "lineCDistance": "Line C distance ({{unit}})", + "lineDDistance": "Line D distance ({{unit}})" + }, + "speedThreshold": { + "title": "Speed Threshold ({{unit}})", + "desc": "Specifies a minimum speed for objects to be considered in this zone.", + "toast": { + "error": { + "pointLengthError": "Speed estimation has been disabled for this zone. Zones with speed estimation must have exactly 4 points.", + "loiteringTimeError": "Zones with loitering times greater than 0 should not be used with speed estimation." + } + } + }, + "toast": { + "success": "Zone ({{zoneName}}) has been saved." + } + }, + "motionMasks": { + "label": "Motion Mask", + "documentTitle": "Edit Motion Mask - Frigate", + "desc": { + "title": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.", + "documentation": "Documentation" + }, + "add": "New Motion Mask", + "edit": "Edit Motion Mask", + "context": { + "title": "Motion masks are used to prevent unwanted types of motion from triggering detection (example: tree branches, camera timestamps). Motion masks should be used very sparingly, over-masking will make it more difficult for objects to be tracked." + }, + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "polygonAreaTooLarge": { + "title": "The motion mask is covering {{polygonArea}}% of the camera frame. Large motion masks are not recommended.", + "tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead." + }, + "toast": { + "success": { + "title": "{{polygonName}} has been saved.", + "noName": "Motion Mask has been saved." + } + } + }, + "objectMasks": { + "label": "Object Masks", + "documentTitle": "Edit Object Mask - Frigate", + "desc": { + "title": "Object filter masks are used to filter out false positives for a given object type based on location.", + "documentation": "Documentation" + }, + "add": "Add Object Mask", + "edit": "Edit Object Mask", + "context": "Object filter masks are used to filter out false positives for a given object type based on location.", + "point_one": "{{count}} point", + "point_other": "{{count}} points", + "clickDrawPolygon": "Click to draw a polygon on the image.", + "objects": { + "title": "Objects", + "desc": "The object type that applies to this object mask.", + "allObjectTypes": "All object types" + }, + "toast": { + "success": { + "title": "{{polygonName}} has been saved.", + "noName": "Object Mask has been saved." + } + } + } + }, + "motionDetectionTuner": { + "title": "Motion Detection Tuner", + "unsavedChanges": "Unsaved Motion Tuner changes ({{camera}})", + "desc": { + "title": "Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.", + "documentation": "Read the Motion Tuning Guide" + }, + "Threshold": { + "title": "Threshold", + "desc": "The threshold value dictates how much of a change in a pixel's luminance is required to be considered motion. Default: 30" + }, + "contourArea": { + "title": "Contour Area", + "desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. Default: 10" + }, + "improveContrast": { + "title": "Improve Contrast", + "desc": "Improve contrast for darker scenes. Default: ON" + }, + "toast": { + "success": "Motion settings have been saved." + } + }, + "debug": { + "title": "Debug", + "detectorDesc": "Frigate uses your detectors ({{detectors}}) to detect objects in your camera's video stream.", + "desc": "Debugging view shows a real-time view of tracked objects and their statistics. The object list shows a time-delayed summary of detected objects.", + "openCameraWebUI": "Open {{camera}}'s Web UI", + "debugging": "Debugging", + "objectList": "Object List", + "noObjects": "No objects", + "audio": { + "title": "Audio", + "noAudioDetections": "No audio detections", + "score": "score", + "currentRMS": "Current RMS", + "currentdbFS": "Current dbFS" + }, + "boundingBoxes": { + "title": "Bounding boxes", + "desc": "Show bounding boxes around tracked objects", + "colors": { + "label": "Object Bounding Box Colors", + "info": "
  • At startup, different colors will be assigned to each object label
  • A dark blue thin line indicates that object is not detected at this current point in time
  • A gray thin line indicates that object is detected as being stationary
  • A thick line indicates that object is the subject of autotracking (when enabled)
  • " + } + }, + "timestamp": { + "title": "Timestamp", + "desc": "Overlay a timestamp on the image" + }, + "zones": { + "title": "Zones", + "desc": "Show an outline of any defined zones" + }, + "mask": { + "title": "Motion masks", + "desc": "Show motion mask polygons" + }, + "motion": { + "title": "Motion boxes", + "desc": "Show boxes around areas where motion is detected", + "tips": "

    Motion Boxes


    Red boxes will be overlaid on areas of the frame where motion is currently being detected

    " + }, + "regions": { + "title": "Regions", + "desc": "Show a box of the region of interest sent to the object detector", + "tips": "

    Region Boxes


    Bright green boxes will be overlaid on areas of interest in the frame that are being sent to the object detector.

    " + }, + "paths": { + "title": "Paths", + "desc": "Show significant points of the tracked object's path", + "tips": "

    Paths


    Lines and circles will indicate significant points the tracked object has moved during its lifecycle.

    " + }, + "objectShapeFilterDrawing": { + "title": "Object Shape Filter Drawing", + "desc": "Draw a rectangle on the image to view area and ratio details", + "tips": "Enable this option to draw a rectangle on the camera image to show its area and ratio. These values can then be used to set object shape filter parameters in your config.", + "score": "Score", + "ratio": "Ratio", + "area": "Area" + } + }, + "users": { + "title": "Users", + "management": { + "title": "User Management", + "desc": "Manage this Frigate instance's user accounts." + }, + "addUser": "Add User", + "updatePassword": "Reset Password", + "toast": { + "success": { + "createUser": "User {{user}} created successfully", + "deleteUser": "User {{user}} deleted successfully", + "updatePassword": "Password updated successfully.", + "roleUpdated": "Role updated for {{user}}" + }, + "error": { + "setPasswordFailed": "Failed to save password: {{errorMessage}}", + "createUserFailed": "Failed to create user: {{errorMessage}}", + "deleteUserFailed": "Failed to delete user: {{errorMessage}}", + "roleUpdateFailed": "Failed to update role: {{errorMessage}}" + } + }, + "table": { + "username": "Username", + "actions": "Actions", + "role": "Role", + "noUsers": "No users found.", + "changeRole": "Change user role", + "password": "Reset Password", + "deleteUser": "Delete user" + }, + "dialog": { + "form": { + "user": { + "title": "Username", + "desc": "Only letters, numbers, periods and underscores allowed.", + "placeholder": "Enter username" + }, + "password": { + "title": "Password", + "placeholder": "Enter password", + "show": "Show password", + "hide": "Hide password", + "confirm": { + "title": "Confirm Password", + "placeholder": "Confirm Password" + }, + "strength": { + "title": "Password strength: ", + "weak": "Weak", + "medium": "Medium", + "strong": "Strong", + "veryStrong": "Very Strong" + }, + "requirements": { + "title": "Password requirements:", + "length": "At least 8 characters", + "uppercase": "At least one uppercase letter", + "digit": "At least one digit", + "special": "At least one special character (!@#$%^&*(),.?\":{}|<>)" + }, + "match": "Passwords match", + "notMatch": "Passwords don't match" + }, + "newPassword": { + "title": "New Password", + "placeholder": "Enter new password", + "confirm": { + "placeholder": "Re-enter new password" + } + }, + "currentPassword": { + "title": "Current Password", + "placeholder": "Enter your current password" + }, + "usernameIsRequired": "Username is required", + "passwordIsRequired": "Password is required" + }, + "createUser": { + "title": "Create New User", + "desc": "Add a new user account and specify an role for access to areas of the Frigate UI.", + "usernameOnlyInclude": "Username may only include letters, numbers, . or _", + "confirmPassword": "Please confirm your password" + }, + "deleteUser": { + "title": "Delete User", + "desc": "This action cannot be undone. This will permanently delete the user account and remove all associated data.", + "warn": "Are you sure you want to delete {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Password cannot be empty", + "doNotMatch": "Passwords do not match", + "currentPasswordRequired": "Current password is required", + "incorrectCurrentPassword": "Current password is incorrect", + "passwordVerificationFailed": "Failed to verify password", + "updatePassword": "Update Password for {{username}}", + "setPassword": "Set Password", + "desc": "Create a strong password to secure this account.", + "multiDeviceWarning": "Any other devices where you are logged in will be required to re-login within {{refresh_time}}.", + "multiDeviceAdmin": "You can also force all users to re-authenticate immediately by rotating your JWT secret." + }, + "changeRole": { + "title": "Change User Role", + "select": "Select a role", + "desc": "Update permissions for {{username}}", + "roleInfo": { + "intro": "Select the appropriate role for this user:", + "admin": "Admin", + "adminDesc": "Full access to all features.", + "viewer": "Viewer", + "viewerDesc": "Limited to Live dashboards, Review, Explore, and Exports only.", + "customDesc": "Custom role with specific camera access." + } + } + } + }, + "roles": { + "management": { + "title": "Viewer Role Management", + "desc": "Manage custom viewer roles and their camera access permissions for this Frigate instance." + }, + "addRole": "Add Role", + "table": { + "role": "Role", + "cameras": "Cameras", + "actions": "Actions", + "noRoles": "No custom roles found.", + "editCameras": "Edit Cameras", + "deleteRole": "Delete Role" + }, + "toast": { + "success": { + "createRole": "Role {{role}} created successfully", + "updateCameras": "Cameras updated for role {{role}}", + "deleteRole": "Role {{role}} deleted successfully", + "userRolesUpdated_one": "{{count}} user assigned to this role has been updated to 'viewer', which has access to all cameras.", + "userRolesUpdated_other": "{{count}} users assigned to this role have been updated to 'viewer', which has access to all cameras." + }, + "error": { + "createRoleFailed": "Failed to create role: {{errorMessage}}", + "updateCamerasFailed": "Failed to update cameras: {{errorMessage}}", + "deleteRoleFailed": "Failed to delete role: {{errorMessage}}", + "userUpdateFailed": "Failed to update user roles: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Create New Role", + "desc": "Add a new role and specify camera access permissions." + }, + "editCameras": { + "title": "Edit Role Cameras", + "desc": "Update camera access for the role {{role}}." + }, + "deleteRole": { + "title": "Delete Role", + "desc": "This action cannot be undone. This will permanently delete the role and assign any users with this role to the 'viewer' role, which will give viewer access to all cameras.", + "warn": "Are you sure you want to delete {{role}}?", + "deleting": "Deleting..." + }, + "form": { + "role": { + "title": "Role Name", + "placeholder": "Enter role name", + "desc": "Only letters, numbers, periods and underscores allowed.", + "roleIsRequired": "Role name is required", + "roleOnlyInclude": "Role name may only include letters, numbers, . or _", + "roleExists": "A role with this name already exists." + }, + "cameras": { + "title": "Cameras", + "desc": "Select cameras this role has access to. At least one camera is required.", + "required": "At least one camera must be selected." + } + } + } + }, + "notification": { + "title": "Notifications", + "notificationSettings": { + "title": "Notification Settings", + "desc": "Frigate can natively send push notifications to your device when it is running in the browser or installed as a PWA." + }, + "notificationUnavailable": { + "title": "Notifications Unavailable", + "desc": "Web push notifications require a secure context (https://…). This is a browser limitation. Access Frigate securely to use notifications." + }, + "globalSettings": { + "title": "Global Settings", + "desc": "Temporarily suspend notifications for specific cameras on all registered devices." + }, + "email": { + "title": "Email", + "placeholder": "e.g. example@email.com", + "desc": "A valid email is required and will be used to notify you if there are any issues with the push service." + }, + "cameras": { + "title": "Cameras", + "noCameras": "No cameras available", + "desc": "Select which cameras to enable notifications for." + }, + "deviceSpecific": "Device Specific Settings", + "registerDevice": "Register This Device", + "unregisterDevice": "Unregister This Device", + "sendTestNotification": "Send a test notification", + "unsavedRegistrations": "Unsaved Notification registrations", + "unsavedChanges": "Unsaved Notification changes", + "active": "Notifications Active", + "suspended": "Notifications suspended {{time}}", + "suspendTime": { + "suspend": "Suspend", + "5minutes": "Suspend for 5 minutes", + "10minutes": "Suspend for 10 minutes", + "30minutes": "Suspend for 30 minutes", + "1hour": "Suspend for 1 hour", + "12hours": "Suspend for 12 hours", + "24hours": "Suspend for 24 hours", + "untilRestart": "Suspend until restart" + }, + "cancelSuspension": "Cancel Suspension", + "toast": { + "success": { + "registered": "Successfully registered for notifications. Restarting Frigate is required before any notifications (including a test notification) can be sent.", + "settingSaved": "Notification settings have been saved." + }, + "error": { + "registerFailed": "Failed to save notification registration." + } + } + }, + "frigatePlus": { + "title": "Frigate+ Settings", + "apiKey": { + "title": "Frigate+ API Key", + "validated": "Frigate+ API key is detected and validated", + "notValidated": "Frigate+ API key is not detected or not validated", + "desc": "The Frigate+ API key enables integration with the Frigate+ service.", + "plusLink": "Read more about Frigate+" + }, + "snapshotConfig": { + "title": "Snapshot Configuration", + "desc": "Submitting to Frigate+ requires both snapshots and clean_copy snapshots to be enabled in your config.", + "cleanCopyWarning": "Some cameras have snapshots enabled but have the clean copy disabled. You need to enable clean_copy in your snapshot config to be able to submit images from these cameras to Frigate+.", + "table": { + "camera": "Camera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + } + }, + "modelInfo": { + "title": "Model Information", + "modelType": "Model Type", + "trainDate": "Train Date", + "baseModel": "Base Model", + "plusModelType": { + "baseModel": "Base Model", + "userModel": "Fine-Tuned" + }, + "supportedDetectors": "Supported Detectors", + "cameras": "Cameras", + "loading": "Loading model information…", + "error": "Failed to load model information", + "availableModels": "Available Models", + "loadingAvailableModels": "Loading available models…", + "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." + }, + "unsavedChanges": "Unsaved Frigate+ settings changes", + "restart_required": "Restart required (Frigate+ model changed)", + "toast": { + "success": "Frigate+ settings have been saved. Restart Frigate to apply changes.", + "error": "Failed to save config changes: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Triggers", + "semanticSearch": { + "title": "Semantic Search is disabled", + "desc": "Semantic Search must be enabled to use Triggers." + }, + "management": { + "title": "Triggers", + "desc": "Manage triggers for {{camera}}. Use the thumbnail type to trigger on similar thumbnails to your selected tracked object, and the description type to trigger on similar descriptions to text you specify." + }, + "addTrigger": "Add Trigger", + "table": { + "name": "Name", + "type": "Type", + "content": "Content", + "threshold": "Threshold", + "actions": "Actions", + "noTriggers": "No triggers configured for this camera.", + "edit": "Edit", + "deleteTrigger": "Delete Trigger", + "lastTriggered": "Last triggered" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Description" + }, + "actions": { + "notification": "Send Notification", + "sub_label": "Add Sub Label", + "attribute": "Add Attribute" + }, + "dialog": { + "createTrigger": { + "title": "Create Trigger", + "desc": "Create a trigger for camera {{camera}}" + }, + "editTrigger": { + "title": "Edit Trigger", + "desc": "Edit the settings for trigger on camera {{camera}}" + }, + "deleteTrigger": { + "title": "Delete Trigger", + "desc": "Are you sure you want to delete the trigger {{triggerName}}? This action cannot be undone." + }, + "form": { + "name": { + "title": "Name", + "placeholder": "Name this trigger", + "description": "Enter a unique name or description to identify this trigger", + "error": { + "minLength": "Field must be at least 2 characters long.", + "invalidCharacters": "Field can only contain letters, numbers, underscores, and hyphens.", + "alreadyExists": "A trigger with this name already exists for this camera." + } + }, + "enabled": { + "description": "Enable or disable this trigger" + }, + "type": { + "title": "Type", + "placeholder": "Select trigger type", + "description": "Trigger when a similar tracked object description is detected", + "thumbnail": "Trigger when a similar tracked object thumbnail is detected" + }, + "content": { + "title": "Content", + "imagePlaceholder": "Select a thumbnail", + "textPlaceholder": "Enter text content", + "imageDesc": "Only the most recent 100 thumbnails are displayed. If you can't find your desired thumbnail, please review earlier objects in Explore and set up a trigger from the menu there.", + "textDesc": "Enter text to trigger this action when a similar tracked object description is detected.", + "error": { + "required": "Content is required." + } + }, + "threshold": { + "title": "Threshold", + "desc": "Set the similarity threshold for this trigger. A higher threshold means a closer match is required to fire the trigger.", + "error": { + "min": "Threshold must be at least 0", + "max": "Threshold must be at most 1" + } + }, + "actions": { + "title": "Actions", + "desc": "By default, Frigate fires an MQTT message for all triggers. Sub labels add the trigger name to the object label. Attributes are searchable metadata stored separately in the tracked object metadata.", + "error": { + "min": "At least one action must be selected." + } + } + } + }, + "wizard": { + "title": "Create Trigger", + "step1": { + "description": "Configure the basic settings for your trigger." + }, + "step2": { + "description": "Set up the content that will trigger this action." + }, + "step3": { + "description": "Configure the threshold and actions for this trigger." + }, + "steps": { + "nameAndType": "Name and Type", + "configureData": "Configure Data", + "thresholdAndActions": "Threshold and Actions" + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} created successfully.", + "updateTrigger": "Trigger {{name}} updated successfully.", + "deleteTrigger": "Trigger {{name}} deleted successfully." + }, + "error": { + "createTriggerFailed": "Failed to create trigger: {{errorMessage}}", + "updateTriggerFailed": "Failed to update trigger: {{errorMessage}}", + "deleteTriggerFailed": "Failed to delete trigger: {{errorMessage}}" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/en/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/en/views/system.json new file mode 100644 index 0000000..73c6d65 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/en/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "cameras": "Cameras Stats - Frigate", + "storage": "Storage Stats - Frigate", + "general": "General Stats - Frigate", + "enrichments": "Enrichments Stats - Frigate", + "logs": { + "frigate": "Frigate Logs - Frigate", + "go2rtc": "Go2RTC Logs - Frigate", + "nginx": "Nginx Logs - Frigate" + } + }, + "title": "System", + "metrics": "System metrics", + "logs": { + "download": { + "label": "Download Logs" + }, + "copy": { + "label": "Copy to Clipboard", + "success": "Copied logs to clipboard", + "error": "Could not copy logs to clipboard" + }, + "type": { + "label": "Type", + "timestamp": "Timestamp", + "tag": "Tag", + "message": "Message" + }, + "tips": "Logs are streaming from the server", + "toast": { + "error": { + "fetchingLogsFailed": "Error fetching logs: {{errorMessage}}", + "whileStreamingLogs": "Error while streaming logs: {{errorMessage}}" + } + } + }, + "general": { + "title": "General", + "detector": { + "title": "Detectors", + "inferenceSpeed": "Detector Inference Speed", + "temperature": "Detector Temperature", + "cpuUsage": "Detector CPU Usage", + "cpuUsageInformation": "CPU used in preparing input and output data to/from detection models. This value does not measure inference usage, even if using a GPU or accelerator.", + "memoryUsage": "Detector Memory Usage" + }, + "hardwareInfo": { + "title": "Hardware Info", + "gpuUsage": "GPU Usage", + "gpuMemory": "GPU Memory", + "gpuEncoder": "GPU Encoder", + "gpuDecoder": "GPU Decoder", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Output", + "returnCode": "Return Code: {{code}}", + "processOutput": "Process Output:", + "processError": "Process Error:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Output", + "name": "Name: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Close GPU info" + }, + "copyInfo": { + "label": "Copy GPU info" + }, + "toast": { + "success": "Copied GPU info to clipboard" + } + }, + "npuUsage": "NPU Usage", + "npuMemory": "NPU Memory", + "intelGpuWarning": { + "title": "Intel GPU Stats Warning", + "message": "GPU stats unavailable", + "description": "This is a known bug in Intel's GPU stats reporting tools (intel_gpu_top) where it will break and repeatedly return a GPU usage of 0% even in cases where hardware acceleration and object detection are correctly running on the (i)GPU. This is not a Frigate bug. You can restart the host to temporarily fix the issue and confirm that the GPU is working correctly. This does not affect performance." + } + }, + "otherProcesses": { + "title": "Other Processes", + "processCpuUsage": "Process CPU Usage", + "processMemoryUsage": "Process Memory Usage" + } + }, + "storage": { + "title": "Storage", + "overview": "Overview", + "recordings": { + "title": "Recordings", + "tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.", + "earliestRecording": "Earliest recording available:" + }, + "shm": { + "title": "SHM (shared memory) allocation", + "warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB." + }, + "cameraStorage": { + "title": "Camera Storage", + "camera": "Camera", + "unusedStorageInformation": "Unused Storage Information", + "storageUsed": "Storage", + "percentageOfTotalUsed": "Percentage of Total", + "bandwidth": "Bandwidth", + "unused": { + "title": "Unused", + "tips": "This value may not accurately represent the free space available to Frigate if you have other files stored on your drive beyond Frigate's recordings. Frigate does not track storage usage outside of its recordings." + } + } + }, + "cameras": { + "title": "Cameras", + "overview": "Overview", + "info": { + "aspectRatio": "aspect ratio", + "cameraProbeInfo": "{{camera}} Camera Probe Info", + "streamDataFromFFPROBE": "Stream data is obtained with ffprobe.", + "fetching": "Fetching Camera Data", + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Codec:", + "resolution": "Resolution:", + "fps": "FPS:", + "unknown": "Unknown", + "audio": "Audio:", + "error": "Error: {{error}}", + "tips": { + "title": "Camera Probe Info" + } + }, + "framesAndDetections": "Frames / Detections", + "label": { + "camera": "camera", + "detect": "detect", + "skipped": "skipped", + "ffmpeg": "FFmpeg", + "capture": "capture", + "overallFramesPerSecond": "overall frames per second", + "overallDetectionsPerSecond": "overall detections per second", + "overallSkippedDetectionsPerSecond": "overall skipped detections per second", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} capture", + "cameraDetect": "{{camName}} detect", + "cameraFramesPerSecond": "{{camName}} frames per second", + "cameraDetectionsPerSecond": "{{camName}} detections per second", + "cameraSkippedDetectionsPerSecond": "{{camName}} skipped detections per second" + }, + "toast": { + "success": { + "copyToClipboard": "Copied probe data to clipboard." + }, + "error": { + "unableToProbeCamera": "Unable to probe camera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Last refreshed: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} has high FFmpeg CPU usage ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)", + "healthy": "System is healthy", + "reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)", + "cameraIsOffline": "{{camera}} is offline", + "detectIsSlow": "{{detect}} is slow ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)", + "shmTooLow": "/dev/shm allocation ({{total}} MB) should be increased to at least {{min}} MB." + }, + "enrichments": { + "title": "Enrichments", + "infPerSecond": "Inferences Per Second", + "averageInf": "Average Inference Time", + "embeddings": { + "image_embedding": "Image Embedding", + "text_embedding": "Text Embedding", + "face_recognition": "Face Recognition", + "plate_recognition": "Plate Recognition", + "image_embedding_speed": "Image Embedding Speed", + "face_embedding_speed": "Face Embedding Speed", + "face_recognition_speed": "Face Recognition Speed", + "plate_recognition_speed": "Plate Recognition Speed", + "text_embedding_speed": "Text Embedding Speed", + "yolov9_plate_detection_speed": "YOLOv9 Plate Detection Speed", + "yolov9_plate_detection": "YOLOv9 Plate Detection", + "review_description": "Review Description", + "review_description_speed": "Review Description Speed", + "review_description_events_per_second": "Review Description", + "object_description": "Object Description", + "object_description_speed": "Object Description Speed", + "object_description_events_per_second": "Object Description" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/audio.json b/sam2-cpu/frigate-dev/web/public/locales/es/audio.json new file mode 100644 index 0000000..16288b2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/audio.json @@ -0,0 +1,429 @@ +{ + "bark": "Ladrido", + "sheep": "Oveja", + "goat": "Cabra", + "car": "Coche", + "boat": "Barco", + "bus": "Autobus", + "motorcycle": "Motocicleta", + "train": "Tren", + "skateboard": "Monopatín", + "sink": "Fregadero", + "blender": "Batidora", + "hair_dryer": "Secador de pelo", + "scissors": "Tijeras", + "clock": "Reloj", + "camera": "Cámara", + "door": "Puerta", + "dog": "Perro", + "horse": "Caballo", + "toothbrush": "Cepillo de dientes", + "bird": "Pájaro", + "vehicle": "Vehículo", + "mouse": "Ratón", + "bicycle": "Bicicleta", + "cat": "Gato", + "keyboard": "Teclado", + "animal": "Animal", + "yell": "Grito", + "bellow": "Voz de trueno", + "whoop": "Aullido", + "crying": "Llanto", + "synthetic_singing": "Canto sintético", + "rapping": "Rap", + "humming": "Tarareo", + "groan": "Gemido", + "grunt": "Gruñido", + "whistling": "Silbido", + "breathing": "Respiración", + "wheeze": "Sibilancia", + "snoring": "Ronquido", + "gasp": "Jadeo", + "snort": "Resoplido", + "cough": "Tos", + "sneeze": "Estornudo", + "sniff": "Oler", + "run": "Correr", + "shuffle": "Arrastrar los pies", + "footsteps": "Pasos", + "chewing": "Masticar", + "biting": "Morder", + "gargling": "Hacer gárgaras", + "stomach_rumble": "Rugido de estómago", + "burping": "Eructo", + "finger_snapping": "Chasquido de dedos", + "clapping": "Aplausos", + "heartbeat": "Latido del corazón", + "heart_murmur": "Soplo cardíaco", + "cheering": "Aclamación", + "applause": "Aplausos", + "whispering": "Susurro", + "speech": "Habla", + "mantra": "Mantra", + "fart": "Pedos", + "snicker": "Risa maliciosa", + "yodeling": "Yodeling", + "laughter": "Risa", + "child_singing": "Canto infantil", + "pant": "Jadeo", + "throat_clearing": "Despejar la garganta", + "sigh": "Suspiro", + "choir": "Coro", + "babbling": "Balbuceo", + "singing": "Canto", + "hands": "Manos", + "hiccup": "Hipido", + "chant": "Cántico", + "chatter": "Charla", + "crowd": "Multitud", + "children_playing": "Niños jugando", + "pets": "Mascotas", + "yip": "Ladrido corto", + "howl": "Aullido", + "bow_wow": "Guau", + "growling": "Gruñido", + "whimper_dog": "Gemido de perro", + "purr": "Ronroneo", + "hiss": "Siseo", + "caterwaul": "Aullido de gato", + "livestock": "Ganado", + "clip_clop": "Trote de caballo", + "neigh": "Relincho", + "cattle": "Ganado", + "moo": "Muu", + "cowbell": "Campanilla de vaca", + "pig": "Cerdo", + "oink": "Oink", + "bleat": "Balido", + "fowl": "Aves", + "cluck": "Cacareo", + "cock_a_doodle_doo": "Quiquiriquí", + "turkey": "Pavo", + "gobble": "Gluglú", + "duck": "Pato", + "goose": "Ganso", + "honk": "Bocina", + "wild_animals": "Animales salvajes", + "roar": "Rugido", + "chirp": "Canto (de insecto o pájaro)", + "pigeon": "Paloma", + "coo": "Arrullo", + "caw": "Grito de cuervo", + "owl": "Búho", + "dogs": "Perros", + "insect": "Insecto", + "cricket": "Grillo", + "mosquito": "Mosquito", + "buzz": "Zumbido", + "frog": "Rana", + "croak": "Croar", + "snake": "Serpiente", + "rattle": "Sonajero", + "whale_vocalization": "Vocalización de ballena", + "plucked_string_instrument": "Instrumento de cuerda punteada", + "guitar": "Guitarra", + "steel_guitar": "Guitarra de acero", + "tapping": "Tapping (técnica de guitarra)", + "strum": "Rasgueo", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cítara", + "ukulele": "Ukulele", + "piano": "Piano", + "organ": "Órgano", + "electronic_organ": "Órgano electrónico", + "hammond_organ": "Órgano Hammond", + "sampler": "Sampler", + "harpsichord": "Clavicémbalo", + "percussion": "Percusión", + "drum_kit": "Batería", + "drum_machine": "Caja de ritmos", + "drum": "Tambor", + "snare_drum": "Caja (o redoblante)", + "rimshot": "Golpe en el borde del tambor", + "tabla": "Tabla", + "cymbal": "Platillo", + "hi_hat": "Hi-Hat", + "wood_block": "Bloque de madera", + "tambourine": "Pandereta", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Campanas tubulares", + "mallet_percussion": "Percusión con mazas", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "steelpan": "Steelpan", + "orchestra": "Orquesta", + "trumpet": "Trompeta", + "string_section": "Sección de cuerdas", + "violin": "Violín", + "double_bass": "Contrabajo", + "wind_instrument": "Instrumento de viento", + "flute": "Flauta", + "saxophone": "Saxofón", + "harp": "Arpa", + "jingle_bell": "Campanilla", + "bicycle_bell": "Campana de bicicleta", + "tuning_fork": "Diapasón", + "chime": "Campanilla", + "wind_chime": "Campanario de viento", + "harmonica": "Armónica", + "accordion": "Acordeón", + "didgeridoo": "Didgeridoo", + "theremin": "Theremín", + "singing_bowl": "Cuenco tibetano", + "scratching": "Rasguñado", + "hip_hop_music": "Música hip-hop", + "rock_music": "Música rock", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "progressive_rock": "Progressive rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Rock psicodélico", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Música soul", + "country": "Country", + "swing_music": "Música swing", + "disco": "Disco", + "house_music": "Música House", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronica", + "electronic_dance_music": "Música Dance Electronica", + "music_of_latin_america": "Música de América Latina", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para niños", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A capella", + "afrobeat": "Afrobeat", + "music_of_asia": "Música de Asia", + "carnatic_music": "Música Carnatic", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "song": "Canción", + "background_music": "Música Background", + "soundtrack_music": "Música de Pelicula", + "lullaby": "Lullaby", + "video_game_music": "Música de Videojuego", + "christmas_music": "Música Navideña", + "sad_music": "Música triste", + "tender_music": "Música suave", + "exciting_music": "Música emocionante", + "angry_music": "Música enojada", + "scary_music": "Música aterradora", + "wind": "Viento", + "rustling_leaves": "Hojas susurrantes", + "wind_noise": "Ruido del viento", + "thunderstorm": "Tormenta eléctrica", + "thunder": "Trueno", + "raindrop": "Gota de lluvia", + "stream": "Arroyo", + "waterfall": "Cascada", + "ocean": "Oceano", + "steam": "Vapor", + "gurgling": "Gorgoteo", + "sailboat": "Vela", + "rowboat": "Bote de remos", + "motorboat": "Lancha motora", + "motor_vehicle": "Vehículo a motor", + "toot": "Pitido", + "tire_squeal": "Chillido de neumáticos", + "car_passing_by": "Coche pasando", + "ambulance": "Ambulancia", + "fire_engine": "Camión de bomberos", + "traffic_noise": "Ruido de tráfico", + "rail_transport": "Transporte ferroviario", + "aircraft_engine": "Aeronave motor", + "engine": "Motor", + "chainsaw": "Motosierra", + "medium_engine": "Motor de tamaño medio", + "heavy_engine": "Motor pesado", + "engine_knocking": "Golpeteo del motor", + "engine_starting": "Arranque del motor", + "idling": "Ralentí", + "accelerating": "Acelerando", + "doorbell": "Timbre", + "ding-dong": "Ding Dong", + "sliding_door": "Puerta corredera", + "slam": "Portazo", + "knock": "Golpe", + "tap": "Golpe suave", + "cupboard_open_or_close": "Apertura o cierre del armario", + "drawer_open_or_close": "Apertura o cierre del cajón", + "dishes": "Platos", + "cutlery": "Cubertería", + "chopping": "Cortando", + "frying": "Freír", + "microwave_oven": "Horno Microondas", + "water_tap": "Grifo de Agua", + "toilet_flush": "Descarga del Inodoro", + "electric_toothbrush": "Cepillo de Dientes Eléctrico", + "vacuum_cleaner": "Aspiradora", + "zipper": "Cremallera", + "keys_jangling": "Llaves Tintineando", + "coin": "Moneda", + "electric_shaver": "Afeitadora Eléctrica", + "shuffling_cards": "Barajar Cartas", + "typing": "Teclear", + "typewriter": "Máquina de Escribir", + "computer_keyboard": "Teclado de Computadora", + "writing": "Escribir", + "alarm": "Alarma", + "telephone": "Teléfono", + "telephone_bell_ringing": "Timbre de Teléfono Sonando", + "ringtone": "Tono de Llamada", + "telephone_dialing": "Marcación de Teléfono", + "dial_tone": "Tono de Marcación", + "busy_signal": "Señal de Ocupado", + "alarm_clock": "Reloj Despertador", + "siren": "Sirena", + "civil_defense_siren": "Sirena de Defensa Civil", + "buzzer": "Zumbador", + "fire_alarm": "Alarma de Incendio", + "foghorn": "Bocina de Niebla", + "whistle": "Silbato", + "steam_whistle": "Silbato de Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Trinquete", + "tick": "Tictac", + "tick-tock": "Tictoc", + "gears": "Engranajes", + "pulleys": "Poleas", + "sewing_machine": "Máquina de Coser", + "mechanical_fan": "Ventilador Mecánico", + "air_conditioning": "Aire Acondicionado", + "cash_register": "Caja Registradora", + "printer": "Impresora", + "fly": "Mosca", + "patter": "Golpeteo", + "bell": "Campana", + "meow": "Miau", + "squawk": "Chillido", + "classical_music": "Música Clásica", + "cello": "Violoncello", + "quack": "Cuac", + "hoot": "Ulular", + "synthesizer": "Sintetizador", + "happy_music": "Música Alegre", + "timpani": "Tímpano", + "bowed_string_instrument": "Instrumento de cuerda frotada", + "jazz": "Jazz", + "train_whistle": "Silbido de tren", + "car_alarm": "Alarma de coche", + "truck": "Camion", + "ice_cream_truck": "Camión de helados", + "railroad_car": "Vagón de tren", + "aircraft": "Aeronave", + "helicopter": "Helicóptero", + "light_engine": "Motor ligero", + "dental_drill's_drill": "Talonador dental", + "crow": "Cuervo", + "flapping_wings": "Aleteo de alas", + "opera": "Opera", + "funk": "Funk", + "roaring_cats": "Gatos rugiendo", + "chicken": "Pollo", + "bagpipes": "Gaita", + "rats": "Ratas", + "music": "Música", + "musical_instrument": "Música instrumental", + "electric_guitar": "Guitarra eléctrica", + "bass_drum": "Bombo", + "acoustic_guitar": "Guitarra acústica", + "pizzicato": "Pizzicato", + "beatboxing": "Beatboxing", + "bass_guitar": "Bajo eléctrico", + "bluegrass": "Bluegrass", + "folk_music": "Música Folk", + "electronic_music": "Música electrónica", + "techno": "Techno", + "french_horn": "Trompa francesa", + "ship": "Barco", + "lawn_mower": "Cortacésped", + "electric_piano": "Piano eléctrico", + "train_wheels_squealing": "Chillido de ruedas de tren", + "drum_roll": "Redoble de tambor", + "vibraphone": "Vibrafón", + "trombone": "Trombón", + "brass_instrument": "Instrumento de metal", + "church_bell": "Campana de iglesia", + "clarinet": "Clarinete", + "grunge": "Grunge", + "pop_music": "Música pop", + "jingle": "Single", + "rain_on_surface": "Lluvia sobre superficie", + "emergency_vehicle": "Vehículo de emergencias", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "music_of_africa": "Música de Africa", + "christian_music": "Música Cristiana", + "gospel_music": "Música Gospel", + "traditional_music": "Música Tradicional", + "wedding_music": "Música de Boda", + "rain": "Lluvia", + "waves": "Ondas", + "fire": "Fuego", + "police_car": "Coche de policia", + "squeak": "Chirrido", + "crackle": "Crepitar", + "reggae": "Reggae", + "middle_eastern_music": "Música del Medio Oriente", + "smoke_detector": "Detector de Humo", + "race_car": "Coche de carreras", + "air_horn": "Bocina de aire", + "independent_music": "Música Independiente", + "theme_music": "Música de Película", + "dance_music": "Música Dance", + "fixed-wing_aircraft": "Aeronave de ala fija", + "water": "Agua", + "propeller": "Hélice", + "air_brake": "Freno de aire", + "jet_engine": "Motor a reacción", + "power_windows": "Ventanas eléctricas", + "skidding": "Deslizamiento", + "reversing_beeps": "Bips de marcha atras", + "bathtub": "Bañera", + "train_horn": "Bocina de tren", + "subway": "Metro", + "single-lens_reflex_camera": "Cámara Réflex de un Solo Objetivo", + "tools": "Herramientas", + "hammer": "Martillo", + "filing": "Limar", + "jackhammer": "Martillo Neumático", + "sawing": "Serrar", + "sanding": "Lijar", + "power_tool": "Herramienta Eléctrica", + "burst": "Estallido", + "eruption": "Erupción", + "boom": "Estallido (Boom)", + "firecracker": "Petardo", + "artillery_fire": "Fuego de Artillería", + "cap_gun": "Pistola de Fulminantes", + "fireworks": "Fuegos Artificiales", + "wood": "Madera", + "chop": "Cortar (Madera)", + "splinter": "Astilla", + "crack": "Crujido", + "chink": "Tintineo (de Vidrio)", + "glass": "Vidrio", + "environmental_noise": "Ruido Ambiental", + "sound_effect": "Efecto de sonido", + "shatter": "Romperse", + "static": "Estatico", + "silence": "Silencio", + "scream": "Grito", + "white_noise": "Ruido Blanco", + "drill": "Taladro", + "field_recording": "Grabación de Campo", + "explosion": "Explosión", + "machine_gun": "Ametralladora", + "television": "Televisión", + "radio": "Radio", + "gunshot": "Disparo", + "fusillade": "Descarga de Fusilería", + "pink_noise": "Ruido Rosa" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/common.json b/sam2-cpu/frigate-dev/web/public/locales/es/common.json new file mode 100644 index 0000000..9f35ee9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/common.json @@ -0,0 +1,287 @@ +{ + "time": { + "yesterday": "Ayer", + "thisMonth": "Este mes", + "yr": "{{time}}año", + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampExcludeSeconds": { + "24hour": "%b %-d, %H:%M", + "12hour": "%b %-d, %I:%M %p" + }, + "formattedTimestamp": { + "24hour": "MMM d, HH:mm:ss", + "12hour": "MMM d, h:mm:ss aaa" + }, + "day_one": "{{time}} día", + "day_many": "{{time}} días", + "day_other": "{{time}} días", + "untilForTime": "Hasta {{time}}", + "untilForRestart": "Hasta que Frigate se reinicie.", + "untilRestart": "Hasta que se reinicie", + "ago": "Hace {{timeAgo}}", + "justNow": "Ahora mismo", + "today": "Hoy", + "last7": "Últimos 7 días", + "last14": "Últimos 14 días", + "last30": "Últimos 30 días", + "thisWeek": "Esta semana", + "lastWeek": "Semana pasada", + "lastMonth": "Mes pasado", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "1hour": "1 hora", + "12hours": "12 horas", + "24hours": "24 horas", + "pm": "pm", + "year_one": "{{time}} año", + "year_many": "{{time}} años", + "year_other": "{{time}} años", + "mo": "{{time}}mes", + "month_one": "{{time}} mes", + "month_many": "{{time}} meses", + "month_other": "{{time}} meses", + "h": "{{time}}h", + "m": "{{time}}m", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "s": "{{time}}s", + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "5minutes": "5 minutos", + "am": "am", + "d": "{{time}}d", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + } + }, + "menu": { + "settings": "Ajustes", + "export": "Exportar", + "user": { + "title": "Usuario", + "account": "Cuenta", + "anonymous": "Anónimo", + "logout": "Cerrar sesión", + "setPassword": "Establecer contraseña", + "current": "Usuario actual: {{user}}" + }, + "systemMetrics": "Métricas del sistema", + "help": "Ayuda", + "system": "Sistema", + "configuration": "Configuración", + "systemLogs": "Registros del sistema", + "configurationEditor": "Editor de configuración", + "languages": "Idiomas", + "language": { + "en": "English (Inglés)", + "zhCN": "简体中文 (Chino simplificado)", + "withSystem": { + "label": "Usar los ajustes del sistema para el idioma" + }, + "ru": "Русский (Ruso)", + "de": "Deutsch (Alemán)", + "ja": "日本語 (Japonés)", + "tr": "Türkçe (Turco)", + "sv": "Svenska (Sueco)", + "nb": "Norsk Bokmål (Noruego Bokmål)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "pl": "Polski (Polaco)", + "uk": "Українська (Ucraniano)", + "he": "עברית (Hebreo)", + "el": "Ελληνικά (Griego)", + "ro": "Română (Rumano)", + "hu": "Magyar (Húngaro)", + "fi": "Suomi (Finlandés)", + "it": "Italian (Italiano)", + "da": "Dansk (Danés)", + "sk": "Slovenčina (Eslovaco)", + "hi": "हिन्दी (Hindi)", + "es": "Español", + "ar": "العربية (Árabe)", + "pt": "Português (Portugues)", + "cs": "Čeština (Checo)", + "nl": "Nederlands (Neerlandés)", + "fr": "Français (Frances)", + "yue": "粵語 (Cantonés)", + "th": "ไทย (Tailandés)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portugués brasileño)", + "sr": "Српски (Serbio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Gallego)", + "id": "Bahasa Indonesia (Indonesio)", + "ur": "اردو (Urdu)" + }, + "appearance": "Apariencia", + "darkMode": { + "label": "Modo oscuro", + "light": "Claro", + "dark": "Oscuro", + "withSystem": { + "label": "Usar los ajustes del sistema para el modo claro u oscuro" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "nord": "Nord", + "red": "Rojo", + "contrast": "Alto contraste", + "default": "Predeterminado", + "highcontrast": "Alto Contraste" + }, + "documentation": { + "title": "Documentación", + "label": "Documentación de Frigate" + }, + "restart": "Reiniciar Frigate", + "live": { + "title": "Directo", + "cameras": { + "title": "Cámaras", + "count_one": "{{count}} Cámara", + "count_many": "{{count}} Cámaras", + "count_other": "{{count}} Cámaras" + }, + "allCameras": "Todas las cámaras" + }, + "review": "Revisar", + "explore": "Explorar", + "uiPlayground": "Zona de pruebas de la interfaz de usuario", + "faceLibrary": "Biblioteca de rostros" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "kph" + }, + "length": { + "meters": "Metros", + "feet": "Pies" + } + }, + "button": { + "off": "APAGADO", + "copyCoordinates": "Copiar coordenadas", + "fullscreen": "Pantalla completa", + "apply": "Aplicar", + "reset": "Restablecer", + "done": "Hecho", + "enable": "Habilitar", + "disabled": "Deshabilitado", + "disable": "Deshabilitar", + "save": "Guardar", + "cancel": "Cancelar", + "close": "Cerrar", + "copy": "Copiar", + "back": "Atrás", + "history": "Historial", + "pictureInPicture": "Imagen en imagen", + "twoWayTalk": "Conversación bidireccional", + "cameraAudio": "Audio de la cámara", + "delete": "Eliminar", + "yes": "Sí", + "no": "No", + "download": "Descargar", + "info": "Información", + "suspended": "Suspendido", + "unsuspended": "Reactivar", + "play": "Reproducir", + "unselect": "Deseleccionar", + "export": "Exportar", + "deleteNow": "Eliminar ahora", + "next": "Siguiente", + "edit": "Editar", + "enabled": "Habilitado", + "saving": "Guardando…", + "exitFullscreen": "Salir de pantalla completa", + "on": "ENCENDIDO" + }, + "toast": { + "save": { + "error": { + "noMessage": "No se pudieron guardar los cambios de configuración", + "title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}" + }, + "title": "Guardar" + }, + "copyUrlToClipboard": "URL copiada al portapapeles." + }, + "label": { + "back": "Volver atrás" + }, + "role": { + "title": "Rol", + "admin": "Administrador", + "viewer": "Espectador", + "desc": "Los administradores tienen acceso completo a todas las funciones en la interfaz de usuario de Frigate. Los espectadores están limitados a ver cámaras, elementos de revisión y grabaciones históricas en la interfaz de usuario." + }, + "pagination": { + "label": "paginación", + "previous": { + "title": "Anterior", + "label": "Ir a la página anterior" + }, + "next": { + "title": "Siguiente", + "label": "Ir a la página siguiente" + }, + "more": "Más páginas" + }, + "accessDenied": { + "documentTitle": "Acceso denegado - Frigate", + "desc": "No tienes permiso para ver esta página.", + "title": "Acceso denegado" + }, + "notFound": { + "documentTitle": "No se ha encontrado - Frigate", + "title": "404", + "desc": "Página no encontrada" + }, + "selectItem": "Seleccionar {{item}}", + "readTheDocumentation": "Leer la documentación", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/auth.json new file mode 100644 index 0000000..62d6c84 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nombre de usuario", + "errors": { + "passwordRequired": "Se requiere contraseña", + "rateLimit": "Se ha superado el límite de solicitudes. Intenta de nuevo más tarde.", + "webUnknownError": "Error desconocido. Revisa los registros de la consola.", + "usernameRequired": "Se requiere nombre de usuario", + "unknownError": "Error desconocido. Revisa los registros.", + "loginFailed": "Error de inicio de sesión" + }, + "password": "Contraseña", + "login": "Iniciar sesión", + "firstTimeLogin": "¿Estás tratando de iniciar sesión por primera vez? Las credenciales están impresas en los registros de Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/camera.json new file mode 100644 index 0000000..6960587 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupos de cámaras", + "add": "Agregar grupo de cámaras", + "edit": "Editar grupo de cámaras", + "delete": { + "label": "Eliminar grupo de cámaras", + "confirm": { + "title": "Confirmar eliminación", + "desc": "¿Estás seguro de que quieres eliminar el grupo de cámaras {{name}}?" + } + }, + "name": { + "label": "Nombre", + "placeholder": "Introduce un nombre…", + "errorMessage": { + "mustLeastCharacters": "El nombre del grupo de cámaras debe tener al menos 2 caracteres.", + "nameMustNotPeriod": "El nombre del grupo de cámaras no debe contener un punto.", + "invalid": "Nombre de grupo de cámaras no válido.", + "exists": "El nombre del grupo de cámaras ya existe." + } + }, + "cameras": { + "desc": "Selecciona cámaras para este grupo.", + "label": "Cámaras" + }, + "icon": "Icono", + "success": "El grupo de cámaras ({{name}}) ha sido guardado.", + "camera": { + "setting": { + "title": "Ajustes de transmisión de {{cameraName}}", + "audioIsAvailable": "El audio está disponible para esta transmisión", + "audioIsUnavailable": "El audio no está disponible para esta transmisión", + "audio": { + "tips": { + "title": "El audio debe provenir de tu cámara y estar configurado en go2rtc para esta transmisión.", + "document": "Leer la documentación " + } + }, + "streamMethod": { + "method": { + "noStreaming": { + "desc": "Las imágenes de la cámara solo se actualizarán una vez por minuto y no habrá transmisión en vivo.", + "label": "Sin transmisión" + }, + "smartStreaming": { + "label": "Transmisión inteligente (recomendada)", + "desc": "La transmisión inteligente actualizará la imagen de tu cámara una vez por minuto cuando no se detecte actividad para conservar ancho de banda y recursos. Cuando se detecte actividad, la imagen cambiará sin problemas a una transmisión en vivo." + }, + "continuousStreaming": { + "label": "Transmisión continua", + "desc": { + "title": "La imagen de la cámara siempre será una transmisión en vivo cuando esté visible en el panel de control, incluso si no se detecta ninguna actividad.", + "warning": "La transmisión continua puede causar un alto uso de ancho de banda y problemas de rendimiento. Usa con precaución." + } + } + }, + "label": "Método de transmisión", + "placeholder": "Elige un método de transmisión" + }, + "compatibilityMode": { + "label": "Modo de compatibilidad", + "desc": "Habilita esta opción solo si la transmisión en vivo de tu cámara muestra artefactos de color y tiene una línea diagonal en el lado derecho de la imagen." + }, + "label": "Ajustes de transmisión de la cámara", + "desc": "Cambia las opciones de transmisión en vivo para el panel de control de este grupo de cámaras. Estos ajustes son específicos del dispositivo/navegador.", + "placeholder": "Elige una transmisión", + "stream": "Transmitir" + }, + "birdseye": "Vista Aérea" + } + }, + "debug": { + "options": { + "label": "Ajustes", + "title": "Opciones", + "showOptions": "Mostrar opciones", + "hideOptions": "Ocultar opciones" + }, + "timestamp": "Marca de tiempo", + "zones": "Zonas", + "motion": "Movimiento", + "regions": "Regiones", + "boundingBox": "Caja delimitadora", + "mask": "Máscara" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/dialog.json new file mode 100644 index 0000000..e200c38 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/dialog.json @@ -0,0 +1,134 @@ +{ + "restart": { + "restarting": { + "title": "Frigate se está reiniciando", + "button": "Forzar recarga ahora", + "content": "Esta página se recargará en {{countdown}} segundos." + }, + "title": "¿Estás seguro de que quieres reiniciar Frigate?", + "button": "Reiniciar" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar a Frigate+", + "desc": "Los objetos en ubicaciones que deseas evitar no son falsos positivos. Enviarlos como falsos positivos confundirá al modelo." + }, + "review": { + "false": { + "label": "No confirmar esta etiqueta para Frigate Plus", + "false_one": "Esto no es un {{label}}", + "false_many": "Esto no es un {{label}}", + "false_other": "Esto no es un {{label}}" + }, + "true": { + "true_one": "Esto es un {{label}}", + "true_many": "Esto es un {{label}}", + "true_other": "Esto es un {{label}}", + "label": "Confirmar esta etiqueta para Frigate+" + }, + "state": { + "submitted": "Enviado" + }, + "question": { + "label": "Confirmar esta etiqueta para Frigate Plus", + "ask_a": "¿Es este objeto un {{label}}?", + "ask_an": "¿Es este objeto un {{label}}?", + "ask_full": "¿Es este objeto un {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Ver en el historial" + } + }, + "export": { + "time": { + "fromTimeline": "Seleccionar desde la línea de tiempo", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas", + "custom": "Personalizado", + "start": { + "title": "Hora de inicio", + "label": "Seleccionar hora de inicio" + }, + "end": { + "title": "Hora de finalización", + "label": "Seleccionar hora de finalización" + } + }, + "name": { + "placeholder": "Nombrar la exportación" + }, + "select": "Seleccionar", + "export": "Exportar", + "toast": { + "error": { + "failed": "No se pudo iniciar la exportación: {{error}}", + "noVaildTimeSelected": "No se seleccionó un rango de tiempo válido.", + "endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio." + }, + "success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones." + }, + "fromTimeline": { + "saveExport": "Guardar exportación", + "previewExport": "Vista previa de la exportación" + }, + "selectOrExport": "Seleccionar o exportar" + }, + "streaming": { + "restreaming": { + "disabled": "La retransmisión no está habilitada para esta cámara.", + "desc": { + "title": "Configura go2rtc para opciones adicionales de vista en vivo y audio para esta cámara.", + "readTheDocumentation": "Leer la documentación" + } + }, + "debugView": "Vista de depuración", + "label": "Transmisión", + "showStats": { + "label": "Mostrar estadísticas de transmisión", + "desc": "Habilita esta opción para mostrar las estadísticas de transmisión como una superposición en la imagen de la cámara." + } + }, + "search": { + "saveSearch": { + "label": "Guardar búsqueda", + "desc": "Proporciona un nombre para esta búsqueda guardada.", + "overwrite": "{{searchName}} ya existe. Guardar sobrescribirá el valor existente.", + "success": "La búsqueda ({{searchName}}) ha sido guardada.", + "button": { + "save": { + "label": "Guardar esta búsqueda" + } + }, + "placeholder": "Introduce un nombre para tu búsqueda" + } + }, + "recording": { + "confirmDelete": { + "title": "Confirmar eliminación", + "desc": { + "selected": "¿Estás seguro de que quieres eliminar todo el video grabado asociado con este elemento de revisión?

    Mantén presionada la tecla Shift para omitir este diálogo en el futuro." + }, + "toast": { + "success": "El metraje de video asociado con los elementos de revisión seleccionados se ha eliminado con éxito.", + "error": "No se pudo eliminar: {{error}}" + } + }, + "button": { + "export": "Exportar", + "markAsReviewed": "Marcar como revisado", + "deleteNow": "Eliminar ahora", + "markAsUnreviewed": "Marcar como no revisado" + } + }, + "imagePicker": { + "selectImage": "Seleccione la miniatura de un objeto rastreado", + "search": { + "placeholder": "Búsqueda por etiqueta o sub-etiqueta..." + }, + "noImages": "No se encontraron miniaturas para esta cámara" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/filter.json new file mode 100644 index 0000000..3625030 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/filter.json @@ -0,0 +1,137 @@ +{ + "filter": "Filtro", + "labels": { + "all": { + "short": "Etiquetas", + "title": "Todas las etiquetas" + }, + "count": "{{count}} Etiquetas", + "label": "Etiquetas", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetas" + }, + "zones": { + "all": { + "title": "Todas las zonas", + "short": "Zonas" + }, + "label": "Zonas" + }, + "dates": { + "all": { + "title": "Todas las fechas", + "short": "Fechas" + }, + "selectPreset": "Selecciona un Preajuste…" + }, + "timeRange": "Rango de tiempo", + "subLabels": { + "all": "Todas las subetiquetas", + "label": "Subetiquetas" + }, + "score": "Puntuación", + "estimatedSpeed": "Velocidad estimada ({{unit}})", + "features": { + "label": "Características", + "submittedToFrigatePlus": { + "label": "Enviado a Frigate+", + "tips": "Primero debes filtrar por objetos rastreados que tengan una captura de pantalla.

    Los objetos rastreados sin una captura de pantalla no pueden enviarse a Frigate+." + }, + "hasSnapshot": "Tiene una captura instantánea", + "hasVideoClip": "Tiene un clip de vídeo" + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Fecha (Ascendente)", + "dateDesc": "Fecha (Descendente)", + "scoreAsc": "Puntuación del objeto (Ascendente)", + "scoreDesc": "Puntuación del objeto (Descendente)", + "speedAsc": "Velocidad estimada (Ascendente)", + "speedDesc": "Velocidad estimada (Descendente)", + "relevance": "Relevancia" + }, + "cameras": { + "label": "Filtro de cámaras", + "all": { + "title": "Todas las cámaras", + "short": "Cámaras" + } + }, + "review": { + "showReviewed": "Mostrar revisados" + }, + "motion": { + "showMotionOnly": "Mostrar solo movimiento" + }, + "explore": { + "settings": { + "title": "Configuración", + "defaultView": { + "title": "Vista predeterminada", + "summary": "Resumen", + "desc": "Cuando no se seleccionen filtros, muestra un resumen de los objetos rastreados más recientes por etiqueta, o muestra una cuadrícula sin filtrar.", + "unfilteredGrid": "Cuadrícula sin filtrar" + }, + "searchSource": { + "label": "Fuente de búsqueda", + "options": { + "thumbnailImage": "Miniatura Imagen", + "description": "Descripción" + }, + "desc": "Elige si deseas buscar en las miniaturas o en las descripciones de tus objetos rastreados." + }, + "gridColumns": { + "title": "Columnas de la cuadrícula", + "desc": "Selecciona el número de columnas en la vista de cuadrícula." + } + }, + "date": { + "selectDateBy": { + "label": "Selecciona una fecha para filtrar" + } + } + }, + "reset": { + "label": "Restablecer filtros a los valores predeterminados" + }, + "more": "Más filtros", + "logSettings": { + "label": "Filtrar nivel de registro", + "loading": { + "title": "Cargando", + "desc": "Cuando el panel de registros está desplazado hasta el final, los nuevos registros se transmiten automáticamente a medida que se añaden." + }, + "disableLogStreaming": "Deshabilitar transmisión de registros", + "filterBySeverity": "Filtrar registros por gravedad", + "allLogs": "Todos los registros" + }, + "trackedObjectDelete": { + "title": "Confirmar eliminación", + "desc": "Eliminar estos {{objectLength}} objetos rastreados elimina la captura de pantalla, cualquier incrustación guardada y cualquier entrada asociada al ciclo de vida del objeto. Las grabaciones de estos objetos rastreados en la vista de Historial NO se eliminarán.

    ¿Estás seguro de que quieres proceder?

    Mantén presionada la tecla Shift para omitir este diálogo en el futuro.", + "toast": { + "success": "Objetos rastreados eliminados con éxito.", + "error": "No se pudo eliminar los objetos rastreados: {{errorMessage}}" + } + }, + "recognizedLicensePlates": { + "title": "Matrículas reconocidas", + "loadFailed": "No se pudieron cargar las matrículas reconocidas.", + "loading": "Cargando matrículas reconocidas…", + "placeholder": "Escribe para buscar matrículas…", + "noLicensePlatesFound": "No se encontraron matrículas.", + "selectPlatesFromList": "Selecciona una o más matrículas de la lista.", + "selectAll": "Seleccionar todas", + "clearAll": "Limpiar todas" + }, + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "classes": { + "label": "Clases", + "all": { + "title": "Todas las Clases" + }, + "count_one": "{{count}} Clase", + "count_other": "{{count}} Clases" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/icons.json new file mode 100644 index 0000000..d4112ae --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona un icono", + "search": { + "placeholder": "Buscar un icono…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/input.json new file mode 100644 index 0000000..986f0c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descargar vídeo", + "toast": { + "success": "El vídeo de tu elemento de revisión ha comenzado a descargarse." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/es/components/player.json new file mode 100644 index 0000000..2a3e4de --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "No se encontró vista previa", + "noPreviewFoundFor": "No se encontró vista previa para {{cameraName}}", + "submitFrigatePlus": { + "submit": "Enviar", + "title": "¿Enviar este fotograma a Frigate+?" + }, + "streamOffline": { + "desc": "No se han recibido fotogramas en la transmisión detect de {{cameraName}}, revisa los registros de errores", + "title": "Transmisión desconectada" + }, + "cameraDisabled": "La cámara está deshabilitada", + "stats": { + "streamType": { + "title": "Tipo de transmisión:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Ancho de banda:", + "short": "Ancho de banda" + }, + "latency": { + "title": "Latencia:", + "short": { + "title": "Latencia", + "value": "{{seconds}} seg" + }, + "value": "{{seconds}} segundos" + }, + "totalFrames": "Fotogramas totales:", + "droppedFrames": { + "title": "Fotogramas perdidos:", + "short": { + "title": "Perdidos", + "value": "{{droppedFrames}} fotogramas" + } + }, + "decodedFrames": "Fotogramas decodificados:", + "droppedFrameRate": "Tasa de fotogramas perdidos:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Fotograma enviado correctamente a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Error al enviar el fotograma a Frigate+" + } + }, + "livePlayerRequiredIOSVersion": "Se requiere iOS 17.1 o superior para este tipo de transmisión en vivo.", + "noRecordingsFoundForThisTime": "No se encontraron grabaciones para este momento" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/objects.json b/sam2-cpu/frigate-dev/web/public/locales/es/objects.json new file mode 100644 index 0000000..0e97210 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Persona", + "bicycle": "Bicicleta", + "car": "Coche", + "motorcycle": "Motocicleta", + "airplane": "Avión", + "bus": "Autobus", + "train": "Tren", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Boca de incendios", + "street_sign": "Señal de tráfico", + "stop_sign": "Señal de stop", + "parking_meter": "Parquímetro", + "bench": "Banco", + "dog": "Perro", + "cow": "Vaca", + "elephant": "Elefante", + "bear": "Oso", + "zebra": "Cebra", + "giraffe": "Jirafa", + "hat": "Sombrero", + "backpack": "Mochila", + "shoe": "Zapato", + "eye_glasses": "Gafas", + "handbag": "Bolso de mano", + "tie": "Corbata", + "suitcase": "Maleta", + "frisbee": "Disco Volador", + "skis": "Esquís", + "sports_ball": "Pelota deportiva", + "kite": "Cometa", + "baseball_glove": "Guante de béisbol", + "skateboard": "Monopatín", + "surfboard": "Tabla de surf", + "tennis_racket": "Raqueta de tenis", + "bottle": "Botella", + "plate": "Plato", + "wine_glass": "Copa de vino", + "cup": "Taza", + "fork": "Tenedor", + "spoon": "Cuchara", + "bowl": "Cuenco", + "apple": "Manzana", + "orange": "Naranja", + "broccoli": "Brócoli", + "carrot": "Zanahoria", + "hot_dog": "Perrito caliente", + "pizza": "Pizza", + "donut": "Donut", + "chair": "Silla", + "couch": "Sofá", + "potted_plant": "Planta en maceta", + "bed": "Cama", + "mirror": "Espejo", + "dining_table": "Mesa de comedor", + "window": "Ventana", + "desk": "Escritorio", + "toilet": "Inodoro", + "door": "Puerta", + "laptop": "Portátil", + "mouse": "Ratón", + "remote": "Mando a distancia", + "keyboard": "Teclado", + "cell_phone": "Teléfono móvil", + "microwave": "Microondas", + "toaster": "Tostadora", + "sink": "Fregadero", + "refrigerator": "Frigorífico", + "blender": "Batidora", + "clock": "Reloj", + "vase": "Jarrón", + "scissors": "Tijeras", + "teddy_bear": "Osito de peluche", + "hair_dryer": "Secador de pelo", + "vehicle": "Vehículo", + "squirrel": "Ardilla", + "deer": "Ciervo", + "bark": "Ladrido", + "rabbit": "Conejo", + "face": "Rostro", + "license_plate": "Matrícula", + "package": "Paquete", + "bbq_grill": "Parrilla de barbacoa", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "umbrella": "Paraguas", + "horse": "Caballo", + "tv": "Televisión", + "on_demand": "Bajo demanda", + "toothbrush": "Cepillo de dientes", + "hair_brush": "Cepillo de pelo", + "amazon": "Amazon", + "sheep": "Oveja", + "bird": "Pájaro", + "knife": "Cuchillo", + "cake": "Tarta", + "baseball_bat": "Bate de béisbol", + "oven": "Horno", + "waste_bin": "Papelera", + "snowboard": "Snowboard", + "sandwich": "Sandwich", + "fox": "Zorro", + "nzpost": "NZPost", + "cat": "Gato", + "banana": "Plátano", + "book": "Libro", + "raccoon": "Mapache", + "dpd": "DPD", + "goat": "Cabra", + "robot_lawnmower": "Cortacésped robotizado", + "animal": "Animal", + "postnord": "PostNord", + "usps": "USPS", + "gls": "GLS" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/classificationModel.json new file mode 100644 index 0000000..4890ed0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/classificationModel.json @@ -0,0 +1,59 @@ +{ + "documentTitle": "Modelos de Clasificación", + "button": { + "deleteClassificationAttempts": "Borrar Imágenes de Clasificación.", + "renameCategory": "Renombrar Clase", + "deleteCategory": "Borrar Clase", + "deleteImages": "Borrar Imágenes", + "trainModel": "Entrenar Modelo", + "addClassification": "Añadir Clasificación", + "deleteModels": "Borrar Modelos", + "editModel": "Editar Modelo" + }, + "toast": { + "success": { + "deletedCategory": "Clase Borrada", + "deletedImage": "Imágenes Borradas", + "deletedModel_one": "Borrado con éxito {{count}} modelo", + "deletedModel_many": "Borrados con éxito {{count}} modelos", + "deletedModel_other": "Borrados con éxito {{count}} modelos", + "categorizedImage": "Imagen Clasificada Correctamente", + "trainedModel": "Modelo entrenado correctamente.", + "trainingModel": "Entrenamiento del modelo iniciado correctamente.", + "updatedModel": "Configuración del modelo actualizada correctamente", + "renamedCategory": "Clase renombrada correctamente a {{name}}" + }, + "error": { + "deleteImageFailed": "Fallo al borrar: {{errorMessage}}", + "deleteCategoryFailed": "Fallo al borrar clase: {{errorMessage}}", + "deleteModelFailed": "Fallo al borrar modelo: {{errorMessage}}", + "categorizeFailed": "Fallo al categorizar imagen: {{errorMessage}}", + "trainingFailed": "El entrenamiento del modelo ha fallado. Revisa los registros de Frigate para más detalles.", + "updateModelFailed": "Fallo al actualizar modelo: {{errorMessage}}", + "trainingFailedToStart": "No se pudo iniciar el entrenamiento del modelo: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Borrar Clase", + "desc": "¿Esta seguro de que quiere borrar la clase {{name}}? Esto borrará permanentemente todas las imágenes asociadas y requerirá reentrenar el modelo." + }, + "deleteModel": { + "title": "Borrar Modelo de Clasificación", + "single": "¿Está seguro de que quiere eliminar {{name}}? Esto borrar permanentemente todos los datos asociados incluidas las imágenes y los datos de entrenamiento. Esta acción no se puede deshacer.", + "desc_one": "¿Estas seguro de que quiere borrar {{count}} modelo? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.", + "desc_many": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha.", + "desc_other": "¿Estas seguro de que quiere borrar {{count}} modelos? Esto borrara permanentemente todos los datos asociados, incluyendo imágenes y datos de entrenamiento. Esta acción no puede ser desehecha." + }, + "edit": { + "title": "Editar modelo de clasificación" + }, + "tooltip": { + "noChanges": "No se han realizado cambios en el conjunto de datos desde el último entrenamiento.", + "modelNotReady": "El modelo no está listo para el entrenamiento", + "trainingInProgress": "El modelo está entrenándose actualmente.", + "noNewImages": "No hay imágenes nuevas para entrenar. Clasifica antes más imágenes del conjunto de datos." + }, + "details": { + "scoreInfo": "La puntuación representa la confianza media de clasificación en todas las detecciones de este objeto." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/configEditor.json new file mode 100644 index 0000000..3b9f277 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor de Configuración", + "copyConfig": "Copiar configuración", + "saveAndRestart": "Guardar y reiniciar", + "saveOnly": "Guardar solo", + "toast": { + "success": { + "copyToClipboard": "Configuración copiada al portapapeles." + }, + "error": { + "savingError": "Error al guardar la configuración" + } + }, + "documentTitle": "Editor de Configuración - Frigate", + "confirm": "¿Salir sin guardar?", + "safeConfigEditor": "Editor de Configuración (Modo Seguro)", + "safeModeDescription": "Frigate esta en modo seguro debido a un error en la configuración." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/events.json new file mode 100644 index 0000000..b2b4001 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/events.json @@ -0,0 +1,60 @@ +{ + "alerts": "Alertas", + "detections": "Detecciones", + "motion": { + "label": "Movimiento", + "only": "Solo movimiento" + }, + "allCameras": "Todas las cámaras", + "empty": { + "alert": "No hay alertas para revisar", + "detection": "No hay detecciones para revisar", + "motion": "No se encontraron datos de movimiento" + }, + "timeline": "Línea de tiempo", + "timeline.aria": "Seleccionar línea de tiempo", + "events": { + "label": "Eventos", + "aria": "Seleccionar eventos", + "noFoundForTimePeriod": "No se encontraron eventos para este período de tiempo." + }, + "documentTitle": "Revisión - Frigate", + "markAsReviewed": "Marcar como revisado", + "newReviewItems": { + "label": "Ver nuevos elementos de revisión", + "button": "Nuevos elementos para revisar" + }, + "camera": "Cámara", + "recordings": { + "documentTitle": "Grabaciones - Frigate" + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markTheseItemsAsReviewed": "Marcar estos elementos como revisados", + "selected": "{{count}} seleccionados", + "selected_one": "{{count}} seleccionados", + "selected_other": "{{count}} seleccionados", + "detected": "detectado", + "suspiciousActivity": "Actividad Sospechosa", + "threateningActivity": "Actividad Amenzadora", + "zoomIn": "Agrandar", + "zoomOut": "Alejar", + "detail": { + "label": "Detalle", + "trackedObject_one": "{{count}} objeto", + "trackedObject_other": "{{count}} objetos", + "noObjectDetailData": "No hay datos detallados del objeto.", + "settings": "Configuración de la Vista Detalle", + "noDataFound": "No hay datos detallados para revisar", + "aria": "Alternar vista de detalles", + "alwaysExpandActive": { + "title": "Expandir siempre los activos", + "desc": "Expandir siempre los detalles del objeto activo cuando esten disponibles." + } + }, + "objectTrack": { + "clickToSeek": "Clic para ir a este momento", + "trackedPoint": "Puntro trazado" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/explore.json new file mode 100644 index 0000000..7fcd50f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/explore.json @@ -0,0 +1,241 @@ +{ + "generativeAI": "Inteligencia Artificial Generativa", + "exploreIsUnavailable": { + "title": "Explorar no está disponible", + "embeddingsReindexing": { + "startingUp": "Iniciando…", + "estimatedTime": "Tiempo estimado restante:", + "finishingShortly": "Finalizando en breve", + "step": { + "thumbnailsEmbedded": "Miniaturas incrustadas: ", + "descriptionsEmbedded": "Descripciones incrustadas: ", + "trackedObjectsProcessed": "Objetos rastreados procesados: " + }, + "context": "Explorar puede usarse después de que las incrustaciones de objetos rastreados hayan terminado de reindexarse." + }, + "downloadingModels": { + "context": "Frigate está descargando los modelos de incrustaciones necesarios para soportar la función de Búsqueda Semántica. Esto puede tomar varios minutos dependiendo de la velocidad de tu conexión de red.", + "error": "Ha ocurrido un error. Revisa los registros de Frigate.", + "setup": { + "visionModelFeatureExtractor": "Extractor de características del modelo de visión", + "visionModel": "Modelo de visión", + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de texto" + }, + "tips": { + "context": "Es posible que desees reindexar las incrustaciones de tus objetos rastreados una vez que se hayan descargado los modelos.", + "documentation": "Leer la documentación" + } + } + }, + "details": { + "timestamp": "Marca de tiempo", + "item": { + "title": "Detalles del elemento de revisión", + "desc": "Detalles del elemento de revisión", + "button": { + "share": "Compartir este elemento de revisión", + "viewInExplore": "Ver en Explorar" + }, + "toast": { + "success": { + "updatedSublabel": "Subetiqueta actualizada con éxito.", + "regenerate": "Se ha solicitado una nueva descripción a {{provider}}. Dependiendo de la velocidad de tu proveedor, la nueva descripción puede tardar algún tiempo en regenerarse.", + "updatedLPR": "Matrícula actualizada con éxito.", + "audioTranscription": "Transcripción de audio solicitada con éxito." + }, + "error": { + "regenerate": "No se pudo llamar a {{provider}} para una nueva descripción: {{errorMessage}}", + "updatedSublabelFailed": "No se pudo actualizar la subetiqueta: {{errorMessage}}", + "updatedLPRFailed": "No se pudo actualizar la matrícula: {{errorMessage}}", + "audioTranscription": "Transcripción de audio solicitada falló: {{errorMessage}}" + } + }, + "tips": { + "mismatch_one": "Se detectó y se incluyó en este elemento de revisión un objeto {{count}} no disponible. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "mismatch_many": "Se detectaron y se incluyeron en este elemento de revisión {{count}} objetos no disponibles. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "mismatch_other": "Se detectaron y se incluyeron en este elemento de revisión {{count}} objetos no disponibles. Esos objetos no calificaron como alerta o detección o ya han sido limpiados/eliminados.", + "hasMissingObjects": "Ajusta tu configuración si quieres que Frigate guarde los objetos rastreados para las siguientes etiquetas: {{objects}}" + } + }, + "topScore": { + "label": "Puntuación máxima", + "info": "La puntuación máxima es la mediana más alta para el objeto rastreado, por lo que puede diferir de la puntuación mostrada en la miniatura del resultado de búsqueda." + }, + "description": { + "aiTips": "Frigate no solicitará una descripción a tu proveedor de Inteligencia Artificial Generativa hasta que el ciclo de vida del objeto rastreado haya terminado.", + "placeholder": "Descripción del objeto rastreado", + "label": "Descripción" + }, + "expandRegenerationMenu": "Expandir menú de regeneración", + "regenerateFromSnapshot": "Regenerar desde captura de pantalla", + "regenerateFromThumbnails": "Regenerar desde miniaturas", + "tips": { + "descriptionSaved": "Descripción guardada con éxito", + "saveDescriptionFailed": "No se pudo actualizar la descripción: {{errorMessage}}" + }, + "zones": "Zonas", + "label": "Etiqueta", + "editSubLabel": { + "title": "Editar subetiqueta", + "descNoLabel": "Introduce una nueva subetiqueta para este objeto rastreado", + "desc": "Introduce una nueva subetiqueta para este {{label}}" + }, + "button": { + "regenerate": { + "label": "Regenerar descripción del objeto rastreado", + "title": "Regenerar" + }, + "findSimilar": "Buscar similares" + }, + "objects": "Objetos", + "estimatedSpeed": "Velocidad estimada", + "camera": "Cámara", + "editLPR": { + "title": "Editar matrícula", + "desc": "Introduce un nuevo valor de matrícula para este {{label}}", + "descNoLabel": "Introduce un nuevo valor de matrícula para este objeto rastreado" + }, + "recognizedLicensePlate": "Matrícula Reconocida", + "snapshotScore": { + "label": "Puntuación de Instantánea" + }, + "score": { + "label": "Puntuación" + } + }, + "documentTitle": "Explorar - Frigate", + "trackedObjectDetails": "Detalles del objeto rastreado", + "type": { + "snapshot": "captura instantánea", + "video": "vídeo", + "object_lifecycle": "ciclo de vida del objeto", + "details": "detalles", + "thumbnail": "miniatura", + "tracking_details": "detalles de seguimiento" + }, + "objectLifecycle": { + "title": "Ciclo de vida del objeto", + "noImageFound": "No se encontró ninguna imagen para esta marca de tiempo.", + "createObjectMask": "Crear máscara de objeto", + "adjustAnnotationSettings": "Ajustar configuración de anotaciones", + "scrollViewTips": "Desplázate para ver los momentos significativos del ciclo de vida de este objeto.", + "lifecycleItemDesc": { + "visible": "{{label}} detectado", + "entered_zone": "{{label}} entró en {{zones}}", + "attribute": { + "other": "{{label}} reconocido como {{attribute}}", + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}" + }, + "gone": "{{label}} salió", + "heard": "{{label}} escuchado", + "external": "{{label}} detectado", + "active": "{{label}} se activó", + "stationary": "{{label}} se volvió estacionario", + "header": { + "zones": "Zonas", + "ratio": "Proporción", + "area": "Área" + } + }, + "annotationSettings": { + "offset": { + "label": "Desplazamiento de anotación", + "millisecondsToOffset": "Milisegundos para desplazar las anotaciones de detección. Valor por defecto: 0", + "desc": "Estos datos provienen de la transmisión de detección de tu cámara, pero se superponen en imágenes de la transmisión de grabación. Es poco probable que ambas transmisiones estén perfectamente sincronizadas. Como resultado, la caja delimitadora y la grabación no estarán perfectamente alineadas. Sin embargo, el campo annotation_offset se puede usar para ajustar esto.", + "documentation": "Leer la documentación ", + "tips": "CONSEJO: Imagina que hay un clip de evento con una persona caminando de izquierda a derecha. Si la caja delimitadora de la línea de tiempo del evento está constantemente a la izquierda de la persona, entonces se debe disminuir el valor. Del mismo modo, si una persona camina de izquierda a derecha y la caja delimitadora está constantemente por delante de la persona, entonces se debe aumentar el valor.", + "toast": { + "success": "El desplazamiento de anotación para {{camera}} se ha guardado en el archivo de configuración. Reinicia Frigate para aplicar los cambios." + } + }, + "showAllZones": { + "title": "Mostrar todas las zonas", + "desc": "Mostrar siempre las zonas en los fotogramas donde los objetos hayan entrado en una zona." + }, + "title": "Configuración de anotaciones" + }, + "carousel": { + "previous": "Diapositiva anterior", + "next": "Siguiente diapositiva" + }, + "autoTrackingTips": "Las posiciones de las cajas delimitadoras serán inexactas para cámaras con seguimiento automático.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Punto Rastreado" + }, + "itemMenu": { + "downloadVideo": { + "label": "Descargar video", + "aria": "Descargar video" + }, + "downloadSnapshot": { + "label": "Descargar captura de pantalla", + "aria": "Descargar captura de pantalla" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida del objeto", + "aria": "Mostrar el ciclo de vida del objeto" + }, + "findSimilar": { + "label": "Buscar similares", + "aria": "Buscar objetos rastreados similares" + }, + "submitToPlus": { + "label": "Enviar a Frigate+", + "aria": "Enviar a Frigate Plus" + }, + "viewInHistory": { + "aria": "Ver en Historial", + "label": "Ver en Historial" + }, + "deleteTrackedObject": { + "label": "Eliminar este objeto rastreado" + }, + "audioTranscription": { + "label": "Transcribir", + "aria": "Solicitar transcripción de audio" + }, + "addTrigger": { + "label": "Añadir disparador", + "aria": "Añadir disparador para el objeto seguido" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmar eliminación", + "desc": "Eliminar este objeto rastreado elimina la captura de pantalla, cualquier incrustación guardada y cualquier entrada asociada al ciclo de vida del objeto. Las grabaciones de este objeto rastreado en la vista de Historial NO se eliminarán.

    ¿Estás seguro de que quieres proceder?" + } + }, + "noTrackedObjects": "No se encontraron objetos rastreados", + "fetchingTrackedObjectsFailed": "Error al obtener objetos rastreados: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado eliminado con éxito.", + "error": "No se pudo eliminar el objeto rastreado: {{errorMessage}}" + } + }, + "tooltip": "Coincidencia con {{type}} al {{confidence}}%" + }, + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "{{count}} objetos rastreados ", + "exploreMore": "Explora más objetos {{label}}", + "aiAnalysis": { + "title": "Análisis AI" + }, + "concerns": { + "label": "Preocupaciones" + }, + "trackingDetails": { + "title": "Detalles del seguimiento", + "noImageFound": "No se ha encontrado imagen en este momento.", + "createObjectMask": "Crear máscara de objeto", + "adjustAnnotationSettings": "Ajustar configuración de anotaciones", + "scrollViewTips": "Haz clic para ver los momentos relevantes del ciclo de vida de este objeto.", + "count": "{{first}} de {{second}}", + "lifecycleItemDesc": { + "visible": "{{label}} detectado" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/exports.json new file mode 100644 index 0000000..9de2fa3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Búsqueda", + "documentTitle": "Exportar - Frigate", + "noExports": "No se encontraron exportaciones", + "deleteExport": "Eliminar exportación", + "editExport": { + "desc": "Introduce un nuevo nombre para esta exportación.", + "saveExport": "Guardar exportación", + "title": "Renombrar exportación" + }, + "toast": { + "error": { + "renameExportFailed": "No se pudo renombrar la exportación: {{errorMessage}}" + } + }, + "deleteExport.desc": "¿Estás seguro de que quieres eliminar {{exportName}}?", + "tooltip": { + "shareExport": "Compartir exportación", + "downloadVideo": "Descargar video", + "editName": "Editar nombre", + "deleteExport": "Eliminar exportación" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/faceLibrary.json new file mode 100644 index 0000000..25fa983 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "description": { + "addFace": "Agregar una nueva colección a la Biblioteca de Rostros subiendo tu primera imagen.", + "placeholder": "Introduce un nombre para esta colección", + "invalidName": "Nombre inválido. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos y guiones." + }, + "details": { + "person": "Persona", + "confidence": "Confianza", + "face": "Detalles del rostro", + "faceDesc": "Detalles del objeto rastreado que generó este rostro", + "timestamp": "Marca de tiempo", + "subLabelScore": "Puntuación de Etiqueta Secundaria", + "scoreInfo": "La puntuación de etiqueta secundaria es la puntuación ponderada de todas las confidencias de rostros reconocidos, por lo que puede diferir de la puntuación mostrada en la instantánea.", + "unknown": "Desconocido" + }, + "documentTitle": "Biblioteca de Rostros - Frigate", + "uploadFaceImage": { + "title": "Subir imagen del rostro", + "desc": "Sube una imagen para escanear rostros e incluirla en {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Crear colección", + "desc": "Crear una nueva colección", + "new": "Crear nuevo rostro", + "nextSteps": "Para construir una base sólida:
  • Usa la pestaña Reconocimientos Recientes para seleccionar y entrenar con imágenes de cada persona detectada.
  • Céntrate en imágenes frontales para obtener los mejores resultados; evita entrenar con imágenes que capturen rostros de perfil.
  • " + }, + "train": { + "title": "Reconocimientos Recientes", + "aria": "Seleccionar reconocimientos recientes", + "empty": "No hay intentos recientes de reconocimiento facial" + }, + "selectItem": "Seleccionar {{item}}", + "selectFace": "Seleccionar rostro", + "deleteFaceLibrary": { + "title": "Eliminar nombre", + "desc": "¿Estás seguro de que quieres eliminar la colección {{name}}? Esto eliminará permanentemente todos los rostros asociados." + }, + "button": { + "deleteFaceAttempts": "Eliminar Rostros", + "addFace": "Agregar rostro", + "uploadImage": "Subir imagen", + "reprocessFace": "Reprocesar rostro", + "renameFace": "Renombrar Rostro", + "deleteFace": "Eliminar Rostro" + }, + "imageEntry": { + "validation": { + "selectImage": "Por favor, selecciona un archivo de imagen." + }, + "dropActive": "Suelta la imagen aquí…", + "dropInstructions": "Arrastra y suelta, o pega una imagen aquí, o haz clic para seleccionar", + "maxSize": "Tamaño máximo: {{size}}MB" + }, + "toast": { + "success": { + "addFaceLibrary": "¡{{name}} ha sido añadido con éxito a la Biblioteca de Rostros!", + "trainedFace": "Rostro entrenado con éxito.", + "deletedName_one": "{{count}} rostro ha sido eliminado con éxito.", + "deletedName_many": "{{count}} rostros han sido eliminados con éxito.", + "deletedName_other": "{{count}} rostros han sido eliminados con éxito.", + "updatedFaceScore": "Puntuación del rostro actualizada con éxito.", + "deletedFace_one": "{{count}} rostro eliminado con éxito", + "deletedFace_many": "{{count}} rostros eliminados con éxito", + "deletedFace_other": "{{count}} rostros eliminados con éxito", + "uploadedImage": "Imagen subida con éxito.", + "renamedFace": "Rostro renombrado con éxito a {{name}}" + }, + "error": { + "uploadingImageFailed": "No se pudo subir la imagen: {{errorMessage}}", + "addFaceLibraryFailed": "No se pudo establecer el nombre del rostro: {{errorMessage}}", + "deleteFaceFailed": "No se pudo eliminar: {{errorMessage}}", + "deleteNameFailed": "No se pudo eliminar el nombre: {{errorMessage}}", + "trainFailed": "No se pudo entrenar: {{errorMessage}}", + "updateFaceScoreFailed": "No se pudo actualizar la puntuación del rostro: {{errorMessage}}", + "renameFaceFailed": "No se pudo renombrar el rostro: {{errorMessage}}" + } + }, + "readTheDocs": "Leer la documentación", + "trainFaceAs": "Entrenar rostro como:", + "trainFace": "Entrenar rostro", + "steps": { + "faceName": "Introducir Nombre de Rostro", + "uploadFace": "Subir Imagen de Rostro", + "nextSteps": "Próximos Pasos", + "description": { + "uploadFace": "Sube una imagen de {{name}} que muestre su rostro desde un ángulo frontal. La imagen no necesita estar recortada solo a su rostro." + } + }, + "renameFace": { + "title": "Renombrar Rostro", + "desc": "Introduce un nuevo nombre para {{name}}" + }, + "deleteFaceAttempts": { + "title": "Eliminar Rostros", + "desc_one": "¿Estás seguro de que quieres eliminar {{count}} rostro? Esta acción no se puede deshacer.", + "desc_many": "¿Estás seguro de que quieres eliminar {{count}} rostros? Esta acción no se puede deshacer.", + "desc_other": "¿Estás seguro de que quieres eliminar {{count}} rostros? Esta acción no se puede deshacer." + }, + "collections": "Colecciones", + "nofaces": "No hay rostros disponibles", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/live.json new file mode 100644 index 0000000..3d8c0b0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/live.json @@ -0,0 +1,179 @@ +{ + "documentTitle": "Directo - Frigate", + "documentTitle.withCamera": "{{camera}} - Directo - Frigate", + "twoWayTalk": { + "enable": "Habilitar conversación bidireccional", + "disable": "Deshabilitar conversación bidireccional" + }, + "cameraAudio": { + "enable": "Habilitar audio de la cámara", + "disable": "Deshabilitar audio de la cámara" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Haz clic en el marco para centrar la cámara", + "enable": "Habilitar clic para mover", + "disable": "Deshabilitar clic para mover" + }, + "up": { + "label": "Mover la cámara PTZ hacia arriba" + }, + "down": { + "label": "Mover la cámara PTZ hacia abajo" + }, + "right": { + "label": "Mover la cámara PTZ hacia la derecha" + }, + "left": { + "label": "Mover la cámara PTZ hacia la izquierda" + } + }, + "zoom": { + "in": { + "label": "Acercar la cámara PTZ" + }, + "out": { + "label": "Alejar la cámara PTZ" + } + }, + "frame": { + "center": { + "label": "Haz clic en el marco para centrar la cámara PTZ" + } + }, + "presets": "Preajustes de cámara PTZ", + "focus": { + "in": { + "label": "Enfocar camara PTZ" + }, + "out": { + "label": "Desenfocar camara PTZ" + } + } + }, + "camera": { + "enable": "Habilitar cámara", + "disable": "Deshabilitar cámara" + }, + "muteCameras": { + "enable": "Silenciar todas las cámaras", + "disable": "Activar sonido de todas las cámaras" + }, + "detect": { + "enable": "Habilitar detección", + "disable": "Deshabilitar detección" + }, + "recording": { + "enable": "Habilitar grabación", + "disable": "Deshabilitar grabación" + }, + "snapshots": { + "enable": "Habilitar capturas de pantalla", + "disable": "Desactivar instantáneas" + }, + "audioDetect": { + "enable": "Activar detección de audio", + "disable": "Desactivar detección de audio" + }, + "autotracking": { + "enable": "Activar seguimiento automático", + "disable": "Desactivar seguimiento automático" + }, + "streamStats": { + "enable": "Mostrar estadísticas de transmisión", + "disable": "Ocultar estadísticas de transmisión" + }, + "manualRecording": { + "title": "Bajo demanda", + "tips": "Iniciar un evento manual basado en la configuración de retención de grabaciones de esta cámara.", + "playInBackground": { + "label": "Reproducir en segundo plano", + "desc": "Habilitar esta opción para continuar transmitiendo cuando el reproductor esté oculto." + }, + "showStats": { + "label": "Mostrar estadísticas", + "desc": "Habilitar esta opción para mostrar estadísticas de transmisión como una superposición en la transmisión de la cámara." + }, + "debugView": "Vista de depuración", + "started": "Grabación manual bajo demanda iniciada.", + "failedToStart": "No se pudo iniciar la grabación manual bajo demanda.", + "start": "Iniciar grabación bajo demanda", + "recordDisabledTips": "Dado que la grabación está deshabilitada o restringida en la configuración de esta cámara, solo se guardará una captura de pantalla.", + "end": "Finalizar grabación bajo demanda", + "ended": "Finalizó la grabación manual bajo demanda.", + "failedToEnd": "No se pudo finalizar la grabación manual bajo demanda." + }, + "lowBandwidthMode": "Modo de bajo ancho de banda", + "streamingSettings": "Ajustes de transmisión", + "notifications": "Notificaciones", + "audio": "Audio", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmisión", + "audio": { + "tips": { + "documentation": "Leer la documentación ", + "title": "El audio debe provenir de tu cámara y estar configurado en go2rtc para esta transmisión." + }, + "available": "El audio está disponible para esta transmisión", + "unavailable": "El audio no está disponible para esta transmisión" + }, + "twoWayTalk": { + "tips.documentation": "Leer la documentación ", + "available": "La conversación bidireccional está disponible para esta transmisión", + "unavailable": "La conversación bidireccional no está disponible para esta transmisión", + "tips": "Tu dispositivo debe soportar la función y WebRTC debe estar configurado para la conversación bidireccional." + }, + "lowBandwidth": { + "tips": "La vista en vivo está en modo de bajo ancho de banda debido a problemas de almacenamiento en búfer o errores de transmisión.", + "resetStream": "Restablecer transmisión" + }, + "playInBackground": { + "label": "Reproducir en segundo plano", + "tips": "Habilita esta opción para continuar la transmisión cuando el reproductor esté oculto." + }, + "debug": { + "picker": "Selección de transmisión no disponible en mode de debug. La vista de debug siempre usa la transmisión con el rol de deteccción asignado." + } + }, + "cameraSettings": { + "title": "Ajustes de {{camera}}", + "objectDetection": "Detección de objetos", + "audioDetection": "Detección de audio", + "recording": "Grabación", + "snapshots": "Capturas de pantalla", + "autotracking": "Seguimiento automático", + "cameraEnabled": "Cámara habilitada", + "transcription": "Transcripción de Audio" + }, + "history": { + "label": "Mostrar grabaciones históricas" + }, + "effectiveRetainMode": { + "modes": { + "motion": "Movimiento", + "active_objects": "Objetos activos", + "all": "Todo" + }, + "notAllTips": "Tu configuración de retención de grabación de {{source}} está establecida en modo: {{effectiveRetainMode}}, por lo que esta grabación bajo demanda solo mantendrá segmentos con {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar diseño", + "group": { + "label": "Editar grupo de cámaras" + }, + "exitEdit": "Salir de la edición" + }, + "transcription": { + "enable": "Habilitar transcripción de audio en tiempo real", + "disable": "Deshabilitar transcripción de audio en tiempo real" + }, + "noCameras": { + "title": "No hay cámaras configuradas", + "description": "Comienza conectando una cámara.", + "buttonText": "Añade Cámara" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/recording.json new file mode 100644 index 0000000..ad362aa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportar", + "calendar": "Calendario", + "filter": "Filtro", + "filters": "Filtros", + "toast": { + "error": { + "noValidTimeSelected": "No se ha seleccionado un rango de tiempo válido", + "endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/search.json new file mode 100644 index 0000000..7458c49 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "Búsqueda", + "savedSearches": "Búsquedas Guardadas", + "searchFor": "Búsqueda de {{inputValue}}", + "button": { + "save": "Guardar búsqueda", + "delete": "Eliminar búsqueda guardada", + "clear": "Borrar búsqueda", + "filterInformation": "Información de filtro", + "filterActive": "Filtros activos" + }, + "trackedObjectId": "ID de Objeto Rastreado", + "filter": { + "label": { + "cameras": "Cámaras", + "labels": "Etiquetas", + "zones": "Zonas", + "sub_labels": "Subetiquetas", + "search_type": "Tipo de Búsqueda", + "time_range": "Rango de Tiempo", + "before": "Antes", + "after": "Después", + "min_score": "Puntuación Mínima", + "max_score": "Puntuación Máxima", + "min_speed": "Velocidad Mínima", + "max_speed": "Velocidad Máxima", + "recognized_license_plate": "Matrícula Reconocida", + "has_clip": "Tiene Clip", + "has_snapshot": "Tiene Instantánea" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descripción" + }, + "toast": { + "error": { + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La 'velocidad máxima' debe ser mayor o igual que la 'velocidad mínima'.", + "maxScoreMustBeGreaterOrEqualMinScore": "La 'puntuación máxima' debe ser mayor o igual que la 'puntuación mínima'.", + "beforeDateBeLaterAfter": "La fecha 'antes' debe ser posterior a la fecha 'después'.", + "minScoreMustBeLessOrEqualMaxScore": "La 'puntuación mínima' debe ser menor o igual que la 'puntuación máxima'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "La 'velocidad mínima' debe ser menor o igual que la 'velocidad máxima'.", + "afterDatebeEarlierBefore": "La fecha 'después' debe ser anterior a la fecha 'antes'." + } + }, + "tips": { + "title": "Cómo usar filtros de texto", + "desc": { + "text": "Los filtros te ayudan a reducir los resultados de tu búsqueda. Aquí te explicamos cómo usarlos en el campo de entrada:", + "example": "Ejemplo: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Escribe un nombre de filtro seguido de dos puntos (por ejemplo, \"cameras:\").
    • Selecciona un valor de las sugerencias o escribe el tuyo propio.
    • Usa múltiples filtros añadiéndolos uno tras otro con un espacio entre ellos.
    • Los filtros de fecha (before: y after:) usan el formato {{DateFormat}}.
    • El filtro de rango de tiempo usa el formato {{exampleTime}}.
    • Elimina filtros haciendo clic en la 'x' junto a ellos.
    ", + "step4": "Los filtros de fecha (antes: y después:) usan el formato {{DateFormat}}.", + "step6": "Elimina filtros haciendo clic en la 'x' junto a ellos.", + "exampleLabel": "Ejemplo:", + "step1": "Escribe un nombre de clave de filtro seguido de dos puntos (por ejemplo, \"cámaras:\").", + "step2": "Selecciona un valor de las sugerencias o escribe el tuyo propio.", + "step3": "Usa múltiples filtros añadiéndolos uno tras otro con un espacio entre ellos.", + "step5": "El filtro de rango de tiempo usa el formato {{exampleTime}}." + } + }, + "header": { + "currentFilterType": "Valores de Filtro", + "noFilters": "Filtros", + "activeFilters": "Filtros Activos" + } + }, + "similaritySearch": { + "title": "Búsqueda por Similitud", + "active": "Búsqueda por similitud activa", + "clear": "Borrar búsqueda por similitud" + }, + "placeholder": { + "search": "Buscar…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/settings.json new file mode 100644 index 0000000..7fe10b3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/settings.json @@ -0,0 +1,911 @@ +{ + "documentTitle": { + "masksAndZones": "Editor de máscaras y zonas - Frigate", + "object": "Depurar - Frigate", + "default": "Configuración - Frigate", + "authentication": "Configuración de autenticación - Frigate", + "camera": "Configuración de cámara - Frigate", + "motionTuner": "Ajuste de movimiento - Frigate", + "classification": "Configuración de clasificación - Frigate", + "general": "Configuración de Interfaz de Usuario - Frigate", + "frigatePlus": "Configuración de Frigate+ - Frigate", + "notifications": "Configuración de Notificaciones - Frigate", + "enrichments": "Configuración de Análisis Avanzado - Frigate", + "cameraManagement": "Administrar Cámaras - Frigate", + "cameraReview": "Revisar Configuración de Cámaras - Frigate" + }, + "menu": { + "cameras": "Configuración de Cámara", + "debug": "Depuración", + "ui": "Interfaz de usuario", + "classification": "Clasificación", + "motionTuner": "Ajuste de movimiento", + "masksAndZones": "Máscaras / Zonas", + "frigateplus": "Frigate+", + "users": "Usuarios", + "notifications": "Notificaciones", + "enrichments": "Análisis avanzado", + "triggers": "Disparadores", + "roles": "Rols", + "cameraManagement": "Administración", + "cameraReview": "Revisar" + }, + "dialog": { + "unsavedChanges": { + "title": "Tienes cambios sin guardar.", + "desc": "¿Quieres guardar los cambios antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Cámara", + "noCamera": "Sin cámara" + }, + "general": { + "liveDashboard": { + "automaticLiveView": { + "label": "Vista en directo automática", + "desc": "Cambiar automáticamente a la vista en directo de una cámara cuando se detecta actividad. Si se desactiva esta opción, las imágenes de las cámaras en el panel en directo solo se actualizarán una vez por minuto." + }, + "playAlertVideos": { + "label": "Reproducir vídeos de alertas", + "desc": "De forma predeterminada, las alertas recientes en el panel en directo se reproducen como pequeños vídeos en bucle. Desactiva esta opción para mostrar solo una imagen estática de las alertas recientes en este dispositivo/navegador." + }, + "title": "Panel en directo" + }, + "cameraGroupStreaming": { + "desc": "La configuración de transmisión de cada grupo de cámaras se guarda en el almacenamiento local de tu navegador.", + "title": "Configuración de transmisión de grupo de cámaras", + "clearAll": "Borrar toda la configuración de transmisión" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Velocidad de reproducción predeterminada", + "desc": "Velocidad de reproducción predeterminada para la reproducción de grabaciones." + }, + "title": "Visor de grabaciones" + }, + "calendar": { + "firstWeekday": { + "label": "Primer día de la semana", + "sunday": "Domingo", + "desc": "El día con el que comienzan las semanas en el calendario de revisión.", + "monday": "Lunes" + }, + "title": "Calendario" + }, + "storedLayouts": { + "desc": "El diseño de las cámaras en un grupo de cámaras se puede arrastrar y redimensionar. Las posiciones se guardan en el almacenamiento local de tu navegador.", + "title": "Diseños guardados", + "clearAll": "Borrar todos los diseños" + }, + "title": "Ajustes de Interfaz de Usuario", + "toast": { + "success": { + "clearStoredLayout": "Diseño almacenado eliminado para {{cameraName}}", + "clearStreamingSettings": "Se ha borrado la configuración de transmisión de todos los grupos de cámaras." + }, + "error": { + "clearStreamingSettingsFailed": "Error al borrar la configuración de transmisión: {{errorMessage}}", + "clearStoredLayoutFailed": "Error al borrar el diseño guardado: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Búsqueda semántica", + "desc": "La búsqueda semántica en Frigate te permite encontrar objetos rastreados dentro de tus elementos de revisión utilizando la propia imagen, una descripción escrita por el usuario o una generada automáticamente.", + "readTheDocumentation": "Leer la documentación", + "reindexNow": { + "confirmTitle": "Confirmar reindexación", + "confirmButton": "Reindexar", + "success": "La reindexación comenzó con éxito.", + "label": "Reindexar ahora", + "desc": "La reindexación regenerará las incrustaciones para todos los objetos rastreados. Este proceso se ejecuta en segundo plano y puede maximizar el uso de tu CPU y tomar una cantidad considerable de tiempo dependiendo del número de objetos rastreados que tengas.", + "confirmDesc": "¿Estás seguro de que quieres reindexar todas las incrustaciones de objetos rastreados? Este proceso se ejecutará en segundo plano, pero puede maximizar el uso de tu CPU y tomar una cantidad considerable de tiempo. Puedes ver el progreso en la página de Explorar.", + "alreadyInProgress": "La reindexación ya está en curso.", + "error": "No se pudo iniciar la reindexación: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Usar pequeño emplea una versión cuantizada del modelo que utiliza menos RAM y se ejecuta más rápido en la CPU con una diferencia muy insignificante en la calidad de las incrustaciones.", + "title": "pequeño" + }, + "large": { + "title": "grande", + "desc": "Usar grande emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU si es aplicable." + }, + "label": "Tamaño del modelo", + "desc": "El tamaño del modelo utilizado para las incrustaciones de búsqueda semántica." + } + }, + "title": "Configuración de clasificación", + "faceRecognition": { + "title": "Reconocimiento facial", + "modelSize": { + "large": { + "title": "grande", + "desc": "Usar grande emplea un modelo de incrustación facial ArcFace y se ejecutará automáticamente en la GPU si es aplicable." + }, + "small": { + "desc": "Usar pequeño emplea un modelo de incrustación facial FaceNet que se ejecuta de manera eficiente en la mayoría de las CPUs.", + "title": "pequeño" + }, + "label": "Tamaño del modelo", + "desc": "El tamaño del modelo utilizado para el reconocimiento facial." + }, + "readTheDocumentation": "Leer la documentación", + "desc": "El reconocimiento facial permite asignar nombres a las personas y, cuando se reconoce su rostro, Frigate asignará el nombre de la persona como una subetiqueta. Esta información se incluye en la interfaz de usuario, los filtros y también en las notificaciones." + }, + "licensePlateRecognition": { + "title": "Reconocimiento de matrículas", + "desc": "Frigate puede reconocer matrículas en vehículos y agregar automáticamente los caracteres detectados al campo recognized_license_plate o un nombre conocido como subetiqueta a objetos que sean del tipo coche. Un caso de uso común puede ser leer las matrículas de los coches que entran en un camino de entrada o de los coches que pasan por una calle.", + "readTheDocumentation": "Leer la documentación" + }, + "toast": { + "success": "Los ajustes de clasificación han sido guardados. Reinicia Frigate para aplicar tus cambios.", + "error": "No se pudieron guardar los cambios de configuración: {{errorMessage}}" + }, + "birdClassification": { + "title": "Clasificación de Aves", + "desc": "La clasificación de aves identifica aves conocidas utilizando un modelo de TensorFlow cuantizado. Cuando se reconoce una ave conocida, su nombre común se añadirá como una subetiqueta. Esta información se incluye en la interfaz de usuario, en los filtros y en las notificaciones." + }, + "restart_required": "Es necesario reiniciar (se han cambiado las configuraciones de clasificación)", + "unsavedChanges": "Cambios en la configuración de clasificación no guardados" + }, + "camera": { + "review": { + "alerts": "Alertas ", + "desc": "Activar/desactivar temporalmente las alertas y detecciones para esta cámara hasta que Frigate se reinicie. Cuando está desactivado, no se generarán nuevos elementos de revisión. ", + "detections": "Detecciones ", + "title": "Revisar" + }, + "reviewClassification": { + "readTheDocumentation": "Leer la documentación", + "noDefinedZones": "No se han definido zonas para esta cámara.", + "selectAlertsZones": "Seleccionar zonas para Alertas", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones, independientemente de en qué zona se encuentren.", + "text": "Todos los objetos {{detectionsLabels}} no categorizados en {{zone}} en {{cameraName}} se mostrarán como Detecciones.", + "notSelectDetections": "Todos los objetos {{detectionsLabels}} detectados en {{zone}} en {{cameraName}} que no estén categorizados como Alertas se mostrarán como Detecciones, independientemente de en qué zona se encuentren." + }, + "desc": "Frigate clasifica los elementos de revisión como Alertas y Detecciones. Por defecto, todos los objetos persona y coche se consideran Alertas. Puedes refinar la categorización de tus elementos de revisión configurando zonas requeridas para ellos.", + "objectDetectionsTips": "Todos los objetos {{detectionsLabels}} no categorizados en {{cameraName}} se mostrarán como Detecciones, independientemente de en qué zona se encuentren.", + "zoneObjectAlertsTips": "Todos los objetos {{alertsLabels}} detectados en {{zone}} en {{cameraName}} se mostrarán como Alertas.", + "title": "Clasificación de revisión", + "objectAlertsTips": "Todos los objetos {{alertsLabels}} en {{cameraName}} se mostrarán como Alertas.", + "selectDetectionsZones": "Seleccionar zonas para Detecciones", + "limitDetections": "Limitar detecciones a zonas específicas", + "toast": { + "success": "La configuración de clasificación de revisión ha sido guardada. Reinicia Frigate para aplicar los cambios." + }, + "unsavedChanges": "Configuración de clasificación de revisión no guardada para {{camera}}" + }, + "title": "Ajustes de la cámara", + "streams": { + "title": "Transmisiones", + "desc": "Desactivar temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de esta cámara por parte de Frigate. La detección, grabación y depuración no estarán disponibles.
    Nota: Esto no desactiva las retransmisiones de go2rtc." + }, + "object_descriptions": { + "title": "Descripciones de objetos de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de objetos de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los objetos rastreados en esta cámara." + }, + "review_descriptions": { + "title": "Descripciones de revisión de IA generativa", + "desc": "Habilitar/deshabilitar temporalmente las descripciones de revisión de IA generativa para esta cámara. Cuando está deshabilitado, no se solicitarán descripciones generadas por IA para los elementos de revisión en esta cámara." + }, + "addCamera": "Añadir nueva cámara", + "editCamera": "Editar cámara:", + "selectCamera": "Seleccionar una cámara", + "backToSettings": "Volver a la configuración de la cámara", + "cameraConfig": { + "add": "Añadir cámara", + "edit": "Editar cámara", + "description": "Configurar los ajustes de la cámara, incluyendo las entradas de flujo y los roles.", + "name": "Nombre de la cámara", + "nameRequired": "El nombre de la cámara es obligatorio", + "nameInvalid": "El nombre de la cámara debe contener solo letras, números, guiones bajos o guiones", + "namePlaceholder": "p. ej., puerta_principal", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Flujos de entrada", + "path": "Ruta del flujo", + "pathRequired": "La ruta del flujo es obligatoria", + "pathPlaceholder": "rtsp://...", + "roles": "Roles", + "rolesRequired": "Se requiere al menos un rol", + "rolesUnique": "Cada rol (audio, detección, grabación) solo puede asignarse a un flujo", + "addInput": "Añadir flujo de entrada", + "removeInput": "Eliminar flujo de entrada", + "inputsRequired": "Se requiere al menos un flujo de entrada" + }, + "toast": { + "success": "Cámara {{cameraName}} guardada con éxito" + }, + "nameLength": "Nombre de cámara debe ser de mínimo 24 caracteres." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "alreadyExists": "Ya existe una zona con este nombre para esta cámara.", + "mustNotBeSameWithCamera": "El nombre de la zona no debe ser el mismo que el nombre de la cámara.", + "hasIllegalCharacter": "El nombre de la zona contiene caracteres no permitidos.", + "mustBeAtLeastTwoCharacters": "El nombre de la zona debe tener al menos 2 caracteres.", + "mustNotContainPeriod": "El nombre de la zona no debe contener puntos." + } + }, + "distance": { + "error": { + "mustBeFilled": "Todos los campos de distancia deben estar completados para usar la estimación de velocidad.", + "text": "La distancia debe ser mayor o igual a 0.1." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "La inercia debe ser mayor a 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "El tiempo de merodeo debe ser mayor o igual a 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "true": "Ajustar puntos", + "false": "No ajustar puntos" + }, + "delete": { + "desc": "¿Estás seguro de que quieres eliminar el {{type}} {{name}}?", + "success": "{{name}} ha sido eliminado.", + "title": "Confirmar eliminación" + }, + "error": { + "mustBeFinished": "El dibujo del polígono debe estar terminado antes de guardar." + }, + "reset": { + "label": "Borrar todos los puntos" + }, + "removeLastPoint": "Eliminar el último punto" + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "El umbral de velocidad debe ser mayor o igual a 0,1." + } + } + }, + "zones": { + "label": "Zonas", + "desc": { + "title": "Las zonas te permiten definir un área específica del cuadro para que puedas determinar si un objeto está o no dentro de un área particular.", + "documentation": "Documentación" + }, + "add": "Agregar zona", + "edit": "Editar zona", + "loiteringTime": { + "title": "Tiempo de merodeo", + "desc": "Establece una cantidad mínima de tiempo en segundos que el objeto debe estar en la zona para que se active. Predeterminado: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que aplican a esta zona." + }, + "inertia": { + "desc": "Especifica cuántos fotogramas debe estar un objeto en una zona antes de que se considere que está en la zona. Predeterminado: 3", + "title": "Inercia" + }, + "name": { + "title": "Nombre", + "inputPlaceHolder": "Introduce un nombre…", + "tips": "El nombre debe tener al menos 2 caracteres y no debe ser el nombre de una cámara ni de otra zona." + }, + "documentTitle": "Editar Zona - Frigate", + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "speedEstimation": { + "desc": "Habilitar la estimación de velocidad para objetos en esta zona. La zona debe tener exactamente 4 puntos.", + "title": "Estimación de velocidad", + "docs": "Leer la documentación", + "lineBDistance": "Distancia de la línea B ({{unit}})", + "lineCDistance": "Distancia de la línea C ({{unit}})", + "lineDDistance": "Distancia de la línea D ({{unit}})", + "lineADistance": "Distancia de la línea A ({{unit}})" + }, + "speedThreshold": { + "toast": { + "error": { + "pointLengthError": "La estimación de velocidad ha sido deshabilitada para esta zona. Las zonas con estimación de velocidad deben tener exactamente 4 puntos.", + "loiteringTimeError": "Las zonas con tiempos de merodeo mayores a 0 no deberían usarse con la estimación de velocidad." + } + }, + "title": "Umbral de velocidad ({{unit}})", + "desc": "Especifica una velocidad mínima para que los objetos sean considerados en esta zona." + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos", + "allObjects": "Todos los objetos", + "toast": { + "success": "La zona ({{zoneName}}) ha sido guardada. Reinicia Frigate para aplicar los cambios." + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "No se pudieron copiar las coordenadas al portapapeles." + }, + "success": { + "copyCoordinates": "Coordenadas copiadas para {{polyName}} al portapapeles." + } + }, + "filter": { + "all": "Todas las máscaras y zonas" + }, + "motionMasks": { + "label": "Máscara de movimiento", + "desc": { + "documentation": "Documentación", + "title": "Las máscaras de movimiento se utilizan para evitar que tipos de movimiento no deseados activen la detección. Un exceso de enmascaramiento puede dificultar el seguimiento de objetos." + }, + "add": "Nueva Máscara de Movimiento", + "edit": "Editar Máscara de Movimiento", + "context": { + "documentation": "Leer la documentación", + "title": "Las máscaras de movimiento se utilizan para evitar que tipos de movimiento no deseados activen la detección (por ejemplo: ramas de árboles, marcas de tiempo de la cámara). Las máscaras de movimiento deben usarse con moderación, un exceso de enmascaramiento dificultará el seguimiento de objetos." + }, + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "polygonAreaTooLarge": { + "documentation": "Leer la documentación", + "title": "La máscara de movimiento está cubriendo el {{polygonArea}}% del marco de la cámara. No se recomiendan máscaras de movimiento grandes.", + "tips": "Las máscaras de movimiento no impiden que se detecten objetos. Deberías usar una zona requerida en su lugar." + }, + "toast": { + "success": { + "noName": "La máscara de movimiento ha sido guardada. Reinicia Frigate para aplicar los cambios.", + "title": "{{polygonName}} ha sido guardado. Reinicia Frigate para aplicar los cambios." + } + }, + "documentTitle": "Editar Máscara de Movimiento - Frigate", + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos" + }, + "objectMasks": { + "label": "Máscaras de Objetos", + "documentTitle": "Editar Máscara de Objetos - Frigate", + "desc": { + "documentation": "Documentación", + "title": "Las máscaras de filtro de objetos se utilizan para descartar falsos positivos de un tipo de objeto específico según su ubicación." + }, + "add": "Añadir Máscara de Objetos", + "edit": "Editar Máscara de Objetos", + "context": "Las máscaras de filtro de objetos se utilizan para descartar falsos positivos de un tipo de objeto específico según su ubicación.", + "objects": { + "title": "Objetos", + "desc": "El tipo de objeto al que se aplica esta máscara de objetos.", + "allObjectTypes": "Todos los tipos de objetos" + }, + "toast": { + "success": { + "noName": "La máscara de objetos ha sido guardada. Reinicia Frigate para aplicar los cambios.", + "title": "{{polygonName}} ha sido guardado. Reinicia Frigate para aplicar los cambios." + } + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} puntos", + "point_other": "{{count}} puntos", + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen." + }, + "restart_required": "Es necesario reiniciar (se han cambiado las máscaras/zonas)", + "motionMaskLabel": "Máscara de movimiento {{number}}", + "objectMaskLabel": "Máscara de objeto {{number}} ({{label}})" + }, + "motionDetectionTuner": { + "title": "Sintonizador de Detección de Movimiento", + "desc": { + "documentation": "Lee la Guía de Ajuste de Movimiento", + "title": "Frigate utiliza la detección de movimiento como una primera verificación para determinar si hay algo ocurriendo en el marco que merezca ser analizado con la detección de objetos." + }, + "Threshold": { + "title": "Umbral", + "desc": "El valor del umbral determina cuánto cambio en la luminancia de un píxel es necesario para que se considere movimiento. Predeterminado: 30" + }, + "contourArea": { + "title": "Área de Contorno", + "desc": "El valor del área de contorno se utiliza para decidir qué grupos de píxeles cambiados califican como movimiento. Predeterminado: 10" + }, + "improveContrast": { + "title": "Mejorar Contraste", + "desc": "Mejora el contraste para escenas más oscuras. Predeterminado: ACTIVADO" + }, + "toast": { + "success": "Los ajustes de movimiento han sido guardados." + }, + "unsavedChanges": "Cambios no guardados en el sintonizador de movimiento ({{camera}})" + }, + "debug": { + "title": "Depuración", + "debugging": "Depuración", + "objectList": "Lista de Objetos", + "noObjects": "Sin objetos", + "boundingBoxes": { + "title": "Cajas delimitadoras", + "desc": "Mostrar cajas delimitadoras alrededor de los objetos rastreados", + "colors": { + "label": "Colores de las Cajas Delimitadoras de Objetos", + "info": "
  • Al iniciar, se asignarán diferentes colores a cada etiqueta de objeto
  • Una línea fina azul oscura indica que el objeto no está detectado en este momento actual
  • Una línea fina gris indica que el objeto se detecta como estacionario
  • Una línea gruesa indica que el objeto es el sujeto de seguimiento automático (cuando está habilitado)
  • " + } + }, + "timestamp": { + "title": "Marca de tiempo", + "desc": "Superponer una marca de tiempo en la imagen" + }, + "zones": { + "title": "Zonas", + "desc": "Mostrar un contorno de las zonas definidas" + }, + "detectorDesc": "Frigate utiliza tus detectores ({{detectors}}) para detectar objetos en el flujo de video de tu cámara.", + "desc": "La vista de depuración muestra una vista en tiempo real de los objetos rastreados y sus estadísticas. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados.", + "mask": { + "title": "Máscaras de movimiento", + "desc": "Mostrar polígonos de máscaras de movimiento" + }, + "motion": { + "title": "Cajas de movimiento", + "desc": "Mostrar cajas alrededor de las áreas donde se detecta movimiento", + "tips": "

    Cajas de Movimiento


    Se superpondrán cajas rojas en las áreas del fotograma donde se está detectando movimiento actualmente

    " + }, + "regions": { + "title": "Regiones", + "desc": "Mostrar una caja de la región de interés enviada al detector de objetos", + "tips": "

    Cajas de Región


    Se superpondrán cajas verdes brillantes en las áreas de interés del fotograma que se envían al detector de objetos.

    " + }, + "objectShapeFilterDrawing": { + "title": "Dibujo de Filtro de Forma de Objetos", + "desc": "Dibuja un rectángulo en la imagen para ver los detalles de área y proporción", + "tips": "Habilita esta opción para dibujar un rectángulo en la imagen de la cámara y mostrar su área y proporción. Estos valores pueden usarse luego para establecer parámetros de filtro de forma de objetos en tu configuración.", + "document": "Lee la documentación ", + "score": "Puntuación", + "ratio": "Proporción", + "area": "Área" + }, + "paths": { + "title": "Rutas", + "desc": "Mostrar puntos significativos de la ruta del objeto rastreado", + "tips": "

    Rutas


    Líneas y círculos indicarán los puntos significativos por los que se ha movido el objeto rastreado durante su ciclo de vida.

    " + }, + "openCameraWebUI": "Abrir Web UI de {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "No hay detecciones de audio", + "score": "puntuación", + "currentRMS": "RMS actual", + "currentdbFS": "dbFS actual" + } + }, + "users": { + "title": "Usuarios", + "management": { + "title": "Gestión de Usuarios", + "desc": "Gestiona las cuentas de usuario de esta instancia de Frigate." + }, + "addUser": "Añadir usuario", + "toast": { + "success": { + "createUser": "Usuario {{user}} creado correctamente", + "deleteUser": "Usuario {{user}} eliminado correctamente", + "updatePassword": "Contraseña actualizada correctamente.", + "roleUpdated": "Rol actualizado para {{user}}" + }, + "error": { + "createUserFailed": "Error al crear el usuario: {{errorMessage}}", + "deleteUserFailed": "Error al eliminar el usuario: {{errorMessage}}", + "roleUpdateFailed": "Error al actualizar el rol: {{errorMessage}}", + "setPasswordFailed": "Error al guardar la contraseña: {{errorMessage}}" + } + }, + "table": { + "username": "Nombre de usuario", + "actions": "Acciones", + "role": "Rol", + "noUsers": "No se encontraron usuarios.", + "changeRole": "Cambiar el rol del usuario", + "password": "Contraseña", + "deleteUser": "Eliminar usuario" + }, + "dialog": { + "form": { + "user": { + "title": "Nombre de usuario", + "placeholder": "Introduce el nombre de usuario", + "desc": "Solo se permiten letras, números, puntos y guiones bajos." + }, + "password": { + "title": "Contraseña", + "placeholder": "Introduce la contraseña", + "confirm": { + "title": "Confirma la contraseña", + "placeholder": "Confirma la contraseña" + }, + "strength": { + "title": "Fortaleza de la contraseña: ", + "weak": "Débil", + "medium": "Media", + "strong": "Fuerte", + "veryStrong": "Muy fuerte" + }, + "match": "Las contraseñas coinciden", + "notMatch": "Las contraseñas no coinciden" + }, + "newPassword": { + "title": "Nueva contraseña", + "placeholder": "Introduce la nueva contraseña", + "confirm": { + "placeholder": "Vuelve a introducir la nueva contraseña" + } + }, + "usernameIsRequired": "Se requiere el nombre de usuario", + "passwordIsRequired": "Se requiere contraseña" + }, + "passwordSetting": { + "updatePassword": "Actualizar contraseña para {{username}}", + "setPassword": "Establecer contraseña", + "desc": "Crear una contraseña fuerte para asegurar esta cuenta.", + "cannotBeEmpty": "La contraseña no puede estar vacía", + "doNotMatch": "Las contraseñas no coinciden" + }, + "createUser": { + "desc": "Añadir una nueva cuenta de usuario y especificar un rol para el acceso a áreas de la interfaz de usuario de Frigate.", + "title": "Crear nuevo usuario", + "usernameOnlyInclude": "El nombre de usuario solo puede incluir letras, números, . o _", + "confirmPassword": "Por favor, confirma tu contraseña" + }, + "changeRole": { + "title": "Cambiar rol de usuario", + "desc": "Actualizar permisos para {{username}}", + "roleInfo": { + "intro": "Selecciona el rol adecuado para este usuario:", + "adminDesc": "Acceso completo a todas las funciones.", + "viewerDesc": "Limitado a paneles en vivo, revisión, exploración y exportaciones únicamente.", + "viewer": "Espectador", + "admin": "Administrador", + "customDesc": "Rol personalizado con acceso a cámaras." + }, + "select": "Selecciona un rol" + }, + "deleteUser": { + "warn": "¿Estás seguro de que quieres eliminar {{username}}?", + "title": "Eliminar usuario", + "desc": "Esta acción no se puede deshacer. Esto eliminará permanentemente la cuenta de usuario y eliminará todos los datos asociados." + } + }, + "updatePassword": "Actualizar contraseña" + }, + "notification": { + "title": "Notificaciones", + "notificationSettings": { + "title": "Configuración de notificaciones", + "desc": "Frigate puede enviar notificaciones push a tu dispositivo de forma nativa cuando se ejecuta en el navegador o está instalado como una PWA.", + "documentation": "Leer la documentación" + }, + "notificationUnavailable": { + "title": "Notificaciones no disponibles", + "documentation": "Leer la documentación", + "desc": "Las notificaciones push web requieren un contexto seguro (https://…). Esto es una limitación del navegador. Accede a Frigate de forma segura para usar las notificaciones." + }, + "globalSettings": { + "title": "Configuración global", + "desc": "Suspender temporalmente las notificaciones de cámaras específicas en todos los dispositivos registrados." + }, + "email": { + "title": "Correo electrónico", + "placeholder": "p.ej. ejemplo@correo.com", + "desc": "Se requiere un correo electrónico válido y se utilizará para notificarte si hay algún problema con el servicio de notificaciones push." + }, + "cameras": { + "title": "Cámaras", + "noCameras": "No hay cámaras disponibles", + "desc": "Selecciona qué cámaras habilitar para las notificaciones." + }, + "deviceSpecific": "Configuración específica del dispositivo", + "registerDevice": "Registrar este dispositivo", + "sendTestNotification": "Enviar una notificación de prueba", + "active": "Notificaciones activas", + "suspended": "Notificaciones suspendidas {{time}}", + "suspendTime": { + "5minutes": "Suspender por 5 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "untilRestart": "Suspender hasta reiniciar", + "30minutes": "Suspender por 30 minutos", + "24hours": "Suspender por 24 horas", + "10minutes": "Suspender por 10 minutos", + "suspend": "Suspender" + }, + "cancelSuspension": "Cancelar suspensión", + "toast": { + "success": { + "settingSaved": "La configuración de notificaciones se ha guardado.", + "registered": "Registrado correctamente para las notificaciones. Es necesario reiniciar Frigate antes de que se puedan enviar notificaciones (incluida una notificación de prueba)." + }, + "error": { + "registerFailed": "Error al guardar el registro de notificaciones." + } + }, + "unregisterDevice": "Cancelar el registro de este dispositivo", + "unsavedRegistrations": "Registros de notificaciones no guardados", + "unsavedChanges": "Cambios de notificaciones no guardados" + }, + "frigatePlus": { + "title": "Configuración de Frigate+", + "apiKey": { + "title": "Clave API de Frigate+", + "notValidated": "La clave API de Frigate+ no ha sido detectada o no ha sido validada", + "plusLink": "Lee más sobre Frigate+", + "desc": "La clave API de Frigate+ permite la integración con el servicio Frigate+.", + "validated": "La clave API de Frigate+ ha sido detectada y validada" + }, + "snapshotConfig": { + "title": "Configuración de instantáneas", + "documentation": "Leer la documentación", + "table": { + "camera": "Cámara", + "snapshots": "Instantáneas", + "cleanCopySnapshots": "clean_copy Instantáneas" + }, + "desc": "Enviar a Frigate+ requiere que tanto las capturas instantáneas como las capturas clean_copy estén habilitadas en tu configuración.", + "cleanCopyWarning": "Algunas cámaras tienen las instantáneas habilitadas pero tienen la copia limpia desactivada. Necesitas habilitar clean_copy en tu configuración de instantáneas para poder enviar imágenes de estas cámaras a Frigate+." + }, + "modelInfo": { + "title": "Información del modelo", + "modelType": "Tipo de modelo", + "baseModel": "Modelo base", + "supportedDetectors": "Detectores compatibles", + "dimensions": "Dimensiones", + "cameras": "Cámaras", + "loading": "Cargando información del modelo…", + "error": "No se pudo cargar la información del modelo", + "availableModels": "Modelos disponibles", + "loadingAvailableModels": "Cargando modelos disponibles…", + "modelSelect": "Tus modelos disponibles en Frigate+ se pueden seleccionar aquí. Ten en cuenta que solo se pueden seleccionar modelos compatibles con tu configuración actual de detectores.", + "trainDate": "Fecha de entrenamiento", + "plusModelType": { + "baseModel": "Modelo Base", + "userModel": "Ajustado Finamente" + } + }, + "toast": { + "success": "La configuración de Frigate+ se ha guardado. Reinicia Frigate para aplicar los cambios.", + "error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}" + }, + "restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)", + "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados" + }, + "enrichments": { + "title": "Configuración de Análisis Avanzado", + "unsavedChanges": "Cambios sin guardar en la configuración de Análisis Avanzado", + "birdClassification": { + "title": "Clasificación de Aves", + "desc": "La clasificación de aves identifica especies conocidas utilizando un modelo cuantizado de TensorFlow. Cuando se reconoce un ave conocida, su nombre se añade como una subetiqueta (sub_label). Esta información se incluye en la interfaz de usuario, los filtros y las notificaciones." + }, + "semanticSearch": { + "title": "Búsqueda Semántica", + "desc": "La búsqueda semántica en Frigate te permite encontrar objetos rastreados dentro de tus elementos de revisión utilizando ya sea la imagen en sí, una descripción de texto definida por el usuario o una generada automáticamente.", + "readTheDocumentation": "Leer la Documentación", + "reindexNow": { + "confirmTitle": "Confirmar Re-Indexado", + "confirmDesc": "¿Estás seguro de que quieres re-indexar todas las representaciones (embeddings) de objetos rastreados? Este proceso se ejecutará en segundo plano, pero puede usar al máximo tu CPU y tardar una cantidad considerable de tiempo, dependiendo de la cantidad de objetos registrados. Puedes seguir el progreso en la página Explorar (Explore).", + "confirmButton": "Re-Indexar", + "success": "El proceso de re-indexado ha comenzado.", + "alreadyInProgress": "El proceso de re-indexado ya se está ejecutando.", + "error": "Ha ocurrido un error al intentar iniciar el proceso de re-indexado: {{errorMessage}}", + "label": "Re-indexar Ahora", + "desc": "La re-indexación regenerará las embeddings para todos los objetos rastreados. Este proceso se ejecuta en segundo plano y puede utilizar al máximo tu CPU, además de tomar una cantidad considerable de tiempo dependiendo de la cantidad de objetos rastreados que tengas." + }, + "modelSize": { + "label": "Tamaño del Modelo", + "small": { + "title": "pequeño", + "desc": "Usar la opción small emplea una versión cuantizada del modelo que consume menos memoria RAM y se ejecuta más rápido en la CPU, con una diferencia muy pequeña o casi imperceptible en la calidad de las representaciones (embeddings)." + }, + "large": { + "title": "grande", + "desc": "Usar la opción large emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU, si está disponible." + }, + "desc": "Tamaño del modelo usado para la búsqueda semántica." + } + }, + "faceRecognition": { + "title": "Reconocimiento Facial", + "readTheDocumentation": "Leer la Documentación", + "modelSize": { + "label": "Tamaño del Modelo", + "desc": "Tamaño del modelo a ser utilizado para el reconocimiento facial.", + "small": { + "title": "pequeño", + "desc": "Usar la opción small emplea un modelo de FaceNet para embeddings faciales que se ejecuta de manera eficiente en la mayoría de las CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar la opción large emplea un modelo de embeddings faciales ArcFace y se ejecutará automáticamente en la GPU, si está disponible." + } + }, + "desc": "El reconocimiento facial permite asignar nombres a las personas y, cuando su rostro es reconocido, Frigate asignará el nombre de la persona como una subetiqueta (sub label). Esta información se incluye en la interfaz de usuario, los filtros y también en las notificaciones." + }, + "licensePlateRecognition": { + "title": "Reconocimiento de Matrículas (LPR)", + "readTheDocumentation": "Leer la Documentación", + "desc": "Frigate puede reconocer matrículas de vehículos y agregar automáticamente los caracteres detectados al campo recognized_license_plate, o bien asignar un nombre conocido como sub-etiqueta (sub_label) a los objetos de tipo coche (car). Un caso de uso común es leer las matrículas de los autos que ingresan a una cochera o que pasan por una calle." + }, + "restart_required": "Es necesario reiniciar Frigate (La configuración de Enrichments han cambiado)", + "toast": { + "success": "Los ajustes de enriquecimientos se han guardado. Reinicia Frigate para aplicar los cambios.", + "error": "No se pudieron guardar los cambios en la configuración: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Disparadores", + "management": { + "title": "Gestión de disparadores", + "desc": "Gestionar disparadores para {{camera}}. Usa el tipo de miniatura para activar en miniaturas similares al objeto rastreado seleccionado, y el tipo de descripción para activar en descripciones similares al texto que especifiques." + }, + "addTrigger": "Añadir Disparador", + "table": { + "name": "Nombre", + "type": "Tipo", + "content": "Contenido", + "threshold": "Umbral", + "actions": "Acciones", + "noTriggers": "No hay disparadores configurados para esta cámara.", + "edit": "Editar", + "deleteTrigger": "Eliminar Disparador", + "lastTriggered": "Última activación" + }, + "type": { + "description": "Descripción", + "thumbnail": "Miniatura" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificación" + }, + "dialog": { + "createTrigger": { + "title": "Crear Disparador", + "desc": "Crear un disparador par la cámara {{camera}}" + }, + "editTrigger": { + "title": "Editar Disparador", + "desc": "Editar configuractión del disparador para cámara {{camera}}" + }, + "deleteTrigger": { + "title": "Eliminar Disparador", + "desc": "Está seguro de que desea eliminar el disparador {{triggerName}}? Esta acción no se puede deshacer." + }, + "form": { + "name": { + "title": "Nombre", + "placeholder": "Entre nombre de disparador", + "error": { + "minLength": "El nombre debe tener al menos 2 caracteres.", + "invalidCharacters": "El nombre sólo puede contener letras, números, guiones bajos, y guiones.", + "alreadyExists": "Un disparador con este nombre ya existe para esta cámara." + } + }, + "enabled": { + "description": "Activa o desactiva este disparador" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleccione tipo de disparador" + }, + "friendly_name": { + "title": "Nombre amigable", + "placeholder": "Nombre o describa este disparador", + "description": "Un nombre o texto descriptivo amigable (opcional) para este disparador." + }, + "content": { + "title": "Contenido", + "imagePlaceholder": "Seleccione una imágen", + "textPlaceholder": "Entre contenido de texto", + "error": { + "required": "El contenido es requrido." + }, + "imageDesc": "Seleccione una imágen para iniciar esta acción cuando una imágen similar es detectada.", + "textDesc": "Entre texto para iniciar esta acción cuando la descripción de un objecto seguido similar es detectado." + }, + "threshold": { + "title": "Umbral", + "error": { + "min": "El umbral debe ser al menos 0", + "max": "El umbral debe ser al menos 1" + } + }, + "actions": { + "title": "Acciones", + "error": { + "min": "Al menos una acción debe ser seleccionada." + }, + "desc": "Por defecto, Frigate manda un mensaje MQTT por todos los disparadores. Seleccione una acción adicional que se realizará cuando este disparador se accione." + } + } + }, + "semanticSearch": { + "title": "Búsqueda semántica desactivada", + "desc": "Búsqueda semántica debe estar activada para usar Disparadores." + }, + "toast": { + "success": { + "createTrigger": "Disparador {{name}} creado exitosamente.", + "updateTrigger": "Disparador {{name}} actualizado exitosamente.", + "deleteTrigger": "Disparador {{name}} eliminado exitosamente." + }, + "error": { + "createTriggerFailed": "Fallo al crear el disparador: {{errorMessage}}", + "updateTriggerFailed": "Fallo al actualizar el disparador: {{errorMessage}}", + "deleteTriggerFailed": "Fallo al eliminar el disparador: {{errorMessage}}" + } + } + }, + "roles": { + "management": { + "title": "Administración del rol de visor", + "desc": "Administra roles de visor personalizados y sus permisos de acceso a cámaras para esta instancia de Frigate." + }, + "addRole": "Añade un rol", + "table": { + "role": "Rol", + "cameras": "Cámaras", + "actions": "Acciones", + "noRoles": "No se encontraron roles personalizados.", + "editCameras": "Edita Cámaras", + "deleteRole": "Eliminar Rol" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} creado exitosamente", + "updateCameras": "Cámara actualizada para el rol {{role}}", + "deleteRole": "Rol {{role}} eliminado exitosamente", + "userRolesUpdated_one": "{{count}} usuarios asignados a este rol han sido actualizados a 'visor', que tiene acceso a todas las cámaras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Creación de rol fallida: {{errorMessage}}", + "updateCamerasFailed": "Actualización de cámaras fallida: {{errorMessage}}", + "deleteRoleFailed": "Eliminación de rol fallida: {{errorMessage}}", + "userUpdateFailed": "Actualización de roles de usuario fallida: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Crear Nuevo Rol", + "desc": "Añadir nuevo rol y especificar permisos de acceso a cámaras." + }, + "deleteRole": { + "title": "Eliminar Rol", + "deleting": "Eliminando...", + "desc": "Esta acción no se puede deshacer. El rol va a ser eliminado permanentemente y usuarios associados serán asignados a rol de 'Visor', que les da acceso a ver todas las cámaras.", + "warn": "Estás seguro de que quieres eliminar {{role}}?" + }, + "editCameras": { + "title": "Editar cámaras de rol", + "desc": "Actualizar acceso de cámara para el rol {{role}}." + }, + "form": { + "role": { + "title": "Nombre de rol", + "placeholder": "Entre el nombre del rol", + "desc": "Solo se permiten letras, números, puntos y guión bajo.", + "roleIsRequired": "El nombre del rol es requerido", + "roleOnlyInclude": "El nombre del rol solo incluye letras, números, . o _", + "roleExists": "Un rol con este nombre ya existe." + }, + "cameras": { + "title": "Cámaras", + "desc": "Seleccione las cámaras a las que este rol tiene accceso. Al menos una cámara es requerida.", + "required": "Al menos una cámara debe ser seleccionada." + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/es/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/es/views/system.json new file mode 100644 index 0000000..e54a780 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/es/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "storage": "Estadísticas de almacenamiento - Frigate", + "general": "Estadísticas generales - Frigate", + "logs": { + "frigate": "Registros de Frigate - Frigate", + "go2rtc": "Registros de Go2RTC - Frigate", + "nginx": "Registros de Nginx - Frigate" + }, + "cameras": "Estadísticas de cámaras - Frigate", + "enrichments": "Estadísticas de Enriquecimientos - Frigate" + }, + "logs": { + "copy": { + "label": "Copiar al portapapeles", + "success": "Registros copiados al portapapeles", + "error": "No se pudieron copiar los registros al portapapeles" + }, + "type": { + "label": "Tipo", + "timestamp": "Marca de tiempo", + "tag": "Etiqueta", + "message": "Mensaje" + }, + "tips": "Los registros se están transmitiendo desde el servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Error al obtener los registros: {{errorMessage}}", + "whileStreamingLogs": "Error mientras se transmitían los registros: {{errorMessage}}" + } + }, + "download": { + "label": "Descargar registros" + } + }, + "title": "Sistema", + "metrics": "Métricas del sistema", + "general": { + "title": "General", + "detector": { + "title": "Detectores", + "inferenceSpeed": "Velocidad de inferencia del detector", + "cpuUsage": "Uso de CPU del Detector", + "memoryUsage": "Uso de Memoria del Detector", + "temperature": "Detector de Temperatura", + "cpuUsageInformation": "CPU utilizado para preparar los datos de entrada y salida desde/hacia la detección del modelo. Este valor no mide el uso de inferencia, incluso si se está utilizando una GPU o un acelerador." + }, + "hardwareInfo": { + "title": "Información de Hardware", + "gpuUsage": "Uso de GPU", + "gpuEncoder": "Codificador de GPU", + "gpuDecoder": "Decodificador de GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Salida de Vainfo", + "returnCode": "Código de Retorno: {{code}}", + "processOutput": "Salida del Proceso:", + "processError": "Error del Proceso:" + }, + "nvidiaSMIOutput": { + "cudaComputerCapability": "Capacidad de Cómputo CUDA: {{cuda_compute}}", + "title": "Salida de Nvidia SMI", + "driver": "Controlador: {{driver}}", + "name": "Nombre: {{name}}", + "vbios": "Información de VBios: {{vbios}}" + }, + "toast": { + "success": "Información de GPU copiada al portapapeles" + }, + "copyInfo": { + "label": "Copiar información de GPU" + }, + "closeInfo": { + "label": "Cerrar información de GPU" + } + }, + "gpuMemory": "Memoria de GPU", + "npuMemory": "Memoria de NPU", + "npuUsage": "Uso de NPU" + }, + "otherProcesses": { + "title": "Otros Procesos", + "processCpuUsage": "Uso de CPU del Proceso", + "processMemoryUsage": "Uso de Memoria del Proceso" + } + }, + "storage": { + "recordings": { + "title": "Grabaciones", + "tips": "Este valor representa el almacenamiento total utilizado por las grabaciones en la base de datos de Frigate. Frigate no realiza un seguimiento del uso de almacenamiento de todos los archivos en tu disco.", + "earliestRecording": "Grabación más antigua disponible:" + }, + "overview": "Resumen", + "title": "Almacenamiento", + "cameraStorage": { + "percentageOfTotalUsed": "Porcentaje del Total", + "bandwidth": "Ancho de Banda", + "camera": "Cámara", + "unused": { + "title": "No Utilizado", + "tips": "Este valor puede no representar con precisión el espacio libre disponible para Frigate si tienes otros archivos almacenados en tu disco además de las grabaciones de Frigate. Frigate no realiza un seguimiento del uso de almacenamiento fuera de sus grabaciones." + }, + "title": "Almacenamiento de la Cámara", + "storageUsed": "Almacenamiento", + "unusedStorageInformation": "Información de Almacenamiento No Utilizado" + }, + "shm": { + "title": "Asignación de SHM (memoria compartida)", + "warning": "El tamaño actual de SHM de {{total}}MB es muy pequeño. Aumente al menos a {{min_shm}}MB." + } + }, + "cameras": { + "title": "Cámaras", + "overview": "Resumen", + "info": { + "cameraProbeInfo": "Información de Sondeo de la Cámara {{camera}}", + "streamDataFromFFPROBE": "Los datos del flujo se obtienen con ffprobe.", + "codec": "Codec:", + "fetching": "Obteniendo Datos de la Cámara", + "stream": "Flujo {{idx}}", + "video": "Video:", + "fps": "FPS:", + "resolution": "Resolución:", + "error": "Error: {{error}}", + "unknown": "Desconocido", + "audio": "Audio:", + "tips": { + "title": "Información de Sondeo de la Cámara" + }, + "aspectRatio": "Relación de aspecto" + }, + "framesAndDetections": "Fotogramas / Detecciones", + "label": { + "camera": "cámara", + "skipped": "omitido", + "detect": "detectar", + "ffmpeg": "FFmpeg", + "capture": "captura", + "overallFramesPerSecond": "cuadros por segundo totales", + "overallDetectionsPerSecond": "detecciones por segundo totales", + "cameraSkippedDetectionsPerSecond": "{{camName}} detecciones omitidas por segundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} captura", + "cameraDetect": "{{camName}} detectar", + "cameraFramesPerSecond": "{{camName}} cuadros por segundo", + "cameraDetectionsPerSecond": "{{camName}} detecciones por segundo", + "overallSkippedDetectionsPerSecond": "detecciones omitidas por segundo totales" + }, + "toast": { + "success": { + "copyToClipboard": "Datos de sondeo copiados al portapapeles." + }, + "error": { + "unableToProbeCamera": "No se pudo sondear la cámara: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Última actualización: ", + "enrichments": { + "infPerSecond": "Inferencias Por Segundo", + "embeddings": { + "plate_recognition_speed": "Velocidad de Reconocimiento de Matrículas", + "face_embedding_speed": "Velocidad de Incrustación de Rostros", + "image_embedding_speed": "Velocidad de Incrustación de Imágenes", + "text_embedding_speed": "Velocidad de Incrustación de Texto", + "face_recognition_speed": "Velocidad de Reconocimiento Facial", + "text_embedding": "Incrustación de Texto", + "face_recognition": "Reconocimiento Facial", + "plate_recognition": "Reconocimiento de Matrículas", + "yolov9_plate_detection": "Detección de Matrículas YOLOv9", + "image_embedding": "Incrustación de Imágenes", + "yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9" + }, + "title": "Enriquecimientos" + }, + "stats": { + "ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por detección ({{detectAvg}}%)", + "healthy": "El sistema está saludable", + "reindexingEmbeddings": "Reindexando incrustaciones ({{processed}}% completado)", + "detectIsSlow": "{{detect}} es lento ({{speed}} ms)", + "cameraIsOffline": "{{camera}} está desconectada", + "detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)", + "shmTooLow": "Asignación de /dev/shm ({{total}} MB) debe aumentarse al menos a {{min}} MB." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/audio.json b/sam2-cpu/frigate-dev/web/public/locales/et/audio.json new file mode 100644 index 0000000..8248b37 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/audio.json @@ -0,0 +1,28 @@ +{ + "bicycle": "Jalgratas", + "car": "Auto", + "motorcycle": "Mootorratas", + "bus": "Buss", + "train": "Rong", + "boat": "Väike laev", + "bird": "Lind", + "cat": "Kass", + "dog": "Koer", + "horse": "Hobune", + "sheep": "Lammas", + "skateboard": "Rula", + "breathing": "Hingamine", + "wheeze": "Kähinal hingamine", + "snoring": "Norskamine", + "pets": "Lemmikloomad", + "animal": "Loom", + "children_playing": "Laste mängimine", + "crowd": "Rahvamass", + "applause": "Plaksutamine", + "heartbeat": "Südamelöök", + "heart_murmur": "Südamekahin", + "clapping": "Käteplagin", + "finger_snapping": "Sõrmede naksutamine", + "hands": "Käed", + "camera": "Kaamera" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/common.json b/sam2-cpu/frigate-dev/web/public/locales/et/common.json new file mode 100644 index 0000000..3a6fcbf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/common.json @@ -0,0 +1,118 @@ +{ + "time": { + "untilForTime": "Kuni {{time}}", + "today": "Täna", + "untilForRestart": "Kuni Frigate käivitub uuesti.", + "untilRestart": "Kuni uuesti käivitamiseni", + "ago": "{{timeAgo}} tagasi", + "justNow": "Hetk tagasi", + "yesterday": "Eile", + "last7": "Viimase 7 päeva jooksul", + "last14": "Viimase 14 päeva jooksul", + "last30": "Viimase 30 päeva jooksul", + "thisWeek": "Sel nädalal", + "lastWeek": "Eelmisel nädalal", + "thisMonth": "Sel kuul", + "lastMonth": "Eelmisel kuul", + "5minutes": "5 minutit", + "10minutes": "10 minutit", + "30minutes": "30 minutit", + "1hour": "1 tund", + "12hours": "12 tundi", + "24hours": "24 tundi", + "pm": "pl", + "am": "el", + "yr": "{{time}} a", + "year_one": "{{time}} aasta", + "year_other": "{{time}} aastat", + "mo": "{{time}} k", + "month_one": "{{time}} kuu", + "month_other": "{{time}} kuud", + "d": "{{time}} pv", + "day_one": "{{time}} päev", + "day_other": "{{time}} päeva", + "h": "{{time}} t", + "hour_one": "{{time}} tund", + "hour_other": "{{time}} tundi", + "m": "{{time}} min", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minutit", + "s": "{{time}} sek", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekundit" + }, + "menu": { + "user": { + "setPassword": "Lisa salasõna" + }, + "live": { + "allCameras": "Kõik kaamerad" + }, + "settings": "Seadistused", + "language": { + "withSystem": { + "label": "Kasuta keele jaoks süsteemi seadistusi" + } + } + }, + "unit": { + "speed": { + "mph": "ml/t", + "kph": "km/t" + }, + "data": { + "kbps": "kB/sek", + "mbps": "MB/sek", + "gbps": "GB/sek", + "kbph": "kB/t", + "mbph": "MB/t", + "gbph": "GB/t" + } + }, + "button": { + "apply": "Rakenda", + "reset": "Lähtesta", + "done": "Valmis", + "enabled": "Kasutusel", + "enable": "Võta kasutusele", + "disabled": "Pole kasutusel", + "disable": "Eemalda kasutuselt", + "save": "Salvesta", + "saving": "Salvestan…", + "cancel": "Katkesta", + "close": "Sulge", + "copy": "Kopeeri", + "back": "Tagasi", + "history": "Ajalugu", + "fullscreen": "Täisekraanivaade", + "exitFullscreen": "Välju täisekraanivaatest", + "pictureInPicture": "Pilt pildis vaade", + "twoWayTalk": "Kahepoolne kõneside", + "cameraAudio": "Kaamera heli", + "on": "SEES", + "off": "VÄLJAS", + "edit": "Muuda", + "copyCoordinates": "Kopeeri koordinaadid", + "delete": "Kustuta", + "yes": "Jah", + "no": "Ei", + "download": "Laadi alla", + "info": "Teave" + }, + "label": { + "back": "Mine tagasi", + "hide": "Peida: {{item}}", + "show": "Näita: {{item}}", + "all": "Kõik", + "ID": "Tunnus", + "none": "Puudub" + }, + "list": { + "two": "{{0}} ja {{1}}", + "many": "{{items}} ja {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Valikuline" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/auth.json new file mode 100644 index 0000000..f5588cd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Salasõna", + "errors": { + "passwordRequired": "Salasõna on vajalik", + "usernameRequired": "Kasutajanimi on vajalik", + "rateLimit": "Lubatud päringute ülempiir on käes. Proovi hiljem uuesti.", + "loginFailed": "Sisselogimine ei õnnestunud", + "unknownError": "Tundmatu viga. Lisateavet leiad logidest.", + "webUnknownError": "Tundmatu viga. Lisateavet leiad konsooli logidest." + }, + "user": "Kasutajanimi", + "login": "Logi sisse", + "firstTimeLogin": "Kas proovid esimest korda logida sisse? Kasutajanimi ja salasõna leiduvad Frigate'i logides." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/camera.json new file mode 100644 index 0000000..a62fe5a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/camera.json @@ -0,0 +1,53 @@ +{ + "group": { + "label": "Kaameragrupid", + "camera": { + "setting": { + "label": "Kaamerate voogedastuse seadistused", + "title": "Voogedastuse seadistused: {{cameraName}}", + "stream": "Voogedastus", + "placeholder": "Vali videovoog", + "streamMethod": { + "label": "Voogedastuse meetod", + "placeholder": "Vali voogedastuse meetod" + } + } + }, + "add": "Lisa kaameragrupp", + "edit": "Muuda kaameragruppi", + "delete": { + "label": "Kustuta kaameragrupp", + "confirm": { + "title": "Kinnita kustutamine", + "desc": "Kas oled kindel, et soovid kustutada kaameragrupi: {{name}}?" + } + }, + "name": { + "label": "Nimi", + "placeholder": "Sisesta nimi…", + "errorMessage": { + "mustLeastCharacters": "Kaameragrupi nimi peab olema vähemalt 2 tähemärki pikk.", + "exists": "Sellise nimega kaameragrupp on juba olemas.", + "nameMustNotPeriod": "Kaameragrupi nimes ei tohi olla tühikuid.", + "invalid": "Vigane kaameragrupi nimi." + } + }, + "cameras": { + "label": "Kaamerad", + "desc": "Vali kaamerad selle grupi jaoks." + }, + "icon": "Ikoon", + "success": "Kaameragrupp ({{name}}) on salvestatud." + }, + "debug": { + "options": { + "label": "Seadistused", + "title": "Valikud", + "showOptions": "Näita valikuid", + "hideOptions": "Peida valikud" + }, + "boundingBox": "Piirdekast", + "timestamp": "Ajatempel", + "zones": "Tsoonid" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/dialog.json new file mode 100644 index 0000000..9e03b62 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/dialog.json @@ -0,0 +1,6 @@ +{ + "restart": { + "title": "Kas oled kindel, et soovid Frigate'i uuesti käivitada?", + "button": "Käivita uuesti" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/filter.json new file mode 100644 index 0000000..9782f8d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/filter.json @@ -0,0 +1,57 @@ +{ + "filter": "Filter", + "trackedObjectDelete": { + "toast": { + "error": "Jälgitavate objektide kustutamine ei õnnestunud: {{errorMessage}}", + "success": "Jälgitavate objektide kustutamine õnnestus." + } + }, + "cameras": { + "all": { + "title": "Kõik kaamerad", + "short": "Kaamerad" + } + }, + "labels": { + "all": { + "title": "Kõik sildid", + "short": "Sildid" + } + }, + "subLabels": { + "all": "Kõik alamsildid" + }, + "dates": { + "all": { + "title": "Kõik kuupäevad", + "short": "Kuupäevad" + } + }, + "explore": { + "settings": { + "title": "Seadistused", + "defaultView": { + "title": "Vaikimisi vaade", + "summary": "Kokkuvõte", + "unfilteredGrid": "Filtreerimata ruudustik" + }, + "gridColumns": { + "title": "Ruudustiku veerud", + "desc": "Vali ruudustikus kuvatavate veergude arv." + }, + "searchSource": { + "options": { + "thumbnailImage": "Pisipilt", + "description": "Kirjeldus" + } + } + } + }, + "logSettings": { + "loading": { + "title": "Laadin" + }, + "disableLogStreaming": "Keela logi voogedastus", + "allLogs": "Kõik logid" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/icons.json new file mode 100644 index 0000000..af0569f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Vali ikoon", + "search": { + "placeholder": "Otsi ikooni…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/input.json new file mode 100644 index 0000000..127c8c7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Laadi video alla", + "toast": { + "success": "Sinu ülevaatamisel video allalaadimine algas." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/et/components/player.json new file mode 100644 index 0000000..c70da87 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/components/player.json @@ -0,0 +1,3 @@ +{ + "noRecordingsFoundForThisTime": "Hetkel ei leidu ühtego salvestust" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/objects.json b/sam2-cpu/frigate-dev/web/public/locales/et/objects.json new file mode 100644 index 0000000..efe92e7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/objects.json @@ -0,0 +1,52 @@ +{ + "person": "Inimene", + "bicycle": "Jalgratas", + "car": "Auto", + "motorcycle": "Mootorratas", + "airplane": "Lennuk", + "bus": "Buss", + "train": "Rong", + "boat": "Väike laev", + "traffic_light": "Valgusfoor", + "fire_hydrant": "Tuletõrjehüdrant", + "street_sign": "Liiklusmärk", + "stop_sign": "Stoppmärk", + "parking_meter": "Parkimispiletite automaat", + "bench": "Istepink", + "bird": "Lind", + "cat": "Kass", + "dog": "Koer", + "horse": "Hobune", + "sheep": "Lammas", + "cow": "Lehm", + "elephant": "Elevant", + "bear": "Karu", + "zebra": "Sebra", + "giraffe": "Kaelkirjak", + "hat": "Müts", + "backpack": "Seljakott", + "umbrella": "Vihmavari", + "shoe": "King", + "eye_glasses": "Prillid", + "handbag": "Käekott", + "tie": "Lips", + "suitcase": "Kohver", + "frisbee": "Lendav taldrik", + "skis": "Suusad", + "snowboard": "Lumelaud", + "sports_ball": "Pall", + "kite": "Tuulelohe", + "baseball_bat": "Pesapallikurikas", + "baseball_glove": "Pesapallikinnas", + "skateboard": "Rula", + "surfboard": "Surfilaud", + "tennis_racket": "Tennisereket", + "animal": "Loom", + "bottle": "Pudel", + "plate": "Taldrik", + "wine_glass": "Veiniklaas", + "cup": "Kruus", + "fork": "Kahvel", + "knife": "Nuga", + "spoon": "Lusikas" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/classificationModel.json new file mode 100644 index 0000000..3d71426 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/classificationModel.json @@ -0,0 +1,8 @@ +{ + "toast": { + "success": { + "deletedModel_one": "{{count}} mudeli kustutamine õnnestus", + "deletedModel_other": "{{count}} mudeli kustutamine õnnestus" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/events.json new file mode 100644 index 0000000..a9a849b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/events.json @@ -0,0 +1,31 @@ +{ + "alerts": "Häired", + "allCameras": "Kõik kaamerad", + "detail": { + "settings": "Üksikasjaliku vaate seadistused" + }, + "detections": "Tuvastamise tulemused", + "motion": { + "label": "Liikumine", + "only": "Vaid liikumine" + }, + "empty": { + "alert": "Ülevaatamiseks ei leidu ühtegi häiret", + "detection": "Ülevaatamiseks ei leidu ühtegi tuvastamist", + "motion": "Liikumise andmeid ei leidu" + }, + "select_all": "Kõik", + "camera": "Kaamera", + "detected": "tuvastatud", + "normalActivity": "Tavaline", + "needsReview": "Vajab ülevaatamist", + "securityConcern": "Võib olla turvaprobleem", + "timeline": "Ajajoon", + "timeline.aria": "Vali ajajoon", + "zoomIn": "Suumi sisse", + "zoomOut": "Suumi välja", + "events": { + "label": "Sündmused", + "aria": "Vali sündmused" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/explore.json new file mode 100644 index 0000000..d31e353 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/explore.json @@ -0,0 +1,19 @@ +{ + "trackedObjectsCount_one": "{{count}} jälgitav objekt ", + "trackedObjectsCount_other": "{{count}} jälgitavat objekti ", + "fetchingTrackedObjectsFailed": "Viga jälgitavate objektide laadimisel: {{errorMessage}}", + "noTrackedObjects": "Ühtegi jälgitavat objekti ei leidunud", + "itemMenu": { + "findSimilar": { + "aria": "Otsi sarnaseid jälgitavaid objekte" + } + }, + "trackingDetails": { + "annotationSettings": { + "showAllZones": { + "title": "Näita kõiki tsoone", + "desc": "Kui objekt on sisenenud tsooni, siis alati näida tsooni märgistust." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/exports.json new file mode 100644 index 0000000..5681453 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Eksport Frigate'ist", + "search": "Otsi", + "noExports": "Eksporditud sisu ei leidu", + "deleteExport": "Kustuta eksporditud sisu", + "deleteExport.desc": "Kas sa oled kindel et soovid „{{exportName}}“ kustutada?", + "editExport": { + "title": "Muuda eksporditud sisu nime", + "desc": "Sisesta eksporditud sisu jaoks uus nimi.", + "saveExport": "Salvesta eksporditud sisu" + }, + "tooltip": { + "shareExport": "Jaga eksporditud sisu", + "downloadVideo": "Laadi video alla", + "editName": "Muuda nime", + "deleteExport": "Kustuta eksporditud sisu" + }, + "toast": { + "error": { + "renameExportFailed": "Eksporditud sisu nime muutmine ei õnnestunud: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/faceLibrary.json new file mode 100644 index 0000000..68d072c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/faceLibrary.json @@ -0,0 +1,5 @@ +{ + "button": { + "uploadImage": "Laadi pilt üles" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/live.json new file mode 100644 index 0000000..b98e335 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/live.json @@ -0,0 +1,14 @@ +{ + "muteCameras": { + "enable": "Summuta kõik kaamerad", + "disable": "Lõpeta kõikide kaamerate summutamine" + }, + "streamingSettings": "Voogedastuse seadistused", + "cameraSettings": { + "title": "Seadistused: {{camera}}", + "cameraEnabled": "Kaamera on kasutusel", + "objectDetection": "Objektide tuvastamine", + "audioDetection": "Heli tuvastus", + "transcription": "Heli üleskirjutus" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/recording.json new file mode 100644 index 0000000..57ed975 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Ekspordi", + "calendar": "Kalender", + "filter": "Filter", + "filters": "Filtrid", + "toast": { + "error": { + "noValidTimeSelected": "Ühtegi kehtivat ajavahemikku pole valitud", + "endTimeMustAfterStartTime": "Ajavahemiku lõpp peab olema peale algust" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/search.json new file mode 100644 index 0000000..0a99c54 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/search.json @@ -0,0 +1,6 @@ +{ + "placeholder": { + "search": "Otsi…" + }, + "search": "Otsi" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/settings.json new file mode 100644 index 0000000..71d987d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/settings.json @@ -0,0 +1,139 @@ +{ + "cameraWizard": { + "step1": { + "password": "Salasõna", + "passwordPlaceholder": "Valikuline", + "customUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht", + "connectionSettings": "Ühenduse seadistused" + }, + "step3": { + "streamUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht" + } + }, + "users": { + "updatePassword": "Muuda salasõna", + "toast": { + "success": { + "updatePassword": "Salasõna muutmine õnnestus." + }, + "error": { + "setPasswordFailed": "Salasõna salvestamine ei õnnestunud: {{errorMessage}}" + } + }, + "table": { + "password": "Salasõna" + }, + "dialog": { + "form": { + "password": { + "title": "Salasõna", + "placeholder": "Sisesta salasõna", + "confirm": { + "title": "Korda salasõna", + "placeholder": "Korda salasõna" + }, + "strength": { + "title": "Salasõna tugevus: ", + "weak": "Nõrk", + "medium": "Keskmime", + "strong": "Tugev", + "veryStrong": "Väga tugev" + }, + "match": "Salasõnad klapivad omavahel", + "notMatch": "Salasõnad ei klapi omavahel", + "show": "Näita salasõna", + "hide": "Peida salasõna", + "requirements": { + "title": "Salasõna reeglid:", + "length": "Vähemalt 8 tähemärki", + "uppercase": "Vähemalt üks suurtäht", + "digit": "Vähemalt üks number", + "special": "Vähemalt üks erimärk (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Uus salasõna", + "placeholder": "Sisesta uus salasõna", + "confirm": { + "placeholder": "Sisesta uus salasõna uuesti" + } + }, + "passwordIsRequired": "Salasõna on vajalik", + "currentPassword": { + "title": "Senine salasõna", + "placeholder": "Sisesta oma senine salasõna" + } + }, + "createUser": { + "confirmPassword": "Palun kinnita oma uus salasõna" + }, + "passwordSetting": { + "cannotBeEmpty": "Salasõna ei või jääda tühjaks", + "doNotMatch": "Salasõnad ei klapi omavahel", + "updatePassword": "Muuda kasutaja {{username}} salasõna", + "setPassword": "Sisesta salasõna", + "desc": "Selle kasutajakonto turvalisuse tagamiseks lisa tugev salasõna.", + "currentPasswordRequired": "Senine salasõna on vajalik", + "incorrectCurrentPassword": "Senine salasõna pole õige", + "passwordVerificationFailed": "Salasõna kontrollimine ei õnnestunud" + } + } + }, + "debug": { + "boundingBoxes": { + "desc": "Näita jälgitavate objektide ümber märgiskaste" + } + }, + "documentTitle": { + "default": "Seadistused - Frigate", + "authentication": "Autentimise seadistused - Frigate", + "cameraReview": "Kaamerate kordusvaatuste seadistused - Frigate", + "general": "Kasutajaliidese seadistused - Frigate", + "frigatePlus": "Frigate+ seadistused - Frigate", + "notifications": "Teavituste seadistused - Frigate" + }, + "general": { + "title": "Kasutajaliidese seadistused", + "cameraGroupStreaming": { + "clearAll": "Kustuta kõik voogedastuse seadistused" + } + }, + "cameraManagement": { + "backToSettings": "Tagasi kaameraseadistuste juurde" + }, + "notification": { + "notificationSettings": { + "title": "Teavituste seadistused" + }, + "globalSettings": { + "title": "Üldseadistused" + }, + "deviceSpecific": "Seadmekohased seadistused", + "toast": { + "success": { + "settingSaved": "Teavituste seadistused on salvestatud." + } + } + }, + "frigatePlus": { + "title": "Frigate+ seadistused", + "unsavedChanges": "Frigate+ seadistuste muudatused on salvestamata", + "toast": { + "success": "Frigate+ seadistuste muudatused on salvestatud. Muudatuste kasutuselevõtmiseks käivita Frigate uuesti." + } + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + }, + "motionMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + }, + "objectMasks": { + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkti" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/et/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/et/views/system.json new file mode 100644 index 0000000..2a258b6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/et/views/system.json @@ -0,0 +1,5 @@ +{ + "documentTitle": { + "general": "Üldine statistika - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/audio.json b/sam2-cpu/frigate-dev/web/public/locales/fa/audio.json new file mode 100644 index 0000000..965460f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/audio.json @@ -0,0 +1,27 @@ +{ + "speech": "گفتار", + "babbling": "پر حرفی", + "yell": "فریاد", + "bellow": "صدای نعره", + "whoop": "ضجه", + "whispering": "غیبت کردن", + "laughter": "خنده", + "snicker": "پوزخند", + "crying": "گریه کردن", + "sigh": "حسرت", + "singing": "خواندن آواز", + "choir": "آواز گروهی", + "yodeling": "عیاشی", + "chant": "مناجات", + "mantra": "مانترا", + "cat": "گربه", + "dog": "سگ", + "horse": "اسب", + "bird": "پرنده", + "boat": "قایق", + "car": "ماشین", + "bus": "اتوبوس", + "motorcycle": "موتور سیکلت", + "train": "قطار", + "bicycle": "دوچرخه" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/common.json b/sam2-cpu/frigate-dev/web/public/locales/fa/common.json new file mode 100644 index 0000000..e3b44ba --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/common.json @@ -0,0 +1,7 @@ +{ + "time": { + "untilForTime": "تا {{time}}", + "untilForRestart": "تا زمانی که فریگیت دوباره شروع به کار کند.", + "untilRestart": "تا زمان ری‌استارت" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/auth.json new file mode 100644 index 0000000..b74e4b0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/auth.json @@ -0,0 +1,7 @@ +{ + "form": { + "user": "نام کاربری", + "password": "رمز عبور", + "login": "ورود" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/camera.json new file mode 100644 index 0000000..b556792 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/camera.json @@ -0,0 +1,7 @@ +{ + "group": { + "label": "گروه‌های دوربین", + "add": "افزودن گروه دوربین", + "edit": "ویرایش گروه دوربین" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/dialog.json new file mode 100644 index 0000000..05f66ce --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/dialog.json @@ -0,0 +1,9 @@ +{ + "restart": { + "title": "آیا از ری‌استارت فریگیت اطمینان دارید؟", + "button": "ری‌استارت", + "restarting": { + "title": "فریگیت در حال ری‌استارت شدن" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/filter.json new file mode 100644 index 0000000..97410c8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/filter.json @@ -0,0 +1,6 @@ +{ + "filter": "فیلتر", + "classes": { + "label": "کلاس‌ها" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/icons.json new file mode 100644 index 0000000..20111cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "انتخاب آیکون", + "search": { + "placeholder": "جستجو برای آیکون" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/input.json new file mode 100644 index 0000000..20de892 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "دریافت ویدیو", + "toast": { + "success": "ویدیوی مورد بررسی شما درحال دریافت می‌باشد." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/fa/components/player.json new file mode 100644 index 0000000..dae6c8b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/components/player.json @@ -0,0 +1,5 @@ +{ + "noRecordingsFoundForThisTime": "ویدیویی برای این زمان وجود ندارد", + "noPreviewFound": "پیش‌نمایش پیدا نشد", + "noPreviewFoundFor": "هیچ پیش‌نمایشی برای {{cameraName}} پیدا نشد" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/objects.json b/sam2-cpu/frigate-dev/web/public/locales/fa/objects.json new file mode 100644 index 0000000..278086d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/objects.json @@ -0,0 +1,20 @@ +{ + "person": "شخص", + "bicycle": "دوچرخه", + "car": "ماشین", + "airplane": "هواپیما", + "bus": "اتوبوس", + "train": "قطار", + "boat": "قایق", + "traffic_light": "چراغ راهنمایی", + "motorcycle": "موتور سیکلت", + "fire_hydrant": "شیر آتش‌نشانی", + "street_sign": "تابلو راهنمایی رانندگی", + "stop_sign": "تابلو ایست", + "parking_meter": "پارکومتر", + "bench": "نیمکت", + "bird": "پرنده", + "cat": "گربه", + "dog": "سگ", + "horse": "اسب" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/classificationModel.json new file mode 100644 index 0000000..a4bdd37 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/classificationModel.json @@ -0,0 +1,22 @@ +{ + "button": { + "deleteClassificationAttempts": "حذف تصاویر طبقه بندی", + "renameCategory": "تغییر نام کلاس", + "deleteCategory": "حذف کردن کلاس", + "deleteImages": "حذف کردن عکس ها", + "trainModel": "مدل آموزش" + }, + "toast": { + "success": { + "deletedCategory": "کلاس حذف شده", + "deletedImage": "عکس های حذف شده", + "categorizedImage": "تصویر طبقه بندی شده", + "trainedModel": "مدل آموزش دیده شده.", + "trainingModel": "آموزش دادن مدل با موفقیت شروع شد." + }, + "error": { + "deleteImageFailed": "حذف نشد:{{پیغام خطا}}", + "deleteCategoryFailed": "کلاس حذف نشد:{{پیغام خطا}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/configEditor.json new file mode 100644 index 0000000..0a57836 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/configEditor.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "ویرایشگر کانفیگ - فریگیت", + "configEditor": "ویرایشگر کانفیگ" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/events.json new file mode 100644 index 0000000..4fc9338 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/events.json @@ -0,0 +1,7 @@ +{ + "alerts": "هشدار‌ها", + "detections": "تشخیص‌ها", + "motion": { + "label": "حرکت" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/explore.json new file mode 100644 index 0000000..8cbff25 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/explore.json @@ -0,0 +1,4 @@ +{ + "generativeAI": "هوش مصنوعی تولید کننده", + "documentTitle": "کاوش کردن - فرایگیت" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/exports.json new file mode 100644 index 0000000..a025b07 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/exports.json @@ -0,0 +1,5 @@ +{ + "search": "جستجو", + "documentTitle": "گرفتن خروجی - فریگیت", + "noExports": "هیچ خروجی یافت نشد" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/faceLibrary.json new file mode 100644 index 0000000..d14ad4f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/faceLibrary.json @@ -0,0 +1,6 @@ +{ + "description": { + "addFace": "مراحل اضافه کردن یک مجموعه جدید به کتابخانه چهره را دنبال کنید.", + "placeholder": "نامی برای این مجموعه وارد کنید" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/live.json new file mode 100644 index 0000000..b459a7c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/live.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "زنده - فریگیت", + "documentTitle.withCamera": "{{camera}} - زنده - فریگیت" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/recording.json new file mode 100644 index 0000000..5dbfe95 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/recording.json @@ -0,0 +1,5 @@ +{ + "filter": "فیلتر", + "export": "گرفتن خروجی", + "calendar": "تفویم" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/search.json new file mode 100644 index 0000000..b4931ae --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/search.json @@ -0,0 +1,5 @@ +{ + "search": "جستجو", + "savedSearches": "جستجوهای ذخیره شده", + "searchFor": "جستجو برای {{inputValue}}" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/settings.json new file mode 100644 index 0000000..ea7753a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/settings.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "default": "تنظیمات - فریگیت", + "authentication": "تنظیمات احراز هویت - فریگیت", + "camera": "تنظیمات دوربین - فریگیت" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fa/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/fa/views/system.json new file mode 100644 index 0000000..4cc9550 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fa/views/system.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "cameras": "آمار دوربین‌ها - فریگیت", + "storage": "آمار حافظه - فریگیت", + "general": "آمار عمومی - فریگیت" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/audio.json b/sam2-cpu/frigate-dev/web/public/locales/fi/audio.json new file mode 100644 index 0000000..f066503 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/audio.json @@ -0,0 +1,165 @@ +{ + "speech": "Puhe", + "yell": "Huutaa", + "babbling": "Pulina", + "boat": "Vene", + "cat": "Kissa", + "dog": "Koira", + "horse": "Hevonen", + "sheep": "Lammas", + "bird": "Lintu", + "car": "Auto", + "motorcycle": "Moottoripyörä", + "bus": "Bussi", + "train": "Juna", + "bicycle": "Pyörä", + "skateboard": "Rullalauta", + "bellow": "Karjua", + "whoop": "Huutaa", + "whispering": "Kuiskaus", + "laughter": "Nauru", + "snicker": "Hihittää", + "crying": "Itku", + "sigh": "Huokaus", + "singing": "Laulu", + "choir": "Kuoro", + "yodeling": "Jodlata", + "camera": "Kamera", + "animal": "Eläin", + "goat": "Vuohi", + "mouse": "Hiiri", + "keyboard": "Näppäimistö", + "vehicle": "Ajoneuvo", + "door": "Ovi", + "sink": "Lavuaari", + "blender": "Tehosekoitin", + "scissors": "Sakset", + "hair_dryer": "Hiustenkuivaaja", + "toothbrush": "Hammasharja", + "clock": "Kello", + "bark": "Haukku", + "chant": "Laulaa", + "mantra": "Mantra", + "child_singing": "Lapsi laulaa", + "synthetic_singing": "Synteettinen laulu", + "rapping": "Räppi", + "humming": "Humina", + "groan": "Notkua", + "grunt": "Murahtaa", + "whistling": "Vihellys", + "breathing": "Hengitys", + "wheeze": "Vinkua", + "snoring": "Kuorsaus", + "gasp": "Haukkua", + "pant": "Huohottaa", + "snort": "Haukkua", + "cough": "Yskä", + "sneeze": "Niistää", + "throat_clearing": "Kurkun selvittäminen", + "sniff": "Nuuhkia", + "run": "Juokse", + "shuffle": "Sekoitus", + "hiccup": "Hikka", + "radio": "Radio", + "television": "Televisio", + "environmental_noise": "Ympäristön melu", + "sound_effect": "Äänitehoste", + "silence": "Hiljaisuus", + "glass": "Lasi", + "wood": "Puu", + "eruption": "Purkaus", + "firecracker": "Sähikäinen", + "fireworks": "Ilotulitus", + "artillery_fire": "Tykistötuli", + "machine_gun": "Konekivääri", + "explosion": "Räjähdys", + "drill": "Pora", + "sanding": "Hionta", + "sawing": "Sahaus", + "hammer": "Vasara", + "tools": "Työkalut", + "printer": "Tulostin", + "cash_register": "Kassakone", + "air_conditioning": "Ilmastointi", + "mechanical_fan": "Mekaaninen tuuletin", + "sewing_machine": "Ompelukone", + "gears": "Hammasrattaat", + "ratchet": "Räikkä", + "pigeon": "Kyyhkynen", + "crow": "Varis", + "owl": "Pöllö", + "flapping_wings": "Siipien räpyttely", + "dogs": "Koirat", + "rats": "Rotat", + "insect": "Hyönteinen", + "cricket": "Sirkka", + "mosquito": "Hyttynen", + "fly": "Kärpänen", + "footsteps": "Askelia", + "chewing": "Pureskelu", + "biting": "Pureminen", + "gargling": "Kurlaus", + "stomach_rumble": "Vatsan kurina", + "burping": "Röyhtäily", + "fart": "Pieru", + "hands": "Kädet", + "finger_snapping": "Sormien napsauttaminen", + "clapping": "Taputtaminen", + "heartbeat": "Sydämenlyönti", + "cheering": "Hurraus", + "applause": "Aplodit", + "crowd": "Väkijoukko", + "children_playing": "Lapset leikkivät", + "pets": "Lemmikit", + "whimper_dog": "Koiran vinkuminen", + "meow": "Miau", + "livestock": "Karja", + "cattle": "Nautakarja", + "cowbell": "Lehmänkello", + "pig": "Sika", + "chicken": "Kana", + "duck": "Ankka", + "frog": "Sammakko", + "snake": "Käärme", + "music": "Musiikki", + "musical_instrument": "Musiikki-instrumentti", + "guitar": "Kitara", + "electric_guitar": "Sähkökitara", + "bass_guitar": "Bassokitara", + "acoustic_guitar": "Akustinen kitara", + "tapping": "Napauttaminen", + "piano": "Piano", + "electric_piano": "Sähköpiano", + "organ": "Urku", + "synthesizer": "Syntetisaattori", + "drum_kit": "Rumpusetti", + "drum": "Rumpu", + "wood_block": "Puupalikka", + "steelpan": "Teräspannu", + "trumpet": "Trumpetti", + "violin": "Viulu", + "cello": "Sello", + "flute": "Huilu", + "saxophone": "Saksofoni", + "clarinet": "Klarinetti", + "harp": "Harppu", + "bell": "Kello", + "church_bell": "Kirkonkello", + "bicycle_bell": "Polkupyörän kello", + "tuning_fork": "Virityshaarukka", + "pop_music": "Popmusiikki", + "hip_hop_music": "Hiphop-musiikki", + "rock_music": "Rock-musiikki", + "heavy_metal": "Heavy metal", + "punk_rock": "Punkrock", + "rock_and_roll": "Rock and Roll", + "scream": "Huutaa", + "accelerating": "Kiihdyttäminen", + "air_brake": "Ilmajarru", + "aircraft": "Ilma-alus", + "aircraft_engine": "Lentokoneen moottori", + "alarm": "Hälytys", + "ambient_music": "Tunnelmamusiikki", + "ambulance": "Ambulanssi", + "angry_music": "Vihainen musiikki" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/common.json b/sam2-cpu/frigate-dev/web/public/locales/fi/common.json new file mode 100644 index 0000000..5cebc89 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/common.json @@ -0,0 +1,176 @@ +{ + "time": { + "untilRestart": "Kunnes uudelleenkäynnistyy", + "ago": "{{timeAgo}} sitten", + "justNow": "Juuri nyt", + "today": "Tänään", + "yesterday": "Eilen", + "last14": "Viimeiset 14 päivää", + "untilForTime": "Kunnes {{time}}", + "untilForRestart": "Kunnes Frigate uudelleenkäynnistyy.", + "thisWeek": "Tämä viikko", + "lastWeek": "Viime viikko", + "last7": "Viimeiset 7 päivää", + "thisMonth": "Tämä kuu", + "lastMonth": "Viime kuu", + "last30": "Viimeiset 30 päivää", + "5minutes": "5 minuuttia", + "10minutes": "10 minuuttia", + "30minutes": "30 minuuttia", + "1hour": "1 tunti", + "12hours": "12 tuntia", + "24hours": "24 tuntia", + "pm": "ip", + "am": "ap", + "yr": "{{time}}v", + "year_one": "{{time}} vuosi", + "year_other": "{{time}} vuotta", + "mo": "{{time}}kk", + "month_one": "{{time}} kuukausi", + "month_other": "{{time}} kuukaudet", + "d": "{{time}}pv", + "day_one": "{{time}} päivä", + "day_other": "{{time}} päivät", + "h": "{{time}}t", + "hour_one": "{{time}} tunti", + "hour_other": "{{time}} tuntia", + "m": "{{time}}m", + "s": "{{time}}s", + "minute_one": "{{time}}minuutti", + "minute_other": "{{time}}minuuttia", + "second_one": "{{time}}sekuntti", + "second_other": "{{time}}sekunttia", + "formattedTimestampHourMinute": { + "24hour": "HH:mm" + } + }, + "pagination": { + "next": { + "title": "Seuraava", + "label": "Mene seuraavalle sivulle" + }, + "more": "Lisää sivuja", + "previous": { + "title": "Edellinen", + "label": "Mene edelliselle sivulle" + }, + "label": "sivutus" + }, + "accessDenied": { + "documentTitle": "Pääsy kielletty - Frigate", + "title": "Pääsy kielletty", + "desc": "Sinulla ei ole oikeuksia tarkastella tätä sivua." + }, + "role": { + "admin": "Järjestelmänvalvoja", + "viewer": "Katselija", + "desc": "Järjestelmänvalvojalla on täysi käyttöoikeus kaikkiin Frigaten käyttöliittymän toimintoihin. Katselijoiden oikeudet on rajoitettu kameroiden katseluun, kohteiden arviointiin ja historian tarkasteluun.", + "title": "Rooli" + }, + "notFound": { + "documentTitle": "Ei löytynyt - Frigate", + "title": "404", + "desc": "Sivua ei löytynyt" + }, + "selectItem": "Valitse {{item}}", + "menu": { + "live": { + "title": "Suora", + "cameras": { + "title": "Kamerat", + "count_one": "{{count}} kamera", + "count_other": "{{count}} kameraa" + }, + "allCameras": "Kaikki kamerat" + }, + "explore": "Selaa", + "export": "Vienti", + "uiPlayground": "UI-leikkikenttä", + "user": { + "account": "Tili", + "current": "Nykyinen käyttäjä: {{user}}", + "anonymous": "anonyymi", + "title": "Käyttäjä", + "logout": "Kirjaudu ulos", + "setPassword": "Aseta salasana" + }, + "appearance": "Ulkonäkö", + "darkMode": { + "label": "Tumma tila", + "light": "Valoisa", + "dark": "Tumma", + "withSystem": { + "label": "Käytä järjestelmän asetuksia valoisalle tai tummalle tilalle" + } + }, + "faceLibrary": "Kasvokirjasto", + "language": { + "ca": "Katalaani", + "withSystem": { + "label": "Käytä järjestelmän asetuksia kielelle" + } + }, + "review": "Esikatselu", + "theme": { + "highcontrast": "Korkea resoluutio", + "blue": "Sininen", + "green": "Vihreä", + "default": "Oletus", + "nord": "Pohjoismainen", + "red": "Punainen", + "label": "Teema" + }, + "withSystem": "Järjestelmä", + "help": "Apua", + "documentation": { + "title": "Dokumentaatio", + "label": "Frigaten dokumentaatio" + }, + "restart": "Käynnistä uudelleen", + "languages": "Kielet", + "system": "Järjestelmä", + "settings": "Asetukset", + "configuration": "Konfiguraatio" + }, + "toast": { + "copyUrlToClipboard": "URL kopioitu leikepöydälle.", + "save": { + "title": "Tallenna", + "error": { + "title": "Konfiguraatiomuutosten tallennus epäonnistui: {{errorMessage}}", + "noMessage": "Konfiguraatiomuutosten tallennus epäonnistui" + } + } + }, + "button": { + "on": "ON", + "disabled": "Pois käytöstä", + "done": "Valmis", + "enabled": "Käytössä", + "enable": "Ota käyttöön", + "disable": "Poista käytöstä", + "save": "Tallenna", + "saving": "Tallennetaan…", + "cancel": "Peruuta", + "reset": "Nollaa", + "close": "Sulje", + "off": "OFF", + "edit": "Muokkaa", + "yes": "Kyllä", + "copy": "Kopioi", + "no": "Ei", + "download": "Lataa", + "back": "Takaisin", + "history": "Historia", + "play": "Toista", + "next": "Seuraava", + "delete": "Poista", + "info": "Info" + }, + "unit": { + "length": { + "feet": "jalka" + } + }, + "readTheDocumentation": "Lue dokumentaatio" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/auth.json new file mode 100644 index 0000000..f81993d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "password": "Salasana", + "user": "Käyttäjänimi", + "login": "Kirjaudu", + "errors": { + "usernameRequired": "Käyttäjänimi vaaditaan", + "passwordRequired": "Salasana vaaditaan", + "rateLimit": "Käyttöraja ylitetty. Yritä myöhemmin uudelleen.", + "loginFailed": "Kirjautuminen epäonnistui", + "unknownError": "Tuntematon virhe. Tarkista logit.", + "webUnknownError": "Tuntematon virhe. Tarkista konsolilogi." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/camera.json new file mode 100644 index 0000000..a641ca6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kameraryhmä", + "add": "Lisää kameraryhmä", + "edit": "Muokkaa kameraryhmää", + "delete": { + "label": "Poista kameraryhmä", + "confirm": { + "title": "Varmista poisto", + "desc": "Oletko varma että haluat poistaa kameraryhmän {{name}}?" + } + }, + "name": { + "label": "Nimi", + "placeholder": "Anna nimi…", + "errorMessage": { + "mustLeastCharacters": "Kameraryhmän nimi täytyy olla vähintään 2 kirjainta.", + "exists": "Kameraryhmän nimi on jo olemassa.", + "nameMustNotPeriod": "Kameraryhmän nimi ei voi sisältää pistettä.", + "invalid": "Virheellinen kameraryhmän nimi." + } + }, + "cameras": { + "label": "Kamerat", + "desc": "Valitse ryhmän kamera." + }, + "icon": "Ikoni", + "success": "Kameraryhmä ({{name}}) on tallennettu.", + "camera": { + "setting": { + "label": "Kameran suoratoistoasetukset", + "title": "{{cameraName}} suoratoistoasetukset", + "desc": "Muuta tämän kameraryhmän kojelaudan live-suoratoistoasetuksia.Nämä asetukset ovat laite/selainkohtaisia.", + "audioIsAvailable": "Ääni on saatavilla tähän suoratoistoon", + "audioIsUnavailable": "Ääni ei ole saatavilla tähän suoratoistoon", + "audio": { + "tips": { + "title": "Äänen on oltava kytkettynä kameraan ja määritettynä go2rtc:ssä tätä suoratoistoa varten.", + "document": "Lue dokumentaatio " + } + }, + "streamMethod": { + "label": "Suoratoistomenetelmä", + "method": { + "noStreaming": { + "label": "Ei suoratoistoa", + "desc": "Kamerakuvat päivittyvät vain kerran minuutissa, eikä suoratoistoa tapahdu." + }, + "smartStreaming": { + "label": "Älykäs suoratoisto (suositus)", + "desc": "Älykäs suoratoisto päivittää kamerakuvan kerran minuutissa kun havaittavaa toimintaa ei tapahdu, säästääkseen kaistanleveyttä ja resursseja. Kun toimintaa havaitaan, kuva vaihtuu saumattomasti reaaliaikaiseksi suoratoistoksi." + }, + "continuousStreaming": { + "label": "Jatkuva suoratoisto", + "desc": { + "title": "Kamerakuva näkyy aina reaaliaikaisena suoratoistona kojelaudassa, vaikka mitään liikettä ei havaitaisi.", + "warning": "Jatkuva suoratoisto voi lisätä kaistanleveyden käyttöä ja suorituskykyongelmia. Käytä varoen." + } + } + }, + "placeholder": "Valitse toiston tyyppi" + }, + "compatibilityMode": { + "label": "Yhteensopivuustila", + "desc": "Ota tämä vaihtoehto käyttöön vain, jos kamerasi live-suoratoistossa näkyy väriartefakteja ja kuvan oikealla puolella on vinoviiva." + }, + "stream": "Kuvavirta", + "placeholder": "Valitse kuvavirta" + }, + "birdseye": "Linnun silmä" + } + }, + "debug": { + "options": { + "label": "Asetukset", + "title": "Vaihtoehdot", + "showOptions": "Näytä vaihtoehdot", + "hideOptions": "Piilota vaihtoehdot" + }, + "boundingBox": "Rajauslaatikko", + "timestamp": "Aikaleima", + "zones": "Vyöhykkeet", + "mask": "Peite", + "motion": "Liike", + "regions": "Alueet" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/dialog.json new file mode 100644 index 0000000..819e4a5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/dialog.json @@ -0,0 +1,87 @@ +{ + "restart": { + "title": "Haluatko varmasti käynnistää Frigaten uudelleen?", + "button": "Uudelleenkäynnistys", + "restarting": { + "title": "Fregatti käynnistyy uudelleen", + "content": "Tämä sivu latautuu uudelleen {{countdown}} sekunnin kuluttua.", + "button": "Pakota uudelleenlataus nyt" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Lähetä Frigate+:lle", + "desc": "Välttämissäsi paikoissa olevat kohteet eivät ole vääriä positiivisia. Niiden lähettäminen väärinä positiivisina sekoittaa mallia." + }, + "review": { + "question": { + "label": "Vahvista tämä nimike Frigate Plussalle", + "ask_a": "Onko kohde {{label}}?", + "ask_an": "Onko tämä kohde {{label}}?", + "ask_full": "Onko tämä kohde {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Lähetetty" + } + } + }, + "video": { + "viewInHistory": "Katso historiaa" + } + }, + "export": { + "time": { + "fromTimeline": "Valitse aikajanalta", + "lastHour_one": "Viimeinen tunti", + "lastHour_other": "Viimeiset {{count}} tuntia", + "start": { + "title": "Aloitusaika", + "label": "Valitse aloitusaika" + }, + "end": { + "title": "Lopetusaika", + "label": "Valitse lopetusaika" + }, + "custom": "Mukautettu" + }, + "name": { + "placeholder": "Nimeä vienti" + }, + "select": "Valitse", + "export": "Vie", + "selectOrExport": "Valitse tai Vie", + "toast": { + "error": { + "failed": "Viennin aloitus epäonnistui: {{error}}", + "endTimeMustAfterStartTime": "Lopetusajan pitää olla aloitusajan jälkeen", + "noVaildTimeSelected": "Sopivaa aikaikkunaa ei valittuna" + }, + "success": "Vienti käynnistettiin onnistuneesti. Katso tiedosto /export kansiossa." + }, + "fromTimeline": { + "saveExport": "Tallenna vienti", + "previewExport": "Esikatsele vientiä" + } + }, + "streaming": { + "label": "Kuvavirta", + "restreaming": { + "disabled": "Uudelleentoisto ei ole käytettävissä tällä kameralla.", + "desc": { + "title": "Määritä go2rtc saadaksesi lisäreaaliaikanäkymän vaihtoehtoja ja ääntä tälle kameralle.", + "readTheDocumentation": "Lue dokumentaatio" + } + } + }, + "search": { + "saveSearch": { + "label": "Tallenna haku" + } + }, + "imagePicker": { + "search": { + "placeholder": "Hae nimikkeen tai alinimikkeen mukaan..." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/filter.json new file mode 100644 index 0000000..c3058bd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/filter.json @@ -0,0 +1,91 @@ +{ + "filter": "Suodatin", + "dates": { + "selectPreset": "Valitse esiasettelu…", + "all": { + "title": "Kaikki päivämäärät", + "short": "Päivämäärät" + } + }, + "more": "Lisää suodattimia", + "reset": { + "label": "Palauta suodattimet oletusarvoihin" + }, + "labels": { + "count_one": "{{count}} nimike", + "label": "Nimikkeet", + "all": { + "title": "Kaikki nimikkeet", + "short": "Nimikkeet" + }, + "count_other": "{{count}} nimikettä" + }, + "zones": { + "label": "Alueet", + "all": { + "title": "Kaikki alueet", + "short": "Alueet" + } + }, + "timeRange": "Aikaikkuna", + "subLabels": { + "label": "Alinimikkeet", + "all": "Kaikki alinimikkeet" + }, + "score": "Piste", + "estimatedSpeed": "Arvioitu nopeus {{unit}}", + "features": { + "label": "Piirteet", + "hasVideoClip": "Videoleike löytyy", + "submittedToFrigatePlus": { + "label": "Lähetetty Frigate+:aan", + "tips": "Sinun on ensin suodatettava seuratut kohteet, joilla on tilannekuva.

    Kohteita, joilla ei ole tilannekuvaa, ei voida lähettää Frigate+:aan." + }, + "hasSnapshot": "Tilannekuva löytyy" + }, + "sort": { + "label": "Järjestä", + "dateAsc": "Päivämäärä (Nouseva)", + "dateDesc": "Päivämäärä (Laskeva)", + "scoreAsc": "Kohteen pisteet (Nouseva)", + "scoreDesc": "Kohteen pisteet (Laskeva)", + "speedAsc": "Arvioitu nopeus (Nouseva)", + "speedDesc": "Arvioitu nopeus (Laskeva)", + "relevance": "Olennaisuus" + }, + "cameras": { + "label": "Kameran suodattimet", + "all": { + "title": "Kaikki kamerat", + "short": "Kamerat" + } + }, + "classes": { + "label": "Luokat", + "all": { + "title": "Kaikki luokat" + }, + "count_one": "{{count}} Luokka", + "count_other": "{{count}} Luokkaa" + }, + "recognizedLicensePlates": { + "clearAll": "Tyhjennä kaikki", + "title": "Tunnistetut rekisterikilvet", + "loadFailed": "Tunnistettujen rekisterikilpien lataaminen epäonnistui.", + "loading": "Ladataan tunnistettuja rekisterikilpiä…", + "placeholder": "Kirjoita hakeaksesi rekisterikilpeä…", + "noLicensePlatesFound": "Rekisterikilpiä ei löytynyt.", + "selectPlatesFromList": "Valitse yksi tai useampi rekisterikilpi luettelosta.", + "selectAll": "Valitse kaikki" + }, + "logSettings": { + "allLogs": "Kaikki lokit", + "filterBySeverity": "Suodata lokit vakavuuden mukaan" + }, + "trackedObjectDelete": { + "title": "Vahvista poisto", + "toast": { + "error": "Seurattujen kohteiden poistaminen epäonnistui: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/icons.json new file mode 100644 index 0000000..20fea8f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Valitse kuvake", + "search": { + "placeholder": "Etsi kuvaketta…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/input.json new file mode 100644 index 0000000..59083d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Lataa Video", + "toast": { + "success": "Tarkistettavan kohteen videon lataus on aloitettu." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/fi/components/player.json new file mode 100644 index 0000000..e40d8b1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Esikatselua ei löytynyt", + "noPreviewFoundFor": "Ei esikatselua {{cameraName}}lle", + "noRecordingsFoundForThisTime": "Ei tallenteita valitulta ajalta", + "submitFrigatePlus": { + "title": "Lähetä tämä kuva Frigate+:aan?", + "submit": "Lähetä" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 tai uudempi vaaditaan tälle suoratoistotyypille.", + "streamOffline": { + "title": "Kamera poissa verkosta", + "desc": "Kuvaruutuja ei vastaanotettu kameran {{cameraName}} detect-kuvavirrasta, tarkista virhelogit" + }, + "cameraDisabled": "Kamera on poistettu käytöstä", + "stats": { + "streamType": { + "title": "Kuvavirran tyyppi:", + "short": "Tyyppi" + }, + "bandwidth": { + "title": "Kaistanleveys:", + "short": "Kaistanleveys" + }, + "latency": { + "title": "Latenssi:", + "value": "{{seconds}} sekuntia", + "short": { + "value": "{{seconds}} sek", + "title": "Latenssi" + } + }, + "totalFrames": "Kehyksiä yhteensä:", + "droppedFrames": { + "title": "Pudotettuja kehyksiä:", + "short": { + "title": "Pudotettu", + "value": "{{droppedFrames}} kehystä" + } + }, + "decodedFrames": "Dekoodatut kehykset:", + "droppedFrameRate": "Pudotettujen kehysten nopeus:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Onnistuneesti lähetetty Frigate+:aan" + }, + "error": { + "submitFrigatePlusFailed": "Frigate+:aan lähetys epäonnistui" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/objects.json b/sam2-cpu/frigate-dev/web/public/locales/fi/objects.json new file mode 100644 index 0000000..524350e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/objects.json @@ -0,0 +1,120 @@ +{ + "frisbee": "Frisbee", + "knife": "Veitsi", + "umbrella": "Sateenvarjo", + "tie": "Kravatti", + "suitcase": "Matkalaukku", + "baseball_glove": "Pesäpallohanska", + "spoon": "Lusikka", + "person": "Henkilö", + "bicycle": "Pyörä", + "car": "Auto", + "motorcycle": "Moottoripyörä", + "airplane": "Lentokone", + "bus": "Bussi", + "train": "Juna", + "boat": "Vene", + "traffic_light": "Liikennevalo", + "fire_hydrant": "Paloposti", + "street_sign": "Tieviitta", + "stop_sign": "Stop merkki", + "parking_meter": "Pysäköintimittari", + "bench": "Penkki", + "bird": "Lintu", + "cat": "Kissa", + "dog": "Koira", + "horse": "Hevonen", + "sheep": "Lammas", + "cow": "Lehmä", + "elephant": "Elefantti", + "bear": "Karhu", + "zebra": "Seepra", + "giraffe": "Kirahvi", + "hat": "Hattu", + "backpack": "Reppu", + "shoe": "Kenkä", + "eye_glasses": "Silmälasit", + "handbag": "Käsilaukku", + "skis": "Sukset", + "snowboard": "Lumilauta", + "sports_ball": "Pallo", + "kite": "Leija", + "baseball_bat": "Pesäpallomaila", + "skateboard": "Rullalauta", + "surfboard": "Surffilauta", + "tennis_racket": "Tennismaila", + "bottle": "Pullo", + "plate": "Lautanen", + "wine_glass": "Viinilasi", + "cup": "Kuppi", + "fork": "Haarukka", + "bowl": "Malja", + "banana": "Banaani", + "apple": "Omena", + "couch": "Sohva", + "keyboard": "Näppäimistö", + "book": "Kirja", + "microwave": "Mikroaaltouuni", + "toaster": "Leivänpaahdin", + "refrigerator": "Jääkaappi", + "sink": "Lavuaari", + "blender": "Tehosekoitin", + "deer": "Peura", + "oven": "Uuni", + "sandwich": "Voileipä", + "orange": "Appelsiini", + "broccoli": "Parsakaali", + "carrot": "Porkkana", + "hot_dog": "Nakkisämpylä", + "pizza": "Pizza", + "donut": "Donitsi", + "cake": "Kakku", + "chair": "Tuoli", + "potted_plant": "Ruukkukasvi", + "bed": "Sänky", + "mirror": "Peili", + "dining_table": "Ruokapöytä", + "window": "Ikkuna", + "desk": "Pöytä", + "toilet": "Vessanpönttö", + "door": "Ovi", + "tv": "TV", + "mouse": "Hiiri", + "laptop": "Kannettava tietokone", + "remote": "Kaukosäädin", + "cell_phone": "Matkapuhelin", + "clock": "Kello", + "vase": "Maljakko", + "scissors": "Sakset", + "teddy_bear": "Nallekarhu", + "hair_dryer": "Hiustenkuivaaja", + "hair_brush": "Hiusharja", + "toothbrush": "Hammasharja", + "vehicle": "Ajoneuvo", + "squirrel": "Orava", + "animal": "Eläin", + "fox": "Kettu", + "goat": "Vuohi", + "bark": "Haukku", + "rabbit": "Kaniini", + "raccoon": "Pesukarhu", + "robot_lawnmower": "Robotti ruohonleikkuri", + "waste_bin": "Jäteastia", + "package": "Paketti", + "bbq_grill": "Grilli", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "gls": "GLS", + "dpd": "DPD", + "postnord": "PostNord", + "nzpost": "NZPost", + "postnl": "PostNL", + "dhl": "DHL", + "purolator": "Purolator", + "an_post": "An Post", + "license_plate": "Rekisterikilpi", + "face": "Kasvot", + "on_demand": "Pyynnöstä" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/configEditor.json new file mode 100644 index 0000000..96990e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Frigaten konfiguraatioeditori", + "confirm": "Poistu tallentamatta?", + "saveOnly": "Vain tallennus", + "toast": { + "error": { + "savingError": "Virhe tallennettaessa konfiguraatiota" + }, + "success": { + "copyToClipboard": "Konfiguraatio kopioitu leikepöydälle." + } + }, + "configEditor": "Konfiguraatioeditori", + "copyConfig": "Kopioi konfiguraatio", + "saveAndRestart": "Tallenna & uudelleenkäynnistä", + "safeConfigEditor": "Konfiguraatioeditori (vikasietotila)", + "safeModeDescription": "Frigate on vikasietotilassa konfiguraation vahvistusvirheen vuoksi." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/events.json new file mode 100644 index 0000000..57eb44a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/events.json @@ -0,0 +1,40 @@ +{ + "alerts": "Hälytyset", + "empty": { + "detection": "Ei havaintoja tarkastettavaksi", + "motion": "Ei liiketietoja", + "alert": "Ei hälyytyksiä tarkastettavaksi" + }, + "detections": "Havainnot", + "motion": { + "label": "Liike", + "only": "Vain liike" + }, + "allCameras": "Kaikki kamerat", + "timeline": "Aikajana", + "timeline.aria": "Valitse aikajana", + "events": { + "label": "Tapahtumat", + "aria": "Valitse tapahtumat", + "noFoundForTimePeriod": "Tapahtumia ei löydetty tältä ajanjaksolta." + }, + "documentTitle": "Tarkastelu - Frigate", + "detected": "havaittu", + "selected_one": "{{count}} valittu", + "selected_other": "{{count}} valittu", + "recordings": { + "documentTitle": "Tallenteet - Frigate" + }, + "calendarFilter": { + "last24Hours": "Viimeiset 24 tuntia" + }, + "markAsReviewed": "Merkitse katselmoiduksi", + "markTheseItemsAsReviewed": "Merkitse nämä kohteet katselmoiduksi", + "newReviewItems": { + "label": "Näytä uudet katselmoitavat kohteet", + "button": "Uudet katselmoitavat kohteet" + }, + "camera": "Kamera", + "suspiciousActivity": "Epäilyttävä toiminta", + "threateningActivity": "Uhkaava toiminta" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/explore.json new file mode 100644 index 0000000..25743e4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/explore.json @@ -0,0 +1,155 @@ +{ + "documentTitle": "Etsi", + "details": { + "timestamp": "Aikaleima", + "item": { + "title": "Tarkastele kohteen tietoja", + "desc": "Tarkastele kohteen tietoja", + "button": { + "share": "Jaa tämä tarkasteltu kohde" + }, + "toast": { + "error": { + "updatedSublabelFailed": "Alatunnisteen päivitys epäonnistui", + "updatedLPRFailed": "Rekisterikilven päivitys epäonnistui" + } + } + }, + "recognizedLicensePlate": "Tunnistettu rekisterikilpi", + "estimatedSpeed": "Arvioitu nopeus", + "objects": "Objektit", + "camera": "Kamera", + "zones": "Alueet", + "label": "Tunniste", + "editSubLabel": { + "title": "Editoi alitunnistetta", + "desc": "Syötä uusi alitunniste tähän", + "descNoLabel": "Lisää uusi alatunniste tähän seurattuun kohteeseen" + }, + "editLPR": { + "title": "Muokkaa rekisterikilpeä", + "desc": "Syötä uusi rekisterikilven arvo tähän", + "descNoLabel": "Syötä uusi rekisterikilven arvo tähän seurattuun objektiin" + }, + "snapshotScore": { + "label": "Tilannekuvan arvosana" + }, + "topScore": { + "label": "Huippuarvosana", + "info": "Ylin pistemäärä on seurattavan kohteen korkein mediaani, joten tämä voi erota hakutuloksen esikatselukuvassa näkyvästä pistemäärästä." + }, + "button": { + "findSimilar": "Etsi samankaltaisia" + }, + "description": { + "label": "Kuvaus" + }, + "score": { + "label": "Pisteet" + } + }, + "exploreIsUnavailable": { + "title": "Selaus on tavoittamattomissa", + "embeddingsReindexing": { + "startingUp": "Käynnistytään…", + "estimatedTime": "Arvioitu aika jäljellä:", + "finishingShortly": "Valmista pian", + "step": { + "trackedObjectsProcessed": "Käsitellyt seuratut objektit: ", + "thumbnailsEmbedded": "Kuvakkeet sisällytetty: ", + "descriptionsEmbedded": "Kuvaukset sisällytetty: " + }, + "context": "Selausta voidaan käyttää sen jälkeen kun seurattavien kohteiden uudelleenindeksöinti on valmistunut." + }, + "downloadingModels": { + "context": "Frigate lataa semanttista hakua varten vaadittavat upotusmallit. Tämä saattaa viedä useamman minuutin, riippuen yhteytesi nopeudesta.", + "setup": { + "visionModel": "Vision-malli", + "textModel": "Tekstimalli", + "textTokenizer": "Tekstin osioija", + "visionModelFeatureExtractor": "Näkömallin piirreluokkain" + }, + "tips": { + "documentation": "Lue dokumentaatio", + "context": "Saatat haluta uudelleenindeksoida seurattavien kohteiden upotukset, kun mallit on ladattu." + }, + "error": "Tapahtui virhe. Tarkista Frigaten lokit." + } + }, + "exploreMore": "Selaa lisää {{label}}-tyyppisiä kohteita", + "generativeAI": "Generatiivinen AI", + "objectLifecycle": { + "annotationSettings": { + "offset": { + "documentation": "Lue dokumentaatio " + }, + "showAllZones": { + "title": "Näytä kaikki vyöhykkeet" + } + }, + "lifecycleItemDesc": { + "header": { + "zones": "Vyöhykkeet", + "ratio": "Suhde", + "area": "Alue" + }, + "active": "{{label}} aktivoitui", + "stationary": "{{label}} pysähtyi", + "attribute": { + "faceOrLicense_plate": "{{attribute}} havaittiin nimikkeelle {{label}}", + "other": "{{label}} tunnistettu {{attribute}}:na" + }, + "gone": "{{label}} poistui", + "entered_zone": "{{label}} ilmestyi vyöhykkeelle {{zones}}", + "visible": "{{label}} havaittu", + "heard": "{{label}} kuului", + "external": "{{label}} havaittiin" + }, + "trackedPoint": "Seurattu piste", + "carousel": { + "previous": "Edellinen", + "next": "Seuraava" + }, + "count": "{{first}} / {{second}}", + "title": "Kohteen elinkaari", + "noImageFound": "Tältä aikaleimalta ei löytynyt kuvia.", + "createObjectMask": "Luo kohdemaski", + "scrollViewTips": "Vieritä katsoaksesi merkittäviä hetkiä kohteen elinkaarelta.", + "autoTrackingTips": "Kohteen rajojen sijainti on epätarkka automaattisesti seuraaville kameroille.", + "adjustAnnotationSettings": "Säädä merkintäasetuksia" + }, + "trackedObjectDetails": "Seurattavien kohteiden tiedot", + "type": { + "details": "tiedot", + "snapshot": "kuvankaappaus", + "video": "video", + "object_lifecycle": "kohteen elinkaari" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Lataa kuvankaappaus", + "aria": "Lataa kuvankaappaus" + }, + "addTrigger": { + "label": "Lisää laukaisin", + "aria": "Lisää laukaisin tälle seurattavalle kohteelle" + }, + "submitToPlus": { + "label": "Lähetä Frigate+:lle" + }, + "downloadVideo": { + "label": "Lataa video", + "aria": "Lataa video" + }, + "viewObjectLifecycle": { + "label": "Tarkastele objektin elinkaarta", + "aria": "Näytä objektin elinkaari" + }, + "findSimilar": { + "label": "Etsi samankaltaisia" + } + }, + "aiAnalysis": { + "title": "AI-analyysi" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/exports.json new file mode 100644 index 0000000..5ee8e88 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/exports.json @@ -0,0 +1,17 @@ +{ + "search": "Etsi", + "documentTitle": "Vie", + "deleteExport.desc": "Oletko varma että haluat poistaa kohteen {{exportName}}?", + "toast": { + "error": { + "renameExportFailed": "Viedyn kohteen uudelleennimeäminen epäonnistui: {{errorMessage}}" + } + }, + "noExports": "Ei vietyjä kohteita", + "deleteExport": "Poista viety kohde", + "editExport": { + "title": "Nimeä uudelleen", + "desc": "Anna uusi nimi viedylle kohteelle.", + "saveExport": "Tallenna vienti" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/faceLibrary.json new file mode 100644 index 0000000..dc69f36 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/faceLibrary.json @@ -0,0 +1,82 @@ +{ + "description": { + "addFace": "Opastus: Uuden kokoelman lisääminen Kasvokirjastoon.", + "invalidName": "Virheellinen nimi. Nimi voi sisältää vain merkkejä, numeroita, välejä, heittomerkkejä, alaviivoja ja väliviivoja.", + "placeholder": "Anna nimi kokoelmalle" + }, + "uploadFaceImage": { + "desc": "Lähetä kuva kasvojen tunnistukseen ja lisää se sivulle {{pageToggle}}", + "title": "Lähetä kasvokuva" + }, + "details": { + "unknown": "Tuntematon", + "faceDesc": "Lisätiedot kohteesta, josta tämä kasvokuva tallennettiin", + "person": "Henkilö", + "timestamp": "Aikaleima", + "subLabelScore": "Alinimikkeen pisteet", + "face": "Kasvojen yksityiskohdat", + "scoreInfo": "Alatunnisteen pistemäärä on kaikkien tunnistettujen kasvojen varmuustasojen painotettu keskiarvo, joten se voi poiketa tilannekuvassa näkyvästä pistemäärästä." + }, + "documentTitle": "Kasvokirjasto - Frigate", + "deleteFaceAttempts": { + "desc_one": "Oletko varma, että haluat poistaa {{count}} kasvon? Tätä toimintoa ei voi perua.", + "desc_other": "Oletko varma, että haluat poistaa {{count}} kasvoa? Tätä toimintoa ei voi perua.", + "title": "Poista kasvot" + }, + "toast": { + "success": { + "deletedFace_one": "{{count}} kasvo poistettu onnistuneesti.", + "deletedFace_other": "{{count}} kasvoa poistettu onnistuneesti.", + "uploadedImage": "Kuva ladattu onnistuneesti." + } + }, + "selectItem": "Valitse {{item}}", + "train": { + "empty": "Ei viimeaikaisia kasvojentunnistusyrityksiä", + "title": "Koulutus", + "aria": "Valitse kouluta" + }, + "collections": "Kokoelmat", + "steps": { + "faceName": "Anna nimi kasvoille", + "uploadFace": "Lähetä kasvokuva", + "nextSteps": "Seuraavat vaiheet", + "description": { + "uploadFace": "Lataa kuva henkilöstä {{name}}, jossa hänen kasvonsa näkyvät suoraan edestä päin. Kuvaa ei tarvitse rajata pelkkiin kasvoihin." + } + }, + "createFaceLibrary": { + "title": "Luo kokoelma", + "desc": "Luo uusi kokoelma", + "new": "Luo uusi kasvo", + "nextSteps": "Hyvän perustan luomiseksi huomioitavaa:
  • Käytä koulutus-välilehteä valitaksesi opetukseen kuvia kustakin tunnistetusta henkilöstä
  • Panosta mahdollisimman suoraan otettuihin kuviin; vältä kouluttamista kulmassa kuvatuilla kuvilla.
  • " + }, + "selectFace": "Valitse kasvo", + "deleteFaceLibrary": { + "title": "Poista nimi", + "desc": "Haluatko varmasti poistaa kokoelman {{name}}? Tämä poistaa pysyvästi kaikki liitetyt kasvot." + }, + "renameFace": { + "title": "Uudelleennimeä kasvot", + "desc": "Anna uusi nimi tälle {{name}}" + }, + "button": { + "deleteFaceAttempts": "Poista kasvot", + "addFace": "Lisää kasvot", + "renameFace": "Uudelleennimeä kasvot", + "deleteFace": "Poista kasvot", + "uploadImage": "Lataa kuva", + "reprocessFace": "Uudelleenprosessointi Kasvot" + }, + "imageEntry": { + "validation": { + "selectImage": "Valitse kuvatiedosto." + }, + "dropActive": "Pudota kuva tähän…", + "dropInstructions": "Vedä ja pudota kuva tähän tai valitse se napsauttamalla", + "maxSize": "Maksimikoko: {{size}}MB" + }, + "nofaces": "Kasvoja ei ole saatavilla", + "pixels": "{{area}}px", + "trainFace": "Kouluta kasvot" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/live.json new file mode 100644 index 0000000..d387035 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/live.json @@ -0,0 +1,171 @@ +{ + "documentTitle": "Suora - Frigate", + "documentTitle.withCamera": "{{camera}} - Suora - Frigate", + "lowBandwidthMode": "Pienen kaistanleveyden tila", + "twoWayTalk": { + "enable": "Ota käyttöön kaksisuuntainen puhe", + "disable": "Poista kaksisuuntainen puhe käytöstä" + }, + "cameraAudio": { + "enable": "Ota kameran ääni käyttöön", + "disable": "Poista kameran ääni käytöstä" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Napsauta ruutua keskittääksesi kameran", + "enable": "Ota käyttöön napsauttamalla siirtäminen", + "disable": "Poista napsauttamalla siirtäminen" + }, + "left": { + "label": "Siirrä PTZ-kameraa vasemmalle" + }, + "up": { + "label": "Siirrä PTZ-kameraa ylös" + }, + "down": { + "label": "Siirrä PTZ-kameraa alas" + }, + "right": { + "label": "Siirrä PTZ-kameraa oikealle" + } + }, + "zoom": { + "out": { + "label": "Zoomaa PTZ-kamera ulos" + }, + "in": { + "label": "Zoomaa PTZ-kamera sisään" + } + }, + "frame": { + "center": { + "label": "Napsauta kehystä keskittääksesi PTZ-kamera" + } + }, + "presets": "PTZ-kameroiden esiasetukset", + "focus": { + "in": { + "label": "Tarkenna PTZ-kamera sisään" + }, + "out": { + "label": "Tarkenna PTZ-kamera ulos" + } + } + }, + "camera": { + "enable": "Ota kamera käyttöön", + "disable": "Poista kamera käytöstä" + }, + "muteCameras": { + "enable": "Mykistä kaikki kamerat", + "disable": "Poista kaikkien kameroiden mykistys" + }, + "detect": { + "enable": "Ota tunnistus käyttöön", + "disable": "Poista tunnistus käytöstä" + }, + "recording": { + "enable": "Ota tallennus käyttöön", + "disable": "Poista tallennus käytöstä" + }, + "snapshots": { + "enable": "Ota tilannekuva käyttöön", + "disable": "Poista tilannekuva käytöstä" + }, + "audioDetect": { + "enable": "Ota käyttöön äänen tunnistus", + "disable": "Poista äänen tunnistus käytöstä" + }, + "autotracking": { + "enable": "Ota automaattinen seuranta käyttöön", + "disable": "Poista automaattinen seuranta käytöstä" + }, + "streamStats": { + "enable": "Näytä suoratoiston tilastot", + "disable": "Piilota suoratoiston tilastot" + }, + "manualRecording": { + "title": "Tallennus pyynnöstä", + "tips": "Aloita manuaalinen tapahtuma tämän kameran tallenteen tallennusasetusten perusteella.", + "playInBackground": { + "label": "Toista taustalla", + "desc": "Ota tämä asetus käyttöön, jos haluat jatkaa suoratoistoa kun soitin on piilotettu." + }, + "showStats": { + "label": "Näytä tilastot", + "desc": "Ota tämä asetus käyttöön, jos haluat näyttää suoratoistotilastot kamerasyötteen päällä." + }, + "debugView": "Virheenkorjausnäkymä", + "start": "Aloita tallennus pyynnöstä", + "started": "Manuaalinen pyynnöstätallennus aloitettu.", + "failedToStart": "Manuaalisen pyynnöstätallennuksen aloittaminen epäonnistui.", + "recordDisabledTips": "Koska tallennus on poistettu käytöstä tai rajoitettu tämän kameran asetuksissa, vain tilannekuva tallennetaan.", + "end": "Lopeta pyynnöstätallennus", + "ended": "Manuaalinen on-demand-tallennus lopetettu.", + "failedToEnd": "Manuaalisen pyynnöstätallennuksen lopettaminen epäonnistui." + }, + "streamingSettings": "Suoratoistoasetukset", + "notifications": "Ilmoitukset", + "audio": "Ääni", + "suspend": { + "forTime": "Keskeytys: " + }, + "stream": { + "title": "Suoratoisto", + "audio": { + "tips": { + "title": "Äänen on oltava kytkettynä kameraan ja määritettynä go2rtc:ssä tätä suoratoistoa varten.", + "documentation": "Lue dokumentaatio " + }, + "available": "Ääni on saatavilla tälle suoratoistolle", + "unavailable": "Ääni ei ole saatavilla tälle suoratoistolle" + }, + "twoWayTalk": { + "tips": "Laitteesi on tuettava ominaisuutta ja WebRTC:n on oltava määritetty kaksisuuntaista ääntä varten.", + "tips.documentation": "Lue dokumentaatio ", + "available": "Kaksisuuntainen ääni on saatavilla tässä suoratoistossa", + "unavailable": "Kaksisuuntainen ääni ei ole käytettävissä tässä suoratoistossa" + }, + "lowBandwidth": { + "tips": "Live-näkymä on matalan kaistanleveyden tilassa puskuroinnin tai suoratoistovirheiden vuoksi.", + "resetStream": "Nollaa suoratoisto" + }, + "playInBackground": { + "label": "Toista taustalla", + "tips": "Ota tämä asetus käyttöön, jos haluat jatkaa suoratoistoa, kun soitin on piilotettu." + } + }, + "cameraSettings": { + "title": "{{camera}} Asetukset", + "cameraEnabled": "Kamera käytössä", + "objectDetection": "Kohteen tunnistus", + "recording": "Nauhoitus", + "snapshots": "Tilannekuvat", + "audioDetection": "Äänen tunnistus", + "autotracking": "Automaattinen seuranta", + "transcription": "Äänitranskriptio" + }, + "history": { + "label": "Näytä historiallista materiaalia" + }, + "effectiveRetainMode": { + "modes": { + "all": "Kaikki", + "motion": "Liike", + "active_objects": "Aktiiviset kohteet" + }, + "notAllTips": "{{source}}-tallenteiden säilytysmäärityksesi on asetettu tila: {{effectiveRetainMode}}, joten tämä tilattu tallenne säilyttää vain ne osat joiden tyyppi on {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Muokkaa asettelua", + "group": { + "label": "Muokkaa kameraryhmää" + }, + "exitEdit": "Poistu muokkauksesta" + }, + "transcription": { + "enable": "Ota käyttöön reaaliaikainen äänitranskriptio", + "disable": "Poista käytöstä reaaliaikainen äänitranskriptio" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/recording.json new file mode 100644 index 0000000..84daba2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/recording.json @@ -0,0 +1,12 @@ +{ + "calendar": "Kalenteri", + "filter": "Suodatin", + "filters": "Suodattimet", + "toast": { + "error": { + "noValidTimeSelected": "Sopimaton aikaväli valittu", + "endTimeMustAfterStartTime": "Loppuaika täytyy olla aloituksen jälkeen" + } + }, + "export": "Vie" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/search.json new file mode 100644 index 0000000..887c9e0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Etsi", + "savedSearches": "Tallennetut haut", + "searchFor": "Etsi {{inputValue}}", + "button": { + "clear": "Tyhjennä haku", + "save": "Tallenna haku", + "delete": "Poista tallennettu haku", + "filterInformation": "Suodattimen tiedot", + "filterActive": "Suodattimia valittuina" + }, + "trackedObjectId": "Seuratun kohteen ID", + "filter": { + "label": { + "cameras": "Kamerat", + "labels": "Nimikkeet", + "zones": "Alueet", + "sub_labels": "Alinimikkeet", + "search_type": "Haun tyyppi", + "time_range": "Aikaikkuna", + "before": "Ennen", + "after": "Jälkeen", + "min_score": "Minimi pisteet", + "max_score": "Maksimi pisteet", + "min_speed": "Minimi nopeus", + "max_speed": "Maksimi nopeus", + "recognized_license_plate": "Tunnistettu rekisterikilpi", + "has_clip": "Leike löytyy", + "has_snapshot": "Tilannekuva löytyy" + }, + "searchType": { + "thumbnail": "Kuvake", + "description": "Kuvaus" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "'Ennen' ajan täytyy olla myöhemmin kun 'jälkeen' aika.", + "afterDatebeEarlierBefore": "'Jälkeen' ajan täytyy olla aiemmin kun 'ennen' aika.", + "minScoreMustBeLessOrEqualMaxScore": "Arvon 'min_score' täytyy olla pienempi tai yhtäsuuri kuin 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Arvon 'max_score' täytyy olla suurempi tai yhtäsuuri kuin 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Minimi nopeus' tulee olla pienempi tai yhtäsuuri kuin 'maksimi nopeus'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Maksimi nopeus' tulee olla suurempi tai yhtä suuri kuin 'minimi nopeus'." + } + }, + "tips": { + "desc": { + "exampleLabel": "Esimerkki:", + "step6": "Poista suodattimet napsauttamalla niiden vieressä olevaa 'x' merkkiä.", + "text": "Suodattimien avulla voit rajata hakutuloksia. Näin käytät niitä syöttökentässä:", + "step1": "Kirjoita suodattimen avaimen nimi ja sen perään kaksoispiste (esim. ”kamerat:”).", + "step2": "Valitse arvo ehdotuksista tai kirjoita oma arvo.", + "step3": "Käytä useita suodattimia lisäämällä ne peräkkäin välilyönnillä erotettuina.", + "step4": "Päivämääräsuodattimet (ennen: ja jälkeen:) käyttävät {{DateFormat}} muotoa.", + "step5": "Aikavälin suodatin käyttää {{exampleTime}} muotoa." + }, + "title": "Tekstisuodattimien käyttö" + }, + "header": { + "currentFilterType": "Suodata arvoja", + "noFilters": "Suodattimet", + "activeFilters": "Käytössä olevat suodattimet" + } + }, + "similaritySearch": { + "title": "Samankaltaisten kohteiden haku", + "active": "Samankaltaisuushaku aktiivinen", + "clear": "Poista samankaltaisuushaku" + }, + "placeholder": { + "search": "Hae…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/settings.json new file mode 100644 index 0000000..cda2719 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/settings.json @@ -0,0 +1,579 @@ +{ + "documentTitle": { + "camera": "Kamera-asetukset - Frigate", + "classification": "Klassifiointiasetukset - Frigate", + "masksAndZones": "Peite ja vyöhykemuokkain - Frigate", + "motionTuner": "Liikesäädin - Frigate", + "default": "Asetukset - Frigate", + "general": "Yleiset asetukset - Frigate", + "frigatePlus": "Frigate+ asetukset - Frigate", + "object": "Virheenjäljitys - Frigate", + "authentication": "Autentikointiuasetukset - Frigate", + "notifications": "Ilmoitusasetukset - Frigate", + "enrichments": "Laajennusasetukset – Frigate" + }, + "menu": { + "ui": "Käyttöliittymä", + "cameras": "Kameroiden asetukset", + "users": "Käyttäjät", + "classification": "Klassifiointi", + "frigateplus": "Frigate+", + "masksAndZones": "Maskit / alueet", + "debug": "Debuggaus", + "motionTuner": "Liikesäädin", + "notifications": "Ilmoitukset", + "enrichments": "Rikasteet", + "triggers": "Laukaisimet" + }, + "dialog": { + "unsavedChanges": { + "desc": "Haluatko tallentaa muutokset ennen jatkamista?", + "title": "Et ole tallentanut muutoksia." + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Ei kameraa" + }, + "general": { + "title": "Yleiset asetukset", + "liveDashboard": { + "automaticLiveView": { + "label": "Automaattinen reaaliaika-näkymä", + "desc": "Vaihda automaattisesti reaaliaikaiseen kameranäkymään kun liikettä on huomattu. Mikäli asetus on kytketty pois päivittyy reaaliaikaisen kojelaudan kuva vain kerran minuutissa." + }, + "title": "Reaaliaikainen kojelauta", + "playAlertVideos": { + "label": "Näytä hälyytysvideot", + "desc": "Vakiona viimeaikaiset hälytykset pyörivät pieninä luuppaavina videoina reaaliaikaisella kojelaudalla. Ota tämä asetus pois päältä näyttääksesi vain staattisen kuvan viimeaikaisista hälytyksistä tässä laitteessa/selaimessa." + } + }, + "storedLayouts": { + "title": "Tallennetut sijoittelut", + "desc": "Kameroiden sijoittelua kameraryhmissä voidaan raahata tai niiden kokoa muuttaa. Sijainnit tallennetaan selaimen paikalliseen muistiin.", + "clearAll": "Tyhjennä kaikki sijoittelut" + }, + "cameraGroupStreaming": { + "title": "Kameraryhmän striimauksen asetukset", + "desc": "Striimauksen asetukset jokaiselle kameraryhmälle tallennetaan selaimesi paikalliseen muistiin.", + "clearAll": "Tyhjennä kaikkai striimauksen asetukset" + }, + "recordingsViewer": { + "title": "Tallennusten näyttäjä", + "defaultPlaybackRate": { + "label": "Toiston vakionopeus", + "desc": "Toiston vakionopeus tallennusten näytölle." + } + }, + "calendar": { + "title": "Kalenteri", + "firstWeekday": { + "label": "Viikon ensimmäinen päivä", + "desc": "Päivä josta kertauskalenterin viikot alkaa.", + "sunday": "sunnuntai", + "monday": "maanantai" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Tyhjennä tallennetut sijoittelut kameralle nimeltä {{cameraName}}", + "clearStreamingSettings": "Tyhjennä striimausasetukset kaikista kameraryhmistä." + }, + "error": { + "clearStoredLayoutFailed": "Sijoittelujen tyhjentäminen ei onnistunut: {{errorMessage}}", + "clearStreamingSettingsFailed": "Striimausasetusten tyhjentäminen ei onnistunut: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Klassifiointiasetukset", + "semanticSearch": { + "reindexNow": { + "label": "Uudelleen indeksoi nyt", + "confirmDesc": "Oletko varma että haluat indeksoida uudelleen kaikki seurattujen kohteiden upotukset? Tämä prosessi toimii taustalla ja saattaa maksimoida prosessorin käytön sekä viedä runsaasti aikaa. Voit seurata prosessin etenemistä tarkastelu -sivulta.", + "desc": "Indeksoinnin luominen uudelleen jälleenrakentaa upotukset kaikkiin seurattuihin kohteisiin. Tämä prosessi toimii taustalla ja saattaa maksimoida prosessorin käytön sekä viedä reilusti aikaa riippuen paljonko seurattavia kohteita sinulla on.", + "confirmButton": "Indeksoi uudelleen", + "success": "Uudelleen indeksointi aloitettiin onnistuneesti.", + "alreadyInProgress": "Uudelleen indeksointi on jo käynnissä.", + "error": "Uudelleen indeksointia ei voitu aloittaa: {{errorMessage}}", + "confirmTitle": "Vahvista uudelleen indeksointi" + }, + "modelSize": { + "label": "Mallin koko", + "small": { + "desc": "Valitessa pieni käytetään kvantisoitunutta versiota mallista joka käyttää vähemmän muistia sekä prosesoria upotuksen laatueron ollessa lähes olematon.", + "title": "pieni" + }, + "large": { + "desc": "Valittaessa suuri käytettään täyttä Jina-mallia joka ajetaan automaattisesti grafiikkaytimellä mikäli mahdollista.", + "title": "suuri" + }, + "desc": "Semanttisen haun upotuksiin käytetyn mallin koko." + }, + "title": "Semanttinen haku", + "readTheDocumentation": "Lue dokumentaatio", + "desc": "Frigaten semanttisen haun kanssa voit hakea seurattuja kohteita esikatseluista joko kuvasta itsestään, käyttäjän määrittelemän teksti-kuvauksen perusteella tai automaattisesti generoidun kuvauksen kanssa." + }, + "faceRecognition": { + "title": "Kasvojentunnistus", + "readTheDocumentation": "Lue dokumentaatio", + "modelSize": { + "label": "Mallin koko", + "desc": "Kasvojentunnistukseen käytetyn mallin koko.", + "small": { + "title": "pieni", + "desc": "Valitessa pieni FaceNet käyttää kasvojen upotukseen mallia joka toimii tehokkaasti suurimmalla osalla prosessoreista." + }, + "large": { + "title": "suuri", + "desc": "Valitessa suuri käytetään ArcFace mallia kasvojen upotukseen joka ajetaan automaattisesti grafiikkaprosessorilla mikäli mahdollista." + } + }, + "desc": "Kasvojentunnistus sallii nimien antamisen ihmisille ja kun heidän kasvonsa tunnistetaan Frigate antaa henkilölle nimen ala-viittenä. Tämä tieto sisällytetään käyttöliittymään, filttereihin sekä ilmoituksiin." + }, + "licensePlateRecognition": { + "title": "Rekisterikilven tunnistus", + "desc": "Frigate voi tunnistaa ajoneuvojen rekisterikilpiä ja lisätä tunnistetut kirjaimet automaattisesti recognized_license_plate -kenttään tai tunnettu nimi sub_label kohteisiin joiden tyyppi on ajoneuvo. Yleinen käyttökohde on lukea pihatielle ajavien tai kadulla ohiajavien ajoneuvojen rekisterikilvet.", + "readTheDocumentation": "Lue dokumentaatio" + }, + "toast": { + "success": "Klassifiointiasetukset on tallennettu. Käynnistä Frigate uudelleen saadaksesi ne käyttöön.", + "error": "Konfiguraatio muutoksia ei voitu tallentaa: {{errorMessage}}" + }, + "restart_required": "Tarvitaan uudelleenkäynnistys (luokitusasetuksia muutettu)", + "birdClassification": { + "title": "Lintujen luokittelu", + "desc": "Lintujen luokittelu tunnistaa tunnetut linnut kvantisoidun Tensorflow-mallin avulla. Kun tunnettu lintu tunnistetaan, sen yleinen nimi lisätään alitunnisteena. Tämä tieto sisältyy käyttöliittymään, suodattimiin ja ilmoituksiin." + } + }, + "camera": { + "title": "Kamera-asetukset", + "streams": { + "title": "Striimit", + "desc": "Poista kamera käytöstä väliaikaisesti, kunnes Frigate uudelleenkäynnistetään. Kameran poiskytkeminen lopettaa kameran videostriimien käsittelyn. Havainnot, tallennus ja debuggaus ovat pois käytöstä.
    Huom: tämä ei poista käytöstä go2rtc uusinta striimejä." + }, + "review": { + "title": "Katselu", + "desc": "Kytke väliaikaisesti päälle/pois hälytykset ja tunnistus tälle kameralle, kunnes Frigate käynnistetään uudelleen. Kun ne ovat pois päältä, uusia katseltavia tapahtumia ei luoda. ", + "alerts": "Hälytykset ", + "detections": "Tunnistukset " + }, + "reviewClassification": { + "title": "Katseluiden klassifiointi", + "readTheDocumentation": "Lue dokumentaatio", + "noDefinedZones": "Tälle kameralle ei ole määritelty vyöhykkeitä.", + "objectAlertsTips": "Kaikki {{alertsLabels}} objektit lähteelle {{cameraName}} näytetään hälytyksinä.", + "zoneObjectAlertsTips": "Kaikki {{alertsLabels}} objektit jotka tunnistetaan alueella {{zone}} lähteessä {{cameraName}} näytetään Hälytyksinä.", + "objectDetectionsTips": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu lähteessä {{cameraName}} näytetään Tunnistuksina niiden vyöhykkeestä huolimatta.", + "zoneObjectDetectionsTips": { + "text": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu vyöhykkeellä {{zone}} lähteessä {{cameraName}} näytetään Tunnistuksina.", + "notSelectDetections": "Kaikki {{detectionsLabels}} objektit jotka tunnistetaan vyöhykkeellä {{zone}} lähteessä {{cameraName}}, joita ei ole kategorisoitu Hälytyksiksi näytetään Tunnistuksina niiden vyöhykkeestä huolimatta.", + "regardlessOfZoneObjectDetectionsTips": "Kaikki {{detectionsLabels}} objektit joita ei ole kategorisoitu lähteessä {{cameraName}} näytetään Tunnistuksina niiden vyöhykkeestä huolimatta." + }, + "selectAlertsZones": "Valitse vyöhykkeet Hälytystä varten", + "desc": "Frigate kategorisoi tahtumia Hälytyksiksi ja Tunnistuksiksi. Vakiona kaikki henkilö sekä ajoneuvo objektit käsitellään Hälytyksinä. Voit kategorisoida uudelleen katseltavat tapahtumat antamalla niille vaaditut alueet.", + "limitDetections": "Rajoita tunnistukset tiettyihin vyöhykkeisiin", + "selectDetectionsZones": "Valitse vyöhykkeet tunnistusta varten", + "toast": { + "success": "Luokittelumäärityksen tarkistus on tallennettu. Käynnistä Frigate uudelleen muutosten käyttöönottamiseksi." + } + }, + "cameraConfig": { + "add": "Lisää kamera", + "ffmpeg": { + "addInput": "Lisää tulovirta" + } + }, + "addCamera": "Lisää uusi kamera" + }, + "masksAndZones": { + "filter": { + "all": "Kaikki peitteet ja vyöhykkeet" + }, + "form": { + "polygonDrawing": { + "delete": { + "desc": "Oletko varma että haluat poistaa {{type}}{{name}}?", + "success": "{{name}} on poistettu.", + "title": "Varmista poistaminen" + }, + "error": { + "mustBeFinished": "Polygonien piirron pitää olla valmis ennen tallennusta." + }, + "removeLastPoint": "Poista edellinen piste", + "snapPoints": { + "true": "Napsauta pisteet", + "false": "Älä napsauta pisteitä" + }, + "reset": { + "label": "Poista kaikki pisteet" + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Alueen nimen tulee olla vähintään 2 merkin pituinen.", + "alreadyExists": "Tämän niminen alue on jo olemassa.", + "mustNotContainPeriod": "Alueen nimessä ei saa olla pisteitä.", + "hasIllegalCharacter": "Vyöhykkeen nimessä on kiellettyjä merkkejä.", + "mustNotBeSameWithCamera": "Alueen nimi ei saa olla sama kuin kameran nimi." + } + }, + "distance": { + "error": { + "text": "Välimatkan tulee olla suurempi tai yhtä suuri kuin 0.1.", + "mustBeFilled": "Kaikki välimatka -kentät tulee olla täytetty jotta nopeusarviota voidaan käyttää." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inertian tulee olla yli 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oleiluaika tulee olla suurempi tai yhtä suuri kuin 0." + } + } + }, + "zones": { + "label": "Vyöhykkeet", + "documentTitle": "Muokkaa vyöhykkeitä - Frigate", + "edit": "Myokkaa vyöhykettä", + "inertia": { + "title": "Inertia", + "desc": "Määrittää, kuinka monta kehystä objektin on oltava vyöhykkeellä, ennen kuin se lasketaan vyöhykkeeksi. Oletus: 3" + }, + "loiteringTime": { + "title": "Oleskeluaika", + "desc": "Asettaa vähimmäisajan sekunteina, jonka objektin on oltava vyöhykkeellä, jotta se aktivoituu. Oletus: 0" + }, + "objects": { + "title": "Kohteet", + "desc": "Luettelo tähän vyöhykkeeseen liittyvistä kohteista." + }, + "desc": { + "title": "Vyöhykkeiden avulla voit määrittää tietyn alueen kuvassa, jotta voit selvittää, onko kohde kyseisellä alueella.", + "documentation": "Dokumentaatio" + }, + "add": "Lisää vyöhyke", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan.", + "name": { + "title": "Nimi", + "inputPlaceHolder": "Anna nimi…", + "tips": "Nimen on oltava vähintään kaksi merkkiä pitkä, eikä se saa olla kameran tai toisen vyöhykkeen nimi." + }, + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "allObjects": "Kaikki kohteet", + "speedEstimation": { + "title": "Nopeuden arviointi", + "desc": "Ota käyttöön nopeuden arviointi tällä vyöhykkeellä oleville kohteille. Sillä on oltava täsmälleen 4 pistettä." + }, + "speedThreshold": { + "title": "Nopeuskynnys ({{unit}})", + "desc": "Määrittää kohteiden vähimmäisnopeuden, joka otetaan huomioon tässä vyöhykkeessä.", + "toast": { + "error": { + "pointLengthError": "Nopeuden arviointi on poistettu käytöstä tällä vyöhykkeellä. Vyöhykkeillä joilla on nopeuden arviointi, on oltava täsmälleen 4 pistettä.", + "loiteringTimeError": "Vyöhykkeitä, joiden oleskeluajat ovat yli 0, ei tule käyttää nopeuden arvioinnissa." + } + } + }, + "toast": { + "success": "Vyöhyke ({{zoneName}}) on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Koordinaattien kopioiminen leikepöydälle epäonnistui." + }, + "success": { + "copyCoordinates": "{{polyName}} - koordinaatit kopioitu leikepöydälle." + } + }, + "restart_required": "Uudelleenkäynnistys vaaditaan (peitteitä/vyöhykeitä muutettu)", + "motionMasks": { + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan.", + "label": "Liikepeitto", + "context": { + "documentation": "Lue dokumentaatio", + "title": "Liikepeittoja käytetään estämään ei-toivottujen liiketyyppien (esimerkiksi puiden oksat, kameroiden aikaleimat) aiheuttamat tunnistukset. Liikepeittoja tulisi käyttää hyvin säästeliäästi, sillä liiallinen maskaus vaikeuttaa kohteiden seurantaa." + }, + "documentTitle": "Muokkaa liikepeittoa - Frigate", + "desc": { + "title": "Liikepeittoa käytetään estämään ei-toivottujen liiketyyppien aiheuttamat tunnistukset. Liiallinen peittäminen vaikeuttaa kohteiden seurantaa.", + "documentation": "Dokumentaatio" + }, + "add": "Uusi liikepeitto", + "edit": "Muokkaa liikepeittoa", + "polygonAreaTooLarge": { + "title": "Liikepeitto peittää {{polygonArea}}% kameran kuvasta. Suuria liikemaskeja ei suositella.", + "tips": "Liikepeitto eivät estä kohteiden havaitsemista. Sinun tulisi sen sijaan käyttää vaadittua vyöhykettä.", + "documentation": "Lue dokumentaatio" + }, + "toast": { + "success": { + "title": "{{polygonName}} on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi.", + "noName": "Liikepeitto on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + } + }, + "objectMasks": { + "point_one": "{{count}} piste", + "point_other": "{{count}} pisteet", + "label": "Kohdepeitot", + "context": "Objektisuodatinpeittoja käytetään suodattamaan pois väärät positiiviset tulokset tietylle kohdetyypille sijainnin perusteella.", + "objects": { + "title": "Kohteet", + "desc": "Kohdetyyppi, jota käytetään tähän kohdepeittoon.", + "allObjectTypes": "Kaikki kohdetyypit" + }, + "toast": { + "success": { + "title": "{{polygonName}} on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi.", + "noName": "Kohdepeitto on tallennettu. Käynnistä Frigatti uudelleen muutosten käyttöönottamiseksi." + } + }, + "documentTitle": "Muokkaa kohdepeittoa - Frigate", + "desc": { + "title": "Objektisuodatinpeittoja käytetään suodattamaan pois väärät positiiviset tulokset tietylle kohdetyypille sijainnin perusteella.", + "documentation": "Dokumentaatio" + }, + "add": "Lisää kohdepeitto", + "edit": "Muokkaa kohdepeittoa", + "clickDrawPolygon": "Napsauta piirtääksesi monikulmion kuvaan." + } + }, + "debug": { + "regions": { + "title": "Alueet", + "desc": "Näytä kohdeilmaisimelle lähetetyn kiinnostuksen kohteena olevan alueen laatikko", + "tips": "

    Aluelaatikot


    Kirkkaanvihreät laatikot peittävät kuvassa olevat kiinnostavat alueet, jotka lähetetään objektinilmaisimelle.

    " + }, + "objectShapeFilterDrawing": { + "title": "Objektin muodon suodattimen piirtäminen", + "desc": "Piirrä kuvaan suorakulmio nähdäksesi pinta-alan ja kuvasuhteen tiedot", + "document": "Lue dokumentaatio ", + "score": "Pisteet", + "ratio": "Suhde", + "area": "Alue", + "tips": "Ota tämä asetus käyttöön piirtääksesi kamerakuvaan suorakulmion, joka näyttää sen pinta-alan ja suhteen. Näitä arvoja voidaan sitten käyttää objektin muodon suodatusparametrien asettamiseen asetuksissasi." + }, + "timestamp": { + "title": "Aikaleima", + "desc": "Lisää aikaleima kuvan päälle" + }, + "noObjects": "Ei kohteita", + "zones": { + "title": "Vyöhykkeet", + "desc": "Näytä määriteltyjen vyöhykkeiden ääriviivat" + }, + "boundingBoxes": { + "colors": { + "info": "
  • Käynnistettäessä kullekin kohteen merkinnälle määritetään eri värit
  • Tummansininen ohut viiva osoittaa, että kohdetta ei ole havaittu tällä hetkellä
  • Harmaa ohut viiva osoittaa, että kohde on havaittu paikallaan olevaksi
  • Paksu viiva osoittaa, että kohde on automaattisen seurannan kohteena (kun se on käytössä)
  • " + } + }, + "mask": { + "title": "Liikepeitot", + "desc": "Näytä liikepeiton monikulmiot" + }, + "motion": { + "title": "Liikelaatikot", + "desc": "Näytä laatikot alueiden ympärillä, joilla liikettä havaitaan", + "tips": "

    Liikelaatikot


    Punaiset laatikot peittävät ruudun alueet, joilla liikettä havaitaan parhaillaan.

    " + } + }, + "users": { + "title": "Käyttäjät", + "management": { + "title": "Käyttäjien hallinta", + "desc": "Hallinnoi tämän Frigate-instanssin käyttäjätilejä." + }, + "addUser": "Lisää käyttäjä", + "updatePassword": "Päivitä salasana", + "toast": { + "success": { + "roleUpdated": "Rooli päivitetty käyttäjälle {{user}}", + "createUser": "Käyttäjä {{user}} luotu onnistuneesti", + "deleteUser": "Käyttäjä {{user}} poistettu onnistuneesti", + "updatePassword": "Salasana päivitetty onnistuneesti." + }, + "error": { + "setPasswordFailed": "Salasanan tallentaminen epäonnistui: {{errorMessage}}", + "createUserFailed": "Käyttäjän luonti epäonnistui: {{errorMessage}}", + "roleUpdateFailed": "Roolin päivittäminen epäonnistui: {{errorMessage}}", + "deleteUserFailed": "Käyttäjän poisto epäonistui: {{errorMessage}}" + } + }, + "table": { + "username": "Käyttäjänimi", + "actions": "Toiminnot", + "noUsers": "Käyttäjiä ei löytynyt.", + "changeRole": "Vaihda käyttäjäroolia", + "password": "Salasana", + "deleteUser": "Poista tili", + "role": "Rooli" + }, + "dialog": { + "form": { + "user": { + "desc": "Vain kirjaimet, numerot, pisteet ja alaviivat sallitaan.", + "placeholder": "Syötä käyttäjätunnus", + "title": "Käyttäjätunnus" + } + }, + "changeRole": { + "roleInfo": { + "admin": "Ylläpitäjä" + } + } + } + }, + "motionDetectionTuner": { + "title": "Liiketunnistuksen säätäminen", + "desc": { + "title": "Frigate käyttää liiketunnistusta ensimmäisenä tarkistuksena nähdäkseen, tapahtuuko kuvassa jotain, mikä kannattaisi tarkistaa objektitunnistuksella.", + "documentation": "Lue liikkeensäädön opas" + }, + "Threshold": { + "title": "Kynnys" + } + }, + "triggers": { + "documentTitle": "Laukaisimet", + "management": { + "title": "Laukaisimen hallinta" + }, + "addTrigger": "Lisää laukaisin", + "table": { + "name": "Nimi", + "type": "Tyyppi", + "content": "Sisältö", + "threshold": "Kynnys", + "actions": "Toiminnot", + "noTriggers": "Tälle kameralle ei ole määritetty laukaisimia.", + "edit": "Muokkaa", + "deleteTrigger": "Poista laukaisin", + "lastTriggered": "Viimeksi laukaistu" + }, + "type": { + "thumbnail": "Kuvake", + "description": "Kuvaus" + }, + "actions": { + "notification": "Lähetä ilmoitus", + "alert": "Merkitse hälytykseksi" + }, + "dialog": { + "createTrigger": { + "title": "Luo laukaisin", + "desc": "Luo laukaisin kameralle {{camera}}" + }, + "editTrigger": { + "title": "Muokkaa laukaisinta", + "desc": "Muokkaa laukaisimen asetuksia kamerasta {{camera}}" + }, + "deleteTrigger": { + "title": "Poista laukaisin", + "desc": "Haluatko varmasti poistaa laukaisimen {{triggerName}}? Tätä toimintoa ei voi peruuttaa." + }, + "form": { + "name": { + "title": "Nimi", + "placeholder": "Syötä laukaisimen nimi", + "error": { + "minLength": "Nimen on oltava vähintään 2 merkkiä pitkä.", + "invalidCharacters": "Nimi voi sisältää vain kirjaimia, numeroita, alaviivoja ja väliviivoja.", + "alreadyExists": "Tällä nimellä oleva laukaisin on jo olemassa tälle kameralle." + } + }, + "enabled": { + "description": "Ota tämä laukaisin käyttöön tai pois käytöstä" + }, + "type": { + "title": "Tyyppi", + "placeholder": "Valitse laukaisintyyppi" + }, + "content": { + "title": "Sisältö", + "imagePlaceholder": "Valitse kuva", + "textPlaceholder": "Kirjoita tekstisisältö", + "imageDesc": "Valitse kuva, joka laukaisee tämän toiminnon, kun samankaltainen kuva havaitaan.", + "textDesc": "Syötä teksti, joka laukaisee tämän toiminnon, kun vastaava seurattavan kohteen kuvaus havaitaan.", + "error": { + "required": "Sisältö on pakollinen." + } + }, + "threshold": { + "title": "Kynnys", + "error": { + "min": "Kynnys on oltava vähintään 0", + "max": "Kynnys on oltava enintään 1" + } + }, + "actions": { + "title": "Toiminnot", + "desc": "Oletuksena Frigate lähettää MQTT-viestin kaikille laukaisimille. Valitse lisätoiminto, joka suoritetaan, kun tämä laukaisija laukeaa.", + "error": { + "min": "Vähintään yksi toiminto on valittava." + } + } + } + }, + "toast": { + "success": { + "createTrigger": "Laukaisin {{name}} luotu onnistuneesti.", + "updateTrigger": "Laukaisin {{name}} päivitetty onnistuneesti.", + "deleteTrigger": "Laukaisin {{name}} poistettu onnistuneesti." + }, + "error": { + "createTriggerFailed": "Laukaisimen luominen epäonnistui: {{errorMessage}}", + "updateTriggerFailed": "Laukaisimen päivitys epäonnistui: {{errorMessage}}", + "deleteTriggerFailed": "Laukaisimen poistaminen epäonnistui: {{errorMessage}}" + } + } + }, + "enrichments": { + "semanticSearch": { + "modelSize": { + "small": { + "title": "pieni", + "desc": "pieni käyttää kvantisoitua versiota mallista, joka käyttää vähemmän RAM-muistia ja toimii nopeammin CPU:lla, mutta ero upotuksen laadussa on hyvin vähäinen." + }, + "large": { + "title": "suuri", + "desc": "suuri käyttää koko Jina-mallia ja toimii automaattisesti GPU:lla, jos se on mahdollista." + }, + "desc": "Semanttisen haun upotuksiin käytetyn mallin koko." + }, + "title": "Semanttinen haku", + "desc": "Semanttisen haun avulla Frigatessa voit etsiä seurattavia kohteita tarkistettavista kohteista joko kuvan, käyttäjän määrittämän tekstikuvauksen tai automaattisesti luodun kuvauksen avulla.", + "reindexNow": { + "label": "Uudelleenindeksoi nyt", + "desc": "Uudelleindeksointi luo uudelleen upotukset kaikille seuratuille objekteille. Tämä prosessi suoritetaan taustalla ja voi kuormittaa prosessorin maksimiin ja viedä melko paljon aikaa riippuen seurattujen objektien määrästä.", + "confirmTitle": "Vahvista uudelleenindeksointi" + } + }, + "faceRecognition": { + "title": "Kasvojentunnistus", + "desc": "Kasvojentunnistuksen avulla ihmisille voidaan antaa nimiä, ja kun heidän kasvonsa tunnistetaan, Frigate lisää henkilön nimen alaluokaksi. Nämä tiedot näkyvät käyttöliittymässä, suodattimissa ja ilmoituksissa.", + "modelSize": { + "label": "Mallin koko", + "desc": "Kasvojentunnistuksessa käytettävän mallin koko.", + "small": { + "title": "pieni", + "desc": "pieni käyttää FaceNet-kasvojen upotusmallia, joka toimii tehokkaasti useimmilla suorittimilla." + }, + "large": { + "title": "suuri", + "desc": "suuri käyttää ArcFace-kasvojen upotusmallia ja toimii automaattisesti GPU:lla, jos se on mahdollista." + } + } + }, + "licensePlateRecognition": { + "title": "Rekisterikilven tunnistaminen" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fi/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/fi/views/system.json new file mode 100644 index 0000000..0495269 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fi/views/system.json @@ -0,0 +1,77 @@ +{ + "logs": { + "type": { + "timestamp": "Aikaleima", + "tag": "Tagi", + "message": "Viesti", + "label": "Tyyppi" + }, + "copy": { + "label": "Kopioi leikepöydälle", + "success": "Lokit kopioitu leikepöydälle", + "error": "Lokeja ei voitu kopioida leikepöydälle" + }, + "download": { + "label": "Lataa lokit" + }, + "tips": "Lokeja toistetaan palvelimelta", + "toast": { + "error": { + "fetchingLogsFailed": "Virhe noudettaessa lokeja: {{errorMessage}}", + "whileStreamingLogs": "Virhe toistettaessa lokeja: {{errorMessage}}" + } + } + }, + "documentTitle": { + "cameras": "Kameroiden tilastot - Frigate", + "storage": "Tallenteiden tilastot - Fgirage", + "general": "Yleiset tilastot - Frigate", + "enrichments": "Rikastetut tilastot - Frigate", + "logs": { + "frigate": "Frigaten lokit - Frigate", + "go2rtc": "Go2RTC lokit - Frigate", + "nginx": "Nginx lokit - Frigate" + } + }, + "title": "Järjestelmä", + "metrics": "Järjestelmämittarit", + "general": { + "hardwareInfo": { + "title": "Laitteiston tiedot", + "gpuUsage": "GPU:n käyttö", + "gpuMemory": "GPU:n muisti", + "gpuEncoder": "GPU-enkooderi", + "gpuDecoder": "GPU-dekooderi", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfon tulostus", + "returnCode": "Paluuarvo: {{code}}" + }, + "toast": { + "success": "Kopioi GPU:n tiedot leikepöydälle" + }, + "copyInfo": { + "label": "Kopioi GPU:n tiedot" + }, + "closeInfo": { + "label": "Sulje GPU:n tiedot" + }, + "nvidiaSMIOutput": { + "driver": "Ajuri: {{driver}}", + "title": "Nvidia SMI tuloste", + "name": "Nimi: {{name}}", + "cudaComputerCapability": "CUDA laskentakapasiteetti: {{cuda_compute}}", + "vbios": "VBios-tiedot: {{vbios}}" + } + } + }, + "detector": { + "memoryUsage": "Ilmaiseman muistinkäyttö", + "title": "Ilmaisimet", + "inferenceSpeed": "Ilmaisimen päättelynopeus", + "cpuUsage": "Ilmaisimen CPU-käyttö", + "temperature": "Ilmaisimen lämpötila" + }, + "title": "Yleinen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/audio.json b/sam2-cpu/frigate-dev/web/public/locales/fr/audio.json new file mode 100644 index 0000000..b346158 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Parole", + "babbling": "Babillage", + "yell": "Cri", + "bicycle": "Vélo", + "car": "Voiture", + "bellow": "Beuglement", + "whispering": "Chuchotement", + "laughter": "Rires", + "snicker": "Ricanement", + "crying": "Pleurs", + "boat": "Bateau", + "bus": "Bus", + "train": "Train", + "motorcycle": "Moto", + "whoop": "Cri strident", + "sigh": "Soupir", + "singing": "Chant", + "choir": "Chorale", + "yodeling": "Yodel", + "chant": "Chant", + "mantra": "Mantra", + "child_singing": "Chant d'enfant", + "bird": "Oiseau", + "cat": "Chat", + "synthetic_singing": "Chant synthétique", + "rapping": "Rap", + "horse": "Cheval", + "dog": "Chien", + "sheep": "Mouton", + "whistling": "Sifflement", + "breathing": "Respiration", + "snoring": "Ronflement", + "gasp": "Souffle coupé", + "pant": "halètement", + "snort": "Ébrouement", + "camera": "Caméra", + "cough": "Toux", + "groan": "Gémissement", + "grunt": "Grognement", + "throat_clearing": "Raclement de gorge", + "wheeze": "Respiration sifflante", + "sneeze": "Éternuement", + "sniff": "Reniflement", + "chewing": "Mastication", + "gargling": "Gargarisme", + "ambulance": "Ambulance", + "police_car": "Voiture de police", + "emergency_vehicle": "Véhicule d'urgence", + "subway": "Métro", + "fire_alarm": "Alarme Incendie", + "smoke_detector": "Détecteur de Fumée", + "siren": "Sirène", + "pulleys": "Poulies", + "gears": "Engrenages", + "clock": "Horloge", + "ratchet": "Cliquet", + "mechanisms": "Mécanismes", + "steam_whistle": "Sifflet à vapeur", + "whistle": "Sifflet", + "foghorn": "Corne de brume", + "tools": "Outils", + "printer": "Imprimante", + "air_conditioning": "Climatisation", + "mechanical_fan": "Ventilateur mécanique", + "sewing_machine": "Machine à coudre", + "wood": "Bois", + "fireworks": "Feux d'artifice", + "glass": "Verre", + "television": "Télévision", + "sound_effect": "Effet sonore", + "burping": "Rots", + "fart": "Pet", + "crowd": "Foule", + "children_playing": "Jeux d'enfants", + "animal": "Animal", + "bark": "Aboiement", + "pig": "Cochon", + "goat": "Chèvre", + "chicken": "Poulet", + "turkey": "Dinde", + "duck": "Canard", + "goose": "Oie", + "wild_animals": "Animaux Sauvages", + "crow": "Corbeau", + "dogs": "Chiens", + "mouse": "Souris", + "insect": "Insecte", + "cricket": "Grillon", + "mosquito": "Moustique", + "fly": "Mouche", + "frog": "Grenouille", + "snake": "Serpent", + "music": "Musique", + "guitar": "Guitare", + "electric_guitar": "Guitare électrique", + "keyboard": "Clavier", + "piano": "Piano", + "vehicle": "Véhicule", + "skateboard": "Skateboard", + "door": "Porte", + "blender": "Mixeur", + "hair_dryer": "Sèche-cheveux", + "toothbrush": "Brosse à dents", + "sink": "Évier", + "scissors": "Ciseaux", + "humming": "Bourdonnement", + "shuffle": "Pas traînants", + "footsteps": "Bruits de pas", + "hiccup": "Hoquet", + "finger_snapping": "Claquement de doigts", + "clapping": "Claquements", + "applause": "Applaudissements", + "heartbeat": "Battements de coeur", + "cheering": "Acclamations", + "electric_shaver": "Rasoir électrique", + "truck": "Camion", + "run": "Course", + "biting": "Mordre", + "stomach_rumble": "Gargouillements d'estomac", + "hands": "Mains", + "heart_murmur": "Souffle au cœur", + "chatter": "Bavardage", + "pets": "Animaux de compagnie", + "yip": "Jappement", + "howl": "Hurlement", + "growling": "Grondement", + "whimper_dog": "Gémissements de chien", + "purr": "Ronronnements", + "caterwaul": "Miaulement", + "meow": "Miaou", + "livestock": "Bétail", + "neigh": "Hennissement", + "quack": "Coin-coin", + "honk": "Cacardement", + "roaring_cats": "Rugissement de félins", + "roar": "Rugissements", + "chirp": "Gazouillis", + "squawk": "Braillement", + "pigeon": "Pigeon", + "coo": "Roucoulement", + "caw": "Croassement", + "owl": "Chouette", + "hoot": "Hululement", + "flapping_wings": "Battement d'ailes", + "rats": "Rats", + "patter": "Crépitements", + "buzz": "Bourdonnement", + "croak": "Coassement", + "rattle": "Cliquetis", + "whale_vocalization": "Chant des baleines", + "musical_instrument": "Instrument de musique", + "plucked_string_instrument": "Instrument à cordes pincées", + "bass_guitar": "Guitare basse", + "acoustic_guitar": "Guitare acoustique", + "tapping": "Tapotement", + "strum": "Grattement", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandoline", + "steel_guitar": "Steel Guitar", + "zither": "Cithare", + "ukulele": "Ukulélé", + "electric_piano": "Piano électrique", + "organ": "Orgue", + "electronic_organ": "Orgue électrique", + "hammond_organ": "Orgue Hammond", + "synthesizer": "Synthétiseur", + "sampler": "Échantillonneur", + "harpsichord": "Clavecin", + "percussion": "Percussions", + "drum_kit": "Batterie", + "drum_machine": "Boîte à rythmes", + "drum": "Tambour", + "snare_drum": "Caisse claire", + "rimshot": "Rimshot", + "drum_roll": "Roulement de tambour", + "bass_drum": "Grosse caisse", + "timpani": "Timbales", + "tabla": "Tabla", + "cymbal": "Cymbale", + "hi_hat": "Charleston", + "wood_block": "Wood Block", + "maraca": "Maraca", + "gong": "Gong", + "tubular_bells": "Carillon tubulaire", + "marimba": "Marimba", + "mallet_percussion": "Maillet de percussion", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibraphone", + "steelpan": "Pan", + "orchestra": "Orchestre", + "brass_instrument": "Cuivres", + "french_horn": "Cor d'harmonie", + "trumpet": "Trompette", + "bowed_string_instrument": "Instrument à cordes frottées", + "string_section": "Section des cordes", + "violin": "Violon", + "pizzicato": "Pizzicato", + "cello": "Violoncelle", + "double_bass": "Contrebasse", + "wind_instrument": "Instrument à vent", + "flute": "Flûte", + "saxophone": "Saxophone", + "clarinet": "Clarinette", + "harp": "Harpe", + "church_bell": "Cloche d'église", + "bell": "Cloche", + "jingle_bell": "Grelot", + "bicycle_bell": "Sonnette de vélo", + "tuning_fork": "Diapason", + "chime": "Carillon", + "wind_chime": "Carillon à vent", + "harmonica": "Harmonica", + "accordion": "Accordéon", + "bagpipes": "Cornemuse", + "didgeridoo": "Didgeridoo", + "theremin": "Thérémine", + "singing_bowl": "Bol chantant", + "scratching": "Scratch", + "pop_music": "Musique pop", + "hip_hop_music": "Musique hip-hop", + "beatboxing": "Beatboxing", + "rock_music": "Musique rock", + "punk_rock": "Punk Rock", + "soul_music": "Musique Soul", + "reggae": "Reggae", + "country": "Country", + "funk": "Funk", + "folk_music": "Musique Folk", + "jazz": "Jazz", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "traditional_music": "Musique traditionnelle", + "independent_music": "Musique indépendante", + "song": "Chanson", + "background_music": "Musique de fond", + "theme_music": "Thème musical", + "jingle": "Tintement", + "soundtrack_music": "Musique de bande originale", + "lullaby": "Berceuse", + "video_game_music": "Musique de jeux vidéo", + "dance_music": "Musique Dance", + "wedding_music": "Musique de mariage", + "happy_music": "Musique joyeuse", + "sad_music": "Musique triste", + "tender_music": "Musique tendre", + "exciting_music": "Musique stimulante", + "angry_music": "Musique agressive", + "scary_music": "Musique effrayante", + "wind": "Vent", + "rustling_leaves": "Bruissements de feuilles", + "wind_noise": "Bruit de vent", + "thunderstorm": "Orage", + "thunder": "Tonnerre", + "water": "Eau", + "rain": "Pluie", + "raindrop": "Goutte de pluie", + "rain_on_surface": "Pluie sur une surface", + "stream": "Flux", + "waterfall": "Cascade", + "ocean": "Océan", + "waves": "Vagues", + "steam": "Vapeur", + "gurgling": "Gargouillis", + "fire": "Feu", + "crackle": "Crépitement", + "sailboat": "Voilier", + "rowboat": "Chaloupe", + "motorboat": "Bateau à moteur", + "ship": "Bateau", + "motor_vehicle": "Véhicule à moteur", + "toot": "Sifflotement", + "car_alarm": "Alarme de voiture", + "power_windows": "Vitres électriques", + "skidding": "Dérapage", + "tire_squeal": "Crissements de pneu", + "car_passing_by": "Passage de voiture", + "race_car": "Voiture de course", + "air_brake": "Frein pneumatique", + "air_horn": "Klaxon à air", + "reversing_beeps": "Bips de marche arrière", + "ice_cream_truck": "Camion de glaces", + "fire_engine": "Camion de pompiers", + "traffic_noise": "Bruit de circulation", + "rail_transport": "Transport ferroviaire", + "train_whistle": "Sifflet de train", + "train_horn": "Klaxon de train", + "railroad_car": "Wagon de chemin de fer", + "train_wheels_squealing": "Crissements de roues de train", + "aircraft": "Aéronef", + "aircraft_engine": "Moteur d'avion", + "jet_engine": "Moteur à réaction", + "propeller": "Hélice", + "helicopter": "Hélicoptère", + "fixed-wing_aircraft": "Avion à voilure fixe", + "engine": "Moteur", + "light_engine": "Moteur léger", + "dental_drill's_drill": "Fraise dentaire", + "lawn_mower": "Tondeuse à gazon", + "chainsaw": "Tronçonneuse", + "heavy_engine": "Moteur lourd", + "engine_knocking": "Détonations de moteur", + "engine_starting": "Démarrage de moteur", + "accelerating": "Accélération", + "doorbell": "Sonnette", + "ding-dong": "Ding-Dong", + "knock": "Coup", + "tap": "Tapotement", + "squeak": "Grincement", + "cupboard_open_or_close": "Ouverture ou fermeture de placard", + "drawer_open_or_close": "Ouverture ou fermeture de tiroir", + "dishes": "Bruit de vaisselle", + "cutlery": "Couverts", + "chopping": "Hacher", + "frying": "Friture", + "microwave_oven": "Four à micro-ondes", + "water_tap": "Robinet d'eau", + "bathtub": "Baignoire", + "toilet_flush": "Chasse d'eau", + "electric_toothbrush": "Brosse à dents électrique", + "vacuum_cleaner": "Aspirateur", + "zipper": "Fermeture éclair", + "keys_jangling": "Tintements de clés", + "coin": "Pièce de monnaie", + "shuffling_cards": "Battement de cartes", + "typing": "Frappe au clavier", + "typewriter": "Machine à écrire", + "writing": "Écriture", + "alarm": "Alarme", + "telephone_bell_ringing": "Sonnerie de téléphone", + "ringtone": "Sonnerie", + "telephone_dialing": "Numérotation téléphonique", + "dial_tone": "Tonalité", + "busy_signal": "Tonalité occupée", + "alarm_clock": "Réveille-matin", + "civil_defense_siren": "Sirène d'alerte aux populations", + "buzzer": "Buzzer", + "tick": "Tic-tac", + "tick-tock": "Tic-Tac", + "cash_register": "Caisse enregistreuse", + "single-lens_reflex_camera": "Appareil photo reflex mono-objectif", + "hammer": "Marteau", + "jackhammer": "Marteau-piqueur", + "sawing": "Sciage", + "filing": "Limage", + "sanding": "Ponçage", + "power_tool": "Outil électrique", + "drill": "Perceuse", + "explosion": "Explosion", + "gunshot": "Coup de feu", + "machine_gun": "Mitrailleuse", + "fusillade": "Fusillade", + "artillery_fire": "Tir d'artillerie", + "cap_gun": "Pistolet à amorces", + "firecracker": "Pétard", + "eruption": "Éruption", + "boom": "Boom", + "chop": "Coup de hache", + "splinter": "Éclat", + "crack": "Fissure", + "chink": "Fente", + "shatter": "Brisure", + "silence": "Silence", + "environmental_noise": "Bruit ambiant", + "static": "Statique", + "white_noise": "Bruit blanc", + "pink_noise": "Bruit rose", + "field_recording": "Enregistrement sur le terrain", + "scream": "Cri", + "tambourine": "Tambourin", + "electronic_music": "Musique électronique", + "rock_and_roll": "Rock and Roll", + "vocal_music": "Musique vocale", + "trombone": "Trombone", + "flamenco": "Flamenco", + "carnatic_music": "Musique carnatique", + "a_capella": "A Capella", + "christmas_music": "Musique de Noël", + "afrobeat": "Afrobeat", + "sliding_door": "Porte coulissante", + "opera": "Opéra", + "music_of_africa": "Musique d'Afrique", + "music_of_latin_america": "Musique d'Amérique Latine", + "blues": "Blues", + "music_for_children": "Musique pour enfants", + "electronica": "Electronica", + "ska": "Ska", + "salsa_music": "Salsa", + "medium_engine": "Moteur moyen", + "heavy_metal": "Heavy Metal", + "disco": "Disco", + "grunge": "Grunge", + "music_of_asia": "Musique d'Asie", + "progressive_rock": "Rock progressif", + "psychedelic_rock": "Rock psychédélique", + "rhythm_and_blues": "Rhythm and Blues", + "electronic_dance_music": "Electronic Dance Music", + "trance_music": "Musique Trance", + "new-age_music": "Musique New Age", + "bluegrass": "Bluegrass", + "swing_music": "Musique Swing", + "ambient_music": "Musique d'ambiance", + "middle_eastern_music": "Musique orientale", + "house_music": "Musique House", + "christian_music": "Musique chrétienne", + "classical_music": "Musique classique", + "gospel_music": "Musique Gospel", + "slam": "Claquement", + "computer_keyboard": "Clavier d'ordinateur", + "burst": "Éclatement", + "music_of_bollywood": "Musique de Bollywood", + "idling": "Ralenti", + "radio": "Radio", + "telephone": "Téléphone", + "bow_wow": "Aboiement", + "hiss": "Sifflement", + "clip_clop": "Clic-clac", + "cattle": "Bétail", + "moo": "Meuglement", + "cowbell": "Clochette", + "oink": "Grouin-grouin", + "bleat": "Bêlement", + "fowl": "Volaille", + "cluck": "Gloussement", + "cock_a_doodle_doo": "Cocorico", + "gobble": "Glouglou", + "chird": "Accord", + "change_ringing": "Carillon de cloches", + "sodeling": "Sodèle", + "shofar": "Choffar", + "liquid": "Liquide", + "splash": "Éclaboussure", + "slosh": "Clapotis", + "squish": "Bruit de pataugeage", + "drip": "Goutte à goutte", + "trickle": "Filet", + "gush": "Jet", + "fill": "Remplir", + "spray": "Pulvérisation", + "pump": "Pompe", + "stir": "Remuer", + "boiling": "Ébullition", + "arrow": "Flèche", + "pour": "Verser", + "sonar": "Sonar", + "whoosh": "Whoosh", + "thump": "Coup sourd", + "thunk": "Bruit sourd", + "electronic_tuner": "Accordeur électronique", + "effects_unit": "Unité d'effets", + "chorus_effect": "Effet de chœur", + "basketball_bounce": "Rebond de basket-ball", + "bang": "Détonation", + "slap": "Gifle", + "whack": "Coup sec", + "smash": "Fracasser", + "breaking": "Bruit de casse", + "bouncing": "Rebondissement", + "whip": "Fouet", + "flap": "Battement", + "scratch": "Grattement", + "scrape": "Raclement", + "rub": "Frottement", + "roll": "Roulement", + "crushing": "Écrasement", + "crumpling": "Froissement", + "tearing": "Déchirure", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "Bruit métallique", + "squeal": "Grincement", + "creak": "Craquer", + "rustle": "Bruissement", + "whir": "Vrombissement", + "clatter": "Bruit", + "sizzle": "Grésillement", + "clicking": "Cliquetis", + "clickety_clack": "Clic-clac", + "rumble": "Grondement", + "plop": "Ploc", + "hum": "Hum", + "harmonic": "Harmonique", + "outside": "Extérieur", + "reverberation": "Réverbération", + "echo": "Écho", + "distortion": "Distorsion", + "vibration": "Vibration", + "zing": "Sifflement", + "crunch": "Croque", + "sine_wave": "Onde sinusoïdale", + "chirp_tone": "Gazouillis", + "pulse": "Impulsion", + "inside": "Intérieur", + "noise": "Bruit", + "mains_hum": "Bourdonnement du secteur", + "sidetone": "Retour de voix", + "cacophony": "Cacophonie", + "throbbing": "Pulsation", + "boing": "Boing" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/common.json b/sam2-cpu/frigate-dev/web/public/locales/fr/common.json new file mode 100644 index 0000000..a1132a0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/common.json @@ -0,0 +1,314 @@ +{ + "time": { + "untilForRestart": "Jusqu'au redémarrage de Frigate", + "untilRestart": "Jusqu'au redémarrage", + "untilForTime": "Jusqu'à {{time}}", + "justNow": "À l'instant", + "today": "Aujourd'hui", + "last7": "7 derniers jours", + "last14": "14 derniers jours", + "ago": "Il y a {{timeAgo}}", + "yesterday": "Hier", + "last30": "30 derniers jours", + "thisWeek": "Cette semaine", + "lastWeek": "La semaine dernière", + "thisMonth": "Ce mois-ci", + "lastMonth": "Le mois dernier", + "10minutes": "10 minutes", + "5minutes": "5 minutes", + "30minutes": "30 minutes", + "12hours": "12 heures", + "h": "{{time}} h", + "pm": "PM", + "am": "AM", + "yr": "{{time}} a", + "year_one": "{{time}} an", + "year_many": "{{time}} ans", + "year_other": "{{time}} ans", + "mo": "{{time}} mois", + "month_one": "{{time}} mois", + "month_many": "{{time}} mois", + "month_other": "{{time}} mois", + "s": "{{time}} s", + "second_one": "{{time}} seconde", + "second_many": "{{time}} secondes", + "second_other": "{{time}} secondes", + "m": "{{time}} min", + "hour_one": "{{time}} heure", + "hour_many": "{{time}} heures", + "hour_other": "{{time}} heures", + "24hours": "24 heures", + "minute_one": "{{time}} minute", + "minute_many": "{{time}} minutes", + "minute_other": "{{time}} minutes", + "d": "{{time}} j", + "day_one": "{{time}} jour", + "day_many": "{{time}} jours", + "day_other": "{{time}} jours", + "1hour": "1 heure", + "formattedTimestamp": { + "12hour": "d MMM HH:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampWithYear": { + "24hour": "%b %-d %Y, %H:%M", + "12hour": "%b %-d %Y, %I:%M %p" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestamp2": { + "12hour": "dd/MM HH:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-HH-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, HH:mm", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, HH:mm", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM,yyyy" + }, + "inProgress": "En cours", + "invalidStartTime": "Heure de début invalide", + "invalidEndTime": "Heure de fin invalide" + }, + "button": { + "apply": "Appliquer", + "reset": "Réinitialiser", + "disabled": "Désactivé", + "save": "Enregistrer", + "saving": "Enregistrement…", + "close": "Fermer", + "copy": "Copier", + "back": "Retour", + "history": "Chronologie", + "pictureInPicture": "Image dans l'image", + "twoWayTalk": "Conversation bidirectionnelle", + "off": "OFF", + "edit": "Modifier", + "copyCoordinates": "Copier les coordonnées", + "delete": "Supprimer", + "yes": "Oui", + "no": "Non", + "unsuspended": "Réactiver", + "play": "Lire", + "unselect": "Désélectionner", + "suspended": "Suspendu", + "enable": "Activer", + "enabled": "Activé", + "info": "Info", + "disable": "Désactiver", + "cancel": "Annuler", + "fullscreen": "Plein écran", + "next": "Suivant", + "exitFullscreen": "Sortir du mode plein écran", + "cameraAudio": "Son de la caméra", + "on": "ON", + "export": "Exporter", + "deleteNow": "Supprimer maintenant", + "download": "Télécharger", + "done": "Terminé", + "continue": "Continuer" + }, + "menu": { + "configuration": "Configuration", + "language": { + "en": "English (Anglais)", + "withSystem": { + "label": "Utiliser les paramètres système pour la langue" + }, + "zhCN": "简体中文 (Chinois simplifié)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Français)", + "ja": "日本語 (Japonais)", + "tr": "Türkçe (Turc)", + "it": "Italiano (Italien)", + "nl": "Nederlands (Néerlandais)", + "sv": "Svenska (Suédois)", + "cs": "Čeština (Tchèque)", + "nb": "Norsk Bokmål (Norvégien Bokmål)", + "ko": "한국어 (Coréen)", + "fa": "فارسی (Persan)", + "pl": "Polski (Polonais)", + "el": "Ελληνικά (Grec)", + "ro": "Română (Roumain)", + "hu": "Magyar (Hongrois)", + "he": "עברית (Hébreu)", + "ru": "Русский (Russe)", + "de": "Deutsch (Allemand)", + "es": "Español (Espagnol)", + "ar": "العربية (Arabe)", + "da": "Dansk (Danois)", + "fi": "Suomi (Finlandais)", + "pt": "Português (Portugais)", + "sk": "Slovenčina (Slovaque)", + "uk": "Українська (Ukrainien)", + "vi": "Tiếng Việt (Vietnamien)", + "yue": "粵語 (Cantonais)", + "th": "ไทย (Thai)", + "ca": "Català (Catalan)", + "ptBR": "Português brasileiro (Portugais brésilien)", + "sr": "Српски (Serbe)", + "sl": "Slovenščina (Slovène)", + "lt": "Lietuvių (Lithuanien)", + "bg": "Български (Bulgare)", + "gl": "Galego (Galicien)", + "id": "Bahasa Indonesia (Indonésien)", + "ur": "اردو (Ourdou)" + }, + "appearance": "Apparence", + "darkMode": { + "light": "Clair", + "dark": "Sombre", + "withSystem": { + "label": "Utiliser les paramètres système pour le mode clair ou sombre" + }, + "label": "Mode sombre" + }, + "review": "Activités", + "explore": "Explorer", + "export": "Exporter", + "user": { + "account": "Compte", + "logout": "Se déconnecter", + "setPassword": "Configurer un mot de passe", + "current": "Utilisateur actuel : {{user}}", + "title": "Utilisateur", + "anonymous": "anonyme" + }, + "systemLogs": "Journaux système", + "documentation": { + "title": "Documentation", + "label": "Documentation de Frigate" + }, + "system": "Système", + "help": "Aide", + "configurationEditor": "Éditeur de configuration", + "theme": { + "contrast": "Contraste élevé", + "blue": "Bleu", + "green": "Vert", + "nord": "Nord", + "red": "Rouge", + "default": "Par défaut", + "label": "Thème", + "highcontrast": "Contraste élevé" + }, + "systemMetrics": "Métriques du système", + "settings": "Paramètres", + "withSystem": "Système", + "restart": "Redémarrer Frigate", + "live": { + "cameras": { + "count_one": "{{count}} caméra", + "count_many": "{{count}} caméras", + "count_other": "{{count}} caméras", + "title": "Caméras" + }, + "allCameras": "Toutes les caméras", + "title": "Direct" + }, + "uiPlayground": "Bac à sable de l'interface", + "faceLibrary": "Bibliothèque de visages", + "languages": "Langues", + "classification": "Classification" + }, + "toast": { + "save": { + "title": "Enregistrer", + "error": { + "noMessage": "Echec lors de l'enregistrement des changements de configuration", + "title": "Échec de l'enregistrement des changements de configuration : {{errorMessage}}" + } + }, + "copyUrlToClipboard": "URL copiée dans le presse-papiers" + }, + "role": { + "title": "Rôle", + "viewer": "Observateur", + "admin": "Administrateur", + "desc": "Les administrateurs ont un accès complet à toutes les fonctionnalités de l'interface Frigate. Les observateurs sont limités à la consultation des caméras, des activités, et à l'historique des enregistrements dans l'interface." + }, + "pagination": { + "next": { + "title": "Suivant", + "label": "Aller à la page suivante" + }, + "more": "Plus de pages", + "previous": { + "label": "Aller à la page précédente", + "title": "Précédent" + }, + "label": "pagination" + }, + "notFound": { + "title": "404", + "documentTitle": "Non trouvé - Frigate", + "desc": "Page non trouvée" + }, + "selectItem": "Sélectionner {{item}}", + "readTheDocumentation": "Lire la documentation", + "accessDenied": { + "title": "Accès refusé", + "documentTitle": "Accès refusé - Frigate", + "desc": "Vous n'avez pas l'autorisation de consulter cette page." + }, + "label": { + "back": "Retour", + "hide": "Masquer {{item}}", + "show": "Afficher {{item}}", + "ID": "ID", + "none": "Aucun", + "all": "Tous" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mph" + }, + "length": { + "feet": "pieds", + "meters": "mètres" + }, + "data": { + "kbps": "ko/s", + "mbps": "Mo/s", + "gbps": "Go/s", + "kbph": "ko/heure", + "mbph": "Mo/heure", + "gbph": "Go/heure" + } + }, + "information": { + "pixels": "{{area}}px" + }, + "field": { + "optional": "Facultatif", + "internalID": "L'ID interne utilisée par Frigate dans la configuration et la base de donnêes" + }, + "list": { + "two": "{{0}} et {{1}}", + "many": "{{items}}, et {{last}}", + "separatorWithSpace": ", " + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/auth.json new file mode 100644 index 0000000..3e600fb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Mot de passe", + "login": "Connexion", + "user": "Nom d'utilisateur", + "errors": { + "unknownError": "Erreur inconnue. Vérifiez les journaux.", + "webUnknownError": "Erreur inconnue. Vérifiez les journaux de la console.", + "passwordRequired": "Mot de passe est requis", + "loginFailed": "Échec de l'authentification", + "usernameRequired": "Nom d'utilisateur requis", + "rateLimit": "Trop de tentatives. Veuillez réessayer plus tard." + }, + "firstTimeLogin": "Première connexion ? Vos identifiants se trouvent dans les journaux de Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/camera.json new file mode 100644 index 0000000..0e95c70 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "edit": "Modifier le groupe de caméras", + "label": "Groupes de caméras", + "add": "Ajouter un groupe de caméras", + "delete": { + "label": "Supprimer le groupe de caméras", + "confirm": { + "title": "Confirmez la suppression", + "desc": "Êtes-vous sûr de vouloir supprimer le groupe de caméras {{name}} ?" + } + }, + "name": { + "placeholder": "Saisissez un nom.", + "label": "Nom", + "errorMessage": { + "mustLeastCharacters": "Le nom du groupe de caméras doit comporter au moins 2 caractères.", + "exists": "Le nom du groupe de caméras existe déjà.", + "nameMustNotPeriod": "Le nom de groupe de caméras ne doit pas contenir de point.", + "invalid": "Nom de groupe de caméras invalide" + } + }, + "cameras": { + "label": "Caméras", + "desc": "Sélectionnez les caméras pour ce groupe." + }, + "success": "Le groupe de caméras ({{name}}) a été enregistré.", + "icon": "Icône", + "camera": { + "setting": { + "label": "Paramètres du flux de la caméra", + "title": "Paramètres du flux de {{cameraName}}", + "audioIsUnavailable": "L'audio n'est pas disponible pour ce flux.", + "audioIsAvailable": "L'audio est disponible pour ce flux.", + "desc": "Modifier les options du flux temps réel pour le tableau de bord de ce groupe de caméras. Ces paramètres sont spécifiques à l'appareil ou au navigateur.", + "audio": { + "tips": { + "document": "Lire la documentation ", + "title": "L'audio doit provenir de la caméra et être configuré dans go2rtc pour ce flux." + } + }, + "streamMethod": { + "label": "Méthode de diffusion", + "method": { + "noStreaming": { + "label": "Aucune diffusion", + "desc": "Les images provenant de la caméra ne seront mises à jour qu'une fois par minute et il n'y aura aucune diffusion en direct." + }, + "smartStreaming": { + "label": "Diffusion intelligente (recommandée)", + "desc": "La diffusion intelligente mettra à jour l'image de la caméra une fois par minute lorsqu'aucune activité n'est détectée, afin de préserver la bande passante et les ressources. Quand une activité est détectée, l'image bascule automatiquement en flux temps réel." + }, + "continuousStreaming": { + "label": "Diffusion en continu", + "desc": { + "title": "L'image de la caméra sera toujours un flux temps réel lorsqu'elle est visible dans le tableau de bord, même si aucune activité n'est détectée.", + "warning": "La diffusion en continu peut entraîner une consommation de bande passante élevée et des problèmes de performance. À utiliser avec prudence." + } + } + }, + "placeholder": "Choisissez une méthode de diffusion." + }, + "compatibilityMode": { + "label": "Mode de compatibilité", + "desc": "Activez cette option uniquement si votre flux temps réel affiche des artefacts chromatiques et présente une ligne diagonale sur le côté droit de l'image." + }, + "stream": "Flux", + "placeholder": "Choisissez un flux." + }, + "birdseye": "Birdseye" + } + }, + "debug": { + "timestamp": "Horodatage", + "motion": "Mouvement", + "mask": "Masque", + "options": { + "showOptions": "Afficher les options", + "title": "Options", + "label": "Paramètres", + "hideOptions": "Masquer les options" + }, + "boundingBox": "Cadre de détection", + "zones": "Zones", + "regions": "Régions" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/dialog.json new file mode 100644 index 0000000..f0b542b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/dialog.json @@ -0,0 +1,136 @@ +{ + "restart": { + "title": "Êtes-vous sûr de vouloir redémarrer Frigate ?", + "restarting": { + "title": "Redémarrage de Frigate en cours", + "content": "Cette page sera rechargée dans {{countdown}} secondes.", + "button": "Forcer l'actualisation maintenant" + }, + "button": "Redémarrer" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Soumettre à Frigate+", + "desc": "Les objets situés dans des zones à ignorer ne doivent pas être signalés comme de faux positifs, car cela nuirait à la précision du modèle." + }, + "review": { + "true": { + "label": "Confirmez cette étiquette pour Frigate Plus", + "true_one": "C'est un {{label}}", + "true_many": "Ce sont des {{label}}", + "true_other": "Ce sont des {{label}}" + }, + "false": { + "false_one": "Ceci n'est pas un {{label}}", + "false_many": "Ceux-ci ne sont pas des {{label}}", + "false_other": "Ceux-ci ne sont pas des {{label}}", + "label": "Ne pas confirmer cette étiquette pour Frigate Plus" + }, + "state": { + "submitted": "Soumis" + }, + "question": { + "label": "Confirmez cette étiquette pour Frigate+.", + "ask_an": "Cet objet est-il un(e) {{label}} ?", + "ask_a": "Cet objet est-il un(e) {{label}} ?", + "ask_full": "Cet objet est-il un(e) {{translatedLabel}}  ?" + } + } + }, + "video": { + "viewInHistory": "Afficher dans la chronologie" + } + }, + "export": { + "time": { + "custom": "Personnalisé", + "fromTimeline": "Sélectionner depuis la chronologie", + "lastHour_one": "Dernière heure", + "lastHour_many": "{{count}} dernières heures", + "lastHour_other": "{{count}} dernières heures", + "end": { + "label": "Sélectionner une heure de fin", + "title": "Heure de fin" + }, + "start": { + "label": "Sélectionner une heure de début", + "title": "Heure de début" + } + }, + "selectOrExport": "Sélectionner ou exporter", + "toast": { + "error": { + "failed": "Échec du démarrage de l'exportation : {{error}}", + "endTimeMustAfterStartTime": "L'heure de fin doit être postérieure à l'heure de début.", + "noVaildTimeSelected": "La plage horaire sélectionnée n'est pas valide." + }, + "success": "Exportation démarrée avec succès. Consultez le fichier sur la page des exportations.", + "view": "Vue" + }, + "select": "Sélectionner", + "name": { + "placeholder": "Nommer l'exportation" + }, + "export": "Exporter", + "fromTimeline": { + "saveExport": "Enregistrer l'exportation", + "previewExport": "Aperçu de l'exportation" + } + }, + "search": { + "saveSearch": { + "desc": "Saisissez un nom pour cette recherche enregistrée.", + "label": "Enregistrer la recherche", + "success": "La recherche ({{searchName}}) a été enregistrée.", + "button": { + "save": { + "label": "Enregistrer cette recherche" + } + }, + "overwrite": "{{searchName}} existe déjà. L'enregistrement écrasera la recherche existante.", + "placeholder": "Saisissez un nom pour votre recherche." + } + }, + "streaming": { + "label": "Flux", + "restreaming": { + "disabled": "La rediffusion n'est pas activée pour cette caméra.", + "desc": { + "readTheDocumentation": "Lire la documentation", + "title": "Configurez go2rtc pour bénéficier d'options de visualisation en direct supplémentaires et de l'audio pour cette caméra." + } + }, + "showStats": { + "label": "Afficher les statistiques du flux", + "desc": "Activez cette option pour afficher les statistiques de diffusion en incrustation sur le flux vidéo de la caméra." + }, + "debugView": "Affichage de débogage" + }, + "recording": { + "confirmDelete": { + "desc": { + "selected": "Êtes-vous sûr(e) de vouloir supprimer toutes les vidéos enregistrées associées à cette activité ?

    Maintenez la touche Maj enfoncée pour éviter cette boîte de dialogue à l'avenir." + }, + "title": "Confirmer la suppression", + "toast": { + "success": "Les vidéos associées aux activités sélectionnées ont été supprimées.", + "error": "Échec de la suppression : {{error}}" + } + }, + "button": { + "export": "Exporter", + "markAsReviewed": "Marquer comme traité", + "deleteNow": "Supprimer maintenant", + "markAsUnreviewed": "Marquer comme non traité" + } + }, + "imagePicker": { + "selectImage": "Sélectionnez une vignette d'objet suivi.", + "search": { + "placeholder": "Rechercher par étiquette ou sous-étiquette" + }, + "noImages": "Aucune vignette trouvée pour cette caméra", + "unknownLabel": "Image de déclencheur enregistrée" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/filter.json new file mode 100644 index 0000000..b8af1d6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "label": "Étiquettes", + "all": { + "title": "Toutes les étiquettes", + "short": "Étiquettes" + }, + "count": "{{count}} Étiquettes", + "count_one": "{{count}} étiquette", + "count_other": "{{count}} étiquettes" + }, + "filter": "Filtre", + "zones": { + "label": "Zones", + "all": { + "title": "Toutes les zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Toutes les dates", + "short": "Dates" + }, + "selectPreset": "Sélectionnez un préréglage." + }, + "more": "Plus de filtres", + "reset": { + "label": "Réinitialiser les filtres aux valeurs par défaut" + }, + "timeRange": "Plage horaire", + "subLabels": { + "label": "Sous-étiquettes", + "all": "Toutes les sous-étiquettes" + }, + "score": "Score", + "estimatedSpeed": "Vitesse estimée ({{unit}})", + "sort": { + "label": "Tri", + "dateDesc": "Date (ordre chronologique inverse)", + "dateAsc": "Date (ordre chronologique)", + "scoreDesc": "Score d'objet (décroissant)", + "scoreAsc": "Score d'objet (croissant)", + "speedAsc": "Vitesse estimée (croissant)", + "speedDesc": "Vitesse estimée (décroissant)", + "relevance": "Pertinence" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Vous devez d'abord filtrer les objets suivis qui ont un instantané.

    Les objets suivis sans instantané ne peuvent pas être soumis à Frigate+.", + "label": "Soumis à Frigate+" + }, + "hasVideoClip": "Avec une séquence vidéo", + "hasSnapshot": "Avec un instantané", + "label": "Caractéristiques" + }, + "explore": { + "settings": { + "title": "Paramètres", + "defaultView": { + "title": "Vue par défaut", + "summary": "Résumé", + "unfilteredGrid": "Grille non filtrée", + "desc": "Lorsqu'aucun filtre n'est sélectionné, afficher un résumé des objets suivis les plus récents par étiquette, ou afficher une grille non filtrée" + }, + "gridColumns": { + "desc": "Sélectionner le nombre de colonnes dans la vue grille.", + "title": "Colonnes de la grille" + }, + "searchSource": { + "label": "Source de recherche", + "options": { + "thumbnailImage": "Miniature", + "description": "Description" + }, + "desc": "Choisissez si vous souhaitez rechercher les miniatures ou les descriptions de vos objets suivis." + } + }, + "date": { + "selectDateBy": { + "label": "Sélectionner une date pour filtrer" + } + } + }, + "review": { + "showReviewed": "Afficher les activités traitées" + }, + "cameras": { + "label": "Filtre des caméras", + "all": { + "short": "Caméras", + "title": "Toutes les caméras" + } + }, + "motion": { + "showMotionOnly": "Afficher uniquement le mouvement" + }, + "logSettings": { + "filterBySeverity": "Filtrer les journaux par gravité", + "loading": { + "title": "Chargement", + "desc": "Lorsque le volet de journalisation est défilé jusqu'en bas, les nouveaux enregistrements s'affichent automatiquement au fur et à mesure qu'ils sont ajoutés." + }, + "label": "Filtrer par niveau de journal", + "disableLogStreaming": "Désactiver le flux des journaux", + "allLogs": "Tous les journaux" + }, + "recognizedLicensePlates": { + "placeholder": "Tapez pour rechercher des plaques d'immatriculation.", + "noLicensePlatesFound": "Aucune plaque d'immatriculation trouvée", + "loading": "Chargement des plaques d'immatriculation reconnues…", + "title": "Plaques d'immatriculation reconnues", + "loadFailed": "Échec du chargement des plaques d'immatriculation reconnues.", + "selectPlatesFromList": "Sélectionnez une ou plusieurs plaques d'immatriculation dans la liste.", + "selectAll": "Tout sélectionner", + "clearAll": "Tout désélectionner" + }, + "trackedObjectDelete": { + "title": "Confirmer la suppression", + "toast": { + "success": "Objets suivis supprimés avec succès.", + "error": "Échec de la suppression des objets suivis : {{errorMessage}}" + }, + "desc": "La suppression de ces {{objectLength}} objets suivis retirera l'instantané, les embeddings enregistrés et les entrées du cycle de vie de l'objet associées. Les séquences enregistrées de ces objets suivis dans la vue Chronologie NE seront PAS supprimées.

    Voulez-vous vraiment continuer?

    Maintenez la touche Maj enfoncée pour ignorer cette boîte de dialogue à l'avenir." + }, + "zoneMask": { + "filterBy": "Filtrer par masque de zone" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Toutes les classes" + }, + "count_one": "{{count}} classe", + "count_other": "{{count}} classes" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/icons.json new file mode 100644 index 0000000..fd5f1f8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Rechercher une icône" + }, + "selectIcon": "Sélectionnez une icône." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/input.json new file mode 100644 index 0000000..0d8130c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Télécharger la vidéo", + "toast": { + "success": "Le téléchargement de la vidéo a commencé." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/fr/components/player.json new file mode 100644 index 0000000..6450c12 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Aucun enregistrement trouvé pour cette période", + "noPreviewFoundFor": "Aucun aperçu trouvé pour {{cameraName}}", + "noPreviewFound": "Aucun aperçu trouvé", + "submitFrigatePlus": { + "title": "Soumettre cette image à Frigate+ ?", + "submit": "Soumettre" + }, + "streamOffline": { + "title": "Flux hors ligne", + "desc": "Aucune image n'a été reçue sur le flux de détection de la caméra {{cameraName}}. Vérifiez le journal d'erreurs." + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ou une version supérieure est requise pour ce type de flux en direct.", + "cameraDisabled": "La caméra est désactivée.", + "stats": { + "streamType": { + "title": "Type de flux :", + "short": "Type" + }, + "bandwidth": { + "title": "Bande passante :", + "short": "Bande passante" + }, + "latency": { + "title": "Latence :", + "value": "{{seconds}} secondes", + "short": { + "title": "Latence", + "value": "{{seconds}} s" + } + }, + "droppedFrames": { + "short": { + "value": "{{droppedFrames}} images", + "title": "Perdues" + }, + "title": "Images perdues :" + }, + "decodedFrames": "Images décodées :", + "droppedFrameRate": "Taux d'images perdues :", + "totalFrames": "Total images :" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Échec de la soumission de l'image à Frigate+" + }, + "success": { + "submittedFrigatePlus": "Image soumise avec succès à Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/objects.json b/sam2-cpu/frigate-dev/web/public/locales/fr/objects.json new file mode 100644 index 0000000..9c9d5a6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/objects.json @@ -0,0 +1,120 @@ +{ + "bicycle": "Vélo", + "car": "Voiture", + "person": "Personne", + "motorcycle": "Moto", + "airplane": "Avion", + "bus": "Bus", + "train": "Train", + "boat": "Bateau", + "traffic_light": "Feu de circulation", + "fire_hydrant": "Bouche d'incendie", + "street_sign": "Panneau de signalisation", + "parking_meter": "Parcmètre", + "bench": "Banc", + "bird": "Oiseau", + "cat": "Chat", + "stop_sign": "Panneau de stop", + "dog": "Chien", + "horse": "Cheval", + "sheep": "Mouton", + "cow": "Vache", + "elephant": "Éléphant", + "bear": "Ours", + "zebra": "Zèbre", + "hat": "Chapeau", + "tie": "Cravate", + "suitcase": "Valise", + "frisbee": "Frisbee", + "skis": "Skis", + "snowboard": "Snowboard", + "sports_ball": "Ballon de sport", + "kite": "Cerf-volant", + "baseball_bat": "Batte de baseball", + "umbrella": "Parapluie", + "giraffe": "Girafe", + "eye_glasses": "Lunettes", + "backpack": "Sac à dos", + "handbag": "Sac à main", + "shoe": "Chaussure", + "clock": "Horloge", + "bottle": "Bouteille", + "baseball_glove": "Gant de baseball", + "skateboard": "Skateboard", + "surfboard": "Planche de surf", + "tennis_racket": "Raquette de tennis", + "plate": "Assiette", + "cup": "Tasse", + "banana": "Banane", + "apple": "Pomme", + "wine_glass": "Verre à vin", + "pizza": "Pizza", + "couch": "Canapé", + "potted_plant": "Plante en pot", + "mirror": "Miroir", + "window": "Fenêtre", + "desk": "Bureau", + "door": "Porte", + "remote": "Télécommande", + "keyboard": "Clavier", + "mouse": "Souris", + "tv": "TV", + "laptop": "Ordinateur portable", + "toaster": "Grille-pain", + "book": "Livre", + "teddy_bear": "Ours en peluche", + "blender": "Mixeur", + "toothbrush": "Brosse à dents", + "hair_brush": "Brosse à cheveux", + "vehicle": "Véhicule", + "fox": "Renard", + "deer": "Cerf", + "animal": "Animal", + "goat": "Chèvre", + "rabbit": "Lapin", + "raccoon": "Raton laveur", + "waste_bin": "Poubelle", + "robot_lawnmower": "Robot tondeuse", + "on_demand": "Sur demande", + "face": "Visage", + "license_plate": "Plaque d'immatriculation", + "bbq_grill": "Barbecue", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "package": "Colis", + "an_post": "An Post", + "gls": "GLS", + "dpd": "DPD", + "postnl": "PostNL", + "amazon": "Amazon", + "hot_dog": "Hot Dog", + "refrigerator": "Réfrigérateur", + "bark": "Aboiement", + "oven": "Four", + "scissors": "Ciseaux", + "toilet": "Toilettes", + "carrot": "Carotte", + "bed": "Lit", + "cell_phone": "Téléphone portable", + "fork": "Fourchette", + "squirrel": "Écureuil", + "microwave": "Micro-ondes", + "hair_dryer": "Sèche-cheveux", + "bowl": "Bol", + "spoon": "Cuillère", + "sandwich": "Sandwich", + "sink": "Évier", + "broccoli": "Brocoli", + "knife": "Couteau", + "nzpost": "NZPost", + "orange": "Orange", + "chair": "Chaise", + "donut": "Donut", + "usps": "USPS", + "cake": "Gâteau", + "dining_table": "Table à manger", + "vase": "Vase", + "purolator": "Purolator", + "postnord": "PostNord" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/classificationModel.json new file mode 100644 index 0000000..c18944f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Modèles de classification - Frigate", + "button": { + "deleteClassificationAttempts": "Supprimer les images de classification", + "renameCategory": "Renommer la classe", + "deleteCategory": "Supprimer la classe", + "deleteImages": "Supprimer les images", + "trainModel": "Entraîner le modèle", + "addClassification": "Ajouter une classification", + "deleteModels": "Supprimer les modèles", + "editModel": "Modifier le modèle" + }, + "toast": { + "success": { + "deletedCategory": "Classe supprimée", + "deletedImage": "Images supprimées", + "categorizedImage": "Image classifiée avec succès", + "trainedModel": "Modèle entraîné avec succès.", + "trainingModel": "L'entraînement du modèle a démarré avec succès.", + "deletedModel_one": "{{count}} modèle supprimé avec succès", + "deletedModel_many": "{{count}} modèles supprimés avec succès", + "deletedModel_other": "{{count}} modèles supprimés avec succès", + "updatedModel": "Configuration du modèle mise à jour avec succès", + "renamedCategory": "Classe renommée en {{name}} avec succès" + }, + "error": { + "deleteImageFailed": "Échec de la suppression : {{errorMessage}}", + "deleteCategoryFailed": "Échec de la suppression de la classe : {{errorMessage}}", + "categorizeFailed": "Échec de la catégorisation de l'image : {{errorMessage}}", + "trainingFailed": "L'entraînement du modèle a échoué. Consultez les journaux de Frigate pour plus de détails.", + "deleteModelFailed": "Impossible de supprimer le modèle : {{errorMessage}}", + "updateModelFailed": "Impossible de mettre à jour le modèle : {{errorMessage}}", + "renameCategoryFailed": "Impossible de renommer la classe : {{errorMessage}}", + "trainingFailedToStart": "Impossible de démarrer l'entraînement du modèle : {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Supprimer la classe", + "desc": "Êtes-vous sûr de vouloir supprimer la classe {{name}} ? Cette action supprimera définitivement toutes les images associées et nécessitera un réentraînement du modèle.", + "minClassesTitle": "Impossible de supprimer la classe", + "minClassesDesc": "Un modèle de classification doit avoir au moins 2 classes. Ajoutez une autre classe avant de supprimer celle-ci." + }, + "deleteDatasetImages": { + "title": "Supprimer les images du jeu de données", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images du jeu de données {{dataset}} ? Cette action est irréversible et nécessitera un réentraînement du modèle." + }, + "deleteTrainImages": { + "title": "Supprimer les images d'entraînement", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} image ? Cette action est irréversible.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} images ? Cette action est irréversible.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} images ? Cette action est irréversible." + }, + "renameCategory": { + "title": "Renommer la classe", + "desc": "Saisissez un nouveau nom pour {{name}}. Vous devrez réentraîner le modèle pour que le changement de nom prenne effet." + }, + "description": { + "invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets." + }, + "train": { + "title": "Classifications récentes", + "aria": "Sélectionner des classifications récentes", + "titleShort": "Récent" + }, + "categories": "Classes", + "createCategory": { + "new": "Créer une nouvelle classe" + }, + "categorizeImageAs": "Classifier comme :", + "categorizeImage": "Classifier l'image", + "noModels": { + "object": { + "title": "Aucun modèle de classification d'objets", + "description": "Créer un modèle personnalisé pour classifier les objets détectés", + "buttonText": "Créer un modèle d'objets" + }, + "state": { + "title": "Aucun modèle de classification d'états", + "description": "Créer un modèle personnalisé pour surveiller et classifier les changements d'état dans des zones de caméra spécifiques", + "buttonText": "Créer un modèle d'états" + } + }, + "wizard": { + "title": "Créer une nouvelle classification", + "steps": { + "nameAndDefine": "Nom et définition", + "stateArea": "Zone d'état", + "chooseExamples": "Choisir des exemples" + }, + "step1": { + "description": "Les modèles d'état surveillent des zones de caméra fixes pour détecter des changements (par ex., porte ouverte/fermée). Les modèles d'objets ajoutent des classifications aux objets détectés (par ex., animaux connus, livreurs, etc.).", + "name": "Nom", + "namePlaceholder": "Saisissez un nom de modèle.", + "type": "Type", + "typeState": "État", + "typeObject": "Objet", + "objectLabel": "Étiquette d'objet", + "objectLabelPlaceholder": "Sélectionnez un type d'objet.", + "classificationType": "Type de classification", + "classificationTypeTip": "En savoir plus sur les types de classification", + "classificationTypeDesc": "Les sous-étiquettes ajoutent du texte supplémentaire à l'étiquette d'objet (par ex., « Personne : UPS »). Les attributs sont des métadonnées recherchables stockées séparément dans les métadonnées de l'objet.", + "classificationSubLabel": "Sous-étiquette", + "classificationAttribute": "Attribut", + "classes": "Classes", + "classesTip": "En savoir plus sur les classes", + "classesStateDesc": "Définissez les différents états que votre zone de caméra peut avoir. Par exemple : « ouvert » et « fermé » pour une porte de garage.", + "classesObjectDesc": "Définissez les différentes catégories pour classifier les objets détectés. Par exemple : « livreur », « résident », « inconnu » pour la classification des personnes.", + "classPlaceholder": "Saisissez le nom de la classe.", + "errors": { + "nameRequired": "Le nom du modèle est requis.", + "nameLength": "Le nom du modèle ne doit pas dépasser 64 caractères.", + "nameOnlyNumbers": "Le nom du modèle ne peut pas contenir uniquement des chiffres.", + "classRequired": "Au moins une classe est requise.", + "classesUnique": "Les noms de classe doivent être uniques.", + "stateRequiresTwoClasses": "Les modèles d'état nécessitent au moins deux classes.", + "objectLabelRequired": "Veuillez sélectionner une étiquette d'objet.", + "objectTypeRequired": "Veuillez sélectionner un type de classification." + }, + "states": "États" + }, + "step2": { + "description": "Sélectionnez les caméras et définissez la zone à surveiller pour chaque caméra. Le modèle classifiera l'état de ces zones.", + "cameras": "Caméras", + "selectCamera": "Sélectionner une caméra", + "noCameras": "Cliquez sur + pour ajouter des caméras.", + "selectCameraPrompt": "Sélectionnez une caméra dans la liste pour définir sa zone de surveillance." + }, + "step3": { + "selectImagesPrompt": "Sélectionner toutes les images contenant : {{className}}", + "selectImagesDescription": "Cliquez sur les images pour les sélectionner. Cliquez sur Continuer lorsque vous avez terminé avec cette classe.", + "generating": { + "title": "Génération d'images d'exemple en cours", + "description": "Frigate récupère des images représentatives à partir de vos enregistrements. Cela peut prendre un moment..." + }, + "training": { + "title": "Entraînement du modèle", + "description": "Votre modèle est en cours d'entraînement en arrière-plan. Fermez cette boîte de dialogue. Votre modèle se lancera dès que l'entraînement sera terminé." + }, + "retryGenerate": "Réessayer la génération", + "noImages": "Aucune image d'exemple générée", + "classifying": "Classification et entraînement en cours...", + "trainingStarted": "Entraînement démarré avec succès", + "errors": { + "noCameras": "Aucune caméra n'est configurée.", + "noObjectLabel": "Aucune étiquette d'objet sélectionnée", + "generateFailed": "Échec de la génération des exemples : {{error}}", + "generationFailed": "Échec de la génération. Veuillez réessayer.", + "classifyFailed": "Échec de la classification des images : {{error}}" + }, + "generateSuccess": "Génération des images d'exemple réussie", + "allImagesRequired_one": "Veuillez classifier toutes les images. {{count}} image restante.", + "allImagesRequired_many": "Veuillez classifier toutes les images. {{count}} images restantes.", + "allImagesRequired_other": "Veuillez classifier toutes les images. {{count}} images restantes.", + "modelCreated": "Modèle créé avec succès. Utilisez la vue Classifications récentes pour ajouter des images pour les états manquants, puis entraînez le modèle.", + "missingStatesWarning": { + "title": "Exemples d'états manquants", + "description": "Pour des résultats optimaux, il est recommandé de sélectionner des exemples pour tous les états. Vous pouvez continuer sans cette étape, mais le modèle ne sera entraîné que lorsque chaque état disposera d'images. Continuez, puis utilisez la vue Classifications récentes pour classer les images manquantes et lancer l'entraînement." + } + } + }, + "deleteModel": { + "title": "Supprimer le modèle de classification", + "single": "Voulez-vous vraiment supprimer {{name}} ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_one": "Voulez-vous vraiment supprimer {{count}} modèle ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_many": "Voulez-vous vraiment supprimer {{count}} modèles ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible.", + "desc_other": "Voulez-vous vraiment supprimer {{count}} modèles ? Cela supprimera définitivement toutes les données associées, y compris les images et les données d'entraînement. Cette action est irréversible." + }, + "menu": { + "objects": "Objets", + "states": "États" + }, + "details": { + "scoreInfo": "Le score représente la moyenne de la confiance de classification pour toutes les détections de cet objet." + }, + "edit": { + "title": "Modifier le modèle de classification", + "descriptionState": "Modifier les classes pour ce modèle de classification d'état. Les modifications nécessiteront un réentraînement du modèle.", + "descriptionObject": "Modifier le type d'objet et le type de classification pour ce modèle de classification d'objet", + "stateClassesInfo": "Note : La modification des classes d'état nécessite un réentraînement du modèle avec les classes mises à jour." + }, + "tooltip": { + "trainingInProgress": "Modèle en cours d'entraînement", + "noNewImages": "Aucune nouvelle image pour l'entraînement. Veuillez d'abord classifier plus d'images dans le jeu de données.", + "modelNotReady": "Le modèle n'est pas prêt pour l'entraînement.", + "noChanges": "Aucune modification du jeu de données depuis le dernier entraînement" + }, + "none": "Aucun" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/configEditor.json new file mode 100644 index 0000000..0ab9b2c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Éditeur de configuration", + "documentTitle": "Éditeur de configuration - Frigate", + "copyConfig": "Copier la configuration", + "saveOnly": "Enregistrer uniquement", + "saveAndRestart": "Enregistrer et redémarrer", + "toast": { + "success": { + "copyToClipboard": "Configuration copiée dans le presse-papiers" + }, + "error": { + "savingError": "Erreur lors de l'enregistrement de la configuration" + } + }, + "confirm": "Quitter sans enregistrer ?", + "safeConfigEditor": "Éditeur de configuration (mode sans échec)", + "safeModeDescription": "Frigate est en mode sans échec en raison d'une erreur de validation de la configuration." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/events.json new file mode 100644 index 0000000..833dc4b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/events.json @@ -0,0 +1,64 @@ +{ + "detections": "Détections", + "motion": { + "label": "Mouvement", + "only": "Mouvement uniquement" + }, + "alerts": "Alertes", + "allCameras": "Toutes les caméras", + "empty": { + "alert": "Aucune alerte à traiter", + "detection": "Aucune détection à traiter", + "motion": "Aucune donnée de mouvement trouvée" + }, + "timeline": "Chronologie", + "events": { + "label": "Événements", + "aria": "Sélectionner les événements", + "noFoundForTimePeriod": "Aucun événement n'a été trouvé pour cette plage de temps." + }, + "documentTitle": "Activités - Frigate", + "recordings": { + "documentTitle": "Enregistrements - Frigate" + }, + "calendarFilter": { + "last24Hours": "Dernières 24 heures" + }, + "timeline.aria": "Sélectionner une chronologie", + "markAsReviewed": "Marquer comme traitê", + "newReviewItems": { + "button": "Nouvelles activités à traiter", + "label": "Afficher les nouvelles activités" + }, + "camera": "Caméra", + "markTheseItemsAsReviewed": "Marquer ces activités comme traitées", + "selected": "{{count}} sélectionné(s)", + "selected_other": "{{count}} sélectionné(s)", + "selected_one": "{{count}} sélectionné(s)", + "detected": "détecté", + "suspiciousActivity": "Activité suspecte", + "threateningActivity": "Activité menaçante", + "detail": { + "noDataFound": "Aucun détail à traiter", + "aria": "Activer/désactiver la vue détaillée", + "trackedObject_one": "{{count}} objet", + "trackedObject_other": "{{count}} objets", + "noObjectDetailData": "Aucun détail d'objet disponible", + "label": "Détail", + "settings": "Paramètres de la vue Détail", + "alwaysExpandActive": { + "title": "Toujours développer l'élément actif", + "desc": "Toujours développer les détails de l'objet pour l'activité en cours" + } + }, + "objectTrack": { + "trackedPoint": "Point suivi", + "clickToSeek": "Cliquez pour atteindre ce moment." + }, + "zoomIn": "Zoom avant", + "zoomOut": "Zoom arrière", + "normalActivity": "Normal", + "needsReview": "À traiter", + "securityConcern": "Problème de sécurité", + "select_all": "Tous" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/explore.json new file mode 100644 index 0000000..542999b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/explore.json @@ -0,0 +1,295 @@ +{ + "generativeAI": "IA générative", + "documentTitle": "Explorer - Frigate", + "exploreIsUnavailable": { + "title": "L'exploration est indisponible", + "embeddingsReindexing": { + "estimatedTime": "Temps restant estimé :", + "finishingShortly": "Bientôt fini", + "context": "L'exploration peut être utilisée une fois la réindexation des embeddings des objets suivis terminée.", + "startingUp": "Démarrage…", + "step": { + "thumbnailsEmbedded": "Embeddings des miniatures : ", + "descriptionsEmbedded": "Embeddings des descriptions  : ", + "trackedObjectsProcessed": "Objets suivis traités : " + } + }, + "downloadingModels": { + "context": "Frigate télécharge les modèles d'embeddings nécessaires pour prendre en charge la fonctionnalité de recherche sémantique. Cette opération peut prendre plusieurs minutes selon la vitesse de votre connexion réseau.", + "setup": { + "visionModelFeatureExtractor": "Extracteur de caractéristiques de modèle de vision", + "textTokenizer": "Tokeniseur de texte", + "visionModel": "Modèle de vision", + "textModel": "Modèle de texte" + }, + "tips": { + "documentation": "Lire la documentation", + "context": "Une fois les modèles téléchargés, il est conseillé de réindexer les embeddings de vos objets suivis." + }, + "error": "Une erreur est survenue. Vérifiez les journaux Frigate." + } + }, + "details": { + "timestamp": "Horodatage", + "item": { + "title": "Détails de l'activité", + "button": { + "share": "Partager cette activité", + "viewInExplore": "Afficher dans Explorer" + }, + "toast": { + "success": { + "regenerate": "Une nouvelle description a été demandée à {{provider}}. Selon la vitesse de votre fournisseur, la régénération de la nouvelle description peut prendre un certain temps.", + "updatedSublabel": "Sous-étiquette mise à jour avec succès", + "updatedLPR": "Plaque d'immatriculation mise à jour avec succès", + "audioTranscription": "Transcription audio demandée avec succès. Selon la vitesse de votre serveur Frigate, la transcription peut prendre un certain temps." + }, + "error": { + "regenerate": "Échec de l'appel de {{provider}} pour une nouvelle description : {{errorMessage}}", + "updatedSublabelFailed": "Échec de la mise à jour de la sous-étiquette : {{errorMessage}}", + "updatedLPRFailed": "Échec de la mise à jour de la plaque d'immatriculation : {{errorMessage}}", + "audioTranscription": "Échec de la demande de transcription audio : {{errorMessage}}" + } + }, + "tips": { + "mismatch_one": "{{count}} objet indisponible a été détecté et intégré dans cette activité. Cet objet n'a pas été qualifié comme une alerte ou une détection, ou a déjà été nettoyé / supprimé.", + "mismatch_many": "{{count}} objets indisponibles ont été détectés et intégrés dans cette activité. Ces objets n'ont pas été qualifiés comme une alerte ou une détection, ou ont déjà été nettoyés / supprimés.", + "mismatch_other": "{{count}} objets indisponibles ont été détectés et intégrés dans cette activité. Ces objets n'ont pas été qualifiés comme une alerte ou une détection, ou ont déjà été nettoyés / supprimés.", + "hasMissingObjects": "Ajustez votre configuration si vous souhaitez que Frigate enregistre les objets suivis pour les étiquettes suivantes : {{objects}}" + }, + "desc": "Détails de l'activité" + }, + "label": "Étiquette", + "editSubLabel": { + "title": "Modifier la sous-étiquette", + "desc": "Saisissez une nouvelle sous-étiquette pour {{label}}.", + "descNoLabel": "Saisissez une nouvelle sous-étiquette pour cet objet suivi." + }, + "topScore": { + "label": "Meilleur score", + "info": "Le meilleur score correspond au score médian le plus élevé de l'objet suivi, il peut donc différer du score affiché sur la miniature du résultat de recherche." + }, + "objects": "Objets", + "button": { + "regenerate": { + "label": "Générer à nouveau la description de l'objet suivi", + "title": "Générer à nouveau" + }, + "findSimilar": "Trouver des éléments similaires" + }, + "description": { + "label": "Description", + "placeholder": "Description de l'objet suivi", + "aiTips": "Frigate ne demandera pas de description à votre fournisseur d'IA générative tant que le cycle de vie de l'objet suivi ne sera pas terminé." + }, + "regenerateFromSnapshot": "Générer à nouveau à partir d'un instantané", + "regenerateFromThumbnails": "Générer à nouveau à partir des miniatures", + "editLPR": { + "title": "Modifier la plaque d'immatriculation", + "desc": "Saisissez une nouvelle valeur de plaque d'immatriculation pour {{label}}.", + "descNoLabel": "Saisissez une nouvelle valeur de plaque d'immatriculation pour cet objet suivi." + }, + "recognizedLicensePlate": "Plaque d'immatriculation reconnue", + "estimatedSpeed": "Vitesse estimée", + "zones": "Zones", + "expandRegenerationMenu": "Développer le menu de régénération", + "camera": "Caméra", + "tips": { + "descriptionSaved": "Description enregistrée avec succès", + "saveDescriptionFailed": "Échec de la mise à jour de la description : {{errorMessage}}" + }, + "snapshotScore": { + "label": "Score de l'instantané" + }, + "score": { + "label": "Score" + } + }, + "type": { + "details": "détails", + "video": "vidéo", + "object_lifecycle": "cycle de vie de l'objet", + "snapshot": "instantané", + "thumbnail": "Miniature", + "tracking_details": "Détails du suivi" + }, + "objectLifecycle": { + "title": "Cycle de vie de l'objet", + "noImageFound": "Aucune image trouvée pour cet horodatage", + "createObjectMask": "Créer un masque d'objet", + "scrollViewTips": "Faites défiler pour voir les moments clés du cycle de vie de cet objet.", + "adjustAnnotationSettings": "Ajuster les paramètres d'annotation", + "autoTrackingTips": "Les positions des cadres englobants seront imprécises pour les caméras à suivi automatique.", + "lifecycleItemDesc": { + "visible": "{{label}} détecté", + "entered_zone": "{{label}} est entré dans {{zones}}.", + "stationary": "{{label}} est devenu stationnaire.", + "attribute": { + "other": "{{label}} reconnu comme {{attribute}}", + "faceOrLicense_plate": "{{attribute}} détecté pour {{label}}" + }, + "gone": "{{label}} parti", + "heard": "{{label}} entendu", + "external": "{{label}} détecté", + "active": "{{label}} est devenu actif.", + "header": { + "zones": "Zones", + "area": "Aire", + "ratio": "Ratio" + } + }, + "annotationSettings": { + "title": "Paramètres d'annotation", + "showAllZones": { + "title": "Afficher toutes les zones", + "desc": "Afficher systématiquement les zones sur les images quand des objets y sont entrés" + }, + "offset": { + "label": "Décalage de l'annotation", + "documentation": "Lire la documentation ", + "desc": "Ces données, issues du flux de détection de votre caméra, sont incrustées dans les images du flux d'enregistrement. Cependant, une synchronisation parfaite entre ces deux flux est rarement garantie. Il est donc possible que le cadre englobant et la séquence ne soient pas parfaitement alignés. Pour corriger ce décalage, vous pouvez utiliser le champ annotation_offset.", + "millisecondsToOffset": "Décalage des annotations de détection en millisecondes. Par défaut : 0", + "tips": "Astuce : Pour mieux comprendre, visualisez un clip où une personne se déplace de gauche à droite. Si le cadre englobant affiché sur la ligne de temps de l'événement se trouve constamment à gauche de la personne, cela signifie que vous devriez réduire la valeur. À l'inverse, si ce même cadre englobant apparaît systématiquement en avance sur la personne qui marche de gauche à droite, alors vous devrez l'augmenter.", + "toast": { + "success": "Le décalage d'annotation pour {{camera}} a été enregistré dans le fichier de configuration. Redémarrez Frigate pour appliquer vos modifications." + } + } + }, + "carousel": { + "next": "Diapositive suivante", + "previous": "Diapositive précédente" + }, + "trackedPoint": "Point de suivi", + "count": "{{first}} de {{second}}" + }, + "trackedObjectDetails": "Détails de l'objet suivi", + "itemMenu": { + "downloadSnapshot": { + "label": "Télécharger l'instantané", + "aria": "Télécharger l'instantané" + }, + "findSimilar": { + "label": "Trouver des éléments similaires", + "aria": "Trouver des objets suivis similaires" + }, + "viewObjectLifecycle": { + "aria": "Afficher le cycle de vie de l'objet", + "label": "Visualiser le cycle de vie de l'objet" + }, + "viewInHistory": { + "label": "Afficher dans la chronologie", + "aria": "Afficher dans la chronologie" + }, + "downloadVideo": { + "label": "Télécharger la vidéo", + "aria": "Télécharger la vidéo" + }, + "submitToPlus": { + "label": "Soumettre à Frigate+", + "aria": "Soumettre à Frigate+" + }, + "deleteTrackedObject": { + "label": "Supprimer cet objet suivi" + }, + "addTrigger": { + "label": "Ajouter un déclencheur", + "aria": "Ajouter un déclencheur pour cet objet suivi" + }, + "audioTranscription": { + "label": "Transcrire", + "aria": "Demander une transcription audio" + }, + "showObjectDetails": { + "label": "Afficher le parcours de l'objet" + }, + "hideObjectDetails": { + "label": "Masquer le parcours de l'objet" + }, + "viewTrackingDetails": { + "label": "Voir les détails du suivi", + "aria": "Afficher les détails du suivi" + }, + "downloadCleanSnapshot": { + "label": "Télécharger l'instantané vierge", + "aria": "Télécharger l'instantané vierge" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmer la suppression", + "desc": "La suppression de cet objet suivi supprime l'instantané, les embeddings enregistrés et les entrées du cycle de vie de l'objet associé. Les images enregistrées de cet objet suivi dans la vue Chronologie NE seront PAS supprimées.

    Êtes-vous sûr de vouloir continuer ?" + } + }, + "noTrackedObjects": "Aucun objet suivi trouvé", + "fetchingTrackedObjectsFailed": "Erreur lors de la récupération des objets suivis : {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objet suivi ", + "trackedObjectsCount_many": "{{count}} objets suivis ", + "trackedObjectsCount_other": "{{count}} objets suivis ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "L'objet suivi a été supprimé avec succès.", + "error": "Échec de la suppression de l'objet suivi : {{errorMessage}}" + } + }, + "tooltip": "Correspondance : {{type}} à {{confidence}}%", + "previousTrackedObject": "Objet suivi précédent", + "nextTrackedObject": "Objet suivi suivant" + }, + "exploreMore": "Explorer plus d'objets {{label}}", + "aiAnalysis": { + "title": "Analyse IA" + }, + "concerns": { + "label": "Points de vigilance" + }, + "trackingDetails": { + "title": "Détails du suivi", + "noImageFound": "Aucune image trouvée pour cet horodatage", + "createObjectMask": "Créer un masque d'objet", + "adjustAnnotationSettings": "Ajuster les paramètres d'annotation", + "scrollViewTips": "Cliquez pour voir les moments significatifs du cycle de vie de cet objet.", + "autoTrackingTips": "Les positions des cadres de détection seront imprécises pour les caméras à suivi automatique.", + "count": "{{first}} sur {{second}}", + "trackedPoint": "Point suivi", + "lifecycleItemDesc": { + "visible": "{{label}} détecté", + "entered_zone": "{{label}} est entré(e) dans {{zones}}.", + "active": "{{label}} est devenu(e) actif(ve).", + "stationary": "{{label}} s'est immobilisé(e)", + "attribute": { + "faceOrLicense_plate": "Détection de {{attribute}} pour {{label}}", + "other": "{{label}} reconnu(e) comme {{attribute}}" + }, + "gone": "Sortie de {{label}}", + "heard": "{{label}} entendu(e)", + "external": "{{label}} détecté(e)", + "header": { + "zones": "Zones", + "ratio": "Ratio", + "area": "Surface", + "score": "Score" + } + }, + "annotationSettings": { + "offset": { + "desc": "Ces données proviennent du flux de détection de votre caméra, mais elles sont superposées aux images du flux d'enregistrement. Il est peu probable que les deux flux soient parfaitement synchronisés. Par conséquent, le cadre de délimitation et la vidéo ne s'aligneront pas parfaitement. Vous pouvez utiliser ce paramètre pour décaler les annotations vers l'avant ou vers l'arrière dans le temps afin de mieux les aligner avec la vidéo enregistrée.", + "millisecondsToOffset": "Millisecondes de décalage pour les annotations de détection. Par défaut : 0", + "tips": "Diminuez la valeur si la lecture vidéo est en avance sur les cadres de détection et les points de tracé, et augmentez-la si la lecture vidéo est en retard sur ceux-ci. Cette valeur peut être négative.", + "toast": { + "success": "Le décalage des annotations pour {{camera}} a été sauvegardé dans le fichier de configuration." + }, + "label": "Décalage d'annotation" + }, + "title": "Paramètres d'annotation", + "showAllZones": { + "title": "Afficher toutes les zones", + "desc": "Toujours afficher les zones sur les images lorsqu'un objet pénètre une zone" + } + }, + "carousel": { + "previous": "Diapositive précédente", + "next": "Diapositive suivante" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/exports.json new file mode 100644 index 0000000..3b698d0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exports - Frigate", + "search": "Rechercher", + "noExports": "Aucune exportation trouvée", + "deleteExport": "Supprimer l'exportation", + "deleteExport.desc": "Êtes-vous sûr de vouloir supprimer {{exportName}} ?", + "editExport": { + "title": "Renommer l'exportation", + "desc": "Saisissez un nouveau nom pour cette exportation.", + "saveExport": "Enregistrer l'exportation" + }, + "toast": { + "error": { + "renameExportFailed": "Échec du renommage de l'exportation : {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Partager l'exportation", + "downloadVideo": "Télécharger la vidéo", + "editName": "Modifier le nom", + "deleteExport": "Supprimer l'exportation" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/faceLibrary.json new file mode 100644 index 0000000..7d65a5e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/faceLibrary.json @@ -0,0 +1,104 @@ +{ + "description": { + "addFace": "Ajoutez une nouvelle collection à la bibliothèque de visages en téléversant votre première image.", + "placeholder": "Saisissez un nom pour cette collection.", + "invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets." + }, + "details": { + "person": "Personne", + "confidence": "Confiance", + "face": "Détails du visage", + "timestamp": "Horodatage", + "faceDesc": "Détails de l'objet suivi qui a généré ce visage", + "subLabelScore": "Score de sous-libellé", + "scoreInfo": "Le score du sous-libellé correspond au score pondéré de tous les scores de confiance des visages reconnus. Il est donc possible qu'il ne corresponde pas au score affiché sur l'instantané.", + "unknown": "Inconnu" + }, + "documentTitle": "Bibliothèque de visages - Frigate", + "uploadFaceImage": { + "title": "Téléverser l'image du visage", + "desc": "Téléversez une image pour rechercher des visages et l'inclure dans {{pageToggle}}." + }, + "createFaceLibrary": { + "title": "Créer une collection", + "desc": "Créer une nouvelle collection", + "new": "Créer un nouveau visage", + "nextSteps": "Pour construire une base solide :
  • Utilisez l'onglet Reconnaissances récentes pour sélectionner et entraîner des images pour chaque personne détectée.
  • Privilégiez les images de face pour de meilleurs résultats et évitez d'entraîner le modèle avec des images où les visages sont de biais.
  • " + }, + "train": { + "title": "Reconnaissances récentes", + "aria": "Sélectionnez des reconnaissances récentes.", + "empty": "Il n'y a pas de tentatives récentes de reconnaissance faciale.", + "titleShort": "Récent" + }, + "selectFace": "Sélectionner un visage", + "button": { + "addFace": "Ajouter un visage", + "uploadImage": "Téléverser une image", + "deleteFaceAttempts": "Supprimer les visages", + "reprocessFace": "Retraiter le visage", + "renameFace": "Renommer le visage", + "deleteFace": "Supprimer le visage" + }, + "selectItem": "Sélectionner {{item}}", + "deleteFaceLibrary": { + "title": "Supprimer le nom", + "desc": "Êtes-vous certain de vouloir supprimer la collection {{name}} ? Cette action supprimera définitivement tous les visages associés." + }, + "imageEntry": { + "dropActive": "Déposez l'image ici.", + "dropInstructions": "Glissez-déposez ou collez une image ici, ou cliquez pour la sélectionner.", + "maxSize": "Taille max : {{size}}Mo", + "validation": { + "selectImage": "Veuillez sélectionner un fichier image." + } + }, + "readTheDocs": "Lire la documentation", + "toast": { + "success": { + "deletedName_one": "{{count}} visage a été supprimé avec succès.", + "deletedName_many": "{{count}} visages ont été supprimés avec succès.", + "deletedName_other": "{{count}} visages ont été supprimés avec succès.", + "uploadedImage": "Image téléversée avec succès", + "addFaceLibrary": "{{name}} a été ajouté avec succès à la bibliothèque de visages !", + "updatedFaceScore": "Score du visage ({{score}}) de {{name}} mis à jour avec succès", + "deletedFace_one": "{{count}} visage supprimé avec succès", + "deletedFace_many": "{{count}} visages supprimés avec succès", + "deletedFace_other": "{{count}} visages supprimés avec succès", + "trainedFace": "Visage entraîné avec succès", + "renamedFace": "Visage renommé avec succès en {{name}}" + }, + "error": { + "uploadingImageFailed": "Échec du téléversement de l'image : {{errorMessage}}", + "deleteFaceFailed": "Échec de la suppression : {{errorMessage}}", + "trainFailed": "Échec de l'entraînement : {{errorMessage}}", + "updateFaceScoreFailed": "Échec de la mise à jour du score du visage : {{errorMessage}}", + "addFaceLibraryFailed": "Échec de l'attribution du nom au visage : {{errorMessage}}", + "deleteNameFailed": "Échec de la suppression du nom : {{errorMessage}}", + "renameFaceFailed": "Échec du changement de nom du visage : {{errorMessage}}" + } + }, + "trainFaceAs": "Entraîner le visage comme :", + "trainFace": "Entraîner le visage", + "steps": { + "uploadFace": "Téléverser une image de visage", + "faceName": "Saisissez le nom du visage.", + "nextSteps": "Prochaines étapes", + "description": { + "uploadFace": "Téléversez une image de {{name}} qui montre son visage de face. L'image n'a pas besoin d'être recadrée pour ne montrer que son visage." + } + }, + "renameFace": { + "title": "Renommer le visage", + "desc": "Saisissez un nouveau nom pour {{name}}." + }, + "collections": "Collections", + "deleteFaceAttempts": { + "title": "Supprimer les visages", + "desc_one": "Êtes-vous sûr de vouloir supprimer {{count}} visage ? Cette action est irréversible.", + "desc_many": "Êtes-vous sûr de vouloir supprimer {{count}} visages ? Cette action est irréversible.", + "desc_other": "Êtes-vous sûr de vouloir supprimer {{count}} visages ? Cette action est irréversible." + }, + "nofaces": "Aucun visage disponible", + "pixels": "{{area}} pixels" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/live.json new file mode 100644 index 0000000..e3edc5a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Direct - Frigate", + "lowBandwidthMode": "Mode bande passante faible", + "documentTitle.withCamera": "{{camera}} - Direct - Frigate", + "twoWayTalk": { + "disable": "Désactiver la conversation bidirectionnelle", + "enable": "Activer la conversation bidirectionnelle" + }, + "cameraAudio": { + "disable": "Désactiver le son de la caméra", + "enable": "Activer le son de la caméra" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Cliquez dans le cadre pour centrer la caméra", + "enable": "Activer le clic pour déplacer", + "disable": "Désactiver le clic pour déplacer" + }, + "left": { + "label": "Déplacer la caméra PTZ vers la gauche" + }, + "up": { + "label": "Déplacer la caméra PTZ vers le haut" + }, + "right": { + "label": "Déplacer la caméra PTZ vers la droite" + }, + "down": { + "label": "Déplacer la caméra PTZ vers le bas" + } + }, + "zoom": { + "in": { + "label": "Zoom avant sur la caméra PTZ" + }, + "out": { + "label": "Zoom arrière sur la caméra PTZ" + } + }, + "frame": { + "center": { + "label": "Cliquez dans le cadre pour centrer la caméra PTZ." + } + }, + "presets": "Préréglages de la caméra PTZ", + "focus": { + "in": { + "label": "Mise au point rapprochée de la caméra PTZ" + }, + "out": { + "label": "Mise au point éloignée de la caméra PTZ" + } + } + }, + "camera": { + "enable": "Activer la caméra", + "disable": "Désactiver la caméra" + }, + "detect": { + "enable": "Activer la détection", + "disable": "Désactiver la détection" + }, + "recording": { + "enable": "Activer l'enregistrement", + "disable": "Désactiver l'enregistrement" + }, + "snapshots": { + "enable": "Activer les instantanés", + "disable": "Désactiver les instantanés" + }, + "muteCameras": { + "enable": "Couper le son de toutes les caméras", + "disable": "Activer le son de toutes les caméras" + }, + "audioDetect": { + "enable": "Activer la détection audio", + "disable": "Désactiver la détection audio" + }, + "manualRecording": { + "playInBackground": { + "label": "Jouer en arrière-plan", + "desc": "Activez cette option pour continuer à diffuser lorsque le lecteur est masqué." + }, + "showStats": { + "label": "Afficher les statistiques", + "desc": "Activez cette option pour afficher les statistiques de flux en surimpression sur le flux de la caméra." + }, + "debugView": "Vue de débogage", + "start": "Démarrer l'enregistrement à la demande", + "failedToStart": "Échec du démarrage de l'enregistrement manuel à la demande", + "end": "Terminer l'enregistrement à la demande", + "ended": "Enregistrement manuel à la demande terminé", + "failedToEnd": "Impossible de terminer l'enregistrement manuel à la demande", + "started": "Enregistrement manuel à la demande démarré", + "recordDisabledTips": "Puisque l'enregistrement est désactivé ou restreint dans la configuration de cette caméra, seul un instantané sera enregistré.", + "title": "À la demande", + "tips": "Téléchargez un instantané ou démarrez un événement manuel en fonction des paramètres de conservation des enregistrements de cette caméra." + }, + "streamingSettings": "Paramètres de diffusion", + "notifications": "Notifications", + "suspend": { + "forTime": "Mettre en pause pour : " + }, + "stream": { + "audio": { + "available": "L'audio est disponible pour ce flux", + "tips": { + "documentation": "Lire la documentation ", + "title": "L'audio doit provenir de votre caméra et être configuré dans go2rtc pour ce flux." + }, + "unavailable": "L'audio n'est pas disponible pour ce flux" + }, + "twoWayTalk": { + "tips": "Votre appareil doit prendre en charge cette fonctionnalité et WebRTC doit être configuré pour la conversation bidirectionnelle.", + "tips.documentation": "Lire la documention ", + "available": "Conversation bidirectionnelle disponible pour ce flux", + "unavailable": "Conversation bidirectionnelle non disponible pour ce flux" + }, + "lowBandwidth": { + "tips": "La vue temps réel est en mode bande passante faible à cause de problèmes de mise en mémoire tampon ou d'erreurs de flux.", + "resetStream": "Réinitialiser le flux" + }, + "playInBackground": { + "tips": "Activez cette option pour continuer la diffusion lorsque le lecteur est masqué.", + "label": "Jouer en arrière-plan" + }, + "title": "Flux", + "debug": { + "picker": "La sélection de flux est indisponible en mode débogage. La vue de débogage utilise systématiquement le flux attribué au rôle de détection." + } + }, + "cameraSettings": { + "objectDetection": "Détection d'objets", + "recording": "Enregistrement", + "snapshots": "Instantanés", + "audioDetection": "Détection audio", + "autotracking": "Suivi automatique", + "cameraEnabled": "Caméra activée", + "title": "Paramètres de {{camera}}", + "transcription": "Transcription audio" + }, + "history": { + "label": "Afficher les vidéos archivées" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tous", + "motion": "Mouvement", + "active_objects": "Objets actifs" + }, + "notAllTips": "Votre configuration de conservation d'enregistrement {{source}} est définie sur mode : {{effectiveRetainMode}}, donc cet enregistrement à la demande ne conservera que les segments avec {{effectiveRetainModeName}}." + }, + "audio": "Audio", + "autotracking": { + "enable": "Activer le suivi automatique", + "disable": "Désactiver le suivi automatique" + }, + "streamStats": { + "enable": "Afficher les statistiques du flux", + "disable": "Masquer les statistiques du flux" + }, + "editLayout": { + "label": "Modifier la mise en page", + "group": { + "label": "Modifier le groupe de caméras" + }, + "exitEdit": "Quitter le mode édition" + }, + "transcription": { + "enable": "Activer la transcription audio en direct", + "disable": "Désactiver la transcription audio en direct" + }, + "noCameras": { + "title": "Aucune caméra n'est configurée", + "description": "Pour commencer, connectez une caméra à Frigate.", + "buttonText": "Ajouter une caméra", + "restricted": { + "title": "Aucune caméra disponible", + "description": "Vous n'avez pas la permission de visionner les caméras de ce groupe." + } + }, + "snapshot": { + "takeSnapshot": "Télécharger un instantané", + "noVideoSource": "Aucune source vidéo disponible pour l'instantané", + "captureFailed": "Échec de la capture de l'instantané", + "downloadStarted": "Téléchargement de l'instantané démarré" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/recording.json new file mode 100644 index 0000000..e1960a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exports", + "calendar": "Calendrier", + "filter": "Filtre", + "filters": "Filtres", + "toast": { + "error": { + "noValidTimeSelected": "Aucune plage horaire valide sélectionnée", + "endTimeMustAfterStartTime": "L'heure de fin doit être postérieure à l'heure de début." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/search.json new file mode 100644 index 0000000..8b76ebe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/search.json @@ -0,0 +1,74 @@ +{ + "savedSearches": "Recherches enregistrées", + "search": "Rechercher", + "searchFor": "Rechercher {{inputValue}}", + "button": { + "clear": "Effacer la recherche", + "filterInformation": "Filtrer les informations", + "filterActive": "Filtres actifs", + "save": "Enregistrer la recherche", + "delete": "Supprimer la recherche enregistrée" + }, + "trackedObjectId": "ID d'objet suivi", + "filter": { + "label": { + "zones": "Zones", + "sub_labels": "Sous-étiquettes", + "search_type": "Type de recherche", + "time_range": "Plage horaire", + "labels": "Étiquettes", + "cameras": "Caméras", + "after": "Après", + "before": "Avant", + "min_speed": "Vitesse minimale", + "max_speed": "Vitesse maximale", + "min_score": "Score minimum", + "recognized_license_plate": "Plaque d'immatriculation reconnue", + "has_clip": "Avec une séquence vidéo", + "has_snapshot": "Avec un instantané", + "max_score": "Score maximum" + }, + "searchType": { + "thumbnail": "Miniature", + "description": "Description" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "La date « avant » doit être postérieure à la date « après ».", + "afterDatebeEarlierBefore": "La date « après » doit être antérieure à la date « avant ».", + "minScoreMustBeLessOrEqualMaxScore": "Le « min_score » doit être inférieur ou égal au « max_score ».", + "maxScoreMustBeGreaterOrEqualMinScore": "Le « max_score » doit être supérieur ou égal au « min_score ».", + "minSpeedMustBeLessOrEqualMaxSpeed": "La vitesse minimale doit être inférieure ou égale à la vitesse maximale.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La « vitesse maximale » doit être supérieure ou égale à la « vitesse minimale »." + } + }, + "header": { + "currentFilterType": "Valeurs du filtre", + "activeFilters": "Filtres actifs", + "noFilters": "Filtres" + }, + "tips": { + "title": "Comment utiliser les filtres de texte", + "desc": { + "text": "Les filtres vous aident à affiner vos résultats de recherche. Voici comment les utiliser dans le champ de saisie :", + "example": "Exemple: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Saisissez un nom de filtre suivi de deux points (par exemple, «cameras:»).
    • Sélectionnez une valeur parmi les suggestions ou saisissez la vôtre.
    • Utilisez plusieurs filtres en les ajoutant les uns après les autres, en laissant un espace entre eux.
    • Les filtres de date (avant: et après:) utilisent le format {{DateFormat}}.
    • Le filtre de plage horaire utilise le format {{exampleTime}}.
    • Supprimez les filtres en cliquant sur le «x» à côté d'eux.
    ", + "step1": "Saisissez un nom de clé de filtre suivi de deux points (par exemple, \"cameras:\").", + "step2": "Sélectionnez une valeur parmi les suggestions ou saisissez la vôtre.", + "step3": "Utilisez plusieurs filtres en les ajoutant les uns après les autres avec un espace entre eux.", + "step5": "Le filtre de plage horaire utilise le format {{exampleTime}}.", + "step6": "Supprimez les filtres en cliquant sur le \"x\" à côté d'eux.", + "step4": "Les filtres de dates (avant : et après :) utilisent le format {{DateFormat}}.", + "exampleLabel": "Exemple :" + } + } + }, + "similaritySearch": { + "title": "Recherche par similarité", + "active": "Recherche par similarité activée", + "clear": "Effacer la recherche par similarité" + }, + "placeholder": { + "search": "Rechercher…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/settings.json new file mode 100644 index 0000000..cae2238 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/settings.json @@ -0,0 +1,1314 @@ +{ + "documentTitle": { + "default": "Paramètres - Frigate", + "authentication": "Paramètres d'authentification - Frigate", + "camera": "Paramètres des caméras - Frigate", + "classification": "Paramètres de classification - Frigate", + "motionTuner": "Réglage de la détection de mouvement - Frigate", + "general": "Paramètres de l'interface utilisateur - Frigate", + "masksAndZones": "Éditeur de masques et de zones - Frigate", + "object": "Débogage - Frigate", + "frigatePlus": "Paramètres Frigate+ - Frigate", + "notifications": "Paramètres de notification - Frigate", + "enrichments": "Paramètres d'enrichissements - Frigate", + "cameraManagement": "Gestion des caméras - Frigate", + "cameraReview": "Paramètres des activités caméra - Frigate" + }, + "menu": { + "ui": "Interface utilisateur", + "classification": "Classification", + "masksAndZones": "Masques / Zones", + "motionTuner": "Réglage de la détection de mouvement", + "debug": "Débogage", + "cameras": "Paramètres des caméras", + "users": "Utilisateurs", + "notifications": "Notifications", + "frigateplus": "Frigate+", + "enrichments": "Enrichissements", + "triggers": "Déclencheurs", + "roles": "Rôles", + "cameraManagement": "Gestion", + "cameraReview": "Activités" + }, + "dialog": { + "unsavedChanges": { + "title": "Vous avez des modifications non enregistrées.", + "desc": "Voulez-vous enregistrer vos modifications avant de continuer ?" + } + }, + "cameraSetting": { + "camera": "Caméra", + "noCamera": "Aucune caméra" + }, + "general": { + "title": "Paramètres de l'interface utilisateur", + "liveDashboard": { + "title": "Tableau de bord en direct", + "automaticLiveView": { + "label": "Vue en direct automatique", + "desc": "Basculez automatiquement vers la vue en direct d'une caméra lorsqu'une activité est détectée. La désactivation de cette option limite la mise à jour des images statiques de la caméra sur le tableau de bord en direct à une fois par minute seulement." + }, + "playAlertVideos": { + "label": "Lire les vidéos d'alerte", + "desc": "Par défaut, les alertes récentes du tableau de bord en direct sont diffusées sous forme de petites vidéos en boucle. Désactivez cette option pour afficher uniquement une image statique des alertes récentes sur cet appareil/navigateur." + }, + "displayCameraNames": { + "label": "Toujours afficher les noms des caméras", + "desc": "Toujours afficher les noms des caméras dans une puce sur le tableau de bord de la vue en direct multi-caméras" + }, + "liveFallbackTimeout": { + "label": "Délai d'attente avant repli (Lecteur en direct)", + "desc": "Lorsque le flux en direct haute qualité d'une caméra est indisponible, le lecteur bascule en mode faible bande passante après ce nombre de secondes. Par défaut : 3." + } + }, + "storedLayouts": { + "title": "Mises en page stockées", + "desc": "La disposition des caméras d'un groupe peut être déplacée/redimensionnée. Les positions sont enregistrées dans le stockage local de votre navigateur.", + "clearAll": "Effacer toutes les mises en page" + }, + "cameraGroupStreaming": { + "title": "Paramètres de diffusion du groupe de caméras", + "desc": "Les paramètres de diffusion en continu pour chaque groupe de caméras sont stockés dans le stockage local de votre navigateur.", + "clearAll": "Effacer tous les paramètres de diffusion" + }, + "recordingsViewer": { + "title": "Visionneuse d'enregistrements", + "defaultPlaybackRate": { + "label": "Vitesse de lecture par défaut", + "desc": "Vitesse de lecture par défaut pour la lecture des enregistrements" + } + }, + "calendar": { + "firstWeekday": { + "label": "Premier jour de la semaine", + "desc": "Jour du début de la semaine du calendrier des activités", + "sunday": "Dimanche", + "monday": "Lundi" + }, + "title": "Calendrier" + }, + "toast": { + "error": { + "clearStoredLayoutFailed": "Échec de l'effacement de la mise en page enregistrée : {{errorMessage}}", + "clearStreamingSettingsFailed": "Échec de l'effacement des paramètres de diffusion : {{errorMessage}}" + }, + "success": { + "clearStreamingSettings": "Paramètres de diffusion effacés pour tous les groupes de caméras.", + "clearStoredLayout": "Mise en page enregistrée effacée pour {{cameraName}}" + } + } + }, + "notification": { + "suspendTime": { + "untilRestart": "Suspendre jusqu'au redémarrage", + "24hours": "Suspendre pendant 24 heures", + "10minutes": "Suspendre pendant 10 minutes", + "12hours": "Suspendre pendant 12 heures", + "5minutes": "Suspendre pendant 5 minutes", + "1hour": "Suspendre pendant 1 heure", + "30minutes": "Suspendre pendant 30 minutes", + "suspend": "Suspendre" + }, + "toast": { + "success": { + "registered": "Inscription réussie aux notifications. Le redémarrage de Frigate est nécessaire avant l'envoi de toute notification (y compris une notification de test).", + "settingSaved": "Les paramètres de notification ont été enregistrés." + }, + "error": { + "registerFailed": "Impossible de sauvegarder l'enregistrement de la notification." + } + }, + "cancelSuspension": "Annuler la suspension", + "notificationSettings": { + "title": "Paramètres de notification", + "documentation": "Lire la documentation", + "desc": "Frigate peut envoyer nativement des notifications push à votre appareil lorsqu'il est exécuté dans le navigateur ou installé en tant que PWA." + }, + "notificationUnavailable": { + "title": "Notifications indisponibles", + "documentation": "Lire la documentation", + "desc": "Les notifications push Web nécessitent un contexte sécurisé (https://…). Il s'agit d'une limitation du navigateur. Accédez à Frigate en toute sécurité pour utiliser les notifications." + }, + "globalSettings": { + "title": "Paramètres globaux", + "desc": "Suspendre temporairement les notifications pour des caméras spécifiques sur tous les appareils enregistrés." + }, + "email": { + "title": "Email", + "desc": "Une adresse e-mail valide est requise et sera utilisée pour vous avertir en cas de problème avec le service push.", + "placeholder": "par ex. exemple@email.com" + }, + "cameras": { + "title": "Caméras", + "noCameras": "Aucune caméra n'est disponible", + "desc": "Sélectionnez les caméras pour lesquelles activer les notifications." + }, + "deviceSpecific": "Paramètres spécifiques de l'appareil", + "suspended": "Notifications suspendues {{time}}", + "title": "Notifications", + "active": "Notifications actives", + "registerDevice": "Enregistrer cet appareil", + "unregisterDevice": "Désenregistrer cet appareil", + "sendTestNotification": "Envoyer une notification de test", + "unsavedChanges": "Modifications des notifications non enregistrées", + "unsavedRegistrations": "Enregistrements des notifications non enregistrés" + }, + "frigatePlus": { + "apiKey": { + "notValidated": "La clé API Frigate+ n'est pas détectée ou n'est pas validée.", + "title": "Clé API Frigate+", + "validated": "La clé API Frigate+ est détectée et validée", + "desc": "La clé API Frigate+ permet l'intégration avec le service Frigate+.", + "plusLink": "En savoir plus sur Frigate+" + }, + "title": "Paramètres Frigate+", + "snapshotConfig": { + "documentation": "Lire la documentation", + "desc": "La soumission à Frigate+ nécessite à la fois que les instantanés et les instantanés clean_copy soient activés dans votre configuration.", + "title": "Configuration des instantanés", + "table": { + "snapshots": "Instantanés", + "camera": "Caméra", + "cleanCopySnapshots": "Instantanés clean_copy" + }, + "cleanCopyWarning": "Certaines caméras ont des instantanés activés, mais la copie propre est désactivée. Vous devez activer clean_copy dans votre configuration d'instantanés pour pouvoir envoyer les images de ces caméras à Frigate+." + }, + "modelInfo": { + "baseModel": "Modèle de base", + "modelType": "Type de modèle", + "cameras": "Caméras", + "supportedDetectors": "Détecteurs pris en charge", + "loading": "Chargement des informations du modèle…", + "title": "Informations sur le modèle", + "trainDate": "Date d'entraînement", + "error": "Échec du chargement des informations du modèle", + "availableModels": "Modèles disponibles", + "dimensions": "Dimensions", + "loadingAvailableModels": "Chargement des modèles disponibles…", + "modelSelect": "Vous pouvez sélectionner ici vos modèles disponibles dans Frigate+. Notez que seuls les modèles compatibles avec votre configuration de détecteur actuelle peuvent être sélectionnés.", + "plusModelType": { + "baseModel": "Modèle de base", + "userModel": "Optimisé" + } + }, + "toast": { + "success": "Les paramètres de Frigate+ ont été enregistrés. Redémarrez Frigate pour appliquer les modifications.", + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}" + }, + "restart_required": "Redémarrage requis (modèle Frigate+ changé)", + "unsavedChanges": "Modifications de paramètres de Frigate+ non enregistrées" + }, + "classification": { + "title": "Paramètres de classification", + "semanticSearch": { + "title": "Recherche sémantique", + "reindexNow": { + "label": "Réindexer maintenant", + "confirmTitle": "Confirmer la réindexation", + "error": "Échec du démarrage de la réindexation : {{errorMessage}}", + "desc": "La réindexation génère à nouveau les plongements vectoriels pour tous les objets suivis. Ce processus s'exécute en arrière-plan et peut saturer votre processeur et prendre un certain temps, selon le nombre d'objets suivis.", + "confirmDesc": "Êtes-vous sûr de vouloir réindexer tous les plongements vectoriels d'objets suivis ? Ce processus s'exécutera en arrière-plan, mais il risque de saturer votre processeur et de prendre un certain temps. Vous pouvez suivre la progression sur la page Explorer.", + "success": "La réindexation a démarré avec succès.", + "alreadyInProgress": "La réindexation est déjà en cours.", + "confirmButton": "Réindexer" + }, + "desc": "La recherche sémantique dans Frigate vous permet de trouver des objets suivis dans vos éléments de revue en utilisant soit l'image elle-même, soit une description textuelle définie par l'utilisateur, soit une description générée automatiquement.", + "modelSize": { + "small": { + "desc": "L'utilisation de petit utilise une version quantifiée du modèle qui utilise moins de mémoire et s'exécute plus rapidement sur le processeur avec une différence très négligeable dans la qualité d'intégration.", + "title": "petit" + }, + "large": { + "desc": "L'utilisation de grand utilise le modèle Jina complet et s'exécutera automatiquement sur la carte graphique si applicable.", + "title": "grand" + }, + "desc": "Taille du modèle utilisé pour les plongements vectoriels de recherche sémantique.", + "label": "Taille du modèle" + }, + "readTheDocumentation": "Lire la documentation" + }, + "faceRecognition": { + "readTheDocumentation": "Lire la documentation", + "modelSize": { + "large": { + "title": "grand", + "desc": "L'utilisation de grand utilise un modèle d'intégration de visage ArcFace et s'exécutera automatiquement sur la carte graphique le cas échéant." + }, + "small": { + "desc": "L'utilisation de petit utilise un modèle d'intégration de visage FaceNet qui fonctionne efficacement sur la plupart des processeurs.", + "title": "petit" + }, + "label": "Taille du modèle", + "desc": "La taille du modèle utilisé pour la reconnaissance faciale." + }, + "desc": "La reconnaissance faciale permet d'attribuer un nom aux personnes. Une fois leur visage reconnu, Frigate attribuera le nom de la personne comme sous-étiquette. Ces informations sont incluses dans l'interface utilisateur, les filtres et les notifications.", + "title": "Reconnaissance faciale" + }, + "licensePlateRecognition": { + "desc": "Frigate peut reconnaître les plaques d'immatriculation des véhicules et ajouter automatiquement les caractères détectés au champ recognized_license_plate, ou un nom connu comme sous-étiquette aux objets de type voiture. Un cas d'utilisation courant est la lecture des plaques d'immatriculation des voitures entrant dans une allée ou circulant dans la rue.", + "readTheDocumentation": "Lire la documentation", + "title": "Reconnaissance de plaque d'immatriculation" + }, + "toast": { + "success": "Les paramètres de classification ont été enregistrés. Redémarrez Frigate pour appliquer vos modifications.", + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}" + }, + "birdClassification": { + "title": "Classification des oiseaux", + "desc": "La classification des oiseaux identifie les oiseaux connus à l'aide d'un modèle Tensorflow quantifié. Lorsqu'un oiseau connu est reconnu, son nom commun sera ajouté en tant que sous-étiquette. Cette information est incluse dans l'interface utilisateur, les filtres, ainsi que dans les notifications." + }, + "restart_required": "Redémarrage requis (paramètres de classification changés)", + "unsavedChanges": "Modifications des paramètres de classification non enregistrées" + }, + "camera": { + "title": "Paramètres de la caméra", + "review": { + "title": "Revue d'événements", + "detections": "Détections ", + "alerts": "Alertes ", + "desc": "Activer/désactiver temporairement les alertes et les détections pour cette caméra jusqu'au redémarrage de Frigate. Si cette option est désactivée, aucun nouvel élément ne sera généré dans la revue d'événements. " + }, + "reviewClassification": { + "title": "Catégorisation de la revue d'évènements", + "objectDetectionsTips": "Tous les objets {{detectionsLabels}} non classés sur {{cameraName}} seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent.", + "zoneObjectDetectionsTips": { + "text": "Tous les objets {{detectionsLabels}} non classés dans {{zone}} sur {{cameraName}} seront affichés comme des détections.", + "regardlessOfZoneObjectDetectionsTips": "Tous les objets {{detectionsLabels}} non classés sur {{cameraName}} seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent.", + "notSelectDetections": "Tous les objets {{detectionsLabels}} détectés dans {{zone}} sur {{cameraName}} non classés comme des alertes seront affichés comme des détections, quelle que soit la zone dans laquelle ils se trouvent." + }, + "selectDetectionsZones": "Sélectionner les zones pour les détections", + "toast": { + "success": "La configuration de la classification de la revue d'événements a été enregistrée. Redémarrez Frigate pour appliquer les modifications." + }, + "readTheDocumentation": "Lire la documentation", + "objectAlertsTips": "Tous les objets {{alertsLabels}} sur {{cameraName}} seront affichés sous forme d'alertes.", + "limitDetections": "Limiter les détections à des zones spécifiques", + "zoneObjectAlertsTips": "Tous les objets {{alertsLabels}} détectés dans {{zone}} sur {{cameraName}} seront affichés sous forme d'alertes.", + "noDefinedZones": "Aucune zone n'est définie pour cette caméra.", + "selectAlertsZones": "Sélectionner les zones pour les alertes", + "desc": "Frigate classe les éléments de la revue d'événements en alertes et détections. Par défaut, toutes les détections de personnes et de voitures sont qualifiées d'alertes. Vous avez la possibilité d'affiner cette catégorisation en configurant des zones spécifiques pour ces éléments.", + "unsavedChanges": "Paramètres de classification de la revue d'événements pour {{camera}} non enregistrés" + }, + "streams": { + "title": "Flux", + "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation complète d'une caméra interrompt le traitement des flux de cette caméra par Frigate. La détection, l'enregistrement et le débogage seront indisponibles.
    Remarque : cela ne désactive pas les rediffusions go2rtc." + }, + "object_descriptions": { + "title": "Description d'objets par IA générative", + "desc": "Activer / désactiver temporairement les descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront pas demandées pour les objets suivis par cette caméra." + }, + "review_descriptions": { + "title": "Revue de descriptions par IA générative", + "desc": "Activer / désactiver temporairement la revue de descriptions d'objets par IA générative pour cette caméra. Lorsqu'elles sont désactivées, les descriptions générées par IA ne seront plus demandées pour la revue d'éléments de cette caméra." + }, + "addCamera": "Ajouter une nouvelle caméra", + "editCamera": "Éditer la caméra :", + "selectCamera": "Sélectionner une caméra", + "backToSettings": "Retour aux paramètres de la caméra", + "cameraConfig": { + "add": "Ajouter une caméra", + "edit": "Éditer la caméra", + "description": "Configurer les paramètres de la caméra y compris les flux et les rôles.", + "name": "Nom de la caméra", + "nameRequired": "Un nom de caméra est nécessaire", + "nameInvalid": "Les noms de caméra peuvent contenir uniquement des lettres, des chiffres, des tirets bas, ou des tirets", + "namePlaceholder": "par exemple, porte_entree", + "enabled": "Activé", + "ffmpeg": { + "inputs": "Flux entrants", + "path": "Chemin d'accès du flux", + "pathRequired": "Un chemin d'accès de flux est nécessaire", + "pathPlaceholder": "rtsp://...", + "roles": "Rôles", + "rolesRequired": "Au moins un rôle est nécessaire", + "rolesUnique": "Chaque rôle (audio, détection, enregistrement) ne peut être assigné qu'à un seul flux", + "addInput": "Ajouter un flux entrant", + "removeInput": "Supprimer le flux entrant", + "inputsRequired": "Au moins un flux entrant est nécessaire" + }, + "toast": { + "success": "Caméra {{cameraName}} enregistrée avec succès" + }, + "nameLength": "Le nom de la caméra doit comporter au plus 24 caractères." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Le nom de la zone doit comporter au moins 2 caractères.", + "mustNotBeSameWithCamera": "Le nom de la zone ne doit pas être le même que le nom de la caméra.", + "mustNotContainPeriod": "Le nom de la zone ne doit pas contenir de points.", + "hasIllegalCharacter": "Le nom de la zone contient des caractères interdits.", + "alreadyExists": "Une zone portant ce nom existe déjà pour cette caméra.", + "mustHaveAtLeastOneLetter": "Le nom de la zone doit comporter au moins une lettre." + } + }, + "distance": { + "error": { + "text": "La distance doit être supérieure ou égale à 0,1.", + "mustBeFilled": "Tous les champs de distance doivent être remplis pour utiliser l'estimation de la vitesse." + } + }, + "polygonDrawing": { + "removeLastPoint": "Supprimer le dernier point", + "delete": { + "title": "Confirmer la suppression", + "desc": "Êtes-vous sûr de vouloir supprimer le {{type}} {{name}} ?", + "success": "{{name}} a été supprimé." + }, + "error": { + "mustBeFinished": "Le dessin du polygone doit être terminé avant d'enregistrer." + }, + "reset": { + "label": "Effacer tous les points" + }, + "snapPoints": { + "true": "Points d'accrochage", + "false": "Ne pas réunir les points" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Le temps de latence doit être supérieur ou égal à 0." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inertie doit être supérieure à 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Le seuil de vitesse doit être supérieur ou égal à 0.1." + } + } + }, + "zones": { + "documentTitle": "Modifier la zone - Frigate", + "desc": { + "title": "Les zones vous permettent de définir une zone spécifique de l'image afin de déterminer si un objet s'y trouve.", + "documentation": "Documentation" + }, + "add": "Ajouter une zone", + "edit": "Modifier une zone", + "name": { + "title": "Nom", + "inputPlaceHolder": "Saisissez un nom.", + "tips": "Le nom doit comporter au moins 2 caractères, dont une lettre, et ne doit pas être le nom d'une caméra ou d'une autre zone sur cette caméra." + }, + "loiteringTime": { + "desc": "Définit une durée minimale en secondes pendant laquelle l'objet doit rester dans la zone pour qu'elle s'active. Par défaut : 0", + "title": "Temps de maraudage" + }, + "speedEstimation": { + "title": "Estimation de la vitesse", + "desc": "Activer l'estimation de la vitesse des objets dans cette zone. La zone doit comporter exactement 4 points.", + "docs": "Lire la documentation", + "lineBDistance": "Distance ligne B ({{unit}})", + "lineCDistance": "Distance ligne C ({{unit}})", + "lineADistance": "Distance ligne A ({{unit}})", + "lineDDistance": "Distance ligne D ({{unit}})" + }, + "speedThreshold": { + "title": "Seuil de vitesse ({{unit}})", + "desc": "Spécifie une vitesse minimale pour que les objets soient pris en compte dans cette zone.", + "toast": { + "error": { + "loiteringTimeError": "Les zones avec des temps de latence supérieurs à 0 ne doivent pas être utilisées avec l'estimation de la vitesse.", + "pointLengthError": "L'estimation de vitesse a été désactivée pour cette zone. Les zones avec estimation de vitesse doivent comporter exactement 4 points." + } + } + }, + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "label": "Zones", + "inertia": { + "desc": "Spécifie le nombre d'images pendant lesquelles un objet doit être dans une zone avant d'être considéré comme y étant. Par défaut : 3", + "title": "Inertie" + }, + "toast": { + "success": "La zone ({{zoneName}}) a été enregistrée." + }, + "objects": { + "title": "Objets", + "desc": "Liste des objets qui s'appliquent à cette zone." + }, + "clickDrawPolygon": "Cliquer pour dessiner un polygone sur l'image.", + "allObjects": "Tous les objets" + }, + "motionMasks": { + "label": "Masque de mouvement", + "documentTitle": "Modifier le masque de mouvement - Frigate", + "context": { + "documentation": "Lire la documentation", + "title": "Les masques de mouvement servent à empêcher les mouvements indésirables de déclencher la détection (par exemple : branches d'arbres, horodatage des caméras). Ils doivent être utilisés avec parcimonie, car un surmasquage complique le suivi des objets." + }, + "polygonAreaTooLarge": { + "title": "Le masque de mouvement couvre {{polygonArea}} % du cadre de la caméra. Les grands masques de mouvement ne sont pas recommandés.", + "tips": "Les masques de mouvement n'empêchent pas la détection des objets. Il est préférable d'utiliser une zone obligatoire.", + "documentation": "Lire la documentation" + }, + "edit": "Modifier le masque de mouvement", + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "clickDrawPolygon": "Cliquer pour dessiner un polygone sur l'image.", + "toast": { + "success": { + "title": "{{polygonName}} a été enregistré.", + "noName": "Le masque de mouvement a été enregistré." + } + }, + "desc": { + "title": "Les masques de mouvement servent à empêcher la détection de mouvements indésirables. Un masquage excessif complique le suivi des objets.", + "documentation": "Documentation" + }, + "add": "Nouveau masque de mouvement" + }, + "objectMasks": { + "label": "Masques d'objet", + "desc": { + "documentation": "Documentation", + "title": "Les masques de filtrage d'objets sont utilisés pour filtrer les faux positifs pour un type d'objet donné en fonction de l'emplacement." + }, + "edit": "Modifier un masque d'objet", + "clickDrawPolygon": "Cliquez pour dessiner un polygone sur l'image.", + "objects": { + "title": "Objets", + "desc": "Le type d'objet qui s'applique à ce masque d'objet.", + "allObjectTypes": "Tous les types d'objet" + }, + "toast": { + "success": { + "noName": "Le masque d'objet a été enregistré.", + "title": "{{polygonName}} a été enregistré." + } + }, + "point_one": "{{count}} point", + "point_many": "{{count}} points", + "point_other": "{{count}} points", + "add": "Ajouter un masque d'objet", + "documentTitle": "Modifier le masque d'objet - Frigate", + "context": "Les masques de filtrage d'objets sont utilisés pour filtrer les faux positifs pour un type d'objet donné en fonction de l'emplacement." + }, + "filter": { + "all": "Tous les masques et zones" + }, + "toast": { + "success": { + "copyCoordinates": "Coordonnées copiées pour {{polyName}} dans le presse-papiers." + }, + "error": { + "copyCoordinatesFailed": "Impossible de copier les coordonnées dans le presse-papiers." + } + }, + "restart_required": "Redémarrage requis (masques/zones changés)", + "objectMaskLabel": "Masque d'objet {{number}} ({{label}})", + "motionMaskLabel": "Masque de mouvement {{number}}" + }, + "motionDetectionTuner": { + "title": "Réglage de la détection de mouvement", + "desc": { + "documentation": "Lisez le guide de réglage de mouvement", + "title": "Frigate utilise la détection de mouvement comme première ligne de contrôle pour voir s'il se passe quelque chose dans l'image qui mérite d'être vérifié avec la détection d'objets." + }, + "Threshold": { + "title": "Seuil", + "desc": "La valeur seuil détermine dans quelle mesure un changement dans la luminance d'un pixel est nécessaire pour être considéré comme un mouvement. Valeur par défaut : 30" + }, + "contourArea": { + "title": "Zone de contour", + "desc": "La valeur de la zone de contour est utilisée pour déterminer quels groupes de pixels modifiés sont qualifiés de mouvement. Par défaut : 10" + }, + "improveContrast": { + "title": "Améliorer le contraste", + "desc": "Améliorer le contraste pour les scènes plus sombres. Par défaut : ACTIVÉ" + }, + "toast": { + "success": "Les paramètres de mouvement ont été enregistrés." + }, + "unsavedChanges": "Modifications des réglages de mouvement non enregistrés ({{camera}})" + }, + "debug": { + "debugging": "Débogage", + "objectList": "Liste d'objets", + "boundingBoxes": { + "title": "Cadres de détection", + "colors": { + "label": "Couleurs des cadres de détection d'objet", + "info": "
  • Au démarrage, différentes couleurs seront attribuées à chaque étiquette d'objet
  • Une fine ligne bleu foncé indique que cet objet n'est pas détecté à ce moment précis
  • Une fine ligne grise indique que cet objet est détecté comme étant immobile
  • Une ligne épaisse indique que cet objet fait l'objet d'un suivi automatique (lorsqu'il est activé)
  • " + }, + "desc": "Afficher les cadres de détection autour des objets suivis" + }, + "timestamp": { + "title": "Horodatage", + "desc": "Superposer un horodatage sur l'image" + }, + "zones": { + "title": "Zones", + "desc": "Afficher un aperçu de toutes les zones définies" + }, + "mask": { + "title": "Masques de mouvement", + "desc": "Afficher les polygones du masque de mouvement" + }, + "motion": { + "desc": "Afficher des cadres autour des zones où un mouvement est détecté", + "title": "Cadres de mouvement", + "tips": "

    Cadres de mouvement


    Des cadres rouges seront superposés sur les zones de l'image où un mouvement est actuellement détecté

    " + }, + "regions": { + "title": "Régions", + "desc": "Afficher un cadre de la région d'intérêt envoyée au détecteur d'objet", + "tips": "

    Cadres de région


    Des cadres verts lumineux seront superposés sur les zones d'intérêt de l'image qui sont envoyées au détecteur d'objets.

    " + }, + "objectShapeFilterDrawing": { + "title": "Dessin de filtre de forme d'objet", + "area": "Zone", + "desc": "Dessinez un rectangle sur l'image pour afficher les détails de la zone et du rapport", + "score": "Score", + "tips": "Activez cette option pour dessiner un rectangle sur l'image de la caméra afin d'afficher sa surface et son ratio. Ces valeurs peuvent ensuite être utilisées pour définir les paramètres de filtre de forme d'objet dans votre configuration.", + "document": "Lire la documentation ", + "ratio": "Ratio" + }, + "noObjects": "Aucun objet", + "title": "Débogage", + "detectorDesc": "Frigate utilise vos détecteurs ({{detectors}}) pour détecter les objets dans le flux vidéo de votre caméra.", + "desc": "La vue de débogage affiche en temps réel les objets suivis et leurs statistiques. La liste des objets affiche un résumé différé des objets détectés.", + "paths": { + "title": "Trajets", + "desc": "Afficher les points notables du trajet de l'objet suivi", + "tips": "

    Trajets


    Les lignes et les cercles indiqueront les points notables où l'objet suivi s'est déplacé pendant son cycle de vie.

    " + }, + "audio": { + "title": "Audio", + "noAudioDetections": "Aucune détection audio", + "score": "score", + "currentRMS": "RMS actuel", + "currentdbFS": "dbFS actuel" + }, + "openCameraWebUI": "Ouvrir l'interface Web de {{camera}}" + }, + "users": { + "title": "Utilisateurs", + "management": { + "title": "Gestion des utilisateurs", + "desc": "Gérez les comptes utilisateurs de cette instance Frigate." + }, + "addUser": "Ajouter un utilisateur", + "updatePassword": "Mettre à jour le mot de passe", + "toast": { + "success": { + "roleUpdated": "Rôle mis à jour pour {{user}}", + "deleteUser": "L'utilisateur {{user}} a été supprimé avec succès", + "createUser": "L'utilisateur {{user}} a été créé avec succès", + "updatePassword": "Mot de passe mis à jour avec succès." + }, + "error": { + "setPasswordFailed": "Échec de l'enregistrement du mot de passe : {{errorMessage}}", + "createUserFailed": "Échec de la création de l'utilisateur : {{errorMessage}}", + "deleteUserFailed": "Échec de la suppression de l'utilisateur : {{errorMessage}}", + "roleUpdateFailed": "Échec de la mise à jour du rôle : {{errorMessage}}" + } + }, + "table": { + "username": "Nom d'utilisateur", + "actions": "Actions", + "noUsers": "Aucun utilisateur trouvé.", + "changeRole": "Changer le rôle d'utilisateur", + "password": "Mot de passe", + "deleteUser": "Supprimer un utilisateur", + "role": "Rôle" + }, + "dialog": { + "form": { + "user": { + "title": "Nom d'utilisateur", + "placeholder": "Saisir le nom d'utilisateur", + "desc": "Seules les lettres, les chiffres, les points et les traits de soulignement sont autorisés." + }, + "password": { + "strength": { + "weak": "Faible", + "title": "Niveau de sécurité du mot de passe : ", + "medium": "Moyen", + "strong": "Fort", + "veryStrong": "Très fort" + }, + "match": "Les mots de passe correspondent", + "notMatch": "Les mots de passe ne correspondent pas.", + "placeholder": "Saisir le mot de passe", + "title": "Mot de passe", + "confirm": { + "title": "Confirmer le mot de passe", + "placeholder": "Confirmer le mot de passe" + }, + "show": "Afficher le mot de passe", + "hide": "Masquer le mot de passe", + "requirements": { + "title": "Critères du mot de passe :", + "length": "Au moins 8 caractères", + "uppercase": "Au moins une lettre majuscule", + "digit": "Au moins un chiffre", + "special": "Au moins un caractère spécial (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nouveau mot de passe", + "placeholder": "Saisissez le nouveau mot de passe.", + "confirm": { + "placeholder": "Confirmez le nouveau mot de passe." + } + }, + "usernameIsRequired": "Nom d'utilisateur requis", + "passwordIsRequired": "Mot de passe requis", + "currentPassword": { + "title": "Mot de passe actuel", + "placeholder": "Saisissez votre mot de passe actuel" + } + }, + "deleteUser": { + "title": "Supprimer un utilisateur", + "desc": "Cette action est irréversible. Elle supprimera définitivement le compte utilisateur et toutes les données associées.", + "warn": "Êtes-vous sûr de vouloir supprimer {{username}} ?" + }, + "passwordSetting": { + "updatePassword": "Mettre à jour le mot de passe pour {{username}}", + "setPassword": "Configurer un mot de passe", + "desc": "Créez un mot de passe fort pour sécuriser ce compte.", + "doNotMatch": "Les mots de passe ne correspondent pas", + "cannotBeEmpty": "Le mot de passe ne peut être vide", + "currentPasswordRequired": "Le mot de passe actuel est requis.", + "incorrectCurrentPassword": "Le mot de passe actuel est incorrect", + "passwordVerificationFailed": "Échec de la vérification du mot de passe", + "multiDeviceWarning": "Tout autre appareil connecté devra se reconnecter dans un délai de {{refresh_time}}.", + "multiDeviceAdmin": "Vous pouvez également forcer la ré-authentification immédiate de tous les utilisateurs en renouvelant votre clé de sécurité JWT." + }, + "changeRole": { + "title": "Changer le rôle de l'utilisateur", + "desc": "Mettre à jour les autorisations pour {{username}}", + "roleInfo": { + "intro": "Sélectionnez le rôle approprié pour cet utilisateur :", + "admin": "Administrateur", + "adminDesc": "Accès complet à l'ensemble des fonctionnalités.", + "viewer": "Observateur", + "viewerDesc": "Limité aux tableaux de bord Direct, Activités, Explorer et Exports.", + "customDesc": "Rôle personnalisé avec accès spécifique à la caméra" + }, + "select": "Sélectionnez un rôle" + }, + "createUser": { + "title": "Créer un nouvel utilisateur", + "desc": "Ajoutez un nouveau compte utilisateur et spécifiez un rôle pour accéder aux zones de l'interface utilisateur Frigate.", + "usernameOnlyInclude": "Le nom d'utilisateur ne peut inclure que des lettres, des chiffres, des points (.) ou des traits de soulignement (_).", + "confirmPassword": "Veuillez confirmer votre mot de passe" + } + } + }, + "enrichments": { + "title": "Paramètres d'enrichissements", + "birdClassification": { + "title": "Identification des oiseaux", + "desc": "L'identification des oiseaux est réalisée à l'aide d'un modèle TensorFlow quantifié. Lorsqu'un oiseau est reconnu, son nom commun est automatiquement ajouté comme sous-étiquette. Cette information est intégrée à l'interface utilisateur, aux filtres de recherche et aux notifications." + }, + "semanticSearch": { + "title": "Recherche sémantique", + "readTheDocumentation": "Lire la documentation", + "reindexNow": { + "label": "Réindexer maintenant", + "desc": "La réindexation va régénérer les embeddings pour tous les objets suivis. Ce processus s'exécute en arrière-plan et peut saturer votre processeur et prendre un temps considérable en fonction du nombre d'objets suivis.", + "confirmTitle": "Confirmer la réindexation", + "confirmButton": "Réindexer", + "success": "La réindexation a démarré avec succès.", + "alreadyInProgress": "La réindexation est déjà en cours.", + "error": "Échec du démarrage de la réindexation : {{errorMessage}}", + "confirmDesc": "Êtes-vous sûr de vouloir réindexer tous les embeddings des objets suivis ? Ce processus s'exécutera en arrière-plan, mais il pourrait saturer votre processeur et prendre un temps considérable. Vous pouvez suivre la progression sur la page Explorer." + }, + "modelSize": { + "desc": "La taille du modèle utilisé pour les embeddings de recherche sémantique", + "small": { + "title": "petit", + "desc": "Utiliser petit emploie une version quantifiée du modèle qui utilise moins de mémoire et s'exécute plus rapidement sur le processeur avec une différence négligeable dans la qualité des embeddings." + }, + "large": { + "title": "grand", + "desc": "Utiliser grand emploie le modèle Jina complet et s'exécutera automatiquement sur le GPU si disponible." + }, + "label": "Taille du modèle" + }, + "desc": "La recherche sémantique de Frigate permet de retrouver des objets suivis au sein de vos activités en utilisant l'image elle-même, une description personnalisée ou une description générée automatiquement." + }, + "unsavedChanges": "Modifications non enregistrées des paramètres d'enrichissements", + "faceRecognition": { + "title": "Reconnaissance faciale", + "readTheDocumentation": "Lire la documentation", + "modelSize": { + "label": "Taille du modèle", + "desc": "La taille du modèle utilisé pour la reconnaissance faciale", + "small": { + "title": "petit", + "desc": "Utiliser petit emploie un modèle d'embedding facial FaceNet qui s'exécute efficacement sur la plupart des processeurs." + }, + "large": { + "title": "grand", + "desc": "Utiliser grand emploie un modèle d'embedding facial ArcFace et s'exécutera automatiquement sur le GPU si disponible." + } + }, + "desc": "La reconnaissance faciale permet à Frigate d'identifier les individus par leur nom. Dès qu'un visage est reconnu, Frigate associe ce nom comme sous-étiquette à l'événement. Ces informations sont ensuite intégrées dans l'interface utilisateur, les options de filtrage et les notifications." + }, + "licensePlateRecognition": { + "title": "Reconnaissance des plaques d'immatriculation", + "readTheDocumentation": "Lire la documentation", + "desc": "Frigate identifie les plaques d'immatriculation des véhicules et peut automatiquement insérer les caractères détectés dans le champ recognized_license_plate. Il est également capable d'assigner un nom familier comme sous-étiquette aux objets de type \"voiture\". Par exemple, cette fonction est souvent utilisée pour lire les plaques des véhicules empruntant une allée ou une rue." + }, + "toast": { + "error": "Échec de l'enregistrement des modifications de configuration : {{errorMessage}}", + "success": "Les paramètres d'enrichissements ont été enregistrés. Redémarrez Frigate pour appliquer vos modifications." + }, + "restart_required": "Redémarrage nécessaire (paramètres d'enrichissements modifiés)" + }, + "triggers": { + "documentTitle": "Déclencheurs", + "management": { + "title": "Déclencheurs", + "desc": "Gérer les déclencheurs pour {{camera}}. Utilisez le type vignette pour déclencher à partir de vignettes similaires à l'objet suivi sélectionné. Utilisez le type description pour déclencher à partir de textes de description similaires que vous avez spécifiés." + }, + "addTrigger": "Ajouter un déclencheur", + "table": { + "name": "Nom", + "type": "Type", + "content": "Contenu", + "threshold": "Seuil", + "actions": "Actions", + "noTriggers": "Aucun déclencheur configuré pour cette caméra.", + "edit": "Modifier", + "deleteTrigger": "Supprimer le déclencheur", + "lastTriggered": "Dernier déclencheur" + }, + "type": { + "thumbnail": "Vignette", + "description": "Description" + }, + "actions": { + "alert": "Marquer comme alerte", + "notification": "Envoyer une notification", + "sub_label": "Ajouter une sous-étiquette", + "attribute": "Ajouter un attribut" + }, + "dialog": { + "createTrigger": { + "title": "Créer un déclencheur", + "desc": "Créer un déclencheur pour la caméra {{camera}}" + }, + "editTrigger": { + "title": "Modifier le déclencheur", + "desc": "Modifier les paramètres du déclencheur de la caméra {{camera}}" + }, + "deleteTrigger": { + "title": "Supprimer le déclencheur", + "desc": "Êtes-vous sûr de vouloir supprimer le déclencheur {{triggerName}} ? Cette action est irréversible." + }, + "form": { + "name": { + "title": "Nom", + "placeholder": "Nommez ce déclencheur", + "error": { + "minLength": "Le champ doit comporter au moins deux caractères.", + "invalidCharacters": "Le champ peut contenir uniquement des lettres, des nombres, des tirets bas, et des tirets.", + "alreadyExists": "Un déclencheur avec le même nom existe déjà pour cette caméra." + }, + "description": "Saisissez un nom ou une description unique pour identifier ce déclencheur." + }, + "enabled": { + "description": "Activer ou désactiver ce déclencheur" + }, + "type": { + "title": "Type", + "placeholder": "Sélectionner un type de déclencheur", + "description": "Déclencher lorsqu'une description d'objet suivi similaire est détectée", + "thumbnail": "Déclencher lorsqu'une vignette d'objet suivi similaire est détectée" + }, + "content": { + "title": "Contenu", + "imagePlaceholder": "Sélectionner une vignette", + "textPlaceholder": "Saisir le contenu du texte", + "imageDesc": "Seules les 100 vignettes les plus récentes sont affichées. Si vous ne trouvez pas la vignette souhaitée, veuillez consulter les objets précédents dans Explorer et configurer un déclencheur à partir de ce menu.", + "textDesc": "Entrez un texte pour déclencher cette action lorsqu'une description similaire d'objet suivi est détectée.", + "error": { + "required": "Le contenu est requis." + } + }, + "threshold": { + "title": "Seuil", + "error": { + "min": "Le seuil doit être au moins 0", + "max": "Le seuil peut être au plus 1" + }, + "desc": "Définissez le seuil de similarité pour ce déclencheur. Un seuil plus élevé signifie qu'une correspondance plus exacte est requise pour activer le déclencheur." + }, + "actions": { + "title": "Actions", + "desc": "Par défaut, Frigate envoie un message MQTT pour tous les déclencheurs. Les sous-étiquettes ajoutent le nom du déclencheur à l'étiquette de l'objet. Les attributs sont des métadonnées recherchables stockées séparément dans les métadonnées de l'objet suivi.", + "error": { + "min": "Au moins une action doit être sélectionnée." + } + }, + "friendly_name": { + "title": "Nom convivial", + "placeholder": "Nommez ou décrivez ce déclencheur", + "description": "Nom convivial ou texte descriptif facultatif pour ce déclencheur." + } + } + }, + "toast": { + "success": { + "createTrigger": "Le déclencheur {{name}} a été créé avec succès.", + "updateTrigger": "Le déclencheur {{name}} a été mis à jour avec succès.", + "deleteTrigger": "Le déclencheur {{name}} a été supprimé avec succès." + }, + "error": { + "createTriggerFailed": "Échec de la création du déclencheur : {{errorMessage}}", + "updateTriggerFailed": "Échec de la mise à jour du déclencheur : {{errorMessage}}", + "deleteTriggerFailed": "Échec de la suppression du déclencheur : {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "La recherche sémantique est désactivée", + "desc": "La recherche sémantique doit être activée pour utiliser les déclencheurs." + }, + "wizard": { + "title": "Créer un déclencheur", + "step1": { + "description": "Configurez les paramètres de base pour votre déclencheur." + }, + "step2": { + "description": "Configurez le contenu qui déclenchera cette action." + }, + "step3": { + "description": "Configurez le seuil et les actions pour ce déclencheur." + }, + "steps": { + "nameAndType": "Nom et type", + "configureData": "Configuration des données", + "thresholdAndActions": "Seuil et actions" + } + } + }, + "roles": { + "management": { + "title": "Gestion des rôles Observateur", + "desc": "Gérer les rôles Observateur personnalisés et leurs permissions d'accès aux caméras pour cette instance de Frigate." + }, + "addRole": "Ajouter un rôle", + "table": { + "role": "Rôle", + "cameras": "Caméras", + "actions": "Actions", + "noRoles": "Aucun rôle personnalisé trouvé.", + "editCameras": "Modifier les caméras", + "deleteRole": "Supprimer le rôle" + }, + "toast": { + "success": { + "createRole": "Rôle {{role}} créé avec succès", + "updateCameras": "Caméras mis à jour pour le rôle {{role}}", + "deleteRole": "Rôle {{role}} supprimé avec succès", + "userRolesUpdated_one": "{{count}} utilisateur affecté à ce rôle a été mis à jour avec des droits \"Observateur\", et a accès à toutes les caméras.", + "userRolesUpdated_many": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras.", + "userRolesUpdated_other": "{{count}} utilisateurs affectés à ce rôle ont été mis à jour avec des droits \"Observateur\", et ont accès à toutes les caméras." + }, + "error": { + "createRoleFailed": "Échec dans la création du rôle : {{errorMessage}}", + "updateCamerasFailed": "Échec de la mise à jour des caméras : {{errorMessage}}", + "deleteRoleFailed": "Échec lors de la suppression du rôle : {{errorMessage}}", + "userUpdateFailed": "Echec lors de la mise à jour des rôles de l'utilisateur : {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Créer un nouveau rôle", + "desc": "Ajouter un nouveau rôle et définir les permissions d'accès à la caméra." + }, + "editCameras": { + "title": "Modifier les caméras du rôle", + "desc": "Mettre à jour les accès aux caméras pour le rôle {{role}}." + }, + "deleteRole": { + "title": "Suppression du rôle", + "desc": "Cette action est irréversible. Elle supprimera définitivement le rôle et tous les utilisateurs associés seront affectés au rôle \"Observateur\", avec un accès à toutes les caméras.", + "warn": "Êtes-vous sûr de vouloir supprimer {{role}} ?", + "deleting": "En cours de suppression..." + }, + "form": { + "role": { + "title": "Nom du rôle", + "placeholder": "Saisissez un nom de rôle.", + "desc": "Seuls les lettres, les chiffres, les points et les traits de soulignement sont autorisés.", + "roleIsRequired": "Un nom de rôle est requis", + "roleOnlyInclude": "Le nom de rôle ne peut inclure que des lettres, des chiffres, des points (.) ou des traits de soulignement (_).", + "roleExists": "Un rôle avec ce nom existe déjà." + }, + "cameras": { + "title": "Caméras", + "desc": "Sélectionnez les caméras auxquelles ce rôle aura accès. Au moins une caméra est requise.", + "required": "Au moins une caméra doit être sélectionnée." + } + } + } + }, + "cameraWizard": { + "title": "Ajouter une caméra", + "description": "Suivez les étapes ci-dessous pour ajouter une nouvelle caméra à votre installation Frigate.", + "steps": { + "nameAndConnection": "Nom et connexion", + "streamConfiguration": "Configuration du flux", + "validationAndTesting": "Validation et tests", + "probeOrSnapshot": "Sondage ou Instantané" + }, + "save": { + "success": "Nouvelle caméra {{cameraName}} enregistrée avec succès", + "failure": "Échec lors de l'enregistrement de {{cameraName}}" + }, + "testResultLabels": { + "resolution": "Résolution", + "video": "Vidéo", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Veuillez saisir une URL de flux valide.", + "testFailed": "Échec du test de flux : {{error}}" + }, + "step1": { + "description": "Saisissez les détails de votre caméra et choisissez d'interroger la caméra ou de sélectionner manuellement la marque.", + "cameraName": "Nom de la caméra", + "cameraNamePlaceholder": "par ex., porte_entree ou apercu_cour_arriere", + "host": "Hôte / Adresse IP", + "port": "Port", + "username": "Nom d'utilisateur", + "usernamePlaceholder": "Facultatif", + "password": "Mot de passe", + "passwordPlaceholder": "Facultatif", + "selectTransport": "Sélectionnez le protocole de transport.", + "cameraBrand": "Marque de la caméra", + "selectBrand": "Sélectionnez la marque de la caméra pour déterminer la forme de l'URL.", + "customUrl": "URL de flux personnalisé", + "brandInformation": "Information sur la marque", + "brandUrlFormat": "Pour les caméras avec un format d'URL RTSP comme : {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomutilisateur:motdepasse@hote:port/chemin", + "testConnection": "Tester la connexion", + "testSuccess": "Test de connexion réussi !", + "testFailed": "Échec du test de connexion. Veuillez vérifier votre saisie et réessayez.", + "streamDetails": "Détails du flux", + "warnings": { + "noSnapshot": "Impossible de récupérer un instantané à partir du flux configuré" + }, + "errors": { + "brandOrCustomUrlRequired": "Sélectionnez une marque de caméra avec hôte/IP ou choisissez « Autre » avec une URL personnalisée.", + "nameRequired": "Le nom de la caméra est requis.", + "nameLength": "Le nom de la caméra ne doit pas dépasser 64 caractères.", + "invalidCharacters": "Le nom de la caméra contient des caractères invalides.", + "nameExists": "Ce nom de caméra est déjà utilisé.", + "brands": { + "reolink-rtsp": "Le protocole RTSP de Reolink est déconseillé. Activez le protocole HTTP dans les paramètres du firmware de la caméra, puis relancez l'assistant." + }, + "customUrlRtspRequired": "Les URL personnalisées doivent commencer par \"rtsp://\". Une configuration manuelle est requise pour les flux de caméra non-RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Vérification des métadonnées de la caméra en cours...", + "fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..." + }, + "connectionSettings": "Paramètres de connexion", + "detectionMethod": "Méthode de détection du flux", + "onvifPort": "Port ONVIF", + "probeMode": "Interroger la caméra", + "manualMode": "Sélection manuelle", + "detectionMethodDescription": "Interrogez la caméra avec ONVIF (si pris en charge) pour trouver les URL de flux de la caméra, ou sélectionnez manuellement la marque de la caméra pour utiliser des URL prédéfinies. Pour saisir une URL RTSP personnalisée, choisissez la méthode manuelle et sélectionnez \"Autre\".", + "onvifPortDescription": "Pour les caméras prenant en charge ONVIF, il s'agit généralement de 80 ou 8080.", + "useDigestAuth": "Utiliser l'authentification Digest", + "useDigestAuthDescription": "Utilisez l'authentification Digest HTTP pour ONVIF. Certaines caméras peuvent nécessiter un nom d'utilisateur/mot de passe ONVIF dédié au lieu de l'utilisateur administrateur standard." + }, + "step2": { + "description": "Interrogez la caméra pour les flux disponibles ou configurez des paramètres manuels en fonction de la méthode de détection sélectionnée.", + "streamsTitle": "Flux de caméra", + "addStream": "Ajouter un flux", + "addAnotherStream": "Ajouter un autre flux", + "streamTitle": "Flux {{number}}", + "streamUrl": "URL du flux", + "streamUrlPlaceholder": "rtsp://nomutilisateur:motdepasse@hote:port/chemin", + "url": "URL", + "resolution": "Résolution", + "selectResolution": "Sélectionnez la résolution.", + "quality": "Qualité", + "selectQuality": "Sélectionnez la qualité.", + "roles": "Rôles", + "roleLabels": { + "record": "Enregistrement", + "audio": "Audio", + "detect": "Détection d'objets" + }, + "testStream": "Tester la connexion", + "testSuccess": "Test de connexion réussi !", + "testFailed": "Échec du test de connexion. Veuillez vérifier votre saisie et réessayer.", + "testFailedTitle": "Échec du test", + "connected": "Connecté", + "notConnected": "Non connecté", + "featuresTitle": "Caractéristiques", + "go2rtc": "Réduire le nombre de connexions à la caméra", + "detectRoleWarning": "Pour continuer, au moins un flux doit avoir le rôle \"détection\".", + "rolesPopover": { + "title": "Rôles du flux", + "detect": "Flux principal pour la détection d'objets", + "record": "Enregistre des extraits du flux vidéo en fonction des paramètres de configuration.", + "audio": "Flux pour la détection audio" + }, + "featuresPopover": { + "title": "Fonctionnalités du flux", + "description": "Utilisez la rediffusion du flux go2rtc pour réduire le nombre de connexions à votre caméra." + }, + "streamDetails": "Détails du flux", + "probing": "Interrogation de la caméra en cours...", + "retry": "Réessayer", + "testing": { + "probingMetadata": "Interrogation des métadonnées de la caméra en cours...", + "fetchingSnapshot": "Récupération de l'instantané de la caméra en cours..." + }, + "probeFailed": "Impossible d'interroger la caméra : {{error}}", + "probingDevice": "Interrogation de l'appareil en cours...", + "probeSuccessful": "Interrogation réussie", + "probeError": "Erreur d'interrogation", + "probeNoSuccess": "Échec de l'interrogation", + "deviceInfo": "Informations sur l'appareil", + "manufacturer": "Fabricant", + "model": "Modèle", + "firmware": "Micrologiciel", + "profiles": "Profils", + "ptzSupport": "Prise en charge PTZ", + "autotrackingSupport": "Prise en charge du suivi automatique", + "presets": "Préréglages", + "rtspCandidates": "Candidats RTSP", + "rtspCandidatesDescription": "Les URL RTSP suivantes ont été trouvées lors de l'interrogation de la caméra. Testez la connexion pour afficher les métadonnées du flux.", + "noRtspCandidates": "Aucune URL RTSP n'a été trouvée sur la caméra. Vos identifiants sont peut-être incorrects, ou la caméra ne prend peut-être pas en charge ONVIF ou la méthode utilisée pour récupérer les URL RTSP. Revenez en arrière et saisissez l'URL RTSP manuellement.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Utiliser", + "uriCopy": "Copier", + "uriCopied": "URI copiée dans le presse-papiers", + "testConnection": "Tester la connexion", + "toggleUriView": "Cliquer pour basculer l'affichage de l'URI complet", + "errors": { + "hostRequired": "L'hôte/adresse IP est requis." + } + }, + "step3": { + "description": "Configurez les rôles des flux et ajoutez des flux supplémentaires pour votre caméra.", + "validationTitle": "Validation du flux", + "connectAllStreams": "Connecter tous les flux", + "reconnectionSuccess": "Reconnexion réussie.", + "reconnectionPartial": "La reconnexion de certains flux a échoué.", + "streamUnavailable": "Aperçu du flux indisponible", + "reload": "Recharger", + "connecting": "Connexion en cours...", + "streamTitle": "Flux {{number}}", + "failed": "Échec", + "notTested": "Non testé", + "connectStream": "Connecter", + "connectingStream": "Connexion en cours", + "disconnectStream": "Déconnecter", + "estimatedBandwidth": "Débit estimé", + "roles": "Rôles", + "none": "Aucun", + "error": "Erreur", + "streamValidated": "Flux {{number}} validé avec succès", + "streamValidationFailed": "La validation du flux {{number}} a échoué", + "saveAndApply": "Enregistrer une nouvelle caméra", + "saveError": "Configuration invalide. Veuillez vérifier vos paramètres.", + "issues": { + "title": "Validation du flux", + "videoCodecGood": "Le codec vidéo est {{codec}}.", + "audioCodecGood": "Le codec audio est {{codec}}.", + "noAudioWarning": "Aucun son n'est détecté sur ce flux, les enregistrements seront muets.", + "audioCodecRecordError": "Le codec audio AAC est requis pour la prise en charge du son dans les enregistrements.", + "audioCodecRequired": "Un flux audio est requis pour prendre en charge la détection audio.", + "restreamingWarning": "La réduction des connexions à la caméra pour le flux d'enregistrement peut augmenter légèrement l'utilisation du processeur.", + "dahua": { + "substreamWarning": "Le flux secondaire 1 est limité en basse résolution. De nombreuses caméras (Dahua, Amcrest, EmpireTech...) proposent des flux supplémentaires qu'il suffit d'activer dans leurs propres paramètres. Il est recommandé de vérifier leur disponibilité et de les utiliser." + }, + "hikvision": { + "substreamWarning": "Le flux secondaire 1 est limité en basse résolution. De nombreuses caméras Hikvision proposent des flux supplémentaires qu'il suffit d'activer dans leurs propres paramètres. Il est recommandé de vérifier leur disponibilité et de les utiliser." + }, + "resolutionHigh": "La résolution {{resolution}} risque d'augmenter l'utilisation des ressources.", + "resolutionLow": "La résolution {{resolution}} risque d'être trop faible pour détecter les petits objets de manière fiable." + }, + "valid": "Valide", + "ffmpegModule": "Utiliser le mode de compatibilité du flux", + "ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.", + "streamsTitle": "Flux de la caméra", + "addStream": "Ajouter un flux", + "addAnotherStream": "Ajouter un autre flux", + "streamUrl": "URL du flux", + "streamUrlPlaceholder": "rtsp://nomdutilisateur:motdepasse@hote:port/chemin", + "selectStream": "Sélectionner un flux", + "searchCandidates": "Rechercher des candidats", + "noStreamFound": "Aucun flux trouvé", + "url": "URL", + "resolution": "Résolution", + "selectResolution": "Sélectionner la résolution", + "quality": "Qualité", + "selectQuality": "Sélectionner la qualité", + "roleLabels": { + "detect": "Détection d'objet", + "record": "Enregistrement", + "audio": "Audio" + }, + "testStream": "Tester la connexion", + "testSuccess": "Test du flux réussi !", + "testFailed": "Échec du test du flux", + "testFailedTitle": "Échec du test", + "connected": "Connecté", + "notConnected": "Non connecté", + "featuresTitle": "Fonctionnalités", + "go2rtc": "Réduire les connexions à la caméra", + "detectRoleWarning": "Au moins un flux doit avoir le rôle 'détection' pour continuer.", + "rolesPopover": { + "title": "Rôles du flux", + "detect": "Flux principal pour la détection d'objet", + "record": "Enregistre des segments du flux vidéo en fonction des paramètres de configuration", + "audio": "Flux pour la détection basée sur l'audio" + }, + "featuresPopover": { + "title": "Fonctionnalités du flux", + "description": "Utiliser la rediffusion go2rtc pour réduire les connexions à votre caméra" + } + }, + "step4": { + "description": "Validation et analyse finales avant d'enregistrer votre nouvelle caméra. Connectez chaque flux avant d'enregistrer.", + "validationTitle": "Validation du flux", + "connectAllStreams": "Connecter tous les flux", + "reconnectionSuccess": "Reconnexion réussie", + "reconnectionPartial": "Certains flux n'ont pas réussi à se reconnecter.", + "streamUnavailable": "Aperçu du flux non disponible", + "reload": "Recharger", + "connecting": "En cours de connexion...", + "streamTitle": "Flux {{number}}", + "valid": "Valide", + "failed": "Échec", + "notTested": "Non testé", + "connectStream": "Connecter", + "connectingStream": "En cours de connexion", + "disconnectStream": "Déconnecter", + "estimatedBandwidth": "Bande passante estimée", + "roles": "Rôles", + "ffmpegModule": "Utiliser le mode de compatibilité du flux", + "ffmpegModuleDescription": "Si le flux ne se charge pas après plusieurs tentatives, essayez d'activer cette option. Lorsqu'elle est activée, Frigate utilisera le module ffmpeg avec go2rtc. Cela peut offrir une meilleure compatibilité avec certains flux de caméra.", + "none": "Aucun", + "error": "Erreur", + "streamValidated": "Flux {{number}} validé avec succès", + "streamValidationFailed": "Échec de la validation du flux {{number}}", + "saveAndApply": "Enregistrer la nouvelle caméra", + "saveError": "Configuration invalide. Veuillez vérifier vos paramètres.", + "issues": { + "title": "Validation du flux", + "videoCodecGood": "Le codec vidéo est {{codec}}.", + "audioCodecGood": "Le codec audio est {{codec}}.", + "resolutionHigh": "Une résolution de {{resolution}} peut entraîner une utilisation accrue des ressources.", + "resolutionLow": "Une résolution de {{resolution}} peut être trop faible pour une détection fiable des petits objets.", + "noAudioWarning": "Aucun audio détecté pour ce flux, les enregistrements n'auront pas de son.", + "audioCodecRecordError": "Le codec audio AAC est requis pour prendre en charge l'audio dans les enregistrements.", + "audioCodecRequired": "Un flux audio est requis pour prendre en charge la détection audio.", + "restreamingWarning": "Réduire les connexions à la caméra pour le flux d'enregistrement peut légèrement augmenter l'utilisation du processeur.", + "brands": { + "reolink-rtsp": "Le RTSP Reolink n'est pas recommandé. Activez HTTP dans les paramètres du micrologiciel de la caméra et redémarrez l'assistant.", + "reolink-http": "Les flux HTTP de Reolink devraient utiliser FFmpeg pour une meilleure compatibilité. Activez 'Utiliser le mode de compatibilité du flux' pour ce flux." + }, + "dahua": { + "substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Dahua / Amcrest / EmpireTech prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles." + }, + "hikvision": { + "substreamWarning": "Le sous-flux 1 est limité à une basse résolution. De nombreuses caméras Hikvision prennent en charge des sous-flux supplémentaires qui doivent être activés dans les paramètres de la caméra. Il est recommandé de vérifier et d'utiliser ces flux s'ils sont disponibles." + } + } + } + }, + "cameraManagement": { + "title": "Gérer les caméras", + "addCamera": "Ajouter une nouvelle caméra", + "editCamera": "Modifier la caméra :", + "selectCamera": "Sélectionnez une caméra", + "backToSettings": "Retour aux paramètres de la caméra", + "streams": { + "title": "Activer / désactiver les caméras", + "desc": "Désactive temporairement une caméra jusqu'au redémarrage de Frigate. La désactivation d'une caméra interrompt complètement le traitement des flux de la caméra par Frigate. La détection, l'enregistrement et le débogage deviennent alors indisponibles.
    Remarque : cela n'affecte pas les rediffusions des flux go2rtc." + }, + "cameraConfig": { + "add": "Ajouter une caméra", + "edit": "Modifier la caméra", + "description": "Configurez les paramètres de la caméra, notamment les flux entrants et les rôles.", + "name": "Nom de la caméra", + "nameRequired": "Le nom de la caméra est requis", + "nameLength": "Le nom de la caméra doit comporter moins de 64 caractères.", + "namePlaceholder": "par exemple, porte d'entrée ou aperçu de la cour arrière", + "enabled": "Activé", + "ffmpeg": { + "inputs": "Flux d'entrée", + "path": "Chemin du flux", + "pathRequired": "Chemin du flux requis", + "pathPlaceholder": "rtsp://...", + "roles": "Rôles", + "rolesRequired": "Au moins un rôle est requis", + "rolesUnique": "Chaque rôle (audio, détection, enregistrement) ne peut être attribué qu'à un seul flux", + "addInput": "Ajouter un flux d'entrée", + "removeInput": "Supprimer le flux d'entrée", + "inputsRequired": "Au moins un flux d'entrée est requis" + }, + "go2rtcStreams": "Flux go2rtc", + "streamUrls": "URL des flux", + "addUrl": "Ajouter une URL", + "addGo2rtcStream": "Ajouter un flux go2rtc", + "toast": { + "success": "La caméra {{cameraName}} a été enregistrée avec succès" + } + } + }, + "cameraReview": { + "title": "Paramètres des activités caméra", + "object_descriptions": { + "title": "Descriptions d'objets par l'IA générative", + "desc": "Active ou désactive temporairement les descriptions d'objets générées par l'IA générative pour cette caméra. Lorsque cette option est désactivée, aucune description par l'IA n'est générée pour les objets suivis sur cette caméra." + }, + "review_descriptions": { + "title": "Descriptions des activités par l'IA générative", + "desc": "Active ou désactive temporairement les descriptions par l'IA générative pour cette caméra. Lorsque cette option est désactivée, aucune description nouvelle n'est générée pour les activités sur cette caméra." + }, + "review": { + "title": "Activités", + "desc": "Active ou désactive temporairement les alertes et les détections pour cette caméra jusqu'au redémarrage de Frigate. Lorsque cette option est désactivée, aucune activité nouvelle n'est générée. ", + "alerts": "Alertes ", + "detections": "Détections " + }, + "reviewClassification": { + "title": "Classification des activités", + "desc": "Frigate classe les activités en deux catégories : \"Alertes\" et \"Détections\". Par défaut, les objets de type personne et voiture sont considérés comme des \"Alertes\". Vous pouvez affiner cette classification en définissant des zones spécifiques pour chaque objet.", + "noDefinedZones": "Aucune zone n'est définie pour cette caméra.", + "objectAlertsTips": "Sur la caméra {{cameraName}}, tous les objets {{alertsLabels}} apparaîtront en tant qu'\"Alertes\".", + "zoneObjectAlertsTips": "Sur la caméra {{cameraName}}, tous les objets {{alertsLabels}} détectés dans la zone {{zone}} apparaîtront en tant qu'\"Alertes\".", + "objectDetectionsTips": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés apparaîtront en tant que \"Détections\", peu importe leur zone.", + "zoneObjectDetectionsTips": { + "text": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés dans la zone {{zone}} apparaîtront en tant que \"Détections\".", + "notSelectDetections": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} détectés dans la zone {{zone}} qui ne sont pas catégorisés comme \"Alertes\" apparaîtront en tant que \"Détections\", et ce, quelle que soit leur zone.", + "regardlessOfZoneObjectDetectionsTips": "Sur la caméra {{cameraName}}, tous les objets {{detectionsLabels}} non catégorisés apparaîtront en tant que \"Détections\", peu importe leur zone." + }, + "unsavedChanges": "Paramètres de classification des activités non enregistrés pour {{camera}}", + "selectAlertsZones": "Sélectionnez les zones pour les alertes", + "selectDetectionsZones": "Sélectionner les zones pour les détections", + "limitDetections": "Limiter les détections à des zones spécifiques", + "toast": { + "success": "La configuration de la classification des activités a été enregistrée. Redémarrez Frigate pour appliquer les modifications." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/fr/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/fr/views/system.json new file mode 100644 index 0000000..53f12b0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/fr/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "storage": "Statistiques de stockage - Frigate", + "cameras": "Statistiques des caméras - Frigate", + "general": "Statistiques générales - Frigate", + "enrichments": "Statistiques d'enrichissements - Frigate", + "logs": { + "frigate": "Journaux de Frigate - Frigate", + "nginx": "Journaux Nginx - Frigate", + "go2rtc": "Journaux Go2RTC - Frigate" + } + }, + "title": "Système", + "metrics": "Métriques du système", + "logs": { + "download": { + "label": "Télécharger les journaux" + }, + "copy": { + "label": "Copier dans le presse-papiers", + "success": "Journaux copiés dans le presse-papiers", + "error": "Échec de la copie des journaux dans le presse-papiers" + }, + "type": { + "label": "Type", + "timestamp": "Horodatage", + "tag": "Balise", + "message": "Message" + }, + "tips": "Les journaux sont diffusés en continu depuis le serveur", + "toast": { + "error": { + "fetchingLogsFailed": "Erreur lors de la récupération des logs : {{errorMessage}}", + "whileStreamingLogs": "Erreur lors de la diffusion des logs : {{errorMessage}}" + } + } + }, + "general": { + "title": "Général", + "detector": { + "title": "Détecteurs", + "inferenceSpeed": "Vitesse d'inférence du détecteur", + "cpuUsage": "Utilisation CPU du détecteur", + "memoryUsage": "Utilisation mémoire du détecteur", + "temperature": "Température du détecteur", + "cpuUsageInformation": "Utilisation CPU pour préparer les données en entrée et en sortie des modèles de détection. Cette valeur ne mesure pas l'utilisation de l'inférence, même si un GPU ou un accélérateur est utilisé." + }, + "hardwareInfo": { + "title": "Informations sur le matériel", + "gpuUsage": "Utilisation du GPU", + "gpuMemory": "Mémoire du GPU", + "gpuEncoder": "Encodeur GPU", + "gpuDecoder": "Décodeur GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Sortie Vainfo", + "returnCode": "Code de retour : {{code}}", + "processOutput": "Sortie du processus :", + "processError": "Erreur du processus :" + }, + "nvidiaSMIOutput": { + "title": "Sortie Nvidia SMI", + "name": "Nom : {{name}}", + "cudaComputerCapability": "Capacité de calcul CUDA : {{cuda_compute}}", + "vbios": "Informations VBios : {{vbios}}", + "driver": "Pilote : {{driver}}" + }, + "copyInfo": { + "label": "Copier les informations du GPU" + }, + "toast": { + "success": "Informations GPU copiées dans le presse-papiers" + }, + "closeInfo": { + "label": "Fermer les informations du GPU" + } + }, + "npuUsage": "Utilisation NPU", + "npuMemory": "Mémoire NPU", + "intelGpuWarning": { + "title": "Avertissement relatif aux statistiques du GPU Intel", + "message": "Statistiques du GPU non disponibles", + "description": "Il s'agit d'un bug connu de l'outil de statistiques GPU d'Intel (intel_gpu_top) : il peut afficher à tort une utilisation de 0 %, même lorsque l'accélération matérielle et la détection d'objets fonctionnent correctement sur l'iGPU. Ce problème ne vient pas de Frigate. Vous pouvez redémarrer l'hôte pour rétablir temporairement l'affichage et confirmer le fonctionnement du GPU. Les performances ne sont pas affectées." + } + }, + "otherProcesses": { + "title": "Autres processus", + "processCpuUsage": "Utilisation CPU du processus", + "processMemoryUsage": "Utilisation mémoire du processus" + } + }, + "storage": { + "title": "Stockage", + "recordings": { + "title": "Enregistrements", + "earliestRecording": "Enregistrement le plus ancien :", + "tips": "Cette valeur correspond au stockage total utilisé par les enregistrements dans la base de données Frigate. Frigate ne suit pas l'utilisation du stockage pour tous les fichiers de votre disque." + }, + "cameraStorage": { + "title": "Stockage de la caméra", + "bandwidth": "Bande passante", + "unused": { + "title": "Inutilisé", + "tips": "Cette valeur peut ne pas représenter précisément l'espace libre disponible pour Frigate si d'autres fichiers sont stockés sur votre disque en plus des enregistrements Frigate. Frigate ne suit pas l'utilisation du stockage en dehors de ses enregistrements." + }, + "percentageOfTotalUsed": "Pourcentage du total", + "storageUsed": "Stockage", + "camera": "Caméra", + "unusedStorageInformation": "Informations sur le stockage non utilisé" + }, + "overview": "Vue d'ensemble", + "shm": { + "title": "Allocation de mémoire partagée SHM", + "warning": "La taille actuelle de la SHM de {{total}} Mo est trop petite. Augmentez-la au moins à {{min_shm}} Mo." + } + }, + "cameras": { + "title": "Caméras", + "info": { + "cameraProbeInfo": "Informations de la sonde pour {{camera}}", + "fetching": "Récupération des données de la caméra en cours", + "stream": "Flux {{idx}}", + "fps": "IPS :", + "unknown": "Inconnu", + "audio": "Audio :", + "tips": { + "title": "Informations de la sonde caméra" + }, + "streamDataFromFFPROBE": "Les données du flux sont obtenues avec ffprobe.", + "resolution": "Résolution :", + "error": "Erreur : {{error}}", + "codec": "Codec :", + "video": "Vidéo :", + "aspectRatio": "rapport d'aspect" + }, + "framesAndDetections": "Images / Détections", + "label": { + "camera": "caméra", + "detect": "détection", + "skipped": "ignorées", + "ffmpeg": "FFmpeg", + "capture": "capture", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraSkippedDetectionsPerSecond": "{{camName}} détections ignorées par seconde", + "overallDetectionsPerSecond": "Moyenne de détections par seconde", + "overallFramesPerSecond": "Moyenne d'images par seconde (IPS)", + "overallSkippedDetectionsPerSecond": "Moyenne de détections ignorées par seconde", + "cameraCapture": "{{camName}} capture", + "cameraDetect": "{{camName}} détection", + "cameraFramesPerSecond": "{{camName}} images par seconde (IPS)", + "cameraDetectionsPerSecond": "{{camName}} détections par seconde" + }, + "overview": "Vue d'ensemble", + "toast": { + "success": { + "copyToClipboard": "Données de la sonde copiées dans le presse-papiers" + }, + "error": { + "unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}" + } + } + }, + "lastRefreshed": "Dernier rafraichissement : ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} a un taux élevé d'utilisation processeur par FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} : charge CPU détection élevée ({{detectAvg}}%)", + "healthy": "Le système est sain", + "reindexingEmbeddings": "Réindexation des embeddings ({{processed}} % terminée)", + "cameraIsOffline": "{{camera}} est hors ligne", + "detectIsSlow": "{{detect}} est lent ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} est très lent ({{speed}} ms)", + "shmTooLow": "L'allocation /dev/shm ({{total}} Mo) devrait être augmentée à au moins {{min}} Mo." + }, + "enrichments": { + "title": "Enrichissements", + "infPerSecond": "Inférences par seconde", + "embeddings": { + "face_embedding_speed": "Vitesse de vectorisation des visages", + "text_embedding_speed": "Vitesse d'embedding de texte", + "image_embedding_speed": "Vitesse d'embedding d'image", + "plate_recognition_speed": "Vitesse de reconnaissance des plaques d'immatriculation", + "face_recognition_speed": "Vitesse de reconnaissance faciale", + "plate_recognition": "Reconnaissance de plaques d'immatriculation", + "image_embedding": "Embedding d'image", + "yolov9_plate_detection": "Détection de plaques d'immatriculation YOLOv9", + "face_recognition": "Reconnaissance faciale", + "text_embedding": "Vitesse d'embedding de visage", + "yolov9_plate_detection_speed": "Vitesse de détection de plaques d'immatriculation YOLOv9", + "review_description": "Description de l'activité", + "review_description_speed": "Vitesse de description des activités", + "review_description_events_per_second": "Description de l'activité", + "object_description": "Description de l'objet", + "object_description_speed": "Vitesse de la description d'objet", + "object_description_events_per_second": "Description de l'objet" + }, + "averageInf": "Temps d'inférence moyen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/audio.json b/sam2-cpu/frigate-dev/web/public/locales/gl/audio.json new file mode 100644 index 0000000..507de04 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/audio.json @@ -0,0 +1,19 @@ +{ + "speech": "Fala", + "babbling": "Balbuxo", + "bicycle": "Bicicleta", + "yell": "Berro", + "car": "Coche", + "crying": "Chorando", + "sigh": "Suspiro", + "singing": "Cantando", + "motorcycle": "Motocicleta", + "bus": "Bus", + "train": "Tren", + "boat": "Bote", + "bird": "Paxaro", + "cat": "Gato", + "bellow": "Abaixo", + "whoop": "Ei carballeira", + "whispering": "Murmurando" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/common.json b/sam2-cpu/frigate-dev/web/public/locales/gl/common.json new file mode 100644 index 0000000..61b9ce5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/common.json @@ -0,0 +1,14 @@ +{ + "time": { + "untilForTime": "Até {{time}}", + "untilForRestart": "Até que se reinicie Frigate.", + "justNow": "Xusto agora", + "last7": "Últimos 7 días", + "last14": "Últimos 14 días", + "thisWeek": "Esta semana", + "today": "Hoxe", + "untilRestart": "Ata o reinicio", + "ago": "Fai {{timeAgo}}" + }, + "readTheDocumentation": "Ler a documentación" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/auth.json new file mode 100644 index 0000000..8b0857d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/auth.json @@ -0,0 +1,13 @@ +{ + "form": { + "user": "Usuario/a", + "password": "Contrasinal", + "errors": { + "passwordRequired": "Contrasinal obrigatorio", + "unknownError": "Erro descoñecido. Revisa os logs.", + "usernameRequired": "Usuario/a obrigatorio", + "rateLimit": "Excedido o límite. Téntao de novo despois." + }, + "login": "Iniciar sesión" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/camera.json new file mode 100644 index 0000000..166eebe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/camera.json @@ -0,0 +1,20 @@ +{ + "group": { + "label": "Grupos de cámaras", + "add": "Engadir Grupo de cámaras", + "delete": { + "confirm": { + "title": "Confirma o borrado", + "desc": "Seguro/a que queres borrar o Grupo de cámaras {{name}}?" + }, + "label": "Borrar o Grupo de Cámaras" + }, + "name": { + "placeholder": "Introduce un nome…", + "errorMessage": { + "nameMustNotPeriod": "Grupo de Cámaras non debe conter un punto." + } + }, + "edit": "Editar o Grupo de Cámaras" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/dialog.json new file mode 100644 index 0000000..d2aff40 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/dialog.json @@ -0,0 +1,24 @@ +{ + "restart": { + "title": "Estás seguro/a que queres reiniciar Frigate?", + "button": "Reiniciar", + "restarting": { + "button": "Forzar reinicio", + "content": "Esta páxina recargarase en {{countdown}} segundos.", + "title": "Frigate está Reiniciando" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "Confirma esta etiqueta para Frigate Plus", + "ask_an": "E isto un obxecto {{label}}?" + } + }, + "submitToPlus": { + "label": "Enviar a Frigate+" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/filter.json new file mode 100644 index 0000000..8ef5f8f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/filter.json @@ -0,0 +1,17 @@ +{ + "filter": "Filtrar", + "labels": { + "label": "Etiquetas", + "count_one": "{{count}} Etiqueta", + "all": { + "short": "Etiquetas", + "title": "Todas as Etiquetas" + }, + "count_other": "{{count}} Etiquetas" + }, + "zones": { + "all": { + "title": "Tódalas zonas" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/icons.json new file mode 100644 index 0000000..73100bc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecciona unha icona", + "search": { + "placeholder": "Pesquisar unha icona…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/input.json new file mode 100644 index 0000000..c230e54 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descargar vídeo", + "toast": { + "success": "O teu vídeo de revisión comezou a descargarse." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/gl/components/player.json new file mode 100644 index 0000000..89bce7f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/components/player.json @@ -0,0 +1,14 @@ +{ + "noRecordingsFoundForThisTime": "Non se atoparon grabacións para ese período", + "noPreviewFound": "Non se atopou previsualización", + "submitFrigatePlus": { + "submit": "Enviar", + "title": "Enviar este frame a Frigate+?" + }, + "stats": { + "streamType": { + "title": "Tipo de emisión:" + } + }, + "noPreviewFoundFor": "Vista Previa non atopada para {{cameraName}}" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/objects.json b/sam2-cpu/frigate-dev/web/public/locales/gl/objects.json new file mode 100644 index 0000000..60c5408 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/objects.json @@ -0,0 +1,18 @@ +{ + "person": "Persoa", + "bicycle": "Bicicleta", + "airplane": "Avión", + "motorcycle": "Motocicleta", + "bus": "Bus", + "train": "Tren", + "boat": "Bote", + "traffic_light": "Luces de tráfico", + "fire_hydrant": "Boca de incendio", + "street_sign": "Sinal de tráfico", + "stop_sign": "Sinal de Stop", + "parking_meter": "Parquímetro", + "bench": "Banco", + "bird": "Paxaro", + "cat": "Gato", + "car": "Coche" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/configEditor.json new file mode 100644 index 0000000..0d84b1a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/configEditor.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Editor de configuración - Frigate", + "configEditor": "Editor de Preferencias", + "saveOnly": "Só gardar", + "toast": { + "error": { + "savingError": "Erro gardando configuración" + } + }, + "saveAndRestart": "Gardar e Reiniciar", + "copyConfig": "Copiar Configuración" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/events.json new file mode 100644 index 0000000..56da9d9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/events.json @@ -0,0 +1,13 @@ +{ + "alerts": "Alertas", + "detections": "Deteccións", + "allCameras": "Tódalas cámaras", + "timeline.aria": "Selecciona liña de tempo", + "motion": { + "only": "Só movemento", + "label": "Movemento" + }, + "empty": { + "alert": "Non hai alertas que revisar" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/explore.json new file mode 100644 index 0000000..6d381d8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/explore.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Explorar - Frigate", + "generativeAI": "IA xenerativa", + "exploreMore": "Explorar máis obxectos {{label}}", + "exploreIsUnavailable": { + "title": "Explorar non está Dispoñible", + "embeddingsReindexing": { + "finishingShortly": "Rematando ceo", + "startingUp": "Comezando…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/exports.json new file mode 100644 index 0000000..0b99666 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/exports.json @@ -0,0 +1,10 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Pesquisar", + "deleteExport.desc": "Seguro que queres borrar {{exportName}}?", + "editExport": { + "saveExport": "Garda exportación" + }, + "deleteExport": "Borrar exportación", + "noExports": "Non se atoparon exportacións" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/faceLibrary.json new file mode 100644 index 0000000..d98ab1c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/faceLibrary.json @@ -0,0 +1,11 @@ +{ + "description": { + "addFace": "Navegar para engadir unha nova colección á Libraría de Caras.", + "placeholder": "Introduce un nome para esta colección", + "invalidName": "Nome non válido. Os nomes só poden incluír letras, números, espazos, apóstrofes, guións baixos e guións." + }, + "details": { + "unknown": "Descoñecido", + "person": "Persoa" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/live.json new file mode 100644 index 0000000..4ae0e6a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/live.json @@ -0,0 +1,19 @@ +{ + "documentTitle": "Directo - Frigate", + "documentTitle.withCamera": "{{camera}} - Directo - Frigate", + "twoWayTalk": { + "disable": "Deshabilita a Conversa de dous sentidos", + "enable": "Habilitar a Conversa de dous sentidos" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Pincha no frame para centrar a cámara" + } + } + }, + "cameraAudio": { + "enable": "Habilitar Audio de cámara" + }, + "lowBandwidthMode": "Modo de Baixa Banda Ancha" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/recording.json new file mode 100644 index 0000000..26a3ed2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/recording.json @@ -0,0 +1,11 @@ +{ + "filter": "Filtrar", + "export": "Exportar", + "calendar": "Calendario", + "toast": { + "error": { + "noValidTimeSelected": "Rango de tempo inválido" + } + }, + "filters": "Filtros" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/search.json new file mode 100644 index 0000000..3a90cf0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/search.json @@ -0,0 +1,15 @@ +{ + "search": "Pesquisar", + "savedSearches": "Pesquisas gardadas", + "button": { + "save": "Gardar pesquisa", + "filterActive": "Filtros activos", + "clear": "Borrar pesquisa" + }, + "filter": { + "label": { + "cameras": "Cámaras" + } + }, + "searchFor": "Procurar por {{inputValue}}" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/settings.json new file mode 100644 index 0000000..6a68c2c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/settings.json @@ -0,0 +1,11 @@ +{ + "documentTitle": { + "default": "Preferencias - Frigate", + "authentication": "Configuracións de Autenticación - Frigate", + "camera": "Configuracións da Cámara - Frigate", + "general": "Configuracións xerais - Frigate", + "notifications": "Configuración de Notificacións - Frigate", + "enrichments": "Configuración complementarias - Frigate", + "masksAndZones": "Editor de máscaras e zonas - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/gl/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/gl/views/system.json new file mode 100644 index 0000000..55c595b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/gl/views/system.json @@ -0,0 +1,17 @@ +{ + "documentTitle": { + "cameras": "Estatísticas de cámaras - Frigate", + "storage": "Estatísticas de Almacenamento - Frigate", + "general": "Estatísticas Xerais - Frigate", + "enrichments": "Estatísticas complementarias - Frigate", + "logs": { + "frigate": "Rexistros de Frigate - Frigate" + } + }, + "title": "Sistema", + "logs": { + "download": { + "label": "Descargar logs" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/audio.json b/sam2-cpu/frigate-dev/web/public/locales/he/audio.json new file mode 100644 index 0000000..841dfa8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/audio.json @@ -0,0 +1,478 @@ +{ + "speech": "דיבור", + "babbling": "ממלמל", + "yell": "לצעוק", + "bellow": "מתחת", + "whoop": "יבבה", + "whispering": "לוחש", + "crying": "בוכה", + "sigh": "אנחה", + "singing": "שר", + "choir": "מקהלה", + "yodeling": "יודלינג", + "chant": "לשיר", + "mantra": "מנטרה", + "child_singing": "ילד שר", + "synthetic_singing": "שירה סינתטית", + "rapping": "ראפ", + "humming": "זמזום", + "groan": "אנקה", + "grunt": "לנחור", + "whistling": "לשרוק", + "breathing": "נשימה", + "wheeze": "גניחה", + "snoring": "נחירה", + "gasp": "להתנשף", + "pant": "להתנשם", + "snort": "שאיפה", + "cough": "שיעול", + "throat_clearing": "גרגור גרון", + "sneeze": "עיטוש", + "sniff": "לרחרח", + "run": "רץ", + "snicker": "לצחקק", + "laughter": "צחוק", + "organ": "אורגן", + "shuffle": "ערבוב", + "footsteps": "צעדים", + "chewing": "לְעִיסָה", + "biting": "נשיכה", + "gargling": "גרגור", + "stomach_rumble": "קרקור בטן", + "burping": "גיהוק", + "hiccup": "שיהוק", + "fart": "פלוץ", + "hands": "ידיים", + "finger_snapping": "לחיצה באצבעות", + "clapping": "מחיאת כף", + "dog": "כלב", + "bark": "נביחה", + "cat": "חתול", + "horse": "סוס", + "sheep": "כבשה", + "goat": "עז", + "pigeon": "יונה", + "bird": "ציפור", + "coo": "קו", + "crow": "עורב", + "caw": "קאו", + "owl": "ינשוף", + "hoot": "צפירה.", + "flapping_wings": "כנפיים מתנפנפות", + "dogs": "כלבים", + "rats": "חולדות", + "mouse": "עכבר", + "patter": "תבנית", + "insect": "חרק", + "cricket": "קריקט", + "mosquito": "יתוש", + "fly": "זבוב", + "buzz": "זמזם.", + "frog": "צפרדע", + "croak": "קִרקוּר", + "snake": "נחש", + "rattle": "טרטור", + "whale_vocalization": "קולות לוויתן", + "music": "מוזיקה", + "musical_instrument": "כלי נגינה", + "plucked_string_instrument": "כלי מיתר פריטה", + "guitar": "גיטרה", + "electric_guitar": "גיטרה חשמלית", + "bass_guitar": "גיטרה בס", + "acoustic_guitar": "גיטרה אקוסטית", + "steel_guitar": "גיטרה פלדה", + "tapping": "להקיש", + "strum": "פריטה", + "banjo": "בנג'ו", + "sitar": "סיטאר", + "mandolin": "מנדולינה", + "zither": "צִיתָר", + "ukulele": "יוקליילי", + "keyboard": "לוח מקשים", + "piano": "פסנתר", + "electric_piano": "פסנתר חשמלי", + "electronic_organ": "אורגן חשמלי", + "hammond_organ": "עוגב המונד", + "synthesizer": "סינתיסייזר", + "sampler": "דגם", + "harpsichord": "צֶ'מבָּלוֹ", + "percussion": "הַקָשָׁה", + "boat": "סירה", + "car": "מכונית", + "motorcycle": "אופנוע", + "bus": "אוטובוס", + "bicycle": "אופניים", + "train": "למד פנים", + "skateboard": "סקייטבורד", + "camera": "מצלמה", + "howl": "יללה", + "bow_wow": "באו וואו", + "growling": "נהמה", + "whimper_dog": "יבבת כלבים", + "purr": "לגרגר", + "meow": "מיאו", + "hiss": "לחישה", + "caterwaul": "קטרוואל", + "livestock": "בעלי חיים", + "clip_clop": "קליפ קלופ", + "neigh": "צהלה", + "cattle": "בקר", + "door": "דלת", + "heartbeat": "דופק", + "heart_murmur": "אוושת לב", + "applause": "תשואות", + "chatter": "פטפוטים", + "crowd": "קהל", + "children_playing": "ילדים משחקים", + "animal": "חיה", + "pets": "חיות מחמד", + "cheering": "תשואות", + "yip": "ייפ", + "moo": "מוו", + "cowbell": "פעמון פרה", + "pig": "חזיר", + "oink": "אוינק", + "bleat": "פעייה", + "fowl": "עוף.", + "chicken": "עוף", + "cluck": "קרקור", + "cock_a_doodle_doo": "קוק-א-דודל-דו", + "turkey": "הודו", + "gobble": "זלילה", + "duck": "ברווז", + "quack": "קוואק", + "goose": "אווז", + "honk": "צפירה", + "wild_animals": "חיית פרא", + "roaring_cats": "חתולים שואגים", + "roar": "שאגה", + "chirp": "ציוץ", + "squawk": "צווחה", + "drum_kit": "מערכת תופים", + "drum_machine": "מכונת תופים", + "drum": "תופים", + "snare_drum": "תוף סנר", + "rimshot": "רימשוט", + "drum_roll": "תוף רול", + "bass_drum": "תופים בס", + "timpani": "טימפאני", + "tabla": "טבלה", + "cymbal": "מצילה", + "hi_hat": "היי-האט", + "wood_block": "בול עץ", + "tambourine": "טמבורין", + "maraca": "מרקה", + "gong": "גונג", + "tubular_bells": "פעמונים צינוריים", + "mallet_percussion": "כלי הקשה מסוג פטיש", + "marimba": "מרימבה", + "glockenspiel": "גלוקנשפיל", + "vibraphone": "ויברפון", + "steelpan": "פלדה-פאן", + "orchestra": "תזמורת", + "brass_instrument": "כלי נשיפה ממתכת", + "french_horn": "צופר צרפתי", + "trumpet": "חצוצרה", + "trombone": "טרומבון", + "bowed_string_instrument": "כלי קשת", + "string_section": "מקטע מחרוזות", + "violin": "כינור", + "pizzicato": "פיציקטו", + "cello": "צ'לו", + "double_bass": "בס כפול", + "wind_instrument": "כלי נשיפה", + "flute": "חליל", + "saxophone": "סקסופון", + "clarinet": "קלרינט", + "harp": "נבל", + "bell": "פעמון", + "church_bell": "פעמון כנסיה", + "jingle_bell": "ג'ינגל בל", + "bicycle_bell": "פעמון אופניים", + "chime": "צִלצוּל", + "wind_chime": "פעמון רוח", + "harmonica": "הרמוניקה", + "accordion": "אקורדיון", + "bagpipes": "חלילים", + "didgeridoo": "דיג'רידו", + "theremin": "תרמין", + "singing_bowl": "קערת שירה", + "scratching": "גירוד", + "pop_music": "מוזיקת פופ", + "hip_hop_music": "מוזיקת היפ הופ", + "beatboxing": "ביטבוקסינג", + "rock_music": "מוזיקת רוק", + "heavy_metal": "מיטל כבד", + "punk_rock": "מוזיקת פאנק", + "grunge": "גראנג'", + "progressive_rock": "רוק פרוגרסיב", + "rock_and_roll": "רוקנרול", + "psychedelic_rock": "רוק פסיכדלי", + "rhythm_and_blues": "רית'ם אנד בלוז", + "soul_music": "מוזיקת סול", + "reggae": "רגיי", + "country": "קאונטרי", + "swing_music": "מוזיקת סווינג", + "bluegrass": "בלוגראס", + "funk": "פאנק", + "folk_music": "מוזיקת פולק", + "middle_eastern_music": "מוזיקה ים תיכונית", + "opera": "אופרה", + "jazz": "ג'אז", + "disco": "דיסקו", + "classical_music": "מוזיקה קלאסית", + "electronic_music": "מוזיקה אלקטרונית", + "house_music": "מוזיקת האוס", + "techno": "טכנו", + "dubstep": "דאבסטפ", + "drum_and_bass": "דראם אנד בס", + "electronica": "אלקטרוניקה", + "electronic_dance_music": "מוזיקת ריקוד אלקטרונית", + "ambient_music": "מוזיקת אמביינט", + "flamenco": "פלמנקו", + "trance_music": "מוזיקת טראנס", + "music_of_latin_america": "מוזיקה לטינית", + "salsa_music": "מוזיקת סלסה", + "blues": "בלוז", + "music_for_children": "מוזיקת ילדים", + "new-age_music": "מוזיקת ניו אייג'", + "vocal_music": "מוזיקה ווקאלית", + "a_capella": "קאפלה", + "music_of_africa": "מוזיקה אפריקאית", + "afrobeat": "אפרוביט", + "christian_music": "מוזיקה נוצרית", + "gospel_music": "מוזיקת גוספל", + "music_of_asia": "מוזיקה אסייתית", + "carnatic_music": "מוזיקה קרנטית", + "music_of_bollywood": "מוזיקה בוליווד", + "ska": "סקא", + "traditional_music": "מוזיקה מסורתית", + "independent_music": "מחיקת המשתמש נכשלה: {{errorMessage}}", + "song": "שיר", + "background_music": "מוזיקת רקע", + "theme_music": "מוזיקת נושא", + "jingle": "ג'ינגל", + "soundtrack_music": "פסקול מוזיקה", + "lullaby": "שיר ערש", + "video_game_music": "מוזיקת משחקי וידיאו", + "christmas_music": "מוזיקת קריסמיס", + "dance_music": "מוזיקת ריקודים", + "wedding_music": "מוזיקת חתונות", + "happy_music": "מוזיקה שמחה", + "sad_music": "מוזיקה עצובה", + "tender_music": "מוזיקה עדינה", + "exciting_music": "מוזיקה מרגשת", + "angry_music": "מוזיקה כועסת", + "scary_music": "מוזיקה מפחידה", + "wind": "רוח", + "rustling_leaves": "רשרוש עלים", + "wind_noise": "רעש רוח", + "thunderstorm": "סופת רעמים", + "thunder": "רעם", + "water": "מים", + "rain": "גשם", + "rain_on_surface": "גשם על פני השטח", + "stream": "שידור", + "vehicle": "רכב", + "waterfall": "מפל", + "ocean": "ים", + "waves": "גלים", + "steam": "קיטור", + "gurgling": "גרגור", + "fire": "אש", + "crackle": "פיצוח", + "sailboat": "מפרשית", + "rowboat": "סירת משוטים", + "motorboat": "סירת מנוע", + "ship": "ספינה", + "motor_vehicle": "רכב ממונע", + "toot": "תקיעה", + "car_alarm": "אזעקת רכב", + "power_windows": "חלונות חשמליים", + "skidding": "החלקה", + "tire_squeal": "חריקת צמיגים", + "car_passing_by": "מכונית חולפת", + "race_car": "מכונית מרוץ", + "truck": "משאית", + "air_brake": "בלם אוויר", + "air_horn": "צופר אוויר", + "reversing_beeps": "צליל רוורס", + "ice_cream_truck": "אוטו גלידה", + "emergency_vehicle": "רכב חירום", + "police_car": "רכב משטרה", + "ambulance": "אמבולנס", + "fire_engine": "כבאית", + "traffic_noise": "רעש תנועה", + "rail_transport": "רכבת נוסעים", + "train_whistle": "שריקת רכבת", + "train_horn": "צופר רכבת", + "railroad_car": "קרון רכבת", + "train_wheels_squealing": "חריקת גלגלי הרכבת", + "subway": "רכבת תחתית", + "aircraft": "כלי טיס", + "aircraft_engine": "מנוע כלי טיס", + "jet_engine": "מנוע סילון", + "propeller": "פרופלור", + "helicopter": "מסוק", + "fixed-wing_aircraft": "מטוסים בעלי כנף קבועה", + "light_engine": "מנוע קל", + "engine": "מנוע", + "dental_drill's_drill": "מקדחת שיניים", + "lawn_mower": "מכסחת דשא", + "chainsaw": "מסור שרשרת", + "medium_engine": "מנוע בינוני", + "heavy_engine": "מנוע כבד", + "engine_knocking": "דפיקות מנוע", + "engine_starting": "מנוע מוצת", + "idling": "התבטלות", + "sink": "כיור", + "blender": "מערבל", + "accelerating": "מאיץ", + "doorbell": "פעמון דלת", + "ding-dong": "דינג דונג", + "sliding_door": "דלתות הזזה", + "knock": "דפיקה בדלת", + "tap": "הקשה", + "squeak": "חריקה", + "cupboard_open_or_close": "פתיחה או סגירה של ארון", + "cutlery": "סכו\"ם", + "chopping": "קצוץ", + "frying": "טיגון", + "microwave_oven": "מיקרוגל", + "water_tap": "ברז מים", + "bathtub": "אמבטיה", + "dishes": "מנות", + "scissors": "מספריים", + "toothbrush": "מברשת שיניים", + "toilet_flush": "הורדת מים לאסלה", + "electric_toothbrush": "מברשת שיניים חשמלית", + "vacuum_cleaner": "שואב אבק", + "zipper": "רוכסן", + "coin": "מטבע", + "shuffling_cards": "ערבוב קלפים", + "typing": "הקלדה", + "typewriter": "מכונת כתיבה", + "computer_keyboard": "מקלדת מחשב", + "writing": "כתיבה", + "telephone_bell_ringing": "צלצול טלפון", + "ringtone": "צלצול", + "clock": "שעון", + "telephone_dialing": "טלפון מחייג", + "dial_tone": "צליל חיוג", + "busy_signal": "צליל תפוס", + "alarm_clock": "שעון מעורר", + "siren": "סירנה", + "civil_defense_siren": "סירנה של ההגנה האזרחית", + "buzzer": "זמזם", + "smoke_detector": "גלאי עשן", + "fire_alarm": "אזעקת אש", + "foghorn": "צופר ערפל", + "whistle": "שריקה", + "steam_whistle": "שריקת קיטור", + "mechanisms": "מכניקה", + "ratchet": "מַחגֵר", + "tick": "טיק", + "tick-tock": "טיק טוק", + "gears": "הילוכים", + "pulleys": "גלגלות", + "sewing_machine": "מְכוֹנַת תְפִירָה", + "mechanical_fan": "מאוורר מכני", + "air_conditioning": "מיזוג אוויר", + "cash_register": "קוּפָּה רוֹשֶׁמֶת", + "printer": "מדפסת", + "single-lens_reflex_camera": "מצלמת רפלקס עם עדשה יחידה", + "tools": "כלים", + "hammer": "פטיש", + "jackhammer": "פטיש אוויר", + "sawing": "מסור", + "filing": "הגשה", + "sanding": "שיוף", + "power_tool": "כלי חשמלי", + "drill": "מקדחה", + "explosion": "התפוצצות", + "gunshot": "יריה", + "machine_gun": "מכונת יריה", + "fusillade": "פוסילה", + "artillery_fire": "אש ארטילרית", + "cap_gun": "Cap Gun", + "fireworks": "זיקוקים", + "firecracker": "חזיז", + "burst": "הִתפָּרְצוּת", + "eruption": "התפרצות", + "boom": "בום", + "wood": "עץ", + "chop": "לקצוץ", + "splinter": "קיסם", + "crack": "סדק", + "glass": "זכוכית", + "chink": "סדוק", + "shatter": "ניפוץ", + "silence": "השתקה", + "sound_effect": "אפקט קול", + "environmental_noise": "רעש סביבתי", + "static": "סטטי", + "white_noise": "רעש לבן", + "pink_noise": "רעש ורוד", + "radio": "רדיו", + "field_recording": "רישום שדה", + "scream": "צרחה", + "drawer_open_or_close": "מגירה פתוחה או סגורה", + "alarm": "אזעקה", + "television": "טלוויזיה", + "electric_shaver": "מכונת גילוח חשמלית", + "keys_jangling": "צלצול מפתחות", + "hair_dryer": "מייבש שיער", + "slam": "טריקה", + "telephone": "טלפון", + "tuning_fork": "מזלג כוונון", + "raindrop": "טיפות גשם", + "smash": "רסק", + "boiling": "רותח", + "sonar": "סונר", + "arrow": "חץ", + "whack": "מַהֲלוּמָה", + "sine_wave": "גל סינוס", + "harmonic": "הרמוניה", + "chirp_tone": "צליל ציוץ", + "pulse": "דוֹפֶק", + "inside": "בְּתוֹך", + "outside": "בחוץ", + "reverberation": "הִדהוּד", + "echo": "הד", + "noise": "רעש", + "mains_hum": "זמזום ראשי", + "distortion": "סַלְפָנוּת", + "sidetone": "צליל צדדי", + "cacophony": "קָקוֹפוֹניָה", + "throbbing": "פְּעִימָה", + "vibration": "רֶטֶט", + "sodeling": "מיזוג", + "change_ringing": "שינוי צלצול", + "shofar": "שופר", + "liquid": "נוזל", + "splash": "התזה", + "slosh": "שכשוך", + "squish": "מעיכה", + "drip": "טפטוף", + "pour": "לִשְׁפּוֹך", + "trickle": "לְטַפטֵף", + "gush": "פֶּרֶץ", + "fill": "מילוי", + "spray": "ריסוס", + "pump": "משאבה", + "stir": "בחישה", + "whoosh": "מהיר", + "thump": "חֲבָטָה", + "thunk": "תרועה", + "electronic_tuner": "מכוון אלקטרוני", + "effects_unit": "יחידת אפקטים", + "chorus_effect": "אפקט מקהלה", + "basketball_bounce": "קפיצת כדורסל", + "bang": "לִדפּוֹק", + "slap": "סְטִירָה", + "breaking": "שְׁבִירָה", + "bouncing": "הַקפָּצָה", + "whip": "שׁוֹט", + "flap": "מַדָף", + "scratch": "לְגַרֵד" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/common.json b/sam2-cpu/frigate-dev/web/public/locales/he/common.json new file mode 100644 index 0000000..813d44e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/common.json @@ -0,0 +1,274 @@ +{ + "time": { + "justNow": "כעת", + "untilForRestart": "עד לאתחול של Frigate.", + "untilRestart": "עד לאתחול", + "ago": "לפני {{timeAgo}}", + "today": "היום", + "untilForTime": "עד:{{time}}", + "last30": "30 ימים אחרונים", + "last14": "14 ימים אחרונים", + "year_one": "{{time}} שנה", + "year_two": "{{time}} שנים", + "year_other": "{{time}} שנים", + "last7": "7 ימים אחרונים", + "lastMonth": "חודש שעבר", + "10minutes": "10 דקות", + "yesterday": "אתמול", + "thisWeek": "השבוע", + "lastWeek": "שבוע שעבר", + "1hour": "שעה", + "12hours": "12 שעות", + "24hours": "24 שעות", + "pm": "pm", + "am": "am", + "yr": "{{time}}שנה", + "mo": "{{time}}חודש", + "d": "{{time}}יום", + "month_one": "{{time}} חודש", + "month_two": "{{time}} חודשים", + "month_other": "{{time}} חודשים", + "h": "{{time}}שעה", + "day_one": "{{time}} יום", + "day_two": "{{time}} ימים", + "day_other": "{{time}} ימים", + "hour_one": "{{time}} שעה", + "hour_two": "{{time}} שעות", + "hour_other": "{{time}} שעות", + "m": "{{time}}דקות", + "minute_one": "{{time}} דקה", + "minute_two": "{{time}} דקות", + "minute_other": "{{time}} דקות", + "s": "{{time}}שניה", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "second_one": "{{time}} שניה", + "second_two": "{{time}} שניות", + "second_other": "{{time}} שניות", + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "5minutes": "5 דקות", + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "30minutes": "30 דקות", + "thisMonth": "החודש" + }, + "unit": { + "speed": { + "kph": "קמ\"ש", + "mph": "מייל לשעה" + }, + "length": { + "feet": "רגל", + "meters": "מטרים" + } + }, + "label": { + "back": "אחורה" + }, + "button": { + "apply": "החל", + "reset": "איפוס", + "done": "בוצע", + "enabled": "מאופשר", + "enable": "אפשר", + "disabled": "מבוטל", + "disable": "בטל", + "save": "שמירה", + "saving": "שומר…", + "back": "חזרה", + "close": "סגירה", + "cancel": "ביטול", + "copy": "העתקה", + "twoWayTalk": "דיבור דו כיווני", + "exitFullscreen": "יציאה ממסך מלא", + "pictureInPicture": "תמונה בתוך תמונה", + "cameraAudio": "קול ממצלמה", + "off": "כבוי", + "edit": "עריכה", + "copyCoordinates": "העתקת קואורדינטות", + "delete": "מחיקה", + "yes": "כן", + "no": "לא", + "suspended": "מושהה", + "unsuspended": "ביטול השהייה", + "play": "ניגון", + "unselect": "בטל בחירה", + "export": "ייצוא", + "deleteNow": "מחיקה כעת", + "fullscreen": "מסך מלא", + "history": "היסטוריה", + "on": "פעיל", + "download": "הורדה", + "info": "מידע", + "next": "הבא" + }, + "menu": { + "system": "מערכת", + "systemMetrics": "מדדי מערכת", + "configuration": "תצורת מערכת", + "systemLogs": "לוגים מערכת", + "settings": "הגדרות", + "configurationEditor": "עריכת תצורה", + "languages": "שפות", + "language": { + "en": "English (English)", + "es": "ספרדית", + "fr": "צרפתית", + "ar": "ערבית", + "pt": "פורטוגזית", + "it": "איטלקית", + "nl": "הולנדית", + "sv": "שוודית", + "cs": "צ'כית", + "nb": "נורווגית", + "ko": "קוריאנית", + "vi": "ויטנאמית", + "fa": "פרסית", + "pl": "פולנית", + "uk": "אוקראינית", + "he": "עברית", + "el": "יוונית", + "ro": "רומנית", + "hu": "הונגרית", + "fi": "פינית", + "da": "דנית", + "withSystem": { + "label": "השתמש בהגדרות המערכת עבור השפה" + }, + "sk": "סלובקית", + "th": "תאילנדית", + "zhCN": "סינית פשוטה", + "tr": "טורקית", + "hi": "הודית", + "ru": "רוסית", + "ja": "יפנית", + "de": "גרמנית", + "yue": "קנטונזית", + "ca": "קטלה (קטלאנית)", + "ptBR": "פורטוגזית - ברזיל", + "sr": "סרבית", + "sl": "סלובנית", + "lt": "ליטאית", + "bg": "בולגרית", + "gl": "Galego", + "id": "אינדונזית", + "ur": "اردو" + }, + "appearance": "מראה", + "darkMode": { + "label": "מצב כהה", + "light": "בהיר", + "dark": "כהה", + "withSystem": { + "label": "השתמש בהגדרות המערכת עבור מצב בהיר או כהה" + } + }, + "withSystem": "מערכת", + "theme": { + "label": "ערכת נושא", + "blue": "כחול", + "green": "ירוק", + "nord": "נורד", + "red": "אדום", + "highcontrast": "ניגודיות גבוהה", + "default": "ברירת מחדל" + }, + "review": "סקירה", + "explore": "עיון", + "help": "עזרה", + "documentation": { + "title": "תיעוד", + "label": "תיעוד Frigate" + }, + "restart": "הפעלה מחדש", + "live": { + "title": "שידור חי", + "cameras": { + "title": "מצלמות", + "count_one": "{{count}} מצלמה", + "count_two": "{{count}} מצלמות", + "count_other": "{{count}} מצלמות" + }, + "allCameras": "כל המצלמות" + }, + "export": "ייצוא", + "uiPlayground": "ממשק משתמש", + "faceLibrary": "ספריית זיהוי פנים", + "user": { + "account": "חשבון", + "anonymous": "אנונימי", + "logout": "ניתוק", + "current": "משתמש מחובר: {{user}}", + "setPassword": "קביעת סיסמה", + "title": "משתמש" + } + }, + "toast": { + "copyUrlToClipboard": "כתובת האתר המועתקת.", + "save": { + "title": "שמירה", + "error": { + "noMessage": "שמירת שינויי התצורה נכשלה", + "title": "נכשל בניסיון לשמור את ההגדרות: {{errorMessage}}" + } + } + }, + "role": { + "title": "הרשאה", + "admin": "מנהל", + "viewer": "צופה", + "desc": "למנהלי מערכת יש גישה מלאה לכל התכונות בממשק המשתמש של Frigate. הצופים מוגבלים לצפייה במצלמות, סקירת פריטים וצילומים היסטוריים בממשק המשתמש." + }, + "pagination": { + "next": { + "title": "הבא", + "label": "עבור לדף הבא" + }, + "label": "דפדוף", + "previous": { + "title": "הקודם", + "label": "עבור לדף הקודם" + }, + "more": "דפים נוספים" + }, + "accessDenied": { + "title": "הגישה נדחתה", + "desc": "אין לך הרשאה לצפות בדף הזה.", + "documentTitle": "גישה נדחתה - Frigate" + }, + "notFound": { + "documentTitle": "לא נמצא - Frigate", + "title": "404", + "desc": "דף לא נמצא" + }, + "selectItem": "בחירה:{{item}}", + "readTheDocumentation": "קרא את התיעוד" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/auth.json new file mode 100644 index 0000000..17b28cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "שם משתמש", + "password": "סיסמה", + "login": "התחברות", + "errors": { + "usernameRequired": "נדרש שם משתמש", + "passwordRequired": "דרושה סיסמה", + "unknownError": "שגיאה לא ידועה. בדוק את הלוגים.", + "webUnknownError": "שגיאה לא ידועה, בדוק את הלוגים.", + "rateLimit": "חרגת מהמגבלת בקשות. נסה שוב מאוחר יותר.", + "loginFailed": "ההתחברות נכשלה" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/camera.json new file mode 100644 index 0000000..f9de9a6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "קבוצת מצלמות", + "camera": { + "setting": { + "streamMethod": { + "method": { + "smartStreaming": { + "desc": "שידור חכם יעדכן את תמונת המצלמה פעם בדקה כאשר לא מתרחשת פעילות ניתנת לזיהוי כדי לחסוך רוחב פס ומשאבים. כאשר מזוהה פעילות, התמונה עוברת בצורה חלקה לשידור חי.", + "label": "שידור חכם (מומלץ)" + }, + "noStreaming": { + "label": "אין שידור", + "desc": "תמונות המצלמה יתעדכנו רק פעם בדקה ולא יתבצע שידור חי." + }, + "continuousStreaming": { + "label": "שידור רציף", + "desc": { + "title": "תמונת המצלמה תמיד תהיה שידור חי כאשר היא גלויה בדשבורד, גם אם לא זוהתה פעילות.", + "warning": "שידור רציף עלול לגרום לשימוש גבוה ברוחב פס ובעיות ביצועים. יש להשתמש בזהירות." + } + } + }, + "label": "שיטת שידור", + "placeholder": "בחירת שיטת שידור" + }, + "title": "הגדרות סטרימינג של {{cameraName}}", + "label": "הגדרות זרם מצלמה", + "desc": "שנה את אפשרויות הסטרימינג בשידור חי עבור לוח המחוונים של קבוצת מצלמות זו. הגדרות אלו ספציפיות למכשיר/דפדפן.", + "audioIsAvailable": "קול זמין עבור שידור זה", + "audioIsUnavailable": "קול לא זמין לזרם זה", + "audio": { + "tips": { + "title": "יש להפיק קול מהמצלמה שלך ולהגדיר אותו ב-go2rtc עבור שידור זה.", + "document": "קרא את התיעוד " + } + }, + "stream": "זרם", + "placeholder": "בחירת זרם", + "compatibilityMode": { + "label": "מצב תאימות", + "desc": "הפעל אפשרות זו רק אם השידור החי של המצלמה שלך מציג עיוותים בצבע ויש לו קו אלכסוני בצד ימין של התמונה." + } + } + }, + "edit": "ערכית קבוצת מצלמות", + "delete": { + "label": "מחיקת קבוצת מצלמות", + "confirm": { + "title": "אישור מחיקה", + "desc": "האם אתה בטוח שברצונך למחוק את קבוצת המצלמות {{name}}?" + } + }, + "name": { + "label": "שם", + "placeholder": "הכנס שם…", + "errorMessage": { + "mustLeastCharacters": "שם קבוצת המצלמות חייב להיות בן 2 תווים לפחות.", + "exists": "שם קבוצת המצלמות כבר קיים.", + "nameMustNotPeriod": "שם קבוצת המצלמות אינו יכול להכיל נקודה.", + "invalid": "שם קבוצת מצלמות לא חוקי." + } + }, + "cameras": { + "label": "מצלמות", + "desc": "בחירת מצלמות עבור קבוצה זו." + }, + "icon": "אייקון", + "success": "קבוצת המצלמות ({{name}}) נשמרה.", + "add": "הוספת קבוצת מצלמות" + }, + "debug": { + "options": { + "label": "הגדרות", + "title": "אפשרויות", + "showOptions": "הצג אפשרויות", + "hideOptions": "הסתר אפשרויות" + }, + "boundingBox": "תיבת זיהוי", + "zones": "אזורים", + "mask": "מיסוך", + "motion": "תנועה", + "timestamp": "חותמת זמן", + "regions": "אזורים" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/dialog.json new file mode 100644 index 0000000..472d3d5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/dialog.json @@ -0,0 +1,114 @@ +{ + "restart": { + "title": "האם אתה בטוח שברצונך להפעיל מחדש את Frigate?", + "button": "הפעלה מחדש - Frigate", + "restarting": { + "title": "Frigate מופעל מחדש כעת", + "content": "דף זה ייטען מחדש בעוד {{countdown}} שניות.", + "button": "אילוץ טעינה מחדש" + } + }, + "export": { + "toast": { + "error": { + "endTimeMustAfterStartTime": "שעת הסיום חייבת להיות אחרי שעת ההתחלה", + "failed": "נכשל בהתחלת הייצוא: {{error}}", + "noVaildTimeSelected": "לא נבחר טווח זמן תקף" + }, + "success": "הייצוא הוחל בהצלחה. הצג את הקובץ בתיקייה /ייצוא." + }, + "time": { + "end": { + "label": "בחירת זמן סיום", + "title": "זמן סיום" + }, + "fromTimeline": "בחירה מציר זמן", + "lastHour_one": "שעה אחרונה", + "lastHour_two": "שעות אחרונות {{count}}", + "lastHour_other": "שעות אחרונות {{count}}", + "custom": "מותאם אישית", + "start": { + "title": "זמן התחלה", + "label": "בחירת זמן התחלה" + } + }, + "selectOrExport": "בחירה או ייצוא", + "name": { + "placeholder": "תן שם לייצוא" + }, + "select": "בחירה", + "export": "ייצוא", + "fromTimeline": { + "saveExport": "שמירת ייצוא", + "previewExport": "תצוגה מקדימה של ייצוא" + } + }, + "streaming": { + "restreaming": { + "desc": { + "title": "הגדר את go2rtc לקבלת אפשרויות נוספות של תצוגה חיה ושמע עבור מצלמה זו.", + "readTheDocumentation": "קרא את התיעוד" + }, + "disabled": "הזרמה מחדש אינה פעילה עבור מצלמה זו." + }, + "label": "זרם", + "showStats": { + "label": "הצג סטטיסטיקות שידור", + "desc": "הפעל אפשרות זו כדי להציג סטטיסטיקות שידור כשכבת-על על פיד המצלמה." + }, + "debugView": "תצוגת ניפוי שגיאות" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "העלה ל- +Frigate", + "desc": "אובייקטים במיקומים שברצונך להימנע מהם אינם תוצאות חיוביות שגויות. הגשתם כתוצאות חיוביות שגויות תבלבל את המודל." + }, + "review": { + "question": { + "label": "אשר תווית זו עבור +Frigate", + "ask_a": "האם אובייקט זה הוא {{label}}?", + "ask_an": "האם אובייקט זה הוא - {{label}}?", + "ask_full": "האם אובייקט זה הוא {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "נשלח" + } + } + }, + "video": { + "viewInHistory": "צפה בהיסטוריה" + } + }, + "search": { + "saveSearch": { + "label": "שמירת חיפוש", + "desc": "ספק שם לחיפוש שמור זה.", + "placeholder": "הזן שם לחיפוש שלך", + "success": "החיפוש ({{searchName}}) נשמר.", + "button": { + "save": { + "label": "שמירת החיפוש הזה" + } + }, + "overwrite": "{{searchName}} כבר קיים. שמירה תדרוס את הערך הקיים." + } + }, + "recording": { + "confirmDelete": { + "title": "אישור מחיקה", + "desc": { + "selected": "האם אתה בטוח שברצונך למחוק את כל הסרטונים המוקלטים המשויכים לפריט סקירה זה?

    החזק את מקש Shift לחוץ כדי לעקוף תיבת דו-שיח זו בעתיד." + }, + "toast": { + "success": "קטעי וידאו המשויכים לפריטי הסקירה שנבחרו נמחקו בהצלחה.", + "error": "המחיקה נכשלה: {{error}}" + } + }, + "button": { + "export": "ייצוא", + "markAsReviewed": "סמן כסוקר", + "deleteNow": "מחיקה כעת" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/filter.json new file mode 100644 index 0000000..17f7914 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "מסנן", + "features": { + "submittedToFrigatePlus": { + "tips": "עליך תחילה לסנן לפי אובייקטים במעקב שיש להם תמונת מצב.

    לא ניתן לשלוח ל-Frigate+ אובייקטים במעקב ללא תמונת מצב.", + "label": "העלאה ל- +Frigate" + }, + "label": "מאפיינים", + "hasVideoClip": "קיים סרטון", + "hasSnapshot": "קיימת לכידת תמונה" + }, + "labels": { + "label": "תוויות", + "all": { + "title": "כל התוויות", + "short": "תוויות" + }, + "count_one": "{{count}} תווית", + "count_other": "{{count}} תוויות" + }, + "zones": { + "label": "איזורים", + "all": { + "title": "כל האזורים", + "short": "אזורים" + } + }, + "dates": { + "selectPreset": "בחר הגדרה…", + "all": { + "title": "כל התאריכים", + "short": "תאריכים" + } + }, + "more": "מסננים נוספים", + "reset": { + "label": "איפוס מסננים לערכי ברירת מחדל" + }, + "timeRange": "טווח זמן", + "subLabels": { + "label": "תוויות משנה", + "all": "כל תוויות המשנה" + }, + "score": "ציון", + "estimatedSpeed": "מהירות משוערת ({{unit}})", + "sort": { + "label": "מיון", + "dateAsc": "תאריך (עולה)", + "dateDesc": "תאריך (יורד)", + "scoreAsc": "ציון אובייקט (עולה)", + "scoreDesc": "ציון אובייקט (יורד)", + "speedAsc": "מהירות משוערת (עולה)", + "speedDesc": "מהירות משוערת (יורד)", + "relevance": "רלוונטיות" + }, + "cameras": { + "label": "מסנן מצלמות", + "all": { + "title": "כל המצלמות", + "short": "מצלמות" + } + }, + "review": { + "showReviewed": "הצג פריטים שנבדקו" + }, + "motion": { + "showMotionOnly": "הצגת תנועה בלבד" + }, + "explore": { + "settings": { + "title": "הגדרות", + "defaultView": { + "summary": "סיכום", + "unfilteredGrid": "טבלה לא מסוננת", + "title": "תצוגת ברירת מחדל", + "desc": "כאשר לא נבחרו מסננים, הצג סיכום של האובייקטים האחרונים שעברו מעקב לפי תווית, או הצג רשת לא מסוננת." + }, + "gridColumns": { + "title": "עמודות טבלה", + "desc": "בחר את מספר העמודות בטבלה." + }, + "searchSource": { + "label": "חיפוש במקור", + "desc": "בחר אם לחפש בתמונות הממוזערות או בתיאורים של האובייקטים שבמעקב.", + "options": { + "thumbnailImage": "תמונה ממוזערת", + "description": "תיאור" + } + } + }, + "date": { + "selectDateBy": { + "label": "בחר תאריך לפיו יבוצע הסינון" + } + } + }, + "trackedObjectDelete": { + "toast": { + "success": "אובייקטים במעקב נמחקו בהצלחה.", + "error": "מחיקת אובייקטים במעקב נכשלה: {{errorMessage}}" + }, + "title": "אישור מחיקה", + "desc": "מחיקת אובייקטים אלה ({{objectLength}}) שעברו מעקב מסירה את לכידת התמונה, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקטים אלה שעברו מעקב בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?

    החזק את מקש Shift כדי לעקוף תיבת דו-שיח זו בעתיד." + }, + "zoneMask": { + "filterBy": "סינון לפי מיסוך אזור" + }, + "recognizedLicensePlates": { + "title": "לוחיות רישוי מוכרות", + "loadFailed": "טעינת לוחיות הרישוי המזוהות נכשלה.", + "loading": "טוען לוחיות רישוי מזוהות…", + "placeholder": "הקלד כדי לחפש לוחיות רישוי…", + "noLicensePlatesFound": "לא נמצאו לוחיות רישוי.", + "selectPlatesFromList": "בחירת לוחית אחת או יותר מהרשימה.", + "selectAll": "בחר הכל", + "clearAll": "נקה הכל" + }, + "logSettings": { + "label": "סינון רמת לוג", + "filterBySeverity": "סנן לוגים לפי חוּמרָה", + "loading": { + "title": "טוען", + "desc": "כאשר חלונית הלוגים מגוללת לתחתית, לוגים חדשים מוזרמים אוטומטית עם הוספתם." + }, + "disableLogStreaming": "השבתת זרימה של לוגים", + "allLogs": "כל הלוגים" + }, + "classes": { + "label": "מחלקות", + "all": { + "title": "כל המחלקות" + }, + "count_one": "{{count}} מחלקה", + "count_other": "{{count}} מחלקות" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/icons.json new file mode 100644 index 0000000..ecff965 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "בחר סמל", + "search": { + "placeholder": "חפש אייקון…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/input.json new file mode 100644 index 0000000..e427f53 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "הורד וידאו", + "toast": { + "success": "הורדת סרטון הסקירה שלך החלה." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/he/components/player.json new file mode 100644 index 0000000..348cb1d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "לא נמצאו הקלטות לזמן זה", + "stats": { + "droppedFrames": { + "title": "פריימים שנשמטו:", + "short": { + "title": "נשמטו", + "value": "{{droppedFrames}} פריימים" + } + }, + "streamType": { + "title": "סוג זרם:", + "short": "סוג" + }, + "bandwidth": { + "title": "רוחב-פס:", + "short": "רוחב-פס" + }, + "latency": { + "title": "השהיה:", + "value": "{{seconds}} שניות", + "short": { + "title": "השהיה", + "value": "{{seconds}} שניה" + } + }, + "decodedFrames": "פריימים מפוענחים:", + "droppedFrameRate": "קצב פריימים מופחת:", + "totalFrames": "סך כל המסגרות:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "הפריים נשלח בהצלחה ל-Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "שליחת הפריים ל-Frigate+ נכשלה" + } + }, + "noPreviewFound": "לא נמצאה תצוגה מקדימה", + "noPreviewFoundFor": "לא נמצאה תצוגה מקדימה עבור {{cameraName}}", + "submitFrigatePlus": { + "title": "לשלוח את הפריים הזה ל-Frigate+?", + "submit": "אישור" + }, + "livePlayerRequiredIOSVersion": "נדרשת iOS 17.1 ומעלה עבור סוג השידור החי הזה.", + "streamOffline": { + "title": "זרם מצלמה לא זמין", + "desc": "לא התקבלו פריימים בזרם detect של {{cameraName}}, בדקו את יומני השגיאות" + }, + "cameraDisabled": "המצלמה מושבתת" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/objects.json b/sam2-cpu/frigate-dev/web/public/locales/he/objects.json new file mode 100644 index 0000000..68f648d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/objects.json @@ -0,0 +1,120 @@ +{ + "person": "אדם", + "bicycle": "אופניים", + "car": "מכונית", + "motorcycle": "אופנוע", + "airplane": "מטוס", + "bus": "אוטובוס", + "train": "למד פנים", + "boat": "סירה", + "traffic_light": "רמזור", + "fire_hydrant": "ברז כיבוי אש", + "street_sign": "שלט רחוב", + "stop_sign": "תמרור עצור", + "parking_meter": "מדחן חניה", + "bench": "ספסל", + "bird": "ציפור", + "cat": "חתול", + "dog": "כלב", + "horse": "סוס", + "sheep": "כבשה", + "cow": "פרה", + "elephant": "פיל", + "bear": "דב", + "zebra": "זברה", + "giraffe": "ג'ירפה", + "hat": "כובע", + "backpack": "תרמיל", + "umbrella": "מטריה", + "shoe": "נעל", + "eye_glasses": "משקפיים", + "handbag": "תיק יד", + "tie": "עניבה", + "suitcase": "מזוודה", + "frisbee": "צלחת מעופפת", + "skis": "מִגלָשַׁיִם", + "snowboard": "גלשן שלג", + "sports_ball": "כדור ספורט", + "kite": "עפיפון", + "baseball_bat": "כובע בייסבול", + "baseball_glove": "כפפת בייסבול", + "skateboard": "סקייטבורד", + "surfboard": "גלשן", + "bottle": "בקבוק", + "plate": "לוחית", + "wine_glass": "כוס יין", + "mouse": "עכבר", + "keyboard": "לוח מקשים", + "bark": "נביחה", + "fox": "שועל", + "goat": "עז", + "rabbit": "ארנב", + "raccoon": "רקון", + "robot_lawnmower": "מכסחת דשא רובוטית", + "waste_bin": "פח אשפה", + "on_demand": "לפי דרישה", + "face": "פנים", + "license_plate": "לוחית רישוי", + "package": "חבילה", + "bbq_grill": "מנגל", + "amazon": "אמזון", + "usps": "USPS", + "ups": "UPS", + "fedex": "פדקס", + "dhl": "די-אייצ'-אל", + "an_post": "אנפוסט", + "purolator": "פורולטור", + "postnl": "דואר הולנד", + "nzpost": "דואר ניוזילנד", + "postnord": "דואר נורד", + "gls": "G_L_S", + "dpd": "דיפידי", + "tennis_racket": "מחבט טניס", + "pizza": "פיצה", + "donut": "דונאט", + "cake": "עוגה", + "chair": "כיסא", + "couch": "ספה", + "potted_plant": "עציץ", + "bed": "מיטה", + "mirror": "מראה", + "dining_table": "שולחן אוכל", + "window": "חלון", + "desk": "שולחן", + "toilet": "שירותים", + "door": "דלת", + "tv": "טלויזיה", + "cup": "כוס", + "fork": "מזלג", + "knife": "סכין", + "spoon": "כף", + "bowl": "קערה", + "banana": "בננה", + "apple": "תפוח", + "sandwich": "כריך", + "orange": "תפוז", + "broccoli": "ברוקולי", + "carrot": "גזר", + "hot_dog": "נקניקייה", + "laptop": "מחשב נייד", + "remote": "שלט", + "cell_phone": "טלפון נייד", + "microwave": "מיקרוגל", + "oven": "תנור", + "toaster": "טוסטר", + "refrigerator": "מקרר", + "blender": "מערבל", + "book": "ספר", + "clock": "שעון", + "vase": "אגרטל", + "scissors": "מספריים", + "teddy_bear": "דובי", + "hair_dryer": "מייבש שיער", + "toothbrush": "מברשת שיניים", + "hair_brush": "מברשת שיער", + "vehicle": "רכב", + "animal": "חיה", + "squirrel": "סנאי", + "deer": "צבי", + "sink": "כיור" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/configEditor.json new file mode 100644 index 0000000..535619a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "עורך הגדרות - Frigate", + "configEditor": "עורך תצורה", + "copyConfig": "העתקת הגדרות", + "saveAndRestart": "שמירה והפעלה מחדש", + "saveOnly": "שמירה בלבד", + "confirm": "לצאת ללא שמירה?", + "toast": { + "success": { + "copyToClipboard": "התצורה הועתקה ללוח." + }, + "error": { + "savingError": "שגיאה בשמירת ההגדרות" + } + }, + "safeConfigEditor": "עורך תצורה (מצב בטוח)", + "safeModeDescription": "Frigate במצב בטוח עקב שגיאת אימות הגדרות." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/events.json new file mode 100644 index 0000000..fbccbee --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/events.json @@ -0,0 +1,49 @@ +{ + "alerts": "התרעות", + "detections": "גילויים", + "motion": { + "label": "תנועה", + "only": "תנועה בלבד" + }, + "allCameras": "כל המצלמות", + "empty": { + "detection": "אין גילויים לבדיקה", + "alert": "אין התראות להצגה", + "motion": "לא נמצאו נתוני תנועה" + }, + "timeline": "ציר זמן", + "timeline.aria": "בחירת ציר זמן", + "events": { + "label": "אירועים", + "aria": "בחירת אירועים", + "noFoundForTimePeriod": "לא נמצאו אירועים עבור תקופת זמן זו." + }, + "documentTitle": "סקירה - Frigate", + "recordings": { + "documentTitle": "הקלטות - Frigate" + }, + "calendarFilter": { + "last24Hours": "24 שעות אחרונות" + }, + "markAsReviewed": "סימון כנבדק", + "markTheseItemsAsReviewed": "סמן פריטים אלה כנסרקו", + "newReviewItems": { + "label": "הצג פריטי סקירה חדשים", + "button": "פריטים חדשים לסקירה" + }, + "selected_one": "נבחרו {{count}}", + "selected_other": "{{count}} נבחרו", + "camera": "מצלמה", + "detected": "זוהה", + "detail": { + "noDataFound": "אין נתונים מפורטים לבדיקה", + "aria": "הפעלה/כיבוי תצוגת פרטים", + "trackedObject_one": "אובייקט במעקב", + "trackedObject_other": "אובייקטים במעקב", + "noObjectDetailData": "אין נתוני אובייקט זמינים." + }, + "objectTrack": { + "trackedPoint": "נקודה במעקב", + "clickToSeek": "לחץ כדי לחפש את הזמן הזה" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/explore.json new file mode 100644 index 0000000..0646e50 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/explore.json @@ -0,0 +1,209 @@ +{ + "documentTitle": "גלה - פריגטה", + "itemMenu": { + "downloadVideo": { + "label": "הורדת וידיאו", + "aria": "הורדת וידיאו" + }, + "viewObjectLifecycle": { + "label": "צפה בשלבים של האובייקט", + "aria": "הצג את השלבים של האובייקט" + }, + "downloadSnapshot": { + "label": "הורדת לכידת תמונה", + "aria": "הורדת לכידת תמונה" + }, + "findSimilar": { + "label": "מצא דומה", + "aria": "מצא אובייקטים דומים במעקב" + }, + "submitToPlus": { + "label": "שלח ל- +Frigate", + "aria": "שלח ל +Frigate" + }, + "viewInHistory": { + "label": "צפה בהיסטוריה", + "aria": "צפה בהיסטוריה" + }, + "deleteTrackedObject": { + "label": "מחק את אובייקט המעקב הזה" + } + }, + "generativeAI": "Generative - AI", + "exploreMore": "סקור אובייקטים נוספים של {{label}}", + "exploreIsUnavailable": { + "title": "סקירה לא זמינה", + "embeddingsReindexing": { + "context": "ניתן להשתמש בסקירה לאחר סיום בניית המאגר של האובייקטים שזוהו.", + "startingUp": "מתחיל…", + "estimatedTime": "זמן משוער שנותר:", + "finishingShortly": "מסיים בקרוב", + "step": { + "thumbnailsEmbedded": "תמונות ממוזערות מוטמעות: ", + "descriptionsEmbedded": "תיאורים מוטמעים: ", + "trackedObjectsProcessed": "אובייקטים שעובדו לאחר מעקב: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "מודל חוזי", + "visionModelFeatureExtractor": "מחלץ תכונות מודל חוזי", + "textModel": "מודל טקסט", + "textTokenizer": "מפצל טקסט" + }, + "tips": { + "context": "מומלץ לעדכן את המאגר של האובייקטים אחרי שהמודלים ירדו.", + "documentation": "קרא את התיעוד" + }, + "error": "אירעה שגיאה. בדוק את הלוגים.", + "context": "Frigate מורידה את מודלי ההטמעה הדרושים כדי לתמוך בתכונת החיפוש הסמנטי. פעולה זו עשויה להימשך מספר דקות, בהתאם למהירות חיבור הרשת שלך." + } + }, + "trackedObjectDetails": "פרטי אובייקט במעקב", + "type": { + "details": "פרטים", + "snapshot": "לכידת תמונה", + "video": "וידיאו", + "object_lifecycle": "שלבי זיהוי של האובייקט" + }, + "objectLifecycle": { + "title": "שלבי זיהוי של האובייקט", + "noImageFound": "לא נמצאה תמונה עבור חותמת זמן זו.", + "createObjectMask": "יצירת מיסוך אובייקט", + "adjustAnnotationSettings": "שנה את הגדרות הסימון", + "scrollViewTips": "גלול כדי לצפות ברגעים המשמעותיים בשלבים של אובייקט זה.", + "count": "{{first}} מתוך{{second}}", + "trackedPoint": "נקודת מעקב", + "lifecycleItemDesc": { + "visible": "זוהה {{label}}", + "entered_zone": "{{label}} נכנס ל-{{zones}}", + "active": "{{label}} הפך לפעיל", + "stationary": "{{label}} הפך לנייח", + "attribute": { + "faceOrLicense_plate": "זוהה {{attribute}} עבור {{label}}", + "other": "{{label}} זוהה כ-{{attribute}}" + }, + "gone": "{{label}} שמאל", + "heard": "{{label}} נשמעה", + "external": "זוהה {{label}}", + "header": { + "zones": "אזורים", + "ratio": "יחס", + "area": "אזור" + } + }, + "annotationSettings": { + "title": "הגדרות סימון", + "showAllZones": { + "title": "הצג את כל האזורים", + "desc": "הצג תמיד אזורים בפריימים שבהם אובייקטים נכנסו לאזור." + }, + "offset": { + "label": "היסט ההערה", + "documentation": "עיין בתיעוד ", + "toast": { + "success": "קיזוז עבור {{camera}} נשמר בקובץ התצורה. הפעל מחדש את Frigate כדי להחיל את השינויים שלך." + }, + "tips": "טיפ: דמיינו סרטון אירוע שבו אדם הולך משמאל לימין. אם תיבת הגבול של ציר הזמן של האירוע נמצאת באופן עקבי משמאל לאדם, יש להפחית את הערך. באופן דומה, אם אדם הולך משמאל לימין והתיבה התוחמת נמצאת באופן עקבי לפני האדם, יש להגדיל את הערך.", + "millisecondsToOffset": "מספר מילישניות להסטת ההערות שנוצרו מזיהוי ברירת מחדל: 0", + "desc": "נתונים אלה מגיעים משידור הזיהוי של המצלמה שלך, אך הם מונחים על גבי תמונות משידור ההקלטה. לא סביר ששני הזרמים יהיו מסונכרנים לחלוטין. כתוצאה מכך, תיבת הגבול והצילומים לא מסונכרנים בצורה מושלמת. עם זאת, ניתן להשתמש בשדה היסט סימון כדי לסנכרן." + } + }, + "autoTrackingTips": "מיקומי תיבות הסימון לא יהיו מדויקים עבור מצלמות עם מעקב אוטומטי.", + "carousel": { + "previous": "שקופית קודמת", + "next": "שקופית הבאה" + } + }, + "details": { + "timestamp": "חותמת זמן", + "item": { + "tips": { + "mismatch_one": "זוהה אובייקט לא זמין ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "mismatch_two": "זוהו אובייקטים לא זמינים ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "mismatch_other": "זוהו אובייקטים לא זמינים ({{count}}) ונכלל בפריט סקירה זה. אובייקטים אלה לא עמדו בקריטריונים של התראה או זיהוי, או שכבר נוקו/נמחקו.", + "hasMissingObjects": "התאם את התצורה שלך אם ברצונך ש-Frigate ישמור אובייקטים שעוקבים אחריהם עבור התוויות הבאות: {{objects}}" + }, + "button": { + "viewInExplore": "הצג בסקירה", + "share": "שתף פריט זה" + }, + "toast": { + "success": { + "updatedSublabel": "תווית המשנה עודכנה בהצלחה.", + "updatedLPR": "לוחית הרישוי עודכנה בהצלחה.", + "regenerate": "תיאור חדש התבקש מ-{{provider}}. בהתאם למהירות הספק שלך, ייתכן שייקח זמן מה ליצירת התיאור החדש." + }, + "error": { + "regenerate": "ההתקשרות ל-{{provider}} לקבלת תיאור חדש נכשלה: {{errorMessage}}", + "updatedSublabelFailed": "עדכון תווית המשנה נכשל: {{errorMessage}}", + "updatedLPRFailed": "עדכון לוחית הרישוי נכשל: {{errorMessage}}" + } + }, + "title": "סקירת הפריט", + "desc": "סקירת הפריט" + }, + "label": "תווית", + "editSubLabel": { + "title": "עריכת תווית משנה", + "desc": "הזן תווית משנה חדשה עבור {{label}}", + "descNoLabel": "הזן תווית משנה חדשה עבור אובייקט המעקב" + }, + "editLPR": { + "title": "עריכת לוחית זיהוי", + "desc": "הזן ערך לוחית רישוי חדשה עבור {{label}}", + "descNoLabel": "הזן ערך לוחית רישוי חדשה עבור האובייקט במעקב" + }, + "snapshotScore": { + "label": "ציון לכידת תמונה" + }, + "topScore": { + "label": "ציון גבוה", + "info": "הציון הגבוה ביותר הוא הציון החציוני הגבוה ביותר עבור האובייקט במעקב, כך שהוא עשוי להיות שונה מהציון המוצג בתמונה הממוזערת של תוצאת החיפוש." + }, + "recognizedLicensePlate": "לוחית רישוי מוכרת", + "estimatedSpeed": "מהירות משוערת", + "objects": "אובייקט", + "camera": "מצלמה", + "button": { + "findSimilar": "מצא דומה", + "regenerate": { + "title": "צור מחדש", + "label": "צור מחדש את תיאור אובייקט המעקב" + } + }, + "description": { + "label": "תיאור", + "placeholder": "תיאור האובייקט במעקב", + "aiTips": "Frigate לא תבקש תיאור מספק הבינה המלאכותית הגנרטיבית שלך עד לסיום כל השלבים של האובייקט במעקב." + }, + "expandRegenerationMenu": "פתח את תפריט היצירה מחדש", + "regenerateFromSnapshot": "צור מחדש מלכידת התמונה", + "regenerateFromThumbnails": "צור מחדש מתמונות ממוזערות", + "tips": { + "descriptionSaved": "התיאור נשמר בהצלחה", + "saveDescriptionFailed": "עדכון התיאור נכשל: {{errorMessage}}" + }, + "zones": "אזורים" + }, + "dialog": { + "confirmDelete": { + "title": "אישור מחיקה", + "desc": "מחיקת אובייקט זה במעקב מסירה את תמונת המצב, כל ההטמעות שנשמרו וכל ערכי שלבי האובייקט המשויכים. קטעי וידאו מוקלטים של אובייקט זה במעקב בתצוגת היסטוריה לא יימחקו.

    האם אתה בטוח שברצונך להמשיך?" + } + }, + "searchResult": { + "tooltip": "תואם ל-{{type}} ב-{{confidence}}%", + "deleteTrackedObject": { + "toast": { + "error": "מחיקת האובייקט במעקב נכשלה: {{errorMessage}}", + "success": "האובייקט המעקב נמחק בהצלחה." + } + } + }, + "noTrackedObjects": "לא נמצאו אובייקטים במעקב", + "fetchingTrackedObjectsFailed": "שגיאה באחזור אובייקטים במעקב: {{errorMessage}}", + "trackedObjectsCount_one": "אובייקט במעקב ({{count}}) ", + "trackedObjectsCount_two": "אובייקטים במעקב ({{count}}) ", + "trackedObjectsCount_other": "אובייקטים במעקב ({{count}}) " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/exports.json new file mode 100644 index 0000000..93e26a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/exports.json @@ -0,0 +1,17 @@ +{ + "search": "חיפוש", + "toast": { + "error": { + "renameExportFailed": "שינוי שם הייצוא נכשל: {{errorMessage}}" + } + }, + "documentTitle": "ייצוא - Frigate", + "noExports": "לא נמצא יצוא", + "deleteExport": "מחיקת ייצוא", + "deleteExport.desc": "האם אתה בטוח שברצונך למחוק את {{exportName}}?", + "editExport": { + "title": "שנה שם ייצוא", + "desc": "הכנס שם חדש עבור הייצוא הזה.", + "saveExport": "שמירת ייצוא" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/faceLibrary.json new file mode 100644 index 0000000..96b2481 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "description": { + "addFace": "עיין בהוספת אוסף חדש לספריית הפנים.", + "placeholder": "הזנת שם לאוסף זה", + "invalidName": "שם לא חוקי. שמות יכולים לכלול רק אותיות, מספרים, רווחים, גרשים, קווים תחתונים ומקפים." + }, + "createFaceLibrary": { + "nextSteps": "כדי לבנות בסיס חזק:
  • השתמשו בכרטיסייה 'אימון' כדי לבחור ולאמן תמונות עבור כל אדם שזוהה.
  • התמקדו בתמונות ישירות לקבלת התוצאות הטובות ביותר; הימנעו מאימון תמונות שלוכדות פנים בזווית.
  • ", + "title": "יצירת אוסף", + "desc": "יצירת אוסף חדש", + "new": "יצירת פנים חדשות" + }, + "toast": { + "success": { + "deletedName_one": "פנים עבור {{count}} נמחקו בהצלחה.", + "deletedName_two": "פנים של {{count}} נמחקו בהצלחה.", + "deletedName_other": "פנים של {{count}} נמחקו בהצלחה.", + "deletedFace_one": "נמחק בהצלחה {{count}} פנים.", + "deletedFace_two": "נמחקו בהצלחה {{count}} פנים.", + "deletedFace_other": "נמחקו בהצלחה {{count}} פנים.", + "uploadedImage": "התמונה הועלתה בהצלחה.", + "addFaceLibrary": "{{name}} נוסף בהצלחה לספריית הפנים!", + "renamedFace": "שם הפנים שונה בהצלחה ל-{{name}}", + "trainedFace": "פנים אומנו בהצלחה.", + "updatedFaceScore": "ציון הפנים עודכן בהצלחה." + }, + "error": { + "deleteFaceFailed": "המחיקה נכשלה: {{errorMessage}}", + "deleteNameFailed": "מחיקת השם: {{errorMessage}} נכשלה", + "renameFaceFailed": "שינוי שם הפנים נכשל: {{errorMessage}}", + "trainFailed": "אימון הפנים נכשל: {{errorMessage}}", + "updateFaceScoreFailed": "עדכון ציון הפנים נכשל: {{errorMessage}}", + "uploadingImageFailed": "העלאת התמונה נכשלה: {{errorMessage}}", + "addFaceLibraryFailed": "הגדרת שם הפנים נכשלה: {{errorMessage}}" + } + }, + "details": { + "person": "אדם", + "subLabelScore": "ציון תווית משנה", + "face": "פרטי פנים", + "faceDesc": "פרטי האובייקט שגרם לזיהוי הפנים הזה", + "timestamp": "חותמת זמן", + "unknown": "לא ידוע", + "scoreInfo": "ציון תווית המשנה הוא הציון המשוקלל עבור כל זיהוי הפנים המזוהים, כך שהוא עשוי להיות שונה מהציון המוצג בתמונה." + }, + "documentTitle": "ספריית זיהו פנים - Frigate", + "uploadFaceImage": { + "title": "העלאת תמונת פנים", + "desc": "העלה תמונה לסריקה לאיתור פנים והכללה {{pageToggle}}" + }, + "collections": "אוספים", + "steps": { + "faceName": "קביעת שם לפנים", + "uploadFace": "העלאת תמונת פנים", + "nextSteps": "צעדים הבאים", + "description": { + "uploadFace": "העלה תמונה של {{name}} המציגה את פניו מזווית חזית. אין צורך לחתוך את התמונה רק לפנים שלו." + } + }, + "train": { + "title": "רכבת", + "aria": "בחירת אימון", + "empty": "אין ניסיונות זיהוי פנים אחרונים" + }, + "selectItem": "בחירה:{{item}}", + "selectFace": "בחירת פנים", + "deleteFaceLibrary": { + "title": "מחיקת שם", + "desc": "האם אתה בטוח שברצונך למחוק את האוסף {{name}}? פעולה זו תמחק לצמיתות את כל הפרצופים המשויכים." + }, + "deleteFaceAttempts": { + "title": "מחיקת פנים", + "desc_one": "האם אתה בטוח שברצונך למחוק את הפנים עבור {{count}}? פעולה זו אינה ניתנת לביטול.", + "desc_two": "האם אתה בטוח שברצונך למחוק את הפנים של {{count}}? פעולה זו אינה ניתנת לביטול.", + "desc_other": "האם אתה בטוח שברצונך למחוק את הפנים של {{count}}? פעולה זו אינה ניתנת לביטול." + }, + "renameFace": { + "title": "שינוי שם של פנים", + "desc": "הזנת שם חדש עבור {{name}}" + }, + "button": { + "deleteFaceAttempts": "מחיקת פנים", + "addFace": "הוסף פנים", + "renameFace": "שנה שם פנים", + "deleteFace": "מחיקת פנים", + "uploadImage": "העלאת תמונה", + "reprocessFace": "עיבוד מחדש של הפנים" + }, + "imageEntry": { + "validation": { + "selectImage": "בחירת קובץ תמונה." + }, + "dropActive": "שחרר/י את התמונה כאן…", + "dropInstructions": "גרור ושחרר תמונה כאן, או לחץ כדי לבחור", + "maxSize": "גודל מקסימאלי: {{size}}MB" + }, + "nofaces": "אין פנים זמינים", + "pixels": "{{area}}פיקסלים", + "readTheDocs": "עיין בתיעוד", + "trainFaceAs": "אימון פנים כ:", + "trainFace": "אימון פנים" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/live.json new file mode 100644 index 0000000..7b7c535 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/live.json @@ -0,0 +1,166 @@ +{ + "manualRecording": { + "title": "הקלטה לפי דרישה", + "tips": "התחלת אירוע הקלטה ידני המבוסס על הגדרות שמירת ההקלטה של מצלמה זו.", + "playInBackground": { + "label": "ניגון ברקע", + "desc": "הפעל אפשרות זו כדי להמשיך להזרים גם כאשר הנגן מוסתר." + }, + "showStats": { + "label": "הצג סטטיסטיקות", + "desc": "הפעל אפשרות זו כדי להציג סטטיסטיקות שידור כשכבת-על על פיד המצלמה." + }, + "debugView": "תצוגת ניפוי שגיאות", + "start": "התחלת הקלטה לפי דרישה", + "started": "התחילה הקלטה ידנית לפי דרישה.", + "failedToStart": "התחלת הקלטה ידנית לפי דרישה נכשלה.", + "recordDisabledTips": "מכיוון שההקלטה מושבתת או מוגבלת בתצורה של מצלמה זו, רק לכידת תמונה תישמר.", + "end": "סיום הקלטה לפי דרישה", + "ended": "הקלטה ידנית לפי דרישה הסתיימה.", + "failedToEnd": "סיום ההקלטה הידנית לפי דרישה נכשל." + }, + "documentTitle": "שידור חי - Frigate", + "documentTitle.withCamera": "{{camera}} - שידור חי- Frigate", + "lowBandwidthMode": "מצב רוחב פס נמוך", + "twoWayTalk": { + "enable": "אפשור דיבור דו כיווני", + "disable": "ביטול דיבור דו כיווני" + }, + "cameraAudio": { + "enable": "אפשור קול מהמצלמה", + "disable": "ביטול קול מהמצלמה" + }, + "ptz": { + "move": { + "clickMove": { + "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה", + "enable": "הפעל לחיצה לתנועה", + "disable": "ביטול לחיצה לתנועה" + }, + "up": { + "label": "הזזת מצלמה ממונעת למעלה" + }, + "down": { + "label": "הזזת מצלמה ממונעת למטה" + }, + "right": { + "label": "הזזת מצלמה ממונעת ימינה" + }, + "left": { + "label": "הזזת מצלמה ממונעת לשמאל" + } + }, + "zoom": { + "in": { + "label": "מצלמה ממונעת זום פנימה" + }, + "out": { + "label": "מצלמה ממונעת זום החוצה" + } + }, + "frame": { + "center": { + "label": "לחץ בתוך המסגרת כדי למרכז את המצלמה הממונעת" + } + }, + "presets": "מצלמה ממונעת - פריסטים", + "focus": { + "in": { + "label": "כניסת פוקוס מצלמת PTZ" + }, + "out": { + "label": "יציאת פוקוס מצלמת PTZ" + } + } + }, + "camera": { + "enable": "אפשור מצלמה", + "disable": "השבתת מצלמה" + }, + "muteCameras": { + "enable": "השתק את כל המצלמות", + "disable": "ביטול השתקה לכל המצלמות" + }, + "detect": { + "enable": "אפשור זיהוי", + "disable": "השבתת גילוי" + }, + "recording": { + "enable": "אפשור הקלטה", + "disable": "השבתת הקלטה" + }, + "snapshots": { + "enable": "אפשור לכידת תמונה", + "disable": "השבתת לכידת תמונה" + }, + "audioDetect": { + "disable": "השבתת זיהוי קול", + "enable": "הפעלת זיהוי שמע" + }, + "autotracking": { + "enable": "אפשור מעקב אוטומטי", + "disable": "השבתת מעקב אוטומטי" + }, + "streamStats": { + "enable": "הצג סטטיסטיקות שידור", + "disable": "הסתרת סטטיסטיקות שידור" + }, + "stream": { + "twoWayTalk": { + "tips.documentation": "קרא את התיעוד ", + "available": "שיחה דו-כיוונית זמינה עבור שידור זה", + "tips": "המכשיר שלך חייב לתמוך בתכונה ו-WebRTC חייב להיות מוגדר לתקשורת דו-כיוונית.", + "unavailable": "שיחה דו-כיוונית אינה זמינה עבור שידור זה" + }, + "lowBandwidth": { + "resetStream": "איפוס זרם", + "tips": "התצוגה החיה נמצאת במצב של רוחב פס נמוך עקב שגיאות אחסון במאגר או שידור." + }, + "playInBackground": { + "label": "נגן ברקע", + "tips": "הפעל אפשרות זו כדי להמשיך להזרים כאשר הנגן מוסתר." + }, + "title": "שידור", + "audio": { + "tips": { + "title": "יש להפיק קול מהמצלמה שלך ולהגדיר אותו ב-go2rtc עבור שידור זה.", + "documentation": "עיין בתיעוד " + }, + "available": "קול זמין עבור שידור זה", + "unavailable": "קול אינו זמין עבור שידור זה" + } + }, + "cameraSettings": { + "title": "{{camera}} הגדרות", + "cameraEnabled": "המצלמה מאופשרת", + "objectDetection": "זיהוי אובייקטים", + "recording": "הקלטה", + "snapshots": "לכידת תמונה", + "audioDetection": "זיהוי קול", + "autotracking": "מעקב אוטומטי" + }, + "streamingSettings": "הגדרות שידור", + "notifications": "התראות", + "audio": "קול", + "suspend": { + "forTime": "השעיה עבור: " + }, + "history": { + "label": "הצג קטעים היסטוריים" + }, + "effectiveRetainMode": { + "modes": { + "all": "הכל", + "motion": "תנועה", + "active_objects": "אובייקטים פעילים" + }, + "notAllTips": "תצורת שמירת ההקלטה שלך ב-{{source}} מוגדרת ל-מצב: {{effectiveRetainMode}}, כך שהקלטה לפי דרישה זו תשמור רק מקטעים עם {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "עריכת פריסת תצוגה", + "group": { + "label": "עריכת קבוצת מצלמות" + }, + "exitEdit": "יציאה מעריכה" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/recording.json new file mode 100644 index 0000000..1e45f6b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "מסנן", + "export": "ייצוא", + "calendar": "לוח שנה", + "filters": "מסננים", + "toast": { + "error": { + "noValidTimeSelected": "לא נבחר טווח זמן תקף", + "endTimeMustAfterStartTime": "שעת הסיום חייבת להיות אחרי שעת ההתחלה" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/search.json new file mode 100644 index 0000000..0865e46 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "חיפוש", + "savedSearches": "חיפושים שמורים", + "searchFor": "חפש את{{inputValue}}", + "button": { + "clear": "ניקוי חיפוש", + "save": "שמירת החיפוש", + "delete": "מחיקת חיפוש שמור", + "filterInformation": "סינון מידע", + "filterActive": "מסננים פעילים" + }, + "trackedObjectId": "מזהה אובייקט במעקב", + "filter": { + "label": { + "cameras": "מצלמות", + "labels": "תוויות", + "zones": "אזורים", + "sub_labels": "תוויות משנה", + "search_type": "סוג חיפוש", + "time_range": "טווח זמן", + "before": "לפני", + "after": "אחרי", + "min_score": "ציון מינימום", + "max_speed": "מהירות מקסימאלית", + "max_score": "ציון מקסימאלי", + "min_speed": "מהירות מינמאלית", + "recognized_license_plate": "לוחית רישוי מוכרת", + "has_clip": "קיים סרטון קליפ", + "has_snapshot": "לכידת תמונה קיימת" + }, + "searchType": { + "thumbnail": "תמונה ממוזערת", + "description": "תיאור" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "תאריך ה\"לפני\" חייב להיות מאוחר מתאריך ה\"אחרי\".", + "afterDatebeEarlierBefore": "תאריך ה'אחרי' חייב להיות מוקדם יותר מתאריך ה'לפני'.", + "minScoreMustBeLessOrEqualMaxScore": "ה-'min_score' חייב להיות קטן או שווה ל-'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "ה-'max_score' חייב להיות גדול או שווה ל-'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "ה-'min_speed' חייב להיות קטן או שווה ל-'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "ה-'max_speed' חייב להיות גדול או שווה ל-'min_speed'." + } + }, + "tips": { + "title": "כיצד להשתמש במסנני טקסט", + "desc": { + "text": "מסננים עוזרים לך לצמצם את תוצאות החיפוש שלך. כך תוכל להשתמש בהם בשדה הקלט:", + "step1": "הקלד שם של מפתח סינון ואחריו נקודתיים (לדוגמה, \"מצלמות:\").", + "step2": "בחר ערך מההצעות או הקלד ערך משלך.", + "step3": "השתמשו במספר מסננים על ידי הוספתם אחד אחרי השני עם רווח ביניהם.", + "step6": "הסר מסננים על ידי לחיצה על ה-'x' שלידם.", + "step4": "מסנני תאריך (לפני: ואחרי:) משתמשים בפורמט {{DateFormat}}.", + "step5": "מסנן טווח הזמן משתמש בפורמט {{exampleTime}}.", + "exampleLabel": "דוּגמָה:" + } + }, + "header": { + "currentFilterType": "סנן ערכים", + "noFilters": "מסננים", + "activeFilters": "מסננים פעילים" + } + }, + "similaritySearch": { + "title": "חיפוש פריטים דומים", + "active": "חיפוש דומה פעיל", + "clear": "נקה חיפוש דומה" + }, + "placeholder": { + "search": "חיפוש…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/settings.json new file mode 100644 index 0000000..e0737aa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/settings.json @@ -0,0 +1,628 @@ +{ + "camera": { + "reviewClassification": { + "zoneObjectDetectionsTips": { + "notSelectDetections": "כל האובייקטים של {{detectionsLabels}} שזוהו ב-{{zone}} ב-{{cameraName}} שלא סווגו כהתראות יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים.", + "text": "כל האובייקטים של {{detectionsLabels}} שאינם מסווגים ב-{{zone}} ב-{{cameraName}} יוצגו כזיהויים.", + "regardlessOfZoneObjectDetectionsTips": "כל האובייקטים של {{detectionsLabels}} שלא סווגו ב-{{cameraName}} יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים." + }, + "objectAlertsTips": "כל האובייקטים של {{alertsLabels}} ב-{{cameraName}} יוצגו כהתראות.", + "objectDetectionsTips": "כל האובייקטים של {{detectionsLabels}} שלא סווגו ב-{{cameraName}} יוצגו כזיהויים ללא קשר לאזור בו הם נמצאים.", + "noDefinedZones": "בחר ערך מההצעות או הקלד ערך משלך.", + "zoneObjectAlertsTips": "כל האובייקטים של {{alertsLabels}} שזוהו ב-{{zone}} ב-{{cameraName}} יוצגו כהתראות.", + "unsavedChanges": "הגדרות סיווג סקירה שלא נשמרו עבור {{camera}}", + "selectAlertsZones": "בחירת אזורים להתראות", + "selectDetectionsZones": "בחירת אזורים לגילוי", + "limitDetections": "הגבלת הזיהוי לאזורים ספציפיים", + "toast": { + "success": "הגדרת סיווג נשמרה. יש להפעיל מחדש את Frigate כדי שהשינויים ייכנסו לתוקף." + }, + "title": "סיווג סקירה", + "readTheDocumentation": "עיין בתיעוד", + "desc": "Frigate מסווגת פריטי סקירה כהתראות וגילויים. כברירת מחדל, כל האובייקטים של אנשים ומכוניות נחשבים כהתראות. ניתן למקד את הקטגוריה של פריטי הסקירה על ידי הגדרת אזורים נדרשים עבורם." + }, + "title": "הגדרות מצלמה", + "streams": { + "title": "שידורים", + "desc": "השבתת מצלמה עוצרת לחלוטין את עיבוד הזרמים של מצלמה זו על ידי Frigate. זיהוי, הקלטה וניפוי שגיאות לא יהיו זמינים.
    הערה: פעולה זו אינה מבטלת זרמים חוזרים של go2rtc." + }, + "review": { + "title": "סקירה", + "desc": "הפעלה/השבתה של התראות וזיהויים עבור מצלמה זו. כאשר ההגדרה מושבתת, לא ייווצרו פריטי סקירה חדשים. ", + "alerts": "התרעות. ", + "detections": "גילויים. " + } + }, + "masksAndZones": { + "form": { + "inertia": { + "error": { + "mustBeAboveZero": "ההתמדה חייבת להיות מעל 0." + } + }, + "zoneName": { + "error": { + "alreadyExists": "אובייקט המעקב נמחק בהצלחה.", + "mustBeAtLeastTwoCharacters": "שם האזור חייב להיות באורך של לפחות 2 תווים.", + "mustNotBeSameWithCamera": "שם האזור לא חייב להיות זהה לשם המצלמה.", + "mustNotContainPeriod": "שם האזור אינו יכול להכיל נקודות.", + "hasIllegalCharacter": "שם האזור מכיל תווים לא חוקיים." + } + }, + "distance": { + "error": { + "text": "המרחק חייב להיות גדול או שווה ל-0.1.", + "mustBeFilled": "יש למלא את כל שדות המרחק כדי להשתמש בהערכת מהירות." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "זמן ההשהיה חייב להיות גדול או שווה ל-0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "סף המהירות חייב להיות גדול או שווה ל-0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "הסר את הנקודה האחרונה", + "reset": { + "label": "ניקוי כל הנקודות" + }, + "snapPoints": { + "true": "נקודות הצמדה", + "false": "אל תצמיד נקודות" + }, + "delete": { + "title": "אישור מחיקה", + "desc": "האם אתה בטוח שברצונך למחוק את ה-{{type}} {{name}}?", + "success": "{{name}} נמחק." + }, + "error": { + "mustBeFinished": "יש לסיים את ציור הפוליגון לפני השמירה." + } + } + }, + "filter": { + "all": "כל המיסוכים והאזורים" + }, + "restart_required": "נדרשת הפעלה מחדש (מיסוך/אזורים שונו)", + "toast": { + "success": { + "copyCoordinates": "הקואורדינטות עבור {{polyName}} הועתקו ללוח." + }, + "error": { + "copyCoordinatesFailed": "לא ניתן היה להעתיק קואורדינטות ללוח." + } + }, + "motionMaskLabel": "מיסוך תנועה {{number}}", + "objectMaskLabel": "מיסוך אובייקט {{number}} ({{label}})", + "zones": { + "label": "אזורים.", + "documentTitle": "עריכת אזור - Frigate", + "desc": { + "title": "אזורים מאפשרים לך להגדיר אזור ספציפי בפריים, כך שתוכל לקבוע האם אובייקט נמצא בתוך אזור מסוים או לא.", + "documentation": "תיעוד" + }, + "add": "הוספת אזור", + "edit": "עריכת אזור", + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "name": { + "title": "שם", + "inputPlaceHolder": "הזן שם…", + "tips": "השם חייב להיות באורך של לפחות 2 תווים ואינו יכול להיות שם של מצלמה או אזור אחר." + }, + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "inertia": { + "title": "אינרציה", + "desc": "מציין כמה מסגרות אובייקט חייבות להיות באזור לפני שהוא נחשב באזור. ברירת מחדל: 3" + }, + "loiteringTime": { + "title": "זמן שיטוט", + "desc": "קובע את משך הזמן המינימלי בשניות שהאובייקט חייב להיות באזור כדי שיופעל. ברירת מחדל: 0" + }, + "objects": { + "title": "אובייקט", + "desc": "רשימת אובייקטים החלים על אזור זה." + }, + "speedEstimation": { + "title": "הערכת מהירות", + "docs": "עיין בתיעוד", + "lineADistance": "מרחק קו A ({{unit}})", + "lineBDistance": "מרחק קו B ({{unit}})", + "lineCDistance": "מרחק קו C ({{unit}})", + "lineDDistance": "מרחק קו D ({{unit}})", + "desc": "הפעל הערכת מהירות עבור אובייקטים באזור זה. האזור חייב לכלול בדיוק 4 נקודות." + }, + "speedThreshold": { + "title": "סף מהירות ({{unit}})", + "desc": "מציין מהירות מינימלית עבור אובייקטים שיש לקחת בחשבון באזור זה.", + "toast": { + "error": { + "loiteringTimeError": "אזורים עם זמן שהייה גדול מ־0 לא אמורים לשמש להערכת מהירות.", + "pointLengthError": "הערכת מהירות הושבתה עבור אזור זה. אזורים עם הערכת מהירות חייבים לכלול בדיוק 4 נקודות." + } + } + }, + "toast": { + "success": "האזור ({{zoneName}}) נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + }, + "allObjects": "כל האובייקטים" + }, + "motionMasks": { + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "label": "מיסוך תנועה.", + "documentTitle": "עריכת מיסוך תנועה - Frigate", + "desc": { + "documentation": "תיעוד", + "title": "מיסוך תנועה משמש כדי למנוע זיהוי של תנועה לא רצויה. מיסוך יתר יקשה על מעקב אחר אובייקטים." + }, + "add": "מיסוך תנועה חדש", + "edit": "עריכת מיסוך תנועה", + "context": { + "documentation": "עיין בתיעוד", + "title": "מיסוך תנועה משמש כדי למנוע זיהוי מסוגים לא רצויים של תנועה (לדוגמה: ענפי עצים, חותמות זמן של מצלמה). יש להשתמש במיסוך התנועה בזהירות רבה, מיסוך יתר יקשה על מעקב אחר אובייקטים." + }, + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "polygonAreaTooLarge": { + "title": "מיסוך התנועה מכסה {{polygonArea}}% ממסגרת המצלמה. מיסוך תנועה גדול אינו מומלץ.", + "tips": "מיסוך תנועה אינו מונע זיהוי של אובייקטים. עליך להשתמש באזור נדרש במקום זאת.", + "documentation": "עיין בתיעוד" + }, + "toast": { + "success": { + "title": "{{polygonName}} נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים.", + "noName": "מיסוך התנועה נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + } + } + }, + "objectMasks": { + "point_one": "נקודה {{count}}", + "point_two": "נקודות {{count}}", + "point_other": "נקודות {{count}}", + "label": "מיסוך אובייקט", + "documentTitle": "עריכת מיסוך אובייקט - Frigate", + "desc": { + "title": "מיסוך סינון אובייקטים משמש לסינון תוצאות חיוביות שגויות עבור סוג אובייקט נתון בהתבסס על מיקום.", + "documentation": "תיעוד" + }, + "add": "הוספת מיסוך אובייקט", + "edit": "עריכת מיסוך אובייקט", + "context": "מיסוך סינון אובייקטים משמש לסינון תוצאות חיוביות שגויות עבור סוג אובייקט נתון בהתבסס על מיקום.", + "clickDrawPolygon": "לחץ כדי לצייר פוליגון על התמונה.", + "objects": { + "title": "אובייקטים", + "desc": "סוג האובייקט שאליו מתייחס מיסוך האובייקט הזה.", + "allObjectTypes": "כל סוגי האובייקטים" + }, + "toast": { + "success": { + "title": "{{polygonName}} נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים.", + "noName": "מיסוך האובייקט נשמר. הפעל מחדש את Frigate כדי להחיל את השינויים." + } + } + } + }, + "general": { + "recordingsViewer": { + "defaultPlaybackRate": { + "desc": "קצב ניגון ברירת מחדל עבור ניגון הקלטות.", + "label": "קצב השמעה ברירת המחדל" + }, + "title": "מציג הקלטות" + }, + "liveDashboard": { + "automaticLiveView": { + "label": "צפיה בשידור חי אוטומטית", + "desc": "מעבר אוטומטי לתצוגה חיה של מצלמה כאשר מזוהה פעילות. השבתת אפשרות זו גורמת לתמונות סטטיות של המצלמה בדשבורד שידור חי להתעדכן רק פעם בדקה." + }, + "title": "דשבורד שידור חי", + "playAlertVideos": { + "label": "ניגון סרטוני התראות", + "desc": "כברירת מחדל, התראות אחרונות בדשבורד שידור חי מופעלות כסרטונים קצרים בלולאה. השבת אפשרות זו כדי להציג רק תמונה סטטית של התראות אחרונות במכשיר/דפדפן זה." + } + }, + "cameraGroupStreaming": { + "desc": "הגדרות שידור עבור כל קבוצת מצלמות נשמרות באחסון המקומי של הדפדפן שלך.", + "title": "הגדרות הזרמת קבוצת מצלמות", + "clearAll": "נקה את כל הגדרות השידור" + }, + "title": "הגדרות כלליות", + "storedLayouts": { + "title": "פריסות תצוגה שמורות", + "desc": "ניתן לגרור/לשנות את גודל הפריסה של המצלמות בקבוצת מצלמות. המיקומים נשמרים באחסון המקומי של הדפדפן שלך.", + "clearAll": "נקה את כל פריסות התצוגות" + }, + "calendar": { + "title": "לוח שנה", + "firstWeekday": { + "label": "היום הראשון בשבוע", + "sunday": "ראשון", + "monday": "שני", + "desc": "היום שבו מתחיל השבוע בלוח הסקירות." + } + }, + "toast": { + "success": { + "clearStoredLayout": "ניקוי הפריסה השמורה עבור {{cameraName}}", + "clearStreamingSettings": "ניקוי הגדרות השידור עבור כל קבוצות המצלמות בוצע." + }, + "error": { + "clearStoredLayoutFailed": "ניקוי פריסת התצוגה השמורה נכשל: {{errorMessage}}", + "clearStreamingSettingsFailed": "ניקוי הגדרות השידור נכשל: {{errorMessage}}" + } + } + }, + "documentTitle": { + "camera": "הגדרת מצלמה - Frigate", + "enrichments": "הגדרות העשרה - Frigate", + "masksAndZones": "עריכת מיסוך אזור - Frigate", + "motionTuner": "כיול תנועה - Frigate", + "object": "ניפוי שגיאות - Frigate", + "frigatePlus": "הגדרות +Frigate - Frigate", + "notifications": "הגדרת התראות - Frigate", + "authentication": "הגדרות אימות - Frigate", + "default": "הגדרות - Frigate", + "general": "הגדרות כלליות - Frigate", + "cameraManagement": "ניהול מצלמות - Frigate", + "cameraReview": "הגדרות סקירת מצלמה - Frigate" + }, + "menu": { + "ui": "UI - ממשק משתמש", + "cameras": "הגדרות מצלמה", + "masksAndZones": "מיסוך / אזורים", + "motionTuner": "כיול תנועה", + "debug": "ניפוי שגיאות", + "users": "משתמשים", + "notifications": "התראות", + "frigateplus": "+Frigate", + "enrichments": "תוספות", + "triggers": "הפעלות", + "cameraManagement": "ניהול", + "cameraReview": "סְקִירָה", + "roles": "תפקידים" + }, + "dialog": { + "unsavedChanges": { + "title": "ישנם שינויים שלא נשמרו.", + "desc": "האם ברצונך לשמור את השינויים לפני שתמשיך?" + } + }, + "cameraSetting": { + "camera": "מצלמה", + "noCamera": "אין מצלמה" + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmButton": "ביצוע אינדקס מחדש", + "success": "אינדקס מחדש הופעל בהצלחה.", + "error": "התחלת האינדקס מחדש נכשלה: {{errorMessage}}", + "confirmDesc": "האם אתה בטוח שברצונך לבצע אינדקס מחדש להטמעות האובייקטים במעקב? תהליך זה יפעל ברקע אך הוא עשוי להפעיל את המעבד שלך בצורה מקסימלית ועלול לקחת זמן רב. תוכל לצפות בהתקדמות בדף החיפוש.", + "alreadyInProgress": "אינדקס מחדש כבר פעיל כרגע.", + "label": "בצע אינדקס מחדש כעת", + "confirmTitle": "אישור אינדקס מחדש", + "desc": "אינדקס מחדש ייצור מחדש הטמעות עבור כל האובייקטים שבמעקב. תהליך זה פועל ברקע ועשוי למקסם את עוצמת המעבד שלך ולקחת זמן רב בהתאם למספר האובייקטים במעקב שיש לך." + }, + "modelSize": { + "label": "גודל מודל", + "small": { + "title": "קטן", + "desc": "שימוש ב-קטן משתמש בגרסה כמותית של המודל המשתמשת בפחות זיכרון RAM ופועלת מהר יותר על המעבד עם הבדל זניח מאוד באיכות ההטמעה." + }, + "large": { + "title": "גדול", + "desc": "שימוש ב-גדול מפעיל את מודל Jina המלא ויפעל אוטומטית על ה-GPU במידת הצורך." + }, + "desc": "גודל המודל המשמש להטמעות חיפוש סמנטי." + }, + "title": "חיפוש סמנטי", + "desc": "חיפוש סמנטי ב-Frigate מאפשר לך למצוא אובייקטים שעוקבים אחריהם בתוך פריטי הסקירה שלך באמצעות התמונה עצמה, תיאור טקסטואלי שהוגדר על ידי המשתמש, או תיאור שנוצר אוטומטית.", + "readTheDocumentation": "עיין בתיעוד" + }, + "faceRecognition": { + "title": "זיהוי פנים", + "desc": "זיהוי פנים מאפשר להקצות לאנשים שמות, וכאשר פנים מזוהות, Frigate תקצה את שמו של האדם כתווית משנה. מידע זה כלול בממשק המשתמש, במסננים וגם בהתראות.", + "readTheDocumentation": "עיין בתיעוד", + "modelSize": { + "label": "גודל מודל", + "desc": "גודל המודל המשמש לזיהוי פנים.", + "small": { + "title": "קטן", + "desc": "שימוש ב-קטן משתמש במודל הטמעת פנים של FaceNet שפועל ביעילות על רוב המעבדים." + }, + "large": { + "title": "גדול", + "desc": "שימוש ב-גדול משתמש במודל הטמעת פנים של ArcFace ויפעל אוטומטית על גבי ה-GPU במידת הצורך." + } + } + }, + "title": "הגדרות העשרה", + "unsavedChanges": "שינויים בהגדרות העשרה לא נשמרו", + "birdClassification": { + "title": "סיווג ציפורים", + "desc": "סיווג ציפורים מזהה ציפורים ידועות באמצעות מודל Tensorflow כמותי. כאשר ציפור ידועה מזוהה, שמה הנפוץ יתווסף כתווית משנה. מידע זה כלול בממשק המשתמש, במסננים וכן בהתראות." + }, + "licensePlateRecognition": { + "title": "זיהוי לוחיות רישוי", + "readTheDocumentation": "עיין בתיעוד", + "desc": "Frigate יכולה לזהות לוחיות רישוי על כלי רכב ולהוסיף אוטומטית את התווים שזוהו לשדה לוחית רישוי מזוהה או לשם ידוע כתווית משנה לאובייקטים מסוג מכונית. מקרה שימוש נפוץ עשוי להיות קריאת לוחיות הרישוי של מכוניות שנכנסות לחניה או מכוניות שעוברות ברחוב." + }, + "restart_required": "נדרש אתחול (הגדרות ההעשרה שונו)", + "toast": { + "success": "הגדרות העשרה נשמרו. הפעל מחדש את Frigate כדי להחיל את השינויים שלך.", + "error": "שמירת שינויי ההגדרות נכשלה: {{errorMessage}}" + } + }, + "motionDetectionTuner": { + "title": "כיול גילוי תנועה", + "unsavedChanges": "שינויים שלא נשמרו בכיול התנועה ({{camera}})", + "desc": { + "documentation": "עיין במדריך כיול התנועה", + "title": "Frigate משתמשת בזיהוי תנועה כבדיקה ראשונה כדי לראות אם קורה משהו בפריים שכדאי לבדוק עם זיהוי עצמים." + }, + "Threshold": { + "title": "סף", + "desc": "ערך הסף מכתיב כמה שינוי נדרש בבהירות של פיקסל כדי להיחשב כתנועה. ברירת מחדל: 30" + }, + "contourArea": { + "title": "אזור קונטור", + "desc": "ערך שטח המתאר משמש לקביעת אילו קבוצות של פיקסלים שהשתנו ייחשבו כתנועה. ברירת מחדל: 10" + }, + "improveContrast": { + "title": "שיפור ניגודיות", + "desc": "שפר את הניגודיות עבור סצנות כהות יותר. ברירת מחדל: מופעל" + }, + "toast": { + "success": "הגדרות התנועה נשמרו." + } + }, + "debug": { + "title": "ניפוי שגיאות.", + "detectorDesc": "Frigate משתמשת בגלאים שלך ({{detectors}}) כדי לזהות אובייקטים בזרם הווידאו של המצלמה שלך.", + "debugging": "ניפוי שגיאות", + "objectList": "רשימת אובייקטים", + "noObjects": "אין אובייקטים", + "boundingBoxes": { + "title": "תיבות זיהוי", + "desc": "הצגת תיבות זיהוי סביב אובייקטים במעקב", + "colors": { + "label": "צבעי תיבת זיהוי של אובייקטים", + "info": "
  • בהפעלה, יוקצו צבעים שונים לכל תווית אובייקט
  • קו דק כחול כהה מציין שהאובייקט לא זוהה בנקודת זמן הנוכחית
  • קו דק אפור מציין שהאובייקט זוהה כנייח
  • קו עבה מציין שהאובייקט הוא נושא המעקב האוטומטי (כאשר מופעל)
  • " + } + }, + "timestamp": { + "title": "חותמת זמן", + "desc": "הוספת חותמת זמן על התמונה" + }, + "zones": { + "title": "אזורים", + "desc": "הצגת קווי מתאר של כל אזור מוגדר" + }, + "mask": { + "title": "מיסוך תנועה", + "desc": "הצגת פוליגונים של מיסוך תנועה" + }, + "motion": { + "title": "תיבות תנועה", + "desc": "הצג תיבות סביב אזורים שבהם זוהתה תנועה", + "tips": "

    תיבות תנועה


    תיבות אדומות יונחו על אזורים בפריים שבהם מזוהה תנועה כעת

    " + }, + "regions": { + "title": "אזורים", + "desc": "הצג תיבה של אזור העניין שנשלח לגלאי האובייקטים", + "tips": "

    תיבות אזור


    תיבות ירוקות בהירות יונחו על פני אזורים מעניינים בפריים הנשלחים לגלאי האובייקטים.

    " + }, + "objectShapeFilterDrawing": { + "title": "ציור מסנן צורת אובייקט", + "desc": "צייר מלבן על התמונה כדי להציג פרטים על שטח ויחס", + "document": "עיין בתיעוד ", + "score": "ציון", + "ratio": "יחס", + "area": "אזור", + "tips": "הפעל אפשרות זו כדי לצייר מלבן על תמונת המצלמה כדי להציג את השטח והיחס שלה. ניתן להשתמש בערכים אלה כדי להגדיר פרמטרים של מסנן צורת אובייקט בתצורה שלך." + }, + "desc": "תצוגת ניפוי שגיאות מציגה תצוגה בזמן אמת של אובייקטים במעקב והסטטיסטיקות שלהם. רשימת האובייקטים מציגה סיכום בהשהיית זמן של האובייקטים שזוהו." + }, + "users": { + "title": "משתמשים", + "management": { + "title": "ניהול משתמשים", + "desc": "נהל את חשבונות המשתמשים של מופע Frigate זה." + }, + "addUser": "הוספת משתמש", + "updatePassword": "עדכון סיסמה", + "toast": { + "success": { + "createUser": "המשתמש {{user}} נוצר בהצלחה", + "deleteUser": "המשתמש {{user}} נמחק בהצלחה", + "updatePassword": "הסיסמה עודכנה בהצלחה.", + "roleUpdated": "הרשאות עודכנו עבור {{user}}" + }, + "error": { + "createUserFailed": "יצירת משתמש נכשלה: {{errorMessage}}", + "roleUpdateFailed": "עדכון ההרשאות נכשל: {{errorMessage}}", + "deleteUserFailed": "מחיקת משתמש נכשלה: {{errorMessage}}", + "setPasswordFailed": "שמירת הסיסמה נכשלה: {{errorMessage}}" + } + }, + "table": { + "actions": "פעולות", + "role": "הרשאות", + "noUsers": "לא נמצאו משתמשים.", + "changeRole": "שינוי הרשאות משתמש", + "password": "סיסמה", + "deleteUser": "מחיקת משתמש", + "username": "שם משתמש" + }, + "dialog": { + "form": { + "user": { + "title": "שם משתמש", + "desc": "מותר להשתמש רק באותיות, מספרים, נקודות וקו תחתון.", + "placeholder": "הכנס שם משתמש" + }, + "password": { + "title": "סיסמה", + "placeholder": "הכנס סיסמה", + "confirm": { + "placeholder": "אישור סיסמה", + "title": "אישור סיסמה" + }, + "strength": { + "title": "חוזק הסיסמה: ", + "weak": "חלש", + "medium": "בינוני", + "strong": "חזק", + "veryStrong": "מאוד חזק" + }, + "match": "סיסמאות תואמות", + "notMatch": "הסיסמאות אינן תואמות." + }, + "newPassword": { + "title": "סיסמה חדשה", + "placeholder": "הכנס סיסמה חדשה", + "confirm": { + "placeholder": "הזן שוב את הסיסמה החדשה" + } + }, + "usernameIsRequired": "נדרש שם משתמש", + "passwordIsRequired": "נדרשת סיסמה" + }, + "createUser": { + "title": "יצירת משתמש חדש", + "desc": "הוסף חשבון משתמש חדש וציין הרשאות גישה לאזורים בממשק המשתמש של Frigate.", + "usernameOnlyInclude": "שם המשתמש יכול לכלול רק אותיות, מספרים, . או קו תחתון", + "confirmPassword": "אנא אשר את הסיסמה שלך" + }, + "deleteUser": { + "title": "מחיקת משתמש", + "desc": "לא ניתן לבטל פעולה זו. פעולה זו תמחק לצמיתות את חשבון המשתמש ותסיר את כל הנתונים המשויכים.", + "warn": "האם אתה בטוח שברצונך למחוק את {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "הסיסמה לא יכולה להיות ריקה", + "doNotMatch": "הסיסמאות אינן תואמות", + "updatePassword": "עדכון סיסמה עבור {{username}}", + "setPassword": "קבע סיסמה", + "desc": "צור סיסמה חזקה כדי לאבטח חשבון זה." + }, + "changeRole": { + "title": "שינוי הרשאות משתמש", + "select": "בחירת הרשאות", + "desc": "עדכון הרשאות עבור {{username}}", + "roleInfo": { + "intro": "בחר את ההרשאות המתאימות עבור משתמש זה:", + "admin": "מנהל", + "adminDesc": "גישה מלאה לכל התכונות.", + "viewer": "צופה", + "viewerDesc": "מוגבל לדשבורד שידור חי, סקירה, גילוי וייצוא בלבד." + } + } + } + }, + "notification": { + "title": "התראות", + "notificationSettings": { + "title": "הגדרת התראות", + "desc": "Frigate יכולה לשלוח התראות דחיפה באופן טבעי למכשיר שלך כאשר הוא פועל בדפדפן או מותקן כ-PWA.", + "documentation": "עיין בתיעוד" + }, + "notificationUnavailable": { + "title": "התראות לא זמינות", + "desc": "התראות דחיפה באינטרנט דורשות קישור מאובטח (https://…). זוהי מגבלה של הדפדפן. יש לגשת ל-Frigate בצורה מאובטחת כדי להשתמש בהתראות.", + "documentation": "עיין בתיעוד" + }, + "globalSettings": { + "title": "הגדרות כלליות", + "desc": "השהה זמנית התראות עבור מצלמות ספציפיות בכל המכשירים הרשומים." + }, + "email": { + "title": "דואר", + "desc": "נדרשת כתובת דוא\"ל תקינה שתשמש כדי להודיע לך אם ישנן בעיות כלשהן בשירות הדחיפה.", + "placeholder": "e.g. example@email.com" + }, + "cameras": { + "title": "מצלמות", + "noCameras": "אין מצלמות זמינות", + "desc": "בחר עבור אילו מצלמות להפעיל התראות." + }, + "deviceSpecific": "הגדרות ספציפיות למכשיר", + "registerDevice": "רשום מכשיר זה", + "unregisterDevice": "בטל את הרישום של מכשיר זה", + "sendTestNotification": "שלח הודעת בדיקה", + "unsavedRegistrations": "רישומי התראות שלא נשמרו", + "unsavedChanges": "שינויים בהתראות שלא נשמרו", + "active": "התראות פעילות", + "suspended": "התראות הושבתו ב-{{time}}", + "suspendTime": { + "suspend": "מושבת", + "5minutes": "השבתה למשך 5 דקות", + "10minutes": "השבתה למשך 10 דקות", + "30minutes": "השבתה למשך 30 דקות", + "1hour": "השבתה למשך שעה אחת", + "12hours": "השבתה למשך 12 שעות", + "24hours": "השבתה למשך 24 שעות", + "untilRestart": "השבתה עד להפעלה מחדש" + }, + "cancelSuspension": "ביטול השבתה", + "toast": { + "success": { + "settingSaved": "הגדרות ההתראות נשמרו.", + "registered": "רישום לקבלת התראות בוצע בהצלחה. נדרשת הפעלה מחדש של Frigate לפני שניתן יהיה לשלוח התראות כלשהן (כולל הודעת בדיקה)." + }, + "error": { + "registerFailed": "שמירת רישום ההתראות נכשלה." + } + } + }, + "frigatePlus": { + "title": "הגדרות +Frigate", + "apiKey": { + "title": "Frigate+ API מפתח", + "validated": "מפתח ה-API של Frigate+ זוהה ואושר", + "notValidated": "מפתח ה-API של Frigate+ לא זוהה או לא אומת", + "desc": "מפתח ה-API של Frigate+ מאפשר אינטגרציה עם שירות Frigate+.", + "plusLink": "קרא עוד על Frigate+" + }, + "snapshotConfig": { + "title": "תצורת לכידת תמונה", + "desc": "שליחה ל-Frigate+ דורשת הפעלה של לכידת תמונה וגם של תמונות בזק clean_copy בהגדרות שלך.", + "documentation": "עיין בתיעוד", + "cleanCopyWarning": "בחלק מהמצלמות יש אפשרות לתמונות בזק, אך העתקה נקייה מושבתת. עליך להפעיל את clean_copy בתצורת הצילום שלך כדי שתוכל לשלוח תמונות מהמצלמות הללו ל-Frigate+.", + "table": { + "snapshots": "לכידת תמונה", + "camera": "מצלמה", + "cleanCopySnapshots": "תמונות clean_copy" + } + }, + "modelInfo": { + "title": "מידע על המודל", + "modelType": "סוג מודל", + "trainDate": "תאריך אימון", + "baseModel": "דגם בסיסי", + "plusModelType": { + "userModel": "כיוונון עדין", + "baseModel": "מודל בסיסי" + }, + "supportedDetectors": "גלאים נתמכים", + "cameras": "מצלמות", + "loading": "טוען מידע על המודל…", + "error": "טעינת פרטי המודל נכשלה", + "availableModels": "מודלים זמינים", + "loadingAvailableModels": "טוען מודלים זמינים…", + "modelSelect": "ניתן לבחור כאן את הדגמים הזמינים ב-Frigate+. שים לב שניתן לבחור רק דגמים התואמים לתצורת הגלאי הנוכחית שלך." + }, + "unsavedChanges": "שינויים בהגדרות של frigate+ שלא נשמרו", + "restart_required": "נדרש הפעלה מחדש (דגם Frigate+ שונה)", + "toast": { + "success": "הגדרות Frigate+ נשמרו. הפעל מחדש את Frigate כדי להחיל את השינויים.", + "error": "שמירת שינויי התצורה נכשלה: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/he/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/he/views/system.json new file mode 100644 index 0000000..d30f943 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/he/views/system.json @@ -0,0 +1,181 @@ +{ + "lastRefreshed": "רענון אחרון: ", + "stats": { + "ffmpegHighCpuUsage": "ל-{{camera}} יש צריכת מעבד גבוהה של FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "ל-{{camera}} יש צריכת CPU גבוהה ({{detectAvg}}%)", + "healthy": "המערכת פועלת בצורה תקינה", + "reindexingEmbeddings": "אינדקס מחדש של ההטמעות ({{processed}}% הושלם)", + "cameraIsOffline": "{{camera}} לא זמינה", + "detectIsSlow": "{{detect}} איטי ({{speed}} אלפיות שנייה)", + "detectIsVerySlow": "{{detect}} איטי מאוד ({{speed}} אלפיות שנייה)" + }, + "documentTitle": { + "cameras": "מצב מצלמות - Frigate", + "storage": "מצב אחסון - Frigate", + "general": "סטטיסטיקה כללית - Frigate", + "enrichments": "סטטיסטיקת העשרה - Frigate", + "logs": { + "frigate": "לוגים - Frigate", + "go2rtc": "לוגים - Go2RTC - Frigate", + "nginx": "לוגים - Nginx - Frigate" + } + }, + "title": "מערכת", + "metrics": "מדדי מערכת", + "logs": { + "download": { + "label": "הורדת לוגים" + }, + "copy": { + "label": "העתק ללוח עריכה", + "success": "לוגים הועתקו ללוח העריכה", + "error": "לא ניתן היה להעתיק לוגים ללוח" + }, + "type": { + "label": "סוג", + "timestamp": "חותמת זמן", + "tag": "תג", + "message": "הודעה" + }, + "tips": "יומני רישום מוצגים בזרימה מהשרת", + "toast": { + "error": { + "fetchingLogsFailed": "שגיאה באחזור לוגים: {{errorMessage}}", + "whileStreamingLogs": "שגיאה בעת הזרמת לוגים: {{errorMessage}}" + } + } + }, + "general": { + "title": "כללי", + "detector": { + "title": "גלאים", + "inferenceSpeed": "מהירות זיהוי", + "temperature": "טמפרטורת הגלאי", + "cpuUsage": "ניצול מעבד על ידי הגלאי", + "memoryUsage": "שימוש בזיכרון על ידי הגלאי", + "cpuUsageInformation": "המעבד המשמש להכנת נתוני קלט ופלט אל/ממודלי זיהוי. ערך זה אינו מודד את השימוש בהסקה, גם אם נעשה שימוש במעבד גרפי או מאיץ." + }, + "hardwareInfo": { + "gpuMemory": "זיכרון GPU", + "title": "מידע על החומרה", + "gpuUsage": "שימוש GPU", + "gpuEncoder": "מקודד GPU", + "gpuDecoder": "מפענח GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "פלט Vainfo", + "returnCode": "קוד החזרה: {{code}}", + "processOutput": "פלט תהליך:", + "processError": "שגיאת תהליך:" + }, + "nvidiaSMIOutput": { + "title": "פלט SMI של Nvidia", + "name": "שם: {{name}}", + "driver": "מנהל התקן: {{driver}}", + "cudaComputerCapability": "יכולת חישוב CUDA: {{cuda_compute}}", + "vbios": "מידע על VBios: {{vbios}}" + }, + "closeInfo": { + "label": "סגור את המידע על ה-GPU" + }, + "copyInfo": { + "label": "העתק מידע על GPU" + }, + "toast": { + "success": "מידע על ה-GPU הועתק ללוח" + } + }, + "npuUsage": "שימוש ב-NPU", + "npuMemory": "NPU זיכרון" + }, + "otherProcesses": { + "title": "תהליכים אחרים", + "processCpuUsage": "ניצול CPU של התהליך", + "processMemoryUsage": "ניצול זיכרון של תהליך" + } + }, + "enrichments": { + "infPerSecond": "מספר הסקות בשנייה", + "title": "העשרה", + "embeddings": { + "image_embedding": "הטמעת תמונה", + "text_embedding": "הטמעת טקסט", + "face_recognition": "זיהוי פנים", + "plate_recognition": "זיהוי לוחית רישוי", + "image_embedding_speed": "מהירות הטמעת תמונה", + "face_embedding_speed": "מהירות הטמעת פנים", + "face_recognition_speed": "מהירות זיהוי פנים", + "plate_recognition_speed": "מהירות זיהוי לוחית", + "text_embedding_speed": "מהירות הטמעת טקסט", + "yolov9_plate_detection_speed": "מהירות זיהוי לוחיות YOLOv9", + "yolov9_plate_detection": "זיהוי לוחיות YOLOv9" + } + }, + "storage": { + "cameraStorage": { + "storageUsed": "אחסון", + "percentageOfTotalUsed": "אחוז מהסך הכל", + "bandwidth": "רוחב פס", + "unused": { + "title": "לא בשימוש", + "tips": "ייתכן שערך זה לא מייצג במדויק את השטח הפנוי הזמין ל-Frigate אם יש לך קבצים אחרים המאוחסנים בכונן שלך מעבר להקלטות של Frigate. Frigate אינו עוקב אחר ניצול האחסון מלבד להקלטות שלו." + }, + "title": "אחסון מצלמה", + "camera": "מצלמה", + "unusedStorageInformation": "מידע על אחסון שאינו בשימוש" + }, + "title": "אחסון", + "overview": "סקירה כללית", + "recordings": { + "title": "הקלטות", + "earliestRecording": "ההקלטה המוקדמת ביותר הזמינה:", + "tips": "ערך זה מייצג את סך האחסון בו משתמשים ההקלטות במסד הנתונים של Frigate. Frigate אינו עוקב אחר ניצול האחסון עבור כל הקבצים בדיסק שלך." + } + }, + "cameras": { + "title": "מצלמות", + "overview": "סקירה כללית", + "info": { + "aspectRatio": "יחס גובה-רוחב", + "cameraProbeInfo": "פרטי בדיקה של מצלמה {{camera}}", + "streamDataFromFFPROBE": "נתוני השידור מתקבלים באמצעות ffprobe.", + "fetching": "טוען נתוני מצלמה", + "stream": "זרם {{idx}}", + "video": "וידיאו:", + "codec": "מקודד:", + "resolution": "רזולוציה:", + "fps": "פריימים לשניה:", + "audio": "קול:", + "error": "שגיאה: {{error}}", + "tips": { + "title": "טוען נתוני מצלמה" + }, + "unknown": "לא ידוע" + }, + "label": { + "detect": "גילוי", + "skipped": "דילוג", + "ffmpeg": "FFmpeg", + "overallDetectionsPerSecond": "סך כל הזיהויים לשנייה", + "overallSkippedDetectionsPerSecond": "סה״כ זיהויים שדולגו לשנייה", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} לכידה", + "cameraDetect": "זיהוי {{camName}}", + "cameraDetectionsPerSecond": "מספר הזיהויים של {{camName}} לשנייה", + "cameraSkippedDetectionsPerSecond": "כמות הזיהויים שדולגו בשנייה – {{camName}}", + "capture": "לכידה", + "cameraFramesPerSecond": "{{camName}} פריימים לשנייה", + "camera": "מצלמה", + "overallFramesPerSecond": "סך כל הפריימים לשנייה" + }, + "toast": { + "success": { + "copyToClipboard": "העתקת נתוני הבדיקה בוצעה בהצלחה." + }, + "error": { + "unableToProbeCamera": "לא ניתן לבדוק את המצלמה: {{errorMessage}}" + } + }, + "framesAndDetections": "פריימים / זיהויים" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/audio.json b/sam2-cpu/frigate-dev/web/public/locales/hi/audio.json new file mode 100644 index 0000000..0705110 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/audio.json @@ -0,0 +1,145 @@ +{ + "babbling": "बड़बड़ाना", + "yell": "चिल्लाना", + "whispering": "फुसफुसाना", + "crying": "रोना", + "laughter": "हँसना", + "singing": "गाना", + "chant": "जपना", + "mantra": "मंत्र", + "run": "भागना", + "sniff": "सूँघना", + "sneeze": "छींंकना", + "whistling": "सीटी बजाना", + "speech": "बोलना", + "sigh": "आह भरना", + "humming": "गुनगुनाना", + "child_singing": "बच्चे का गाना", + "groan": "कराहना", + "breathing": "साँस लेना", + "snoring": "खर्राटे लेना", + "cough": "खाँसना", + "throat_clearing": "गला साफ़ करना", + "footsteps": "कदमों की आहट", + "chewing": "चबाना", + "biting": "काटना", + "gargling": "गरारे करना", + "stomach_rumble": "पेट की गुड़गुड़ाहट", + "burping": "डकारना", + "hiccup": "हिचकी", + "fart": "पादना", + "heartbeat": "धड़कन", + "cheering": "जयकार करना", + "pets": "पालतू जानवर", + "animal": "जानवर", + "children_playing": "बच्चों का खेलना", + "crowd": "भीड़", + "dog": "कुत्ता", + "hiss": "फुफकारना", + "neigh": "हिनहिनाना", + "cattle": "मवेशी", + "moo": "रंभाहट", + "goat": "बकरी", + "bleat": "मेमियाहट", + "hands": "हाथ", + "pig": "सुअर", + "clapping": "ताली बजाना", + "chatter": "गपशप", + "finger_snapping": "उँगलियाँ चटकाना", + "bark": "भौंकना", + "cowbell": "गाय की घंटी", + "sheep": "भेड़", + "yip": "कूँकना", + "livestock": "पशुधन", + "horse": "घोड़ा", + "cat": "बिल्ली", + "chicken": "मुर्गी", + "wild_animals": "जंगली जानवर", + "bird": "पक्षी", + "chirp": "चहचहाना", + "roar": "दहाड़ना", + "pigeon": "कबूतर", + "crow": "कौआ", + "flapping_wings": "पंख फड़फड़ाना", + "dogs": "कुत्ते", + "insect": "कीड़ा", + "patter": "पटपटाहट", + "cymbal": "झांझ", + "tambourine": "डफली", + "orchestra": "वाद्यवृंद", + "wind_instrument": "वायु वाद्ययंत्र", + "bowed_string_instrument": "धनुष तार वाद्ययंत्र", + "harp": "हार्प", + "bell": "घंटी", + "church_bell": "गिरजाघर का घंटा", + "accordion": "अकोर्डियन", + "opera": "ओपेरा", + "disco": "डिस्को", + "jazz": "जैज़", + "dubstep": "डबस्टेप", + "song": "गीत", + "lullaby": "लोरी", + "sad_music": "दुखभरा संगीत", + "tender_music": "कोमल संगीत", + "wind": "हवा", + "wind_noise": "हवा की आवाज़", + "thunderstorm": "आंधी-तूफ़ान", + "thunder": "गर्जना", + "water": "पानी", + "rain": "बारिश", + "rain_on_surface": "सतह पर गिरती बारिश", + "waterfall": "झरना", + "ocean": "सागर", + "waves": "लहरें", + "stream": "धारा", + "steam": "भाप", + "vehicle": "वाहन", + "car": "गाड़ी", + "boat": "नाव", + "ship": "जहाज़", + "truck": "ट्रक", + "bus": "बस", + "motor_vehicle": "मोटर वाहन", + "motorboat": "इंजन वाली नाव", + "sailboat": "पाल वाली नाव", + "police_car": "पुलिस की गाड़ी", + "saxophone": "सैक्सोफोन", + "sitar": "सितार", + "music": "संगीत", + "snake": "साँप", + "mouse": "चूहा", + "wedding_music": "शादी का संगीत", + "buzz": "भनभनाहट", + "fire": "आग", + "caw": "कांव कांव करना", + "owl": "उल्लू", + "mosquito": "मच्छर", + "scary_music": "डरावना संगीत", + "duck": "बतख", + "hoot": "उल्लू की आवाज़", + "rustling_leaves": "खड़खड़ाते पत्ते", + "rats": "चूहे", + "cricket": "झिंगुर", + "fly": "मक्खी", + "frog": "मेंढक", + "croak": "टर्राना", + "guitar": "गिटार", + "tabla": "तबला", + "trumpet": "तुरही", + "brass_instrument": "पीतल वाद्ययंत्र", + "flute": "बाँसुरी", + "clarinet": "क्लैरिनेट", + "bicycle_bell": "साइकिल की घंटी", + "harmonica": "हारमोनिका", + "bagpipes": "बैगपाइप", + "angry_music": "क्रोधित संगीत", + "music_of_bollywood": "बॉलीवुड संगीत", + "happy_music": "खुशहाल संगीत", + "exciting_music": "रोमांचक संगीत", + "raindrop": "बारिश की बूंद", + "rowboat": "चप्पू वाली नाव", + "aircraft": "विमान", + "bicycle": "साइकिल", + "bellow": "गर्जना करना", + "motorcycle": "मोटरसाइकिल" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/common.json b/sam2-cpu/frigate-dev/web/public/locales/hi/common.json new file mode 100644 index 0000000..d4c4335 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/common.json @@ -0,0 +1,8 @@ +{ + "time": { + "untilForTime": "{{time}} तक", + "untilForRestart": "जब तक फ्रिगेट पुनः रीस्टार्ट नहीं हो जाता।", + "untilRestart": "रीस्टार्ट होने में", + "ago": "{{timeAgo}} पहले" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/auth.json new file mode 100644 index 0000000..c58f433 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "password": "पासवर्ड", + "login": "प्रवेश करें", + "errors": { + "passwordRequired": "पासवर्ड आवश्यक है", + "rateLimit": "दर सीमा पार हो गई है। बाद में पुनः प्रयास करें।", + "unknownError": "अज्ञात त्रुटि। प्रविष्टियाँ जांचें।", + "usernameRequired": "प्रयोक्ता नाम आवश्यक है", + "webUnknownError": "अज्ञात त्रुटि। कंसोल प्रविष्टियाँ जांचें।", + "loginFailed": "लॉगिन असफल हुआ" + }, + "user": "प्रयोक्ता नाम" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/camera.json new file mode 100644 index 0000000..74c05d4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/camera.json @@ -0,0 +1,10 @@ +{ + "group": { + "label": "कैमरा समूह", + "add": "कैमरा समूह जोड़ें", + "edit": "कैमरा समूह संपादित करें", + "delete": { + "label": "कैमरा समूह हटाएँ" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/dialog.json new file mode 100644 index 0000000..bcf4cd0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/dialog.json @@ -0,0 +1,10 @@ +{ + "restart": { + "title": "क्या आप निश्चित हैं कि आप फ्रिगेट को रीस्टार्ट करना चाहते हैं?", + "button": "रीस्टार्ट", + "restarting": { + "title": "फ्रिगेट रीस्टार्ट हो रहा है", + "content": "यह पृष्ठ {{countdown}} सेकंड में पुनः लोड होगा।" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/filter.json new file mode 100644 index 0000000..a89133b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/filter.json @@ -0,0 +1,10 @@ +{ + "filter": "फ़िल्टर", + "labels": { + "label": "लेबल", + "all": { + "title": "सभी लेबल", + "short": "लेबल" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/icons.json new file mode 100644 index 0000000..7f5236e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "चिह्न चुनें", + "search": { + "placeholder": "चिह्न खोजें..।" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/input.json new file mode 100644 index 0000000..13b65c1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "वीडियो डाउनलोड करें", + "toast": { + "success": "आपकी समीक्षा वीडियो डाउनलोड होना शुरू हो गई है।" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/hi/components/player.json new file mode 100644 index 0000000..e5e63a8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/components/player.json @@ -0,0 +1,8 @@ +{ + "noRecordingsFoundForThisTime": "इस समय का कोई रिकॉर्डिंग नहीं मिला", + "noPreviewFound": "कोई प्रीव्यू नहीं मिला", + "noPreviewFoundFor": "{{cameraName}} के लिए कोई पूर्वावलोकन नहीं मिला", + "submitFrigatePlus": { + "title": "इस फ्रेम को फ्रिगेट+ पर सबमिट करें?" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/objects.json b/sam2-cpu/frigate-dev/web/public/locales/hi/objects.json new file mode 100644 index 0000000..a4e93c3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/objects.json @@ -0,0 +1,18 @@ +{ + "horse": "घोड़ा", + "sheep": "भेड़", + "bark": "भौंकना", + "animal": "जानवर", + "dog": "कुत्ता", + "cat": "बिल्ली", + "goat": "बकरी", + "boat": "नाव", + "bus": "बस", + "bird": "पक्षी", + "mouse": "चूहा", + "vehicle": "वाहन", + "car": "गाड़ी", + "person": "व्यक्ति", + "bicycle": "साइकिल", + "motorcycle": "मोटरसाइकिल" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/configEditor.json new file mode 100644 index 0000000..784f8ec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/configEditor.json @@ -0,0 +1,15 @@ +{ + "saveOnly": "केवल सहेजें", + "saveAndRestart": "सहेजें और पुनः प्रारंभ करें", + "configEditor": "विन्यास संपादक", + "copyConfig": "विन्यास कॉपी करें", + "toast": { + "error": { + "savingError": "विन्यास सहेजने में त्रुटि हुई" + }, + "success": { + "copyToClipboard": "विन्यास क्लिपबोर्ड पर कॉपी कर लिया गया है।" + } + }, + "documentTitle": "विन्यास संपादक - Frigate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/events.json new file mode 100644 index 0000000..ae20914 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/events.json @@ -0,0 +1,8 @@ +{ + "alerts": "अलर्टस", + "detections": "खोजें", + "motion": { + "label": "गति", + "only": "केवल गति" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/explore.json new file mode 100644 index 0000000..daafb9c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/explore.json @@ -0,0 +1,8 @@ +{ + "documentTitle": "अन्वेषण करें - फ्रिगेट", + "generativeAI": "जनरेटिव ए आई", + "exploreMore": "और अधिक {{label}} वस्तुओं का अन्वेषण करें", + "exploreIsUnavailable": { + "title": "अन्वेषण अनुपलब्ध है" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/exports.json new file mode 100644 index 0000000..b9e86da --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/exports.json @@ -0,0 +1,6 @@ +{ + "documentTitle": "निर्यात - फ्रिगेट", + "search": "खोजें", + "noExports": "कोई निर्यात नहीं मिला", + "deleteExport": "निर्यात हटाएँ" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/faceLibrary.json new file mode 100644 index 0000000..b305282 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/faceLibrary.json @@ -0,0 +1,7 @@ +{ + "description": { + "addFace": "फेस लाइब्रेरी में नया संग्रह जोड़ने की प्रक्रिया को आगे बढ़ाएं।", + "placeholder": "इस संग्रह का नाम बताएं", + "invalidName": "अमान्य नाम. नाम में केवल अक्षर, संख्याएँ, रिक्त स्थान, एपॉस्ट्रॉफ़ी, अंडरस्कोर और हाइफ़न ही शामिल हो सकते हैं।" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/live.json new file mode 100644 index 0000000..9c7edbe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/live.json @@ -0,0 +1,5 @@ +{ + "documentTitle": "लाइव - फ्रिगेट", + "documentTitle.withCamera": "{{camera}} - लाइव - फ्रिगेट", + "lowBandwidthMode": "कम बैंडविड्थ मोड" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/recording.json new file mode 100644 index 0000000..a9846e4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/recording.json @@ -0,0 +1,11 @@ +{ + "calendar": "पंचांग", + "toast": { + "error": { + "noValidTimeSelected": "कोई मान्य समय सीमा चयनित नहीं है", + "endTimeMustAfterStartTime": "समाप्ति समय प्रारंभ समय के बाद होना चाहिए" + } + }, + "export": "निर्यात", + "filter": "फ़िल्टर" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/search.json new file mode 100644 index 0000000..2ea0c8c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/search.json @@ -0,0 +1,5 @@ +{ + "search": "खोजें", + "savedSearches": "सहेजी गई खोजें", + "searchFor": "{{inputValue}} खोजें" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/settings.json new file mode 100644 index 0000000..d9bf27f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/settings.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "default": "सेटिंग्स - फ्रिगेट", + "authentication": "प्रमाणीकरण सेटिंग्स - फ्रिगेट", + "camera": "कैमरा सेटिंग्स - फ्रिगेट" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hi/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/hi/views/system.json new file mode 100644 index 0000000..23bafa3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hi/views/system.json @@ -0,0 +1,7 @@ +{ + "documentTitle": { + "cameras": "कैमरा आँकड़े - फ्रिगेट", + "storage": "भंडारण आँकड़े - फ्रिगेट", + "general": "सामान्य आँकड़े - फ्रिगेट" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/audio.json b/sam2-cpu/frigate-dev/web/public/locales/hr/audio.json new file mode 100644 index 0000000..5b51811 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/audio.json @@ -0,0 +1,3 @@ +{ + "speech": "Govor" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/common.json b/sam2-cpu/frigate-dev/web/public/locales/hr/common.json new file mode 100644 index 0000000..af7c798 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/common.json @@ -0,0 +1,5 @@ +{ + "time": { + "untilForTime": "Do {{time}}" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/auth.json new file mode 100644 index 0000000..f3b92ea --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/auth.json @@ -0,0 +1,5 @@ +{ + "form": { + "user": "Korisničko ime" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/camera.json new file mode 100644 index 0000000..271949a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/camera.json @@ -0,0 +1,40 @@ +{ + "group": { + "label": "Grupe kamera", + "add": "Dodaj grupu kamera", + "edit": "Uredi grupu kamera", + "delete": { + "label": "Izbriši grupu kamera", + "confirm": { + "title": "Potvrda brisanja", + "desc": "Da li ste sigurni da želite obrisati grupu kamera {{name}}?" + } + }, + "name": { + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Ime grupe kamera mora sadržavati barem 2 karaktera.", + "exists": "Grupa kamera sa ovim imenom već postoji.", + "nameMustNotPeriod": "Naziv grupe kamera ne smije sadržavati točku.", + "invalid": "Nevažeći naziv grupe kamera." + } + }, + "cameras": { + "label": "Kamere", + "desc": "Izaberite kamere za ovu grupu." + }, + "icon": "Ikona", + "success": "Grupa kamera ({{name}}) je pohranjena.", + "camera": { + "birdseye": "Ptičja perspektiva", + "setting": { + "label": "Postavke streamanja kamere", + "title": "{{cameraName}} Streaming Postavke", + "desc": "Promijenite opcije streamanja uživo za nadzornu ploču ove grupe kamera. Ove postavke su specifične za uređaj/preglednik.", + "audioIsAvailable": "Za ovaj prijenos dostupan je zvuk", + "audioIsUnavailable": "Za ovaj prijenos zvuk nije dostupan" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/dialog.json new file mode 100644 index 0000000..660031e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/dialog.json @@ -0,0 +1,5 @@ +{ + "restart": { + "title": "Jeste li sigurni da želite ponovno pokrenuti Frigate?" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/filter.json new file mode 100644 index 0000000..37845aa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/filter.json @@ -0,0 +1,6 @@ +{ + "filter": "Filter", + "classes": { + "label": "Klase" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/icons.json new file mode 100644 index 0000000..b973f10 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/icons.json @@ -0,0 +1,5 @@ +{ + "iconPicker": { + "selectIcon": "Odaberite ikonu" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/input.json new file mode 100644 index 0000000..ffeca81 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/input.json @@ -0,0 +1,7 @@ +{ + "button": { + "downloadVideo": { + "label": "Preuzmi video" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/hr/components/player.json new file mode 100644 index 0000000..752b358 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/components/player.json @@ -0,0 +1,3 @@ +{ + "noRecordingsFoundForThisTime": "Nisu pronađene snimke za ovo vrijeme" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/objects.json b/sam2-cpu/frigate-dev/web/public/locales/hr/objects.json new file mode 100644 index 0000000..afc1338 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/objects.json @@ -0,0 +1,3 @@ +{ + "person": "Osoba" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/configEditor.json new file mode 100644 index 0000000..6443eaa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/configEditor.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Uređivač konfiguracije - Frigate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/events.json new file mode 100644 index 0000000..b387047 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/events.json @@ -0,0 +1,3 @@ +{ + "alerts": "Upozorenja" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/explore.json new file mode 100644 index 0000000..c4f84e7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/explore.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Istražite - Frigate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/exports.json new file mode 100644 index 0000000..529e7c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/exports.json @@ -0,0 +1,4 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Pretraga" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/faceLibrary.json new file mode 100644 index 0000000..7f5754c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/faceLibrary.json @@ -0,0 +1,28 @@ +{ + "description": { + "addFace": "Vodič za dodavanje nove kolekcije u Biblioteku lica." + }, + "steps": { + "faceName": "Unesi Ime Lica", + "uploadFace": "Prenesi Sliku Lica", + "nextSteps": "Sljedeći Koraci", + "description": { + "uploadFace": "Prenesite sliku {{name}} koja prikazuje njezino lice iz prednjeg kuta. Slika ne mora biti obrezana samo na njezino lice." + } + }, + "train": { + "title": "Nedavna Prepoznavanja", + "aria": "Odaberite nedavna prepoznavanja", + "empty": "Nema nedavnih pokušaja prepoznavanja lica" + }, + "deleteFaceLibrary": { + "title": "Izbriši Ime", + "desc": "Jeste li sigurni da želite izbrisati kolekciju {{name}}? Ovim će se trajno izbrisati sva povezana lica." + }, + "deleteFaceAttempts": { + "title": "Izbriši Lica", + "desc_one": "Jeste li sigurni da želite izbrisati {{count}} lice? Ova se radnja ne može poništiti.", + "desc_few": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti.", + "desc_other": "Jeste li sigurni da želite izbrisati {{count}} lica? Ova se radnja ne može poništiti." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/live.json new file mode 100644 index 0000000..93f5997 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/live.json @@ -0,0 +1,3 @@ +{ + "documentTitle": "Uživo - Frigate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/recording.json new file mode 100644 index 0000000..110cf71 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/recording.json @@ -0,0 +1,4 @@ +{ + "filter": "Filter", + "export": "Izvoz" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/search.json new file mode 100644 index 0000000..370cb28 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/search.json @@ -0,0 +1,3 @@ +{ + "search": "Pretraga" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/settings.json new file mode 100644 index 0000000..c2153a6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/settings.json @@ -0,0 +1,5 @@ +{ + "documentTitle": { + "default": "Postavke - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hr/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/hr/views/system.json new file mode 100644 index 0000000..076c823 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hr/views/system.json @@ -0,0 +1,5 @@ +{ + "documentTitle": { + "cameras": "Statistika kamera - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/audio.json b/sam2-cpu/frigate-dev/web/public/locales/hu/audio.json new file mode 100644 index 0000000..cc73f3c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "Beszéd", + "wild_animals": "Vadállatok", + "synthetic_singing": "Szintetikus éneklés", + "cheering": "Éljenzés", + "chatter": "Csevegés", + "bird": "Madár", + "babbling": "Gügyögés", + "yell": "Kiabálás", + "whispering": "Suttogás", + "laughter": "Nevetés", + "bellow": "Ordítás", + "whoop": "Ujjongás", + "snicker": "Kuncogás", + "crying": "Sírás", + "sigh": "Sóhajtás", + "singing": "Éneklés", + "choir": "Kórus", + "yodeling": "Jódlizás", + "chant": "Dalolás", + "mantra": "Mantrázás", + "child_singing": "Gyermek éneklés", + "rapping": "Rappelés", + "groan": "Nyögés", + "grunt": "Morgás", + "whistling": "Fütyülés", + "breathing": "Légzés", + "wheeze": "Zihálás", + "snoring": "Horkolás", + "gasp": "Lihegés", + "pant": "Lihegés", + "snort": "Horkantás", + "cough": "Köhögés", + "sneeze": "Tüsszentés", + "sniff": "Szimatolás", + "run": "Futás", + "shuffle": "Csoszogás", + "footsteps": "Lépés", + "chewing": "Rágás", + "biting": "Maró", + "gargling": "Gargalizálás", + "burping": "Böfögés", + "hiccup": "Csuklás", + "fart": "Szellentés", + "hands": "Kezek", + "finger_snapping": "Csettintés", + "clapping": "Tapsolás", + "heartbeat": "Szívdobbanás", + "heart_murmur": "Szívzörej", + "applause": "Taps", + "crowd": "Tömeg", + "children_playing": "Gyerekek játszanak", + "pets": "Háziállatok", + "dog": "Kutya", + "bark": "Ugatás", + "yip": "Csaholás", + "howl": "Bömbölés", + "bow_wow": "Vau vau", + "growling": "Morgás", + "whimper_dog": "Kutya nyüszítés", + "cat": "Macska", + "purr": "Dorombolás", + "meow": "Nyávogás", + "hiss": "Sziszegés", + "caterwaul": "Nyivákolás", + "livestock": "Állatállomány", + "horse": "Ló", + "clip_clop": "Lódobogás", + "neigh": "Nyerítés", + "cattle": "Marha", + "moo": "Tehénbőgés", + "cowbell": "Kolomp", + "pig": "Disznó", + "oink": "Disznó röfögés", + "goat": "Kecske", + "bleat": "Bégetés", + "sheep": "Juh", + "fowl": "Szárnyas", + "chicken": "Csirke", + "cluck": "Kotkodácsolás", + "cock_a_doodle_doo": "Kukorékolás", + "turkey": "Pulyka", + "gobble": "Kurrogás", + "duck": "Kacsa", + "quack": "Hápogás", + "honk": "Gágogás", + "roaring_cats": "Ordító macskák", + "roar": "Üvöltés", + "chirp": "Csiripelés", + "squawk": "Rikácsolás", + "pigeon": "Galamb", + "coo": "Turbékolás", + "crow": "Varjú", + "caw": "Károgás", + "owl": "Bagoly", + "hoot": "Huhogás", + "flapping_wings": "Csapkodó szárnyak", + "dogs": "Kutyák", + "rats": "Patkányok", + "mouse": "Egér", + "patter": "Kopog", + "insect": "Rovar", + "cricket": "Tücsök", + "mosquito": "Szúnyog", + "fly": "Légy", + "buzz": "Zümmögés", + "frog": "Béka", + "croak": "Brekegés", + "snake": "Kígyó", + "rattle": "Csörgés", + "whale_vocalization": "Bálna bőgés", + "music": "Zene", + "musical_instrument": "Hangszer", + "plucked_string_instrument": "Pengetős hangszer", + "guitar": "Gitár", + "electric_guitar": "Elektromos gitár", + "bass_guitar": "Basszusgitár", + "acoustic_guitar": "Akusztikus gitár", + "steel_guitar": "Acél gitár", + "tapping": "Tappelés", + "strum": "Zongora verés", + "mandolin": "Mandolin", + "banjo": "Bendzsó", + "zither": "Citera", + "sitar": "Szitár", + "ukulele": "Ukulele", + "keyboard": "Szintetizátor", + "piano": "Zongora", + "electric_piano": "Elektromos zongora", + "organ": "Orgona", + "hammond_organ": "Hammond orgona", + "electronic_organ": "Elektromos orgona", + "synthesizer": "Szintetizátor", + "sampler": "Sampler", + "harpsichord": "Csembaló", + "percussion": "Ütős hangszer", + "drum_kit": "Dobszerkó", + "drum_machine": "Dobgép", + "drum": "Dob", + "snare_drum": "Pergődob", + "rimshot": "Peremütés", + "drum_roll": "Dobpergés", + "timpani": "Üstdob", + "bass_drum": "Basszusdob", + "tabla": "Tablá", + "cymbal": "Cintányér", + "hi_hat": "Lábcin", + "boat": "Hajó", + "car": "Autó", + "bus": "Busz", + "motorcycle": "Motor", + "train": "Betanít", + "bicycle": "Bicikli", + "scream": "Sikoly", + "throat_clearing": "Torokköszörülés", + "stomach_rumble": "Gyomorkorgás", + "animal": "Állat", + "goose": "Liba", + "humming": "Zümmögés", + "skateboard": "Gördeszka", + "door": "Ajtó", + "camera": "Kamera", + "ship": "Hajó", + "car_alarm": "Autó riasztó", + "truck": "Teherautó", + "race_car": "Versenyautó", + "ambulance": "Mentő", + "police_car": "Rendőrautó", + "ice_cream_truck": "Jégkrémes autó", + "traffic_noise": "Forgalom zaja", + "helicopter": "Helikopter", + "propeller": "Propeller", + "subway": "Metró", + "chainsaw": "Láncfűrész", + "lawn_mower": "Fűnyíró", + "dental_drill's_drill": "Fogászati Fúró", + "engine": "Motor", + "doorbell": "Csengő", + "accelerating": "Gyorsítás", + "engine_starting": "Motor indítás", + "water_tap": "Csapvíz", + "blender": "Pépesítő gép", + "microwave_oven": "Mikrohullámú sütő", + "cutlery": "Evőeszköz", + "dishes": "Edények", + "alarm": "Vészjelző", + "writing": "Írás", + "computer_keyboard": "Billentyűzet", + "typewriter": "Írógép", + "typing": "Gépelés", + "electric_shaver": "Villanyborotva", + "scissors": "Olló", + "coin": "Érme", + "vacuum_cleaner": "Porszívó", + "electric_toothbrush": "Elektromos fogkefe", + "toothbrush": "Fogkefe", + "toilet_flush": "WC lehúzás", + "hair_dryer": "Hajszárító", + "bathtub": "Fürdőkád", + "sink": "Mosdókagyló", + "ringtone": "Csengőhang", + "telephone_bell_ringing": "Telefon csörgés", + "telephone": "Telefon", + "siren": "Sziréna", + "alarm_clock": "Ébresztőóra", + "busy_signal": "Foglalt jelzés", + "dial_tone": "Hívás hang", + "clock": "Óra", + "fire_alarm": "Tűzjelző", + "smoke_detector": "Füst érzékelő", + "hammer": "Kalapács", + "printer": "Nyomtató", + "cash_register": "Pénztárgép", + "air_conditioning": "Légkondícionálás", + "sawing": "Fűrészelés", + "machine_gun": "Gépfegyver", + "gunshot": "Pisztoly lövés", + "explosion": "Robbanás", + "drill": "Fúrás", + "glass": "Üveg", + "wood": "Fa", + "fireworks": "Tűzijáték", + "white_noise": "Fehér zaj", + "static": "Statikus", + "environmental_noise": "Környezeti Zaj", + "sound_effect": "Hang effekt", + "silence": "Csend", + "radio": "Rádió", + "television": "Televízió", + "pink_noise": "Rózsaszín zaj", + "gong": "Gong", + "saxophone": "Szaxofon", + "cello": "Cselló", + "violin": "Hegedű", + "harmonica": "Harmónika", + "wind_chime": "Szélcsengő", + "bicycle_bell": "Bicikli Csengő", + "church_bell": "Templomi Harang", + "bell": "Harang", + "didgeridoo": "Didzseridú", + "heavy_metal": "Heavy Metál", + "rock_music": "Rock Zene", + "beatboxing": "Beatboxolás", + "hip_hop_music": "Hip-Hop Zene", + "pop_music": "Pop Zene", + "soul_music": "Soul Zene", + "rhythm_and_blues": "Ritmus és Blues", + "psychedelic_rock": "Pszichedelikus Rock", + "rock_and_roll": "Rock and Roll", + "folk_music": "Népzene", + "funk": "Funky", + "classical_music": "Klasszikus Zene", + "disco": "Diszkó", + "jazz": "Jazz", + "middle_eastern_music": "Közép-Keleti Zene", + "drum_and_bass": "Drum and Bass", + "dubstep": "Dubstep", + "techno": "Techno", + "opera": "Opera", + "music_of_latin_america": "Latin-amerikai Zene", + "trance_music": "Trance Zene", + "a_capella": "A Capella", + "music_for_children": "Zene Gyerekeknek", + "blues": "Blues", + "flamenco": "Flamenco", + "music_of_bollywood": "Bollywood-i Zene", + "music_of_asia": "Ázsiai Zene", + "gospel_music": "Gospel Zene", + "christian_music": "Keresztény Zene", + "music_of_africa": "Afrikai Zene", + "background_music": "Háttérzene", + "song": "Dal", + "independent_music": "Független Zene", + "ska": "Ska", + "happy_music": "Boldog Zene", + "wedding_music": "Házassági Zene", + "dance_music": "Tánc Zene", + "christmas_music": "Karácsonyi Zene", + "video_game_music": "Videojáték Zene", + "water": "Víz", + "thunder": "Villám", + "wind_noise": "Szél Zaj", + "wind": "Szél", + "angry_music": "Mérges Zene", + "sad_music": "Szomorú Zene", + "waves": "Hullámok", + "ocean": "Óceán", + "waterfall": "Vízesés", + "stream": "Adás", + "rain_on_surface": "Eső a Felületen", + "raindrop": "Esőcsepp", + "rain": "Eső", + "vehicle": "Jármű", + "fire": "Tűz", + "motorboat": "Motorcsónak", + "car_passing_by": "Áthaladó autó", + "house_music": "House Zene", + "electronic_music": "Elektronikus Zene", + "salsa_music": "Salsa Zene", + "progressive_rock": "Progresszív Rock", + "grunge": "Grunge", + "swing_music": "Swing Zene", + "country": "Ország", + "reggae": "Reggae", + "punk_rock": "Punk Rock", + "clarinet": "Klarinét", + "wood_block": "Fa hasáb", + "tambourine": "Csörgődob", + "maraca": "Maraca", + "tubular_bells": "Csőharang", + "mallet_percussion": "Kalapácsos ütőhangszer", + "marimba": "Marimba", + "glockenspiel": "Harangjáték", + "vibraphone": "Vibrafon", + "steelpan": "Stíldob", + "orchestra": "Zenekar", + "brass_instrument": "Rézfúvós Hangszer", + "french_horn": "Francia Kürt", + "trumpet": "Trombita", + "trombone": "Harsona", + "bowed_string_instrument": "Vonós Hangszer", + "string_section": "Vonós szekció", + "pizzicato": "Pizzicato", + "double_bass": "Nagybőgő", + "wind_instrument": "Fúvós Hangszer", + "flute": "Fuvola", + "harp": "Hárfa", + "jingle_bell": "Csengő", + "tuning_fork": "Hangvilla", + "chime": "Harangjáték", + "accordion": "Harmónika", + "bagpipes": "Skótduda", + "theremin": "Teremin", + "singing_bowl": "Éneklőtál", + "scratching": "Scratchelés", + "bluegrass": "Bluegrass", + "electronica": "Elektronika", + "electronic_dance_music": "Elektronikus Tánczene", + "ambient_music": "Ambient Zene", + "new-age_music": "New Age Zene", + "vocal_music": "Ének Zene", + "afrobeat": "Afrobeat", + "carnatic_music": "Carnatic Zene", + "traditional_music": "Népzene", + "theme_music": "Filmzene", + "jingle": "Reklámzene", + "soundtrack_music": "Zeneszám", + "lullaby": "Altatódal", + "tender_music": "Tender Zene", + "exciting_music": "Izgalmas Zene", + "scary_music": "Ijesztő Zene", + "rustling_leaves": "Susogó Levelek", + "thunderstorm": "Zivatar", + "steam": "Gőz", + "gurgling": "Gurgulázás", + "crackle": "Sercegés", + "sailboat": "Vitorláshajó", + "rowboat": "Csónak", + "motor_vehicle": "Motorkerékpár", + "toot": "Fújás", + "power_windows": "Elektromos ablakok", + "skidding": "csúszás", + "tire_squeal": "Kerékcsikorgás", + "air_brake": "Légfék", + "air_horn": "Légkürt", + "reversing_beeps": "Tolató csipogás", + "emergency_vehicle": "Sürgősségi jármű", + "fire_engine": "Tűzoltóautó", + "rail_transport": "Vasúti közlekedés", + "train_whistle": "Vonatsíp", + "train_horn": "Vonatkürt", + "railroad_car": "Vasúti kocsi", + "train_wheels_squealing": "Vonatkerék csikorgás", + "aircraft": "Repülő", + "aircraft_engine": "Repülő motor", + "jet_engine": "Sugárhajtású motor", + "fixed-wing_aircraft": "Szárnyas repülőgép", + "light_engine": "Kis motor", + "medium_engine": "Közepes motor", + "heavy_engine": "Nagy motor", + "engine_knocking": "Kopogó motor", + "idling": "Alapjárat", + "ding-dong": "Ding-Dong", + "sliding_door": "Tolóajtó", + "slam": "Csapódás", + "knock": "Kopogás", + "tap": "Finom kopogás", + "squeak": "Csipogás", + "cupboard_open_or_close": "Szekrény nyitás, vagy zárás", + "drawer_open_or_close": "Fiók nyitás vagy zárás", + "chopping": "Vágás", + "frying": "Sütés", + "zipper": "Cipzár", + "keys_jangling": "Kulcsok csörgése", + "shuffling_cards": "Kártyakeverés", + "telephone_dialing": "Telefonos tárcsázás", + "civil_defense_siren": "Rendvédelmi sziréna", + "buzzer": "Búgó eszköz", + "foghorn": "Ködkürt", + "whistle": "Síp", + "steam_whistle": "Gőzsíp", + "mechanisms": "Mechanikus alkatrészek", + "ratchet": "Racsni", + "tick": "Bolha", + "tick-tock": "Ketyegés", + "gears": "Fogaskerekek", + "pulleys": "emelő csigák", + "sewing_machine": "Varrógép", + "mechanical_fan": "Ventillátor", + "single-lens_reflex_camera": "Egylencsés reflexkamera", + "tools": "Eszközök", + "jackhammer": "Bontókalapács", + "filing": "Iratrendezés", + "sanding": "Csiszolás", + "power_tool": "Munkagép", + "fusillade": "Sortűz", + "artillery_fire": "Tüzérségi tűz", + "cap_gun": "Kupakos pisztoly", + "firecracker": "Petárda", + "burst": "Kitörés (Burst)", + "eruption": "Kitörés (Eruption)", + "boom": "Bumm", + "chop": "Vágás", + "splinter": "Repedés (fa)", + "crack": "Törés", + "chink": "Csörömpölés", + "shatter": "Összetörés", + "field_recording": "Helyszíni felvétel" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/common.json b/sam2-cpu/frigate-dev/web/public/locales/hu/common.json new file mode 100644 index 0000000..99e0450 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/common.json @@ -0,0 +1,278 @@ +{ + "time": { + "untilForTime": "{{time}} ideig", + "s": "{{time}} másodperc", + "untilForRestart": "Amíg a Frigate újraindul.", + "untilRestart": "Amíg újraindul", + "justNow": "Most", + "ago": "Ennyi ideje: {{timeAgo}}", + "today": "Ma", + "yesterday": "Tegnap", + "last7": "Elmúlt 7 nap", + "last14": "Elmúlt 14 nap", + "last30": "Elmúlt 30 nap", + "thisWeek": "Ezen a héten", + "lastWeek": "Előző héten", + "thisMonth": "Ebben a hónapban", + "lastMonth": "Előző hónapban", + "5minutes": "5 perc", + "10minutes": "10 perc", + "30minutes": "30 perc", + "1hour": "1 óra", + "12hours": "12 óra", + "24hours": "24 óra", + "pm": "du", + "am": "de", + "yr": "{{time}} év", + "mo": "{{time}} hónap", + "d": "{{time}} nap", + "m": "{{time}} perc", + "hour_one": "{{time}} óra", + "hour_other": "{{time}} órák", + "h": "{{time}} óra", + "minute_one": "{{time}} perc", + "minute_other": "{{time}} percek", + "second_one": "{{time}} másodperc", + "second_other": "{{time}} másodpercek", + "year_one": "{{time}} év", + "year_other": "{{time}} évek", + "month_one": "{{time}} hónap", + "month_other": "{{time}} hónapok", + "day_one": "{{time}} nap", + "day_other": "{{time}} napok", + "formattedTimestamp": { + "24hour": "MMM d, HH:mm:ss", + "12hour": "MMM d, h:mm:ss aaa" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "h:mm aaa" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "MM/dd h:mm:ssa" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "MMM d yyyy, HH:mm", + "12hour": "MMM d yyyy, h:mm aaa" + }, + "formattedTimestampFilename": { + "24hour": "yy-MM-dd-HH-mm-ss", + "12hour": "yy-MM-dd-h-mm-ss-a" + }, + "formattedTimestampMonthDayHourMinute": { + "24hour": "MMM d, HH:mm", + "12hour": "MMM d, h:mm aaa" + }, + "formattedTimestampMonthDay": "MMM d" + }, + "menu": { + "darkMode": { + "label": "Sötét Mód", + "withSystem": { + "label": "Használd a rendszerbeállításokat a világos vagy sötét mód megheghatározásához" + }, + "dark": "Sötét", + "light": "Világos" + }, + "withSystem": "Rendszer", + "theme": { + "default": "Alap", + "nord": "Nord", + "red": "Piros", + "highcontrast": "Nagy Kontrasztarány", + "label": "Téma", + "blue": "Kék", + "green": "Zöld" + }, + "documentation": { + "title": "Dokumentáció", + "label": "Frigate dokumentáció" + }, + "help": "Segítség", + "explore": "Felfedezés", + "user": { + "logout": "Kijelentkezés", + "title": "Felhasználó", + "account": "Fiók", + "current": "Jelenlegi Felhazsnáló: {{user}}", + "anonymous": "anoním", + "setPassword": "Jelszó Beállítása" + }, + "export": "Exportálás", + "language": { + "ca": "Katalán", + "withSystem": { + "label": "Használd a rendszerbeállításoknál megadott nyelvet" + }, + "sk": "Szlovák", + "da": "Dán", + "yue": "Kantoni", + "hi": "Hindi", + "vi": "Vietnámi", + "fa": "Perzsa", + "pl": "Lengyel", + "en": "Angol", + "es": "Spanyol", + "uk": "Ukrán", + "ja": "Japán", + "tr": "Török", + "it": "Olasz", + "nl": "Holland", + "sv": "Svéd", + "he": "Héber", + "el": "Görög", + "fr": "Francia", + "nb": "Norvég", + "cs": "Cseh", + "ko": "Koreai", + "zhCN": "Egyszerűsített kínai", + "ar": "Arab", + "pt": "Portugál", + "ru": "Orosz", + "de": "Német", + "ro": "Román", + "hu": "Magyar", + "fi": "Finn", + "th": "Thai", + "ptBR": "Português brasileiro (Brazil portugál)", + "sr": "Српски (Szerb)", + "sl": "Slovenščina (Szlovén)", + "lt": "Lietuvių (Litván)", + "bg": "Български (Bolgár)", + "gl": "Galego (Galíciai)", + "id": "Bahasa Indonesia (Indonéz)", + "ur": "اردو (Urdu)" + }, + "uiPlayground": "UI játszótér", + "faceLibrary": "Arc Könyvtár", + "restart": "Frigate Újraindítása", + "live": { + "title": "Élő", + "allCameras": "Minden Kamera", + "cameras": { + "title": "Kamerák", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kamerák" + } + }, + "review": "Áttekintés", + "appearance": "Megjelenés", + "languages": "Nyelvek", + "configurationEditor": "Konfiguráció Kezelő", + "systemMetrics": "Rendszer metrikák", + "system": "Rendszer", + "configuration": "Konfiguráció", + "systemLogs": "Rendszer naplók", + "settings": "Beállítások" + }, + "role": { + "viewer": "Néző", + "title": "Szerepkör", + "admin": "Adminisztrátor", + "desc": "Az adminisztrátoroknak teljes hozzáférése van az összes feature-höz. A nézők csak a kamerákat láthatják, áttekinthetik az elemeket és az előzményeket a UI-on." + }, + "pagination": { + "next": { + "label": "Következő oldal", + "title": "Következő" + }, + "more": "Több oldal", + "previous": { + "label": "Vissza az előző oldalra", + "title": "Előző" + }, + "label": "lapozás" + }, + "accessDenied": { + "documentTitle": "Belépés Megtagadva - Frigate", + "title": "Belépés Megtagadva", + "desc": "Nincs jogolultsága ehhez az oldalhoz." + }, + "notFound": { + "title": "404", + "desc": "Oldal nem található", + "documentTitle": "Nem található - Frigate" + }, + "toast": { + "copyUrlToClipboard": "URL kimásolva a vágólapra.", + "save": { + "title": "Mentés", + "error": { + "title": "Sikertelen konfiguráció mentés: {{errorMessage}}", + "noMessage": "Konfigurációs változtatások mentése sikertelen" + } + } + }, + "selectItem": "KIválasztani {{item}}-et", + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "láb", + "meters": "méter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/óra", + "mbph": "MB/óra", + "gbph": "GB/óra" + } + }, + "button": { + "save": "Mentés", + "saving": "Mentés…", + "disable": "Kikapcsol", + "close": "Bezár", + "copy": "Másolás", + "back": "Vissza", + "apply": "Alkalmazás", + "done": "Kész", + "reset": "Visszaállítás", + "enabled": "Engedélyezve", + "enable": "Engedélyez", + "disabled": "Kikapcsolva", + "cancel": "Mégsem", + "fullscreen": "Teljes képernyő", + "history": "Előzmények", + "exitFullscreen": "Kilépés a Teljes Képernyőből", + "no": "Nem", + "download": "Letöltés", + "off": "KI", + "info": "Infó", + "twoWayTalk": "Kétirányú kommunikáció", + "pictureInPicture": "Kép a Képben", + "cameraAudio": "Kamera Hang", + "on": "BE", + "edit": "Módosít", + "copyCoordinates": "Koordináták másolása", + "delete": "Törlés", + "yes": "Igen", + "unsuspended": "Bekapcsol", + "suspended": "Felfüggesztve", + "play": "Lejátszás", + "unselect": "Kijelölés megszüntetése", + "export": "Exportálás", + "deleteNow": "Törlés Most", + "next": "Következő" + }, + "label": { + "back": "Vissza" + }, + "readTheDocumentation": "Olvassa el a dokumentációt", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/auth.json new file mode 100644 index 0000000..43b8e9e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Felhasználói név", + "password": "Jelszó", + "login": "Bejelentkezés", + "errors": { + "usernameRequired": "Felhasználónév szükséges", + "passwordRequired": "Jelszó szükséges", + "loginFailed": "Sikertelen bejelentkezés", + "unknownError": "Ismeretlen hiba. Ellenőrizze a naplókat.", + "webUnknownError": "Ismeretlen hiba. Ellenőrizze a konzol naplókat.", + "rateLimit": "Túl sokszor próbálkozott. Próbálja meg később." + }, + "firstTimeLogin": "Először próbálsz bejelentkezni? A hitelesítési adatok a Frigate naplóiban vannak feltüntetve." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/camera.json new file mode 100644 index 0000000..c529481 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kamera Csoportok", + "delete": { + "confirm": { + "desc": "Biztosan törölni akarja a következő kamera csoportot {{name}}?", + "title": "Törlés megerősítése" + }, + "label": "Kamera csoport törlése" + }, + "add": "Kamera csoport hozzáadása", + "edit": "Kamera csoport módosítása", + "name": { + "label": "Név", + "placeholder": "Adjon meg egy nevet…", + "errorMessage": { + "mustLeastCharacters": "A kamera csoport nevének legalább 2 karakterből kell állnia.", + "exists": "Ez a kamera csoport név már létezik.", + "nameMustNotPeriod": "A kamera csoport neve nem tartalmazhat idő intervallumot.", + "invalid": "Hibás kamera csoport név." + } + }, + "cameras": { + "label": "Kamerák", + "desc": "Válassza ki a kamerákat ehhez a csoporthoz." + }, + "icon": "Ikon", + "success": "Kamera csoport {{name}} mentve.", + "camera": { + "setting": { + "audioIsAvailable": "Hang elérhető ehhez az adáshoz", + "audioIsUnavailable": "Nem elérhető hang ehhez az adáshoz", + "audio": { + "tips": { + "document": "Olvassa el a leírást ", + "title": "A hangnak a kamerából kell jönnie és be kell állítva legyen a go2rtc-ben ehhez az adáshoz." + } + }, + "title": "{{cameraName}} Adás Beállításai", + "label": "Kamera adás beállítások", + "stream": "Adás", + "streamMethod": { + "method": { + "noStreaming": { + "label": "Nincs Adás", + "desc": "A kameraképek percenként frissülnek és nem lesz élő adás." + }, + "smartStreaming": { + "label": "Okos Adás (ajánlott)", + "desc": "Az intelligens közvetítés percenként egyszer frissíti a kameraképet, ha nem észlelhető aktivitás, így takarékoskodik a sávszélességgel és az erőforrásokkal. Amikor aktivitást észlel, a kép zökkenőmentesen átvált élő közvetítésre." + }, + "continuousStreaming": { + "label": "Folyamatos Adás", + "desc": { + "title": "A kamerakép mindig élő közvetítés lesz, amikor látható az irányítópulton, még akkor is, ha nincs észlelt aktivitás.", + "warning": "A folyamatos közvetítés magas sávszélesség-használatot és teljesítményproblémákat okozhat. Csak óvatosan használja." + } + } + }, + "label": "Adási Mód", + "placeholder": "Válasszon egy adási módot" + }, + "placeholder": "Válasszon adást", + "compatibilityMode": { + "label": "Kompatibilitás mód", + "desc": "Csak akkor engedélyezze ezt az opciót, ha a kamera élő közvetítése képhibás, és a kép jobb oldalán átlós vonal látható." + }, + "desc": "Változtassa meg az élő adás beállításait ezen kamera csoport kijelzőjén. Ezek a beállítások eszköz/böngésző-specifikusak." + }, + "birdseye": "Madártávlat" + } + }, + "debug": { + "options": { + "label": "Beállítások", + "title": "Lehetőségek", + "showOptions": "Mutasd a Lehetőségeket", + "hideOptions": "Lehetőségek Elrejtése" + }, + "timestamp": "Időbélyeg", + "zones": "Zónák", + "mask": "Maszk", + "motion": "Mozgás", + "regions": "Régiók", + "boundingBox": "Doboz" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/dialog.json new file mode 100644 index 0000000..c45eac1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/dialog.json @@ -0,0 +1,121 @@ +{ + "restart": { + "title": "Biztosan újra szeretnéd indítani a Frigate-et?", + "button": "Újraindítás", + "restarting": { + "title": "A Frigate újraindul", + "content": "Az oldal újratölt {{countdown}} másodperc múlva.", + "button": "Erőltetett újraindítás azonnal" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Küldés a Frigate+-nak", + "desc": "Objektumok, amelyek olyan helyeken vannak, ahol nem akarod, hogy felismerésre kerüljenek, nem számítanak téves egyezésnek. Ha téves egyezésként küldöd be őket, az összezavarja a modellt." + }, + "review": { + "state": { + "submitted": "Elküldve" + }, + "question": { + "ask_a": "Ez a tárgy egy {{label}}?", + "label": "Erősítse meg ezt a cimkét a Frigate plus felé", + "ask_an": "Ez a tárgy egy {{label}}?", + "ask_full": "Ez a tárgy egy {{translatedLabel}} ({{untranslatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Megtekintés az Előzmények között" + } + }, + "export": { + "time": { + "fromTimeline": "Kiválasztás az Idővonalból", + "start": { + "title": "Kezdő időpont", + "label": "Kezdeti Időpont Kiválasztása" + }, + "end": { + "title": "Végső időpont", + "label": "Végső Időpont Kiválasztása" + }, + "lastHour_one": "Előző óra", + "lastHour_other": "Utolsó {{count}} óra", + "custom": "Egyéb" + }, + "name": { + "placeholder": "Export Elnevezése" + }, + "select": "Kiválaszt", + "export": "Exportálás", + "selectOrExport": "Kiválasztás vagy Exportálás", + "toast": { + "success": "Exportálás sikeresen megkezdődött. Tekintse meg a fájl-t a /exports mappában.", + "error": { + "failed": "Nem sikerült elkezdeni az exportálást: {{error}}", + "endTimeMustAfterStartTime": "A végső időpontnak a kezdeti időpont után kell következnie", + "noVaildTimeSelected": "Nincs érvényes idő intervallum kiválasztva" + } + }, + "fromTimeline": { + "saveExport": "Exportálás mentése", + "previewExport": "Exportálás Előnézet" + } + }, + "streaming": { + "label": "Adás", + "restreaming": { + "desc": { + "readTheDocumentation": "Olvassa el a dokumentációt", + "title": "Állítsa be a go2rtc-t további élő nézeti lehetőségek és hang támogatása érdekében ennél a kameránál." + }, + "disabled": "Az újraadás nem engedélyezett ennél a kameránál." + }, + "showStats": { + "label": "Mutasd az adás statisztikákat", + "desc": "Engedélyezze ezt az opciót, hogy a közvetítés statisztikái átfedésként megjelenjenek a kameraképen." + }, + "debugView": "Debug Nézet" + }, + "search": { + "saveSearch": { + "label": "Keresés Mentése", + "placeholder": "Adjon nevet a keresésnek", + "success": "Keresés {{searchName}} mentve.", + "button": { + "save": { + "label": "Keresés mentése" + } + }, + "desc": "Adjon meg egy nevet ehhez a mentett kereséshez.", + "overwrite": "{{searchName}} már létezik. Ha elmenti az felül fogja írni a meglévőt." + } + }, + "recording": { + "confirmDelete": { + "title": "Törlés Megerősítése", + "toast": { + "error": "Törlés sikertelen: {{error}}", + "success": "A kijelölt ellenőrzési elemekhez tartozó videofelvételek sikeresen törölve lettek." + }, + "desc": { + "selected": "Biztosan törölni szeretné az összes rögzített videót, amely ehhez az ellenőrzési elemhez tartozik?

    Tartsa lenyomva a Shift billentyűt, hogy a jövőben kihagyja ezt a párbeszédablakot." + } + }, + "button": { + "markAsReviewed": "Megjelölés áttekintettként", + "deleteNow": "Törlés Most", + "export": "Exportálás", + "markAsUnreviewed": "Megjelölés nem ellenőrzöttként" + } + }, + "imagePicker": { + "selectImage": "Válassza ki egy követett tárgy képét", + "search": { + "placeholder": "Keresés cimke vagy alcimke alapján..." + }, + "noImages": "Nem találhatók bélyegképek ehhez a kamerához" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/filter.json new file mode 100644 index 0000000..8af6361 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Szűrő", + "labels": { + "label": "Cimkék", + "all": { + "title": "Minden cimke", + "short": "Cimkék" + }, + "count_other": "{{count}} cimke", + "count_one": "{{count}} Cimke" + }, + "zones": { + "label": "Zónák", + "all": { + "title": "Minden zóna", + "short": "Zónák" + } + }, + "more": "Több Szűrő", + "timeRange": "Idő intervallum", + "reset": { + "label": "Szűrők visszaállítása alapállapotba" + }, + "subLabels": { + "label": "Alcimkék", + "all": "Minden Alcimke" + }, + "logSettings": { + "label": "Szűrő napló szint", + "loading": { + "title": "Töltés", + "desc": "Amikor a naplópanel az aljára van görgetve, az új naplóbejegyzések automatikusan megjelennek, ahogy hozzáadódnak." + }, + "allLogs": "Minden napló", + "filterBySeverity": "Naplók szűrési fontosság alapján", + "disableLogStreaming": "Napló adásának kikapcsolása" + }, + "trackedObjectDelete": { + "toast": { + "error": "Követett tárgyak törlése sikertelen: {{errorMessage}}", + "success": "Követett tárgyak törlése sikeres." + }, + "title": "Törlés Megerősítése", + "desc": "A(z) {{objectLength}} követett objektum törlése eltávolítja a pillanatképet, az összes mentett beágyazást és az összes kapcsolódó objektum életciklus-bejegyzést. Ezeknek a követett objektumoknak a rögzített felvételei a Történet nézetben NEM kerülnek törlésre.

    Biztosan folytatni szeretné?

    Tartsa lenyomva a Shift billentyűt, hogy a jövőben kihagyja ezt a párbeszédablakot." + }, + "dates": { + "all": { + "title": "Minden Dátum", + "short": "Dátumok" + }, + "selectPreset": "Válasszon sablon beállítást…" + }, + "score": "Pont", + "estimatedSpeed": "Becsült Sebesség {{unit}}", + "features": { + "hasSnapshot": "Van pillanatképe", + "submittedToFrigatePlus": { + "label": "Elküldve a Frigate+-hoz", + "tips": "Először szűrjön azokra a követett objektumokra, amelyek rendelkeznek pillanatképpel.

    A pillanatkép nélküli követett objektumokat nem lehet beküldeni a Frigate+ rendszerébe." + }, + "label": "Funkciók", + "hasVideoClip": "Van videója" + }, + "sort": { + "label": "Rendezés", + "dateAsc": "Dátum szerint (Növekvő)", + "scoreDesc": "Tárgy Pontszám (Csökkenő)", + "scoreAsc": "Tárgy Pontszám (Növekvő)", + "speedAsc": "Becsült Sebesség (Növekvő)", + "dateDesc": "Dátum szerint (Csökkenő)", + "speedDesc": "Becsült Sebesség (Csökkenő)", + "relevance": "Relevancia" + }, + "cameras": { + "all": { + "title": "Minden Kamera", + "short": "Kamerák" + }, + "label": "Kamera szűrő" + }, + "review": { + "showReviewed": "Mutasd az Áttekintetteket" + }, + "motion": { + "showMotionOnly": "Mutasd Csak a Mozgást" + }, + "explore": { + "settings": { + "title": "Beállítások", + "defaultView": { + "title": "Alap Nézet", + "summary": "Összegzés", + "unfilteredGrid": "Szűrőmentes Rács", + "desc": "Ha nincs kiválasztva szűrő, jelenítse meg az egyes címkékhez tartozó legutóbbi követett objektumok összefoglalóját, vagy jelenítsen meg egy szűretlen rácsot." + }, + "searchSource": { + "label": "Keresés Forrás", + "options": { + "description": "Leírás", + "thumbnailImage": "Indexkép" + }, + "desc": "Válassza ki, hogy a követett objektumok bélyegképeiben vagy leírásaiban szeretne keresni." + }, + "gridColumns": { + "title": "Rács Oszlopok", + "desc": "Válassza ki az oszlopok számáta rács nézetben." + } + }, + "date": { + "selectDateBy": { + "label": "Válassza ki a dátumot a szűréshez" + } + } + }, + "zoneMask": { + "filterBy": "Szűrés zóna maszk alapján" + }, + "recognizedLicensePlates": { + "title": "Felismert Rendszámtáblák", + "loadFailed": "Felismert rendszámtáblák betöltése sikertelen.", + "noLicensePlatesFound": "Rendszámtábla nem található.", + "selectPlatesFromList": "Válasszon ki egy vagy több rendszámtáblát a listából.", + "loading": "Felismert rendszámtáblák betöltése…", + "placeholder": "Kezdjen gépelni a rendszámok közötti kereséshez…", + "selectAll": "Mindet kijelöl", + "clearAll": "Mindet törli" + }, + "classes": { + "label": "Osztályok", + "all": { + "title": "Minden Osztály" + }, + "count_one": "{{count}} Osztály", + "count_other": "{{count}} Osztályok" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/icons.json new file mode 100644 index 0000000..5145dc3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Válassz ikont", + "search": { + "placeholder": "Ikon keresése…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/input.json new file mode 100644 index 0000000..8276a31 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Videó Letöltése", + "toast": { + "success": "Az áttekintendő videó letöltése megkezdődött." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/hu/components/player.json new file mode 100644 index 0000000..31ee991 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Ebben az időpontban nem található felvétel", + "noPreviewFound": "Nincs elérhető előkép", + "submitFrigatePlus": { + "title": "Elküldi ezt a képet a Frigate+-nak?", + "submit": "Küldés" + }, + "noPreviewFoundFor": "Nem található előnézet {{cameraName}}-hoz/-hez/-höz", + "livePlayerRequiredIOSVersion": "iOS 17.1 vagy újabb szükséges ehhez az élő adás típushoz.", + "streamOffline": { + "title": "Adás Nem Elérhető", + "desc": "Nem érkezett kép a {{cameraName}}-n, észlelés adásban. Ellenőrizze a hibanaplót" + }, + "cameraDisabled": "Kamera kikapcsolva", + "stats": { + "streamType": { + "title": "Adás Típus:", + "short": "Típus" + }, + "bandwidth": { + "title": "Sávszélesség:", + "short": "Sávszélesség" + }, + "latency": { + "title": "Késleltetés:", + "value": "{{seconds}} másodperc", + "short": { + "title": "Késleltetés", + "value": "{{seconds}} másodperc" + } + }, + "droppedFrames": { + "title": "Hibás Képek:", + "short": { + "value": "{{droppedFrames}} képek", + "title": "Hibás" + } + }, + "decodedFrames": "Dekódolt Képek:", + "droppedFrameRate": "Hibás Kép Sebesség:", + "totalFrames": "Összes Kép:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Kép küldése a Frigate+-hoz meghiúsult" + }, + "success": { + "submittedFrigatePlus": "Kép sikeresen elküldve a Frigate+-nak" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/objects.json b/sam2-cpu/frigate-dev/web/public/locales/hu/objects.json new file mode 100644 index 0000000..4b53d16 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Személy", + "bicycle": "Bicikli", + "car": "Autó", + "motorcycle": "Motor", + "airplane": "Repülőgép", + "bus": "Busz", + "train": "Betanít", + "boat": "Hajó", + "dog": "Kutya", + "cat": "Macska", + "horse": "Ló", + "sheep": "Juh", + "bird": "Madár", + "mouse": "Egér", + "keyboard": "Szintetizátor", + "animal": "Állat", + "bark": "Ugatás", + "goat": "Kecske", + "traffic_light": "Jelzőlámpa", + "fire_hydrant": "Tűzcsap", + "street_sign": "Utcatábla", + "stop_sign": "Stop tábla", + "parking_meter": "Parkoló óra", + "bench": "Pad", + "cow": "Tehén", + "elephant": "Elefánt", + "bear": "Medve", + "zebra": "Zebra", + "giraffe": "Zsiráf", + "hat": "Kalap", + "backpack": "Hátitáska", + "umbrella": "Esernyő", + "shoe": "Cipő", + "eye_glasses": "Szemüveg", + "handbag": "Kézitáska", + "tie": "Nyakkendő", + "suitcase": "Bőrönd", + "frisbee": "Frizbi", + "skis": "Síléc", + "snowboard": "Hódeszka (Snowboard)", + "sports_ball": "Sport labda", + "kite": "Sárkány", + "baseball_bat": "Baseball ütő", + "baseball_glove": "Baseball Kesztyű", + "skateboard": "Gördeszka", + "surfboard": "Szörfdeszka", + "tennis_racket": "Teniszütő", + "bottle": "Üveg", + "plate": "Tányér", + "wine_glass": "Boros üveg", + "cup": "Csésze", + "fork": "Villa", + "knife": "Kés", + "spoon": "Kanál", + "bowl": "Tál", + "banana": "Banán", + "apple": "Alma", + "sandwich": "Szendvics", + "orange": "Narancs", + "broccoli": "Brokkoli", + "carrot": "Répa", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Fánk", + "cake": "Torta", + "chair": "Szék", + "couch": "Kanapé", + "potted_plant": "Cserepes növény", + "bed": "Ágy", + "mirror": "Tükör", + "dining_table": "Ebédlő asztal", + "window": "Ablak", + "desk": "Asztal", + "toilet": "Mosdó", + "door": "Ajtó", + "tv": "TV", + "laptop": "Laptop", + "clock": "Óra", + "scissors": "Olló", + "hair_dryer": "Hajszárító", + "sink": "Mosdókagyló", + "blender": "Pépesítő gép", + "toothbrush": "Fogkefe", + "vehicle": "Jármű", + "remote": "Távoli", + "cell_phone": "Mobiltelefon", + "microwave": "Mikrohullám", + "oven": "Sütő", + "refrigerator": "Hűtő", + "book": "Könyv", + "toaster": "Kenyérpirító", + "vase": "Váza", + "teddy_bear": "Teddy Maci", + "hair_brush": "Hajkefe", + "squirrel": "Mókus", + "deer": "Szarvas", + "fox": "Róka", + "rabbit": "Nyúl", + "raccoon": "Mosómedve", + "robot_lawnmower": "Robotfűnyíró", + "waste_bin": "Kuka", + "on_demand": "Igény Szerint", + "license_plate": "Rendszám tábla", + "package": "Csomag", + "bbq_grill": "BBQ sütő", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolátor", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "face": "Arc" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/classificationModel.json new file mode 100644 index 0000000..5e9d6f5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/classificationModel.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Osztályozási modellek", + "button": { + "deleteClassificationAttempts": "Osztályozási képek törlése", + "deleteImages": "Képek törlése", + "trainModel": "Modell betanítása", + "deleteModels": "Modellek törlése", + "editModel": "Modell szerkesztése" + }, + "toast": { + "success": { + "deletedImage": "Törölt képek", + "deletedModel_one": "Sikeresen törölt {{count}} modellt", + "deletedModel_other": "", + "categorizedImage": "A kép sikeresen osztályozva" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/configEditor.json new file mode 100644 index 0000000..69fa822 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfiguráció Szerkesztő - Frigate", + "configEditor": "Konfiguráció szerkesztő", + "copyConfig": "Konfiguráció másolása", + "saveAndRestart": "Mentés és Újraindítás", + "saveOnly": "Csak mentés", + "toast": { + "success": { + "copyToClipboard": "Konfiguráció átmásolva a vágólapra." + }, + "error": { + "savingError": "Hiba a konfiguráció mentésekor" + } + }, + "confirm": "Kilép mentés nélkül?", + "safeConfigEditor": "Konfiguráció szerkesztő (Biztosnági Mód)", + "safeModeDescription": "Frigate biztonsági módban van konfigurációs hiba miatt." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/events.json new file mode 100644 index 0000000..abea6b4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/events.json @@ -0,0 +1,41 @@ +{ + "alerts": "Riasztások", + "empty": { + "detection": "Nincs megnézendő észlelés", + "alert": "Nincs megnézendő riasztás", + "motion": "Nem található mozgás" + }, + "detections": "Észlelések", + "motion": { + "label": "Mozgás", + "only": "Csak mozgások" + }, + "allCameras": "Összes kamera", + "timeline": "Idővonal", + "detected": "észlelve", + "events": { + "label": "Események", + "aria": "Válassza ki az eseményeket", + "noFoundForTimePeriod": "Nem található esemény ebben az idő intervallumban." + }, + "calendarFilter": { + "last24Hours": "Elmúlt 24 Óra" + }, + "newReviewItems": { + "label": "Új áttekintendő elemek megnézése", + "button": "Áttekintendő Új Elemek" + }, + "camera": "Kamera", + "timeline.aria": "Válassza ki az idővonalat", + "documentTitle": "Áttekintés - Frigate", + "recordings": { + "documentTitle": "Felvételek - Frigate" + }, + "markTheseItemsAsReviewed": "Ezen elemek megjelölése áttekintettként", + "markAsReviewed": "Megjelölés Áttekintettként", + "selected_one": "{{count}} kiválasztva", + "selected_other": "{{count}} kiválasztva", + "suspiciousActivity": "Gyanús Tevékenység", + "threateningActivity": "Fenyegető Tevékenység", + "zoomIn": "Nagyítás" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/explore.json new file mode 100644 index 0000000..cf811cd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/explore.json @@ -0,0 +1,226 @@ +{ + "documentTitle": "Tallózás - Frigate", + "itemMenu": { + "downloadVideo": { + "label": "Video letöltése", + "aria": "Video letöltése" + }, + "submitToPlus": { + "aria": "Küldés a Frigate Plus-nak", + "label": "Küldés a Frigate+-nak" + }, + "deleteTrackedObject": { + "label": "Követett tárgy törlése" + }, + "viewObjectLifecycle": { + "label": "Tárgy életciklusának megtekintése", + "aria": "Mutasd a tárgy életciklusát" + }, + "findSimilar": { + "label": "Keress hasonlót", + "aria": "Keress hasonló követett tárgyat" + }, + "viewInHistory": { + "label": "Megtekintés az Előzményekben", + "aria": "Megtekintés az Előzményekben" + }, + "downloadSnapshot": { + "aria": "Pillanatfelvétel letöltése", + "label": "Pillanatfelvétel letöltése" + }, + "addTrigger": { + "label": "Indító hozzáadása", + "aria": "Indító hozzáadása ehhez a követett tárgyhoz" + }, + "audioTranscription": { + "label": "Átírás", + "aria": "Hangátirat kérése" + } + }, + "details": { + "editLPR": { + "title": "Rendszám módosítása", + "desc": "Új rendszám hozzáadása ehhez {{label}}", + "descNoLabel": "Adjon hozzá egy új rendszámot ehhez a követett tárgyhoz" + }, + "label": "Cimke", + "editSubLabel": { + "desc": "Új alcimke létrehozása ehhez a cimkéhez {{label}}", + "title": "Alcimke módosítása", + "descNoLabel": "Adjon hozzá egy új alcimkét ehhez a követett tárgyhoz" + }, + "camera": "Kamera", + "zones": "Zónák", + "description": { + "placeholder": "Követett tárgy leírása", + "label": "Leírás", + "aiTips": "A Frigate nem fog igényelni leírást a Generatív MI szolgáltatójától, amíg a követett tárgy életciklusa be nem fejeződött." + }, + "timestamp": "Időbélyeg", + "estimatedSpeed": "Becsült Sebesség", + "objects": "Tárgyak", + "topScore": { + "label": "Legnagyobb Pontszám", + "info": "A legmagasabb pontszám a követett objektum legmagasabb medián pontszáma, ezért eltérhet a keresési eredmény bélyegképén megjelenő pontszámtól." + }, + "recognizedLicensePlate": "Felismert Rendszám", + "tips": { + "descriptionSaved": "Leírás sikeresen mentve", + "saveDescriptionFailed": "Leírás frissítése sikertelen: {{errorMessage}}" + }, + "item": { + "toast": { + "error": { + "updatedLPRFailed": "Rendszám frissítése sikertelen: {{errorMessage}}", + "updatedSublabelFailed": "Alcimke frissítése sikertelen: {{errorMessage}}", + "regenerate": "Nem sikerült meghívni a(z) {{provider}} szolgáltatót az új leírásért: {{errorMessage}}", + "audioTranscription": "Nem sikerült hangátiratot kérni: {{errorMessage}}" + }, + "success": { + "updatedSublabel": "Az alcimke sikeresen frissítve.", + "updatedLPR": "Rendszám sikeresen frissítve.", + "regenerate": "Új leírást kértünk a(z) {{provider}} szolgáltatótól. A szolgáltató sebességétől függően az új leírás előállítása eltarthat egy ideig.", + "audioTranscription": "Sikeresen kérte a hangátírást." + } + }, + "button": { + "viewInExplore": "Mutasd a Felfedezésben", + "share": "Áttekintési elem megosztása" + }, + "desc": "Áttekintendő elem részletei", + "title": "Áttekintendő Elem Részletei", + "tips": { + "mismatch_one": "{{count}} nem elérhető objektum lett észlelve és belefogalve ebbe az ellenőrzési elembe. Ez az objektum vagy nem felelt meg a riasztás vagy észlelés feltételeinek, vagy már törlésre/eltávolításra került.", + "mismatch_other": "{{count}} nem elérhető objektumok lettek észlelve és belefogalve ebbe az ellenőrzési elembe. Ezek az objektumok vagy nem feleltek meg a riasztás vagy észlelés feltételeinek, vagy már törlésre/eltávolításra kerültek.", + "hasMissingObjects": "Állítsa be a konfigurációját, ha azt szeretné, hogy a Frigate mentse a követett objektumokat a következő címkékhez: {{objects}}" + } + }, + "snapshotScore": { + "label": "Pillanatfelvétel Pontszáma" + }, + "regenerateFromThumbnails": "Újragenerálás kisképből", + "regenerateFromSnapshot": "Újragenerálás pillanatfelvételből", + "button": { + "regenerate": { + "label": "Követett tárgy leírásának újragenerálása", + "title": "Újragenerálás" + }, + "findSimilar": "Keress Hasonlót" + }, + "expandRegenerationMenu": "Újragenerálási menü kiterjesztése", + "score": { + "label": "Pontszám" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Hiba a követett tárgy törlése közben: {{errorMessage}}", + "success": "Követett tárgy sikeresen törölve." + } + }, + "tooltip": "{{type}} egyezés {{confidence}}%-os megbízhatósággal" + }, + "generativeAI": "Generatív MI", + "exploreIsUnavailable": { + "title": "Felfedezés nem elérhető", + "embeddingsReindexing": { + "startingUp": "Indulás…", + "estimatedTime": "Becsült hátralevő idő:", + "finishingShortly": "Hamarosan végez", + "step": { + "thumbnailsEmbedded": "Beágyazott ikonok: ", + "descriptionsEmbedded": "Beágyazott leírások: ", + "trackedObjectsProcessed": "Feldolgozott követett tárgyak: " + }, + "context": "A felfedezés azután használható, hogy a követett tárgy beágyazások újraindexálása befejeződött." + }, + "downloadingModels": { + "tips": { + "documentation": "Olvassa el a leírást", + "context": "Érdemes lehet újraindexelni a követett objektumok beágyazásait, miután a modellek letöltődtek." + }, + "setup": { + "textModel": "Szöveg modell", + "textTokenizer": "Szöveg tokenizáló", + "visionModel": "Látvány modell", + "visionModelFeatureExtractor": "Látvány modell képesség kinyerő" + }, + "error": "Hiba történt. Ellenőrizze a Frigate naplókat.", + "context": "A Frigate letölti a szemantikus keresés funkcióhoz szükséges beágyazási modelleket. Ez a hálózati kapcsolat sebességétől függően néhány percet is igénybe vehet." + } + }, + "noTrackedObjects": "Nincs követett tárgy", + "trackedObjectsCount_one": "{{count}} követett tárgy. ", + "trackedObjectsCount_other": "{{count}} követett tárgy. ", + "dialog": { + "confirmDelete": { + "title": "Törlés megerősítése", + "desc": "Ennek a követett objektumnak a törlése eltávolítja a pillanatképet, az összes mentett beágyazást és az összes kapcsolódó objektum életciklus-bejegyzést. A Történet nézetben lévő rögzített felvételek NEM kerülnek törlésre.

    Biztosan folytatni szeretné?" + } + }, + "fetchingTrackedObjectsFailed": "Hiba a követett tárgyak betöltése közben: {{errorMessage}}", + "objectLifecycle": { + "title": "Tárgy Életciklus", + "noImageFound": "Ehhez az időbélyeghez nem található kép.", + "createObjectMask": "Tárgy Maszk Létrehozása", + "scrollViewTips": "Görgessen, hogy megnézze ezen tárgy életciklusának jelentős pillanatait.", + "lifecycleItemDesc": { + "heard": "{{label}} meghallva", + "external": "{{label}} észlelve", + "header": { + "zones": "Zónák", + "ratio": "Arány", + "area": "Körzet" + }, + "active": "{{label}} aktiválódott", + "attribute": { + "other": "{{label}} felismerve mint {{attribute}}", + "faceOrLicense_plate": "{{attribute}} észlelve {{label}}-hoz/-hez/-höz" + }, + "entered_zone": "{{label}} belépett {{zones}}-ba/-be", + "visible": "{{label}} észlelve", + "gone": "{{label}} elment", + "stationary": "{{label}} mozdulatlanná vált" + }, + "annotationSettings": { + "offset": { + "documentation": "Olvassa el a leírást ", + "label": "Jelölés eltolás", + "desc": "Ez az információ a kamera észlelési csatornájából származik, de a felvételi csatorna képeire van ráhelyezve. Nem valószínű, hogy a két közvetítés tökéletesen szinkronban van. Ennek eredményeként a kijelölő keret és a felvétel nem lesz teljesen pontosan összehangolva. Azonban az annotation_offset mező segítségével ez korrigálható.", + "millisecondsToOffset": "Ezredmásodpercben megadott érték, amellyel az észlelési jelölések eltolhatók. Alapértelmezett: 0", + "tips": "TIPP: Képzelje el, hogy van egy eseményklip, amelyben egy személy balról jobbra sétál. Ha az esemény idővonalán a kijelölő keret következetesen a személy bal oldalán jelenik meg, akkor az értéket csökkenteni kell. Hasonlóképpen, ha a kijelölő keret folyamatosan a személy előtt van, akkor az értéket növelni kell.", + "toast": { + "success": "A(z) {{camera}} jelöléseltolása el lett mentve a konfigurációs fájlba. Indítsa újra a Frigate-et a módosítások alkalmazásához." + } + }, + "showAllZones": { + "desc": "Mindig mutasd a képen a zónákat, ahol a tárgy belépett a zónába.", + "title": "Mutasd az Összes Zónát" + }, + "title": "Jelölés beállításai" + }, + "carousel": { + "next": "Következő dia", + "previous": "Előző dia" + }, + "trackedPoint": "Követett Pont", + "count": "{{first}}-nek a {{second}}", + "autoTrackingTips": "A doboz helyzete pontatlan lesz az automatikusan követő kamerákhoz.", + "adjustAnnotationSettings": "Címkézés beállításainak módosítása" + }, + "type": { + "video": "videó", + "object_lifecycle": "tárgy életciklus", + "details": "részletek", + "snapshot": "pillanatfelvétel" + }, + "trackedObjectDetails": "Követett Tárgy Részletei", + "exploreMore": "Fedezzen fel több {{label}} tárgyat", + "aiAnalysis": { + "title": "MI-elemzés" + }, + "concerns": { + "label": "Aggodalmak" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/exports.json new file mode 100644 index 0000000..ab07aba --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportálás - Frigate", + "search": "Keresés", + "noExports": "Export nem található", + "deleteExport.desc": "Biztos, hogy törölni akarja {{exportName}}-t?", + "deleteExport": "Export törlése", + "editExport": { + "title": "Exportálás átnevezése", + "desc": "Adjon meg egy új nevet ennek az exportnak.", + "saveExport": "Export mentése" + }, + "toast": { + "error": { + "renameExportFailed": "Sikertelen export átnevezés: {{errorMessage}}" + } + }, + "tooltip": { + "downloadVideo": "Videó letöltése", + "editName": "Név szerkesztése", + "deleteExport": "Export törlése", + "shareExport": "Export megosztása" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/faceLibrary.json new file mode 100644 index 0000000..4f9331f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/faceLibrary.json @@ -0,0 +1,99 @@ +{ + "renameFace": { + "title": "Arc átnevezése", + "desc": "Adjon meg egy új nevet neki: {{name}}" + }, + "details": { + "subLabelScore": "Alcimke érték", + "unknown": "Ismeretlen", + "person": "Ember", + "timestamp": "Időbélyeg", + "face": "Arc részletek", + "faceDesc": "A követett tárgy részletei, amely alapján ez az arc létrejött", + "scoreInfo": "Az alcímke pontszáma az összes felismert arc pontozásának súlyozott átlaga, ezért ez eltérhet a pillanatképen megjelenített pontszámtól." + }, + "button": { + "deleteFace": "Arc törlése", + "renameFace": "Arc átnevezése", + "deleteFaceAttempts": "Arcok törlése", + "addFace": "Arc hozzáadása", + "uploadImage": "Kép feltöltése", + "reprocessFace": "Arc Újrafeldolgozása" + }, + "collections": "Gyűjtemények", + "steps": { + "description": { + "uploadFace": "Töltsön fel egy képet {{name}}-ről amin szemből látható. A képen nem szükséges, hogy csak az arc legyen látható." + }, + "faceName": "Adjon meg egy arcnevet", + "uploadFace": "Arckép feltöltése", + "nextSteps": "Következő lépések" + }, + "deleteFaceAttempts": { + "title": "Arcok törlése", + "desc_one": "Biztos benne, hogy törölni akar {{count}} arcot? Ezt már nem tudja visszavonni.", + "desc_other": "Biztos benne, hogy törölni akar {{count}} arcot? Ezt már nem tudja visszavonni." + }, + "uploadFaceImage": { + "title": "Arckép feltöltése", + "desc": "Töltsön fel egy képet, hogy beolvasson arcokat és beillessze {{pageToggle}}-ba/-be" + }, + "createFaceLibrary": { + "title": "Gyűjtemény létrehozása", + "desc": "Új gyűjtemény létrehozása", + "new": "Új arc létrhozása", + "nextSteps": "A jó alap készítéséhez:
  • Használja a Legutóbbi felismerések fület az egyes észlelt személyekhez tartozó képek kiválasztásához és betanításához.
  • A legjobb eredmény érdekében válassza az egyenesen előre néző arcokat ábrázoló képeket és kerülje a ferde szögből készült arcképeket a tanításhoz." + }, + "description": { + "placeholder": "Adj nevet ennek a gyűjteménynek", + "invalidName": "Nem megfelelő név. A nevek csak betűket, számokat, szóközöket, aposztrófokat, alulhúzásokat és kötőjeleket tartalmazhatnak.", + "addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével." + }, + "selectFace": "Arc kiválasztása", + "deleteFaceLibrary": { + "title": "Név törlése", + "desc": "Biztosan törölni akarja a {{name}} gyűjteményt? Ezzel véglegesen törli a párosított arcokat." + }, + "imageEntry": { + "dropActive": "Húzza a képet ide…", + "validation": { + "selectImage": "Kérem válasszon egy képet." + }, + "maxSize": "Maximális méret: {{size}} MB", + "dropInstructions": "Fogja és húzza a képet ide, vagy kattintson a kiválasztáshoz" + }, + "trainFaceAs": "Arc tanítása mint:", + "trainFace": "Arc tanítása", + "toast": { + "success": { + "addFaceLibrary": "{{name}} sikeresen hozzáadva a az Arc Könyvtárhoz!", + "uploadedImage": "A kép sikeresen feltöltve.", + "deletedName_one": "{{count}} arc sikeresen törölve.", + "deletedName_other": "{{count}} arc sikeresen törölve.", + "renamedFace": "Arc sikeresen átnvezezve {{name}}-ra/-re", + "updatedFaceScore": "Arc pontszáma sikeresen frissítve.", + "trainedFace": "Arc sikeresen betanítva.", + "deletedFace_one": "{{count}} arc sikeresen törölve.", + "deletedFace_other": "{{count}} arc sikeresen törölve." + }, + "error": { + "addFaceLibraryFailed": "Arc névadás sikertelen: {{errorMessage}}", + "deleteFaceFailed": "Törlés sikertelen: {{errorMessage}}", + "deleteNameFailed": "Név törlése sikertelen: {{errorMessage}}", + "uploadingImageFailed": "Kép feltöltése sikertelen: {{errorMessage}}", + "updateFaceScoreFailed": "Arc pontszám frissítése sikertelen: {{errorMessage}}", + "renameFaceFailed": "Arc átnevezése sikertelen: {{errorMessage}}", + "trainFailed": "Sikertelen tanítás: {{errorMessage}}" + } + }, + "readTheDocs": "Olvassa el a dokumentációt", + "nofaces": "Nincs elérhető arc", + "documentTitle": "Arc könyvtár - Frigate", + "train": { + "title": "Tanít", + "empty": "Nincs friss arcfelismerés", + "aria": "Válassza ki a tanítást" + }, + "pixels": "{{area}}px", + "selectItem": "KIválasztani {{item}}-et" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/live.json new file mode 100644 index 0000000..b7a5ff9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/live.json @@ -0,0 +1,185 @@ +{ + "twoWayTalk": { + "enable": "Kétirányú kommunikáció engedélyezése", + "disable": "Kétirányú kommunikáció tiltása" + }, + "documentTitle": "Élő - Frigate", + "lowBandwidthMode": "Alacsony felbontású mód", + "documentTitle.withCamera": "{{camera}} - Élő - Frigate", + "cameraAudio": { + "disable": "Kamera hangjának kikapcsolása", + "enable": "Kamera hangjának bekapcsolása" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kattintson a képre a kamera középre igazításához", + "enable": "Engedélyezze a kattintást a mozgatáshoz", + "disable": "Kattintással húzás kikapcsolása" + }, + "left": { + "label": "PTZ kamera balra mozgatása" + }, + "up": { + "label": "PTZ kamera felfele mozgatása" + }, + "down": { + "label": "PTZ kamera lefele mozgatása" + }, + "right": { + "label": "PTZ kamera jobbra mozgatása" + } + }, + "zoom": { + "in": { + "label": "PTZ kamera közelítés" + }, + "out": { + "label": "PTZ kamera távolodás" + } + }, + "frame": { + "center": { + "label": "Kattinston a képre a PTZ kamera középre igazításához" + } + }, + "presets": "PTZ kamera előzetes beállításai", + "focus": { + "in": { + "label": "PTZ kamera fókuszálás BE" + }, + "out": { + "label": "PTZ kamera fókuszálás KI" + } + } + }, + "camera": { + "enable": "Kamera Engedélyezése", + "disable": "Kamera Kikapcsolása" + }, + "muteCameras": { + "enable": "Minden Kamera Némítása", + "disable": "Minden Kamera Felhangosítása" + }, + "detect": { + "enable": "Észlelés Engedélyezése", + "disable": "Észlelés Kikapcsolása" + }, + "recording": { + "enable": "Felvétel Készítés Engedélyezése", + "disable": "Felvétel Készítés Kikapcsolása" + }, + "snapshots": { + "enable": "Pillanatfelvétel Engedélyezése", + "disable": "Pillanatfelvétel Kikapcsolása" + }, + "audioDetect": { + "enable": "Hang Észlelés Engedélyezése", + "disable": "Hang Észlelés Kikapcsolása" + }, + "autotracking": { + "enable": "Automatikus Követés Engedélyezése", + "disable": "Automatikus Követés Kikapcsolása" + }, + "streamStats": { + "enable": "Adás Statisztika Mutatása", + "disable": "Adás Statisztika Elrejtése" + }, + "manualRecording": { + "playInBackground": { + "label": "Lejátszás a háttérben", + "desc": "Engedélyezze ezt a lehetőséget, hogy az adás tovább folyjon amikor a lejátszó rejtve van." + }, + "showStats": { + "label": "Statisztika Mutatása", + "desc": "Engedélyezze ezt az opciót, hogy a közvetítés statisztikái átfedésként megjelenjenek a kameraképen." + }, + "debugView": "Hibakeresési Nézet", + "title": "Igény Szerinti Felvétel", + "start": "Igény szerinti felvétel indítása", + "started": "Kézi igény szerinti felvétel elindítva.", + "failedToStart": "Kézi igény szerinti felvétel indítása sikertelen.", + "end": "Igény szerinti felvétel befejezése", + "ended": "Kézi igény szerinti felvétel befejezve.", + "failedToEnd": "Kézi igény szerinti felvétel befejezése sikertelen.", + "tips": "Indítson el egy manuális eseményt a kamera felvételmegőrzési beállításai alapján.", + "recordDisabledTips": "Mivel a rögzítés le van tiltva vagy korlátozva van a konfigurációban ennél a kameránál, csak egy pillanatkép kerül mentésre." + }, + "streamingSettings": "Adás Beállítások", + "notifications": "Értesítések", + "audio": "Hang", + "suspend": { + "forTime": "Felfüggesztés: " + }, + "stream": { + "title": "Adás", + "audio": { + "tips": { + "documentation": "Olvassa el a leírást ", + "title": "A hangot a kamerának kell kibocsátania, és a go2rtc-ben kell konfigurálni ehhez a streamhez." + }, + "available": "Ehhez az adáshoz hang elérhető", + "unavailable": "Nem elérhető hang ehhez az adáshoz" + }, + "twoWayTalk": { + "tips.documentation": "Olvassa el a leírást ", + "available": "Kétirányú kommunikáció elérhető ehhez az adáshoz", + "unavailable": "Nem elérhető a kétirányú kommunikáció ehhez az adáshoz", + "tips": "Az eszközének támogatnia kell ezt a funkciót, és a WebRTC-nek kétirányú beszélgetésre kell lennie konfigurálva." + }, + "lowBandwidth": { + "resetStream": "Adás visszaállítása", + "tips": "Az élő nézet alacsony sávszélességű módban van a pufferelés vagy stream hibák miatt." + }, + "playInBackground": { + "label": "Lejátszás a háttérben", + "tips": "Engedélyezze ezt az opciót a folyamatos közvetítéshez akkor is, ha a lejátszó rejtve van." + }, + "debug": { + "picker": "A stream kiválasztása nem érhető el hibakeresési módban. A hibakeresési nézet mindig az észlelési szerepkörhöz rendelt streamet használja." + } + }, + "cameraSettings": { + "title": "{{camera}} Beállítások", + "cameraEnabled": "Kamera Engedélyezve", + "objectDetection": "Tárgy Észlelés", + "recording": "Felvétel", + "audioDetection": "Hang Észlelés", + "snapshots": "Pillanatképek", + "autotracking": "Automatikus követés", + "transcription": "Hang Feliratozás" + }, + "history": { + "label": "Előzmény felvételek megjelenítése" + }, + "effectiveRetainMode": { + "modes": { + "all": "Mind", + "motion": "Mozgás", + "active_objects": "Aktív objektumok" + }, + "notAllTips": "Az Ön {{source}} felvételmegőrzési beállítása akövetkezőre van állítva mode: {{effectiveRetainMode}}, így ez az igény szerinti felvétel csak a {{effectiveRetainModeName}} szegmenseket őrzi meg." + }, + "editLayout": { + "label": "Elrendezés szerkesztése", + "group": { + "label": "Kameracsoport szerkesztése" + }, + "exitEdit": "Szerkesztés bezárása" + }, + "transcription": { + "enable": "Élő Audio Feliratozás Engedélyezése", + "disable": "Élő Audio Feliratozás Kikapcsolása" + }, + "noCameras": { + "title": "Nincsenek kamerák beállítva", + "description": "Kezdje egy kamera csatlakoztatásával.", + "buttonText": "Kamera hozzáadása" + }, + "snapshot": { + "takeSnapshot": "Azonnali pillanatkép letöltése", + "noVideoSource": "Ehhez a pillanatképhez videó forrás nem elérhető.", + "captureFailed": "Pillanatkép készítése sikertelen.", + "downloadStarted": "Pillanatkép letöltése elindítva." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/recording.json new file mode 100644 index 0000000..67aac28 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Szűrő", + "calendar": "Naptár", + "export": "Exportálás", + "filters": "Szűrők", + "toast": { + "error": { + "noValidTimeSelected": "Nem megfelelő idősáv kiválasztva", + "endTimeMustAfterStartTime": "A végpontnak később kell lennie, mint a kezdőpontnak" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/search.json new file mode 100644 index 0000000..185a060 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Keresés", + "savedSearches": "Mentett keresések", + "searchFor": "{{inputValue}} keresése", + "button": { + "clear": "Keresés törlése", + "filterInformation": "Szűrő információ", + "save": "Keresés mentése", + "delete": "Mentett keresések törlése", + "filterActive": "Aktív szűrők" + }, + "trackedObjectId": "Követett Tárgy Azonosító", + "filter": { + "label": { + "cameras": "Kamerák", + "labels": "Cimkék", + "zones": "Zónák", + "sub_labels": "Alcimkék", + "search_type": "Keresés típusa", + "time_range": "Idő intervallum", + "before": "Előtte", + "after": "Utána", + "min_score": "Minimum Pont", + "max_score": "Maximális Pont", + "min_speed": "Minimum Sebesség", + "max_speed": "Maximális Sebesség", + "recognized_license_plate": "Felismert Rendszám", + "has_clip": "Van Klip", + "has_snapshot": "Van pillanatképe" + }, + "searchType": { + "description": "Leírás", + "thumbnail": "Bélyegkép" + }, + "tips": { + "title": "Hogyan használja a szöveg szűrőket", + "desc": { + "exampleLabel": "Példa:", + "step6": "Törölje a szűrőket az 'x'-re kattintva mellettük.", + "step1": "Írjon be egy szűrő kulcs nevet kettősponttal végződően (pl.: \"kamerák:\").", + "step5": "Az időbeli szűrő {{exampleTime}} formátumot használ.", + "step4": "A dátum szűrők (előtte: és utána:) {{DateFormat}} formátumban vannak.", + "text": "A szűrők segítenek szűkíteni a keresési eredményeket. Így használhatja őket a beviteli mezőben:", + "step2": "Válasszon egy értéket a lehetőségek közül, vagy írja be a sajátját.", + "step3": "Több szűrőt is használhat, ha azokat egymás után, szóközzel elválasztva adja meg." + } + }, + "header": { + "currentFilterType": "Szűrő Értékek", + "noFilters": "Szűrők", + "activeFilters": "Aktív Szűrők" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A 'kezdeti' dátumnak később kell lennie, mint a 'későbbi' dátumnak.", + "afterDatebeEarlierBefore": "Az 'későbbi' dátumnam korábban kell lennie, mint az 'előtte' dátumnak.", + "minScoreMustBeLessOrEqualMaxScore": "A 'min_pontszám'-nak kevesebbnek vagy ugyanannyinak kell lennie, mint a 'max_pontszám'.", + "maxScoreMustBeGreaterOrEqualMinScore": "A 'max_pontszám'-nak nagyobb vagy egyenlőnek kell lennie, mint a 'min_pontszám'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "A 'min_sebesség'-nek mindenképp kevesebbnek vagy azonosnak kell lennie, mint a 'max_sebesség'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "A 'max_speed' értékének nagyobbnak vagy egyenlőnek kell lennie a 'min_speed' értékéhez képest." + } + } + }, + "similaritySearch": { + "active": "Hasonlóság keresés aktív", + "title": "Hasonlóság Keresés", + "clear": "Hasonlósági keresés törlése" + }, + "placeholder": { + "search": "Keresés…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/settings.json new file mode 100644 index 0000000..c36e9a5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/settings.json @@ -0,0 +1,853 @@ +{ + "documentTitle": { + "default": "Beállítások - Frigate", + "authentication": "Hitelesítési beállítások - Frigate", + "camera": "Kamera beállítások - Frigate", + "classification": "Osztályozási beállítások - Frigate", + "masksAndZones": "Maszk és zónaszerkesztő - Frigate", + "object": "Hibakeresés - Frigate", + "general": "Áltlános Beállítások - Frigate", + "frigatePlus": "Frigate+ beállítások - Frigate", + "notifications": "Értesítések beállítása - Frigate", + "motionTuner": "Mozgás Hangoló - Frigate", + "enrichments": "Kiegészítés Beállítások - Frigate", + "cameraManagement": "Kamerák kezelése - Frigate", + "cameraReview": "Kamera beállítások áttekintése – Frigate" + }, + "menu": { + "ui": "UI", + "classification": "Osztályozás", + "cameras": "Kamera beállítások", + "masksAndZones": "Maszkok / Zónák", + "motionTuner": "Mozgás finomhangolása", + "debug": "Hibakeresés", + "users": "Felhasználók", + "notifications": "Értesítések", + "frigateplus": "Frigate+", + "enrichments": "Extra funkciók", + "triggers": "Triggerek", + "roles": "Szerepkörök", + "cameraManagement": "Menedzsment", + "cameraReview": "Vizsgálat" + }, + "dialog": { + "unsavedChanges": { + "title": "Vannak nem mentett módosítások.", + "desc": "Szeretné elmenteni a módosításokat a folytatás előtt?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nincs kamera" + }, + "general": { + "liveDashboard": { + "title": "Live irányítópult", + "automaticLiveView": { + "label": "Automata élőkép", + "desc": "Automatikusan váltson át a kamera élő nézetére, amikor aktivitást észlel. Ha ez az opció ki van kapcsolva, akkor az Élő irányítópulton a statikus kameraképek csak percenként egyszer frissülnek." + }, + "playAlertVideos": { + "label": "Riasztási Videók Lejátszása", + "desc": "Alapértelmezetten az Élő irányítópulton a legutóbbi riasztások kis, ismétlődő videóként jelennek meg. Kapcsolja ki ezt az opciót, ha csak állóképet szeretne megjeleníteni a legutóbbi riasztásokról ezen az eszközön/böngészőben." + } + }, + "title": "Alapbeállítások", + "cameraGroupStreaming": { + "title": "Kamera Csoport Adás Beállítások", + "clearAll": "Minden Adás Beállítás Törlése", + "desc": "Az egyes kameracsoportok közvetítési beállításai a böngésző helyi tárolójában kerülnek mentésre." + }, + "recordingsViewer": { + "title": "Felvétel Néző", + "defaultPlaybackRate": { + "label": "Alap Lejátszási Sebesség", + "desc": "Alapértelmezett lejátszási sebesség a felvételek visszajátszásához." + } + }, + "calendar": { + "title": "Naptár", + "firstWeekday": { + "label": "Első Hétköznap", + "sunday": "Vasárnap", + "monday": "Hétfő", + "desc": "A nap, amivel az áttekintési naptár hete kezdődik." + } + }, + "toast": { + "error": { + "clearStreamingSettingsFailed": "Adásbeállítások törlése sikertelen: {{errorMessage}}", + "clearStoredLayoutFailed": "Nem sikerült törölni a mentett elrendezést: {{errorMessage}}" + }, + "success": { + "clearStoredLayout": "Mentett elrendezés törölve ehhez: {{cameraName}}", + "clearStreamingSettings": "Az összes kameracsoport közvetítési beállítása törölve." + } + }, + "storedLayouts": { + "title": "Tárolt Elrendezések", + "desc": "A kameracsoporton belüli kamerák elrendezése áthúzható és átméretezhető. A pozíciók a böngésző helyi tárolójában kerülnek mentésre.", + "clearAll": "Összes elrendezés törlése" + } + }, + "enrichments": { + "semanticSearch": { + "readTheDocumentation": "Olvassa el a Dokumentációt", + "reindexNow": { + "error": "Újraindexálás megkezdése sikertelen: {{errorMessage}}", + "label": "Újraindexálás Most", + "confirmTitle": "Újraindexálás Megerősítése", + "confirmButton": "Újraindexálás", + "success": "Újraindexálás sikeresen megkezdődött.", + "alreadyInProgress": "Újraindexálás már folyamatban.", + "desc": "Az újraindexelés újragenerálja az összes követett objektum beágyazásait. Ez a folyamat a háttérben fut, és a követett objektumok számától függően maximálisan kihasználhatja a CPU-t, valamint jelentős időt vehet igénybe.", + "confirmDesc": "Biztosan újra szeretné indexelni az összes követett objektum beágyazását? Ez a folyamat a háttérben fut, de maximálisan kihasználhatja a CPU-t, és jelentős időt vehet igénybe. A folyamat állapotát az Áttekintés oldalon követheti nyomon." + }, + "modelSize": { + "label": "Modell méret", + "small": { + "title": "kicsi", + "desc": "A small használata egy kvantált modellverziót alkalmaz, amely kevesebb memóriát használ és gyorsabban fut a CPU-n, miközben a beágyazás minősége alig észrevehető mértékben változik." + }, + "large": { + "title": "nagy", + "desc": "A large használata a teljes Jina modellt alkalmazza, és automatikusan a GPU-n futtatja, ha ez elérhető." + }, + "desc": "A szemantikus keresés beágyazásaihoz használt modell mérete." + }, + "title": "Szemantikus keresés", + "desc": "A Frigate szemantikus keresés funkciója lehetővé teszi, hogy a felülvizsgálati elemekben szereplő követett objektumokat megtalálja akár magával a képpel, egy felhasználó által megadott szöveges leírással, vagy egy automatikusan generált leírással." + }, + "birdClassification": { + "title": "Madár Osztályozás", + "desc": "A madárfelismerés egy kvantált Tensorflow modell segítségével azonosít ismert madarakat. Ha egy ismert madarat felismer, annak közismert neve sub_label jelölésként kerül hozzáadásra. Ez az információ megjelenik a felhasználói felületen, a szűrőkben, valamint az értesítésekben is." + }, + "faceRecognition": { + "title": "Arcfelismerés", + "readTheDocumentation": "Olvassa el a Dokumentációt", + "modelSize": { + "label": "Modell Mérete", + "desc": "Az arcfelismeréshez használt modell mérete.", + "small": { + "title": "kis", + "desc": "A small használata egy FaceNet arcbeágyazási modellt alkalmaz, amely a legtöbb CPU-n hatékonyan fut." + }, + "large": { + "title": "nagy", + "desc": "A large használata egy ArcFace arcbeágyazási modellt alkalmaz, és automatikusan a GPU-n fut, ha az elérhető." + } + }, + "desc": "Az arcfelismerés lehetővé teszi, hogy személyekhez neveket rendeljenek, és amikor az arcukat felismeri a Frigate, akkor a személy nevét alcímkeként rendeli hozzá. Ez az információ megjelenik a felhasználói felületen, a szűrőkben és az értesítésekben is." + }, + "licensePlateRecognition": { + "title": "Rendszámtábla Felismerés", + "readTheDocumentation": "Olvassa el a Dokumentációt", + "desc": "A Frigate képes felismerni a járművek rendszámtábláit, és automatikusan hozzáadja a felismert karaktereket a recognized_license_plate mezőhöz, vagy egy ismert nevet al_címkeként rendel az autótípusú objektumokhoz. Egy gyakori felhasználási eset lehet a bejáróra behajtó vagy az utcán elhaladó autók rendszámtáblájának olvasása." + }, + "toast": { + "error": "Konfigurációs változtatások mentése sikertelen: {{errorMessage}}", + "success": "A kiegészítő beállítások elmentésre kerültek. A módosítások alkalmazásához indítsa újra a Frigate-et." + }, + "unsavedChanges": "Mentetlen gazdagítási beállítás változtatások", + "title": "Kiegészítők Beállítása", + "restart_required": "Újraindítás szükséges (a kiegészítő beállítások megváltoztak)" + }, + "notification": { + "title": "Értesítések", + "notificationSettings": { + "documentation": "Olvassa el a Dokumentációt", + "title": "Értesítési Beállítások", + "desc": "A Frigate natívan képes push értesítéseket küldeni az eszközére, ha böngészőben fut vagy PWA-ként van telepítve." + }, + "globalSettings": { + "title": "Globális Beállítások", + "desc": "Ideiglenesen függessze fel az értesítéseket meghatározott kamerákhoz az összes regisztrált eszközön." + }, + "email": { + "title": "E-mail", + "placeholder": "Példa example@email.com", + "desc": "Érvényes e-mail cím szükséges, amelyre értesítést küldünk, ha problémák adódnak a push szolgáltatással." + }, + "suspended": "Értesítések felfüggesztve {{time}}", + "active": "Értesítések Bekapcsolva", + "suspendTime": { + "5minutes": "Felfüggesztés 5 percre", + "24hours": "Felfüggesztés 24 órára", + "suspend": "Felfüggeszt", + "12hours": "Felfüggesztés 12 órára", + "10minutes": "Felfüggesztés 10 percre", + "1hour": "Felfüggesztés 1 órára", + "30minutes": "Felfüggesztés 30 percre", + "untilRestart": "Felfüggesztés újraindításig" + }, + "toast": { + "success": { + "settingSaved": "Értesítési beállítások elmentve.", + "registered": "Sikeresen regisztrált az értesítések fogadására. Az értesítések (beleértve a tesztértesítést is) küldése előtt újra kell indítani a Frigate-et." + }, + "error": { + "registerFailed": "Nem sikerült elmenteni az értesítési regisztrációt." + } + }, + "notificationUnavailable": { + "title": "Értesítés elérhetetlen", + "documentation": "Olvassa el a Dokumentációt", + "desc": "A webes push értesítésekhez biztonságos környezet (https://…) szükséges. Ez egy böngészői korlátozás. A értesítések használatához férjen hozzá biztonságosan a Frigate-hez." + }, + "sendTestNotification": "Teszt értesítés küldése", + "deviceSpecific": "Eszköz Specifikus Beállítások", + "cameras": { + "desc": "Válassza ki mely kamerákhoz engedélyezi az értesítéseket.", + "noCameras": "Nincs elérhető kamera", + "title": "Kamerák" + }, + "unsavedChanges": "Mentetlen Értesítés változtatások", + "cancelSuspension": "Felfüggesztés megszüntetése", + "registerDevice": "Regisztráld ezt az eszközt", + "unregisterDevice": "Eszköz regisztrációjának törlése", + "unsavedRegistrations": "Nem mentett értesítési regisztrációk" + }, + "frigatePlus": { + "modelInfo": { + "error": "Modell információ betöltése sikertelen", + "plusModelType": { + "baseModel": "Alap modell", + "userModel": "Finomhangolt" + }, + "supportedDetectors": "Támogatott Érzékelők", + "baseModel": "Alap modell", + "loadingAvailableModels": "Elérhető modellek betöltése…", + "loading": "Modell információ betöltése…", + "availableModels": "Elérhető modellek", + "modelType": "Modell típus", + "title": "Modell Információ", + "cameras": "Kamerák", + "trainDate": "Tanítás dátum", + "modelSelect": "A Frigate+-on elérhető modelljeit itt választhatja ki. Figyelem: csak az aktuális érzékelő konfigurációval kompatibilis modellek választhatók." + }, + "apiKey": { + "plusLink": "Tudjon meg többet a Frigate+-ról", + "title": "Frigate+ API kulcs", + "validated": "Frigate+ API kulcs észlelve és ellenőrizve", + "notValidated": "Frigate+ API kulcs nem található vagy nem ellenőrzött", + "desc": "A Frigate+ API kulcs lehetővé teszi az integrációt a Frigate+ szolgáltatással." + }, + "snapshotConfig": { + "title": "Pillanatkép Konfiguráció", + "table": { + "snapshots": "Pillanatképek", + "camera": "Kamera", + "cleanCopySnapshots": "clean_copy pillanatképek" + }, + "documentation": "Olvassa el a dokumentációt", + "desc": "A Frigate+ szolgáltatásba történő beküldéshez a konfigurációban engedélyezni kell a pillanatképeket és a clean_copy pillanatképeket is.", + "cleanCopyWarning": "Néhány kamerán engedélyezve vannak a pillanatképek, de a tiszta másolat ki van kapcsolva. Ahhoz, hogy ezekről a kamerákról képeket tudjon beküldeni a Frigate+ szolgáltatásba, engedélyeznie kell a clean_copy beállítást a pillanatkép konfigurációban." + }, + "restart_required": "Újraindítás szükséges ( Frigate+ modell megváltozott)", + "toast": { + "success": "Frigate+ beállítások elmentve. Indítsa újra a Frigate-et a változtatások érvényrejutásához.", + "error": "Konfigurációs módosítások mentése sikertelen: {{errorMessage}}" + }, + "title": "Frigate+ Beállítások", + "unsavedChanges": "Mentetlen Frigate+ beállítás változtatások" + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "viewer": "Néző", + "admin": "Adminisztrátor", + "intro": "Válassza ki a megfelelő szerepkört ehhez a felhasználóhoz:", + "adminDesc": "Teljes hozzáférés az összes funkcióhoz.", + "viewerDesc": "Csak az Élő irányítópultokhoz, Ellenőrzéshez, Felfedezéshez és Exportokhoz korlátozva.", + "customDesc": "Egyéni szerepkör meghatározott kamerahozzáféréssel." + }, + "title": "Felhasználói szerepkör módosítása", + "select": "Válasszon szerepkört", + "desc": "Engedélyek frissítése {{username}} számára" + }, + "form": { + "user": { + "desc": "Csak betűk, számok, pontok és alulhúzások engedélyezettek.", + "title": "Felhasználói név", + "placeholder": "Adja meg a felhasználói nevet" + }, + "password": { + "match": "A jelszavak egyeznek", + "strength": { + "weak": "Gyenge", + "veryStrong": "Nagyon Erős", + "strong": "Erős", + "medium": "Közepes", + "title": "Jelszó erőssége: " + }, + "placeholder": "Adja meg a jelszót", + "title": "Jelszó", + "confirm": { + "placeholder": "Erősítse meg Jelszavát", + "title": "Erősítse meg Jelszavát" + }, + "notMatch": "A jelszavak nem egyeznek" + }, + "newPassword": { + "title": "Új Jelszó", + "placeholder": "Adjon meg egy új jelszót", + "confirm": { + "placeholder": "Adja meg ismét az új jelszót" + } + }, + "usernameIsRequired": "Felhasználói név szükséges", + "passwordIsRequired": "Jelszó szükséges" + }, + "createUser": { + "usernameOnlyInclude": "A felhasználói név csak betűket, számokat, .-t vagy _-t tartalmazhat", + "title": "Új Felhasználó Létrehozása", + "confirmPassword": "Erősítse meg a jelszavát", + "desc": "Új felhasználói fiók hozzáadása, és egy szerepkör megadása a Frigate felhasználói felületének területeihez való hozzáféréshez." + }, + "deleteUser": { + "title": "Felhasználó Törlése", + "warn": "Biztosan törölni akarja {{username}}-t?", + "desc": "Ez a művelet nem vonható vissza. Ez véglegesen törli a felhasználói fiókot és minden kapcsolódó adatot." + }, + "passwordSetting": { + "setPassword": "Jelszó Megadása", + "doNotMatch": "Jelszavak nem egyeznek", + "cannotBeEmpty": "A jelszó nem lehet üres", + "updatePassword": "{{username}} Jelszavának Módosítása", + "desc": "Hozzon létre erős jelszót a fiók védelméhez." + } + }, + "table": { + "noUsers": "Felhasználók nem találhatóak.", + "username": "Felhasználói név", + "password": "Jelszó", + "deleteUser": "Felhasználó törlése", + "actions": "Akciók", + "role": "Szerepkör", + "changeRole": "felhasználói szerepkör módosítása" + }, + "toast": { + "error": { + "setPasswordFailed": "Jelszó mentése sikertelen: {{errorMessage}}", + "deleteUserFailed": "Felhasználó törlése sikertelen: {{errorMessage}}", + "createUserFailed": "Felhasználó létrehozása sikertelen: {{errorMessage}}", + "roleUpdateFailed": "Nem sikerült frissíteni a szerepkört: {{errorMessage}}" + }, + "success": { + "updatePassword": "Jelszó sikeresen módosítva.", + "deleteUser": "{{user}} felhasználó sikeresen törölve", + "createUser": "{{user}} felhasználó sikeresen létrehozva", + "roleUpdated": "A szerepkör frissítve lett a(z) {{user}} számára" + } + }, + "addUser": "Felhasználó hozzáadása", + "updatePassword": "Jelszó Módosítása", + "management": { + "desc": "Ezen Frigate példány felhasználóinak kezelése.", + "title": "Felhasználó kezelés" + }, + "title": "Felhasználók" + }, + "masksAndZones": { + "zones": { + "desc": { + "documentation": "Dokumentáció", + "title": "A zónák lehetővé teszik, hogy meghatározzon egy adott területet a képkockán belül, így eldöntheti, hogy egy objektum egy adott területen belül van-e vagy sem." + }, + "edit": "Zóne Módosítása", + "label": "Zónák", + "documentTitle": "Zóna Módosítása - Frigate", + "name": { + "title": "Név", + "tips": "A névnek legalább 2 karakterből kell állnia és nem a kamera neve vagy egy másik zónáé.", + "inputPlaceHolder": "Adjon meg egy nevet…" + }, + "objects": { + "title": "Tárgyak", + "desc": "Adott zónára vonatkozó tárgyak listája." + }, + "add": "Zóna Hozzáadása", + "allObjects": "Minden Tárgy", + "speedEstimation": { + "title": "Sebesség Becslés", + "docs": "Olvassa el a dokumentációt", + "lineADistance": "A vonal távsolság {{unit}}", + "lineBDistance": "B vonal távolsága {{unit}}", + "lineCDistance": "C vonal távolsága {{unit}}", + "lineDDistance": "D vonal távolsága {{unit}}", + "desc": "Engedélyezze a sebességbecslést a zónában lévő objektumokra. A zónának pontosan 4 pontból kell állnia." + }, + "speedThreshold": { + "title": "Sebesség Határérték {{unit}}", + "desc": "Megadja a minimális sebességet, amely felett az objektumokat ebben a zónában figyelembe kell venni.", + "toast": { + "error": { + "pointLengthError": "A sebességbecslés le lett tiltva ennél a zónánál. A sebességbecslést tartalmazó zónáknak pontosan 4 pontból kell állniuk.", + "loiteringTimeError": "Az olyan zónák, amelyeknél a tartózkodási idő nagyobb, mint 0, nem használhatók sebességbecsléssel együtt." + } + } + }, + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "clickDrawPolygon": "Kattintson a sokszög rajzoláshoz a képen.", + "inertia": { + "title": "Inercia", + "desc": "Megadja, hogy hány képkockán keresztül kell egy objektumnak a zónában lennie, mielőtt azt a zónán belülinek tekintenénk. Alapértelmezett: 3" + }, + "loiteringTime": { + "title": "Lebzselési idő", + "desc": "Beállítja a zónában tartózkodás minimális idejét másodpercben, amelynek teljesülnie kell az aktiváláshoz. Alapértelmezett: 0" + }, + "toast": { + "success": "A(z) {{zoneName}} zóna elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + }, + "objectMasks": { + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "label": "Tárgymaszkok", + "desc": { + "documentation": "Dokumentáció", + "title": "Az objektumszűrő maszkokat arra használják, hogy hely alapján kiszűrjék a téves egyezéseket adott objektumtípus esetén." + }, + "documentTitle": "Tárgymaszk módosítása - Frigate", + "objects": { + "title": "Tárgyak", + "allObjectTypes": "Minden tárgytípus", + "desc": "Az objektumtípus, amelyre ez az objektummaszk vonatkozik." + }, + "edit": "Tárgymaszk módosítása", + "add": "Tárgymaszk hozzáadása", + "context": "Az objektumszűrő maszkokat arra használják, hogy hely alapján kiszűrjék az adott objektumtípus téves egyezéseit.", + "clickDrawPolygon": "Kattintson a képre, hogy poligont rajzoljon.", + "toast": { + "success": { + "title": "A(z) {{polygonName}} elmentve. A módosítások alkalmazásához indítsa újra a Frigate-et.", + "noName": "Az objektummaszk elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + } + }, + "form": { + "polygonDrawing": { + "delete": { + "title": "Törlés Megerősítése", + "desc": "Biztosan törölni akarja {{type}}{{name}}-t?", + "success": "{{name}} törölve." + }, + "error": { + "mustBeFinished": "A sokszög rajzolásnak be kell fejeződnie mentés előtt." + }, + "removeLastPoint": "Utolsó pont törlése", + "reset": { + "label": "Minden pont törlése" + }, + "snapPoints": { + "true": "Képkivágási pontok", + "false": "Ne rögzítse a pontokat" + } + }, + "zoneName": { + "error": { + "hasIllegalCharacter": "A zóna neve olyan karaktert tartalmaz, ami nem megengedett.", + "mustBeAtLeastTwoCharacters": "A zóna neve legalább 2 karakterből kell, hogy álljon.", + "mustNotBeSameWithCamera": "A zóna neve nem egyezhet meg a kamera nevével.", + "alreadyExists": "Egy zóna ezen a néven már létezik ennél a kameránál.", + "mustNotContainPeriod": "A zóna neve nem tartalmazhat pontot." + } + }, + "distance": { + "error": { + "text": "A távolságnak nagyobb vagy egyenlőnek kell lennie, mint 0.1.", + "mustBeFilled": "A sebességbecslés használatához minden távolságmezőt ki kell tölteni." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A tehetetlenségnek nagyobbnak kell lennie 0-nál." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "A tartózkodási időnek nagyobbnak vagy egyenlőnek kell lennie 0-hoz képest." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "A sebességküszöbnek nagyobbnak vagy egyenlőnek kell lennie 0.1-hez képest." + } + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentáció", + "title": "A mozgásmaszkokat arra használják, hogy megakadályozzák a nem kívánt mozgástípusok észlelését. A túlzott maszkolás megnehezíti az objektumok követését." + }, + "context": { + "documentation": "Olvassa el a dokumentációt", + "title": "A mozgásmaszkokat arra használják, hogy megakadályozzák a nem kívánt mozgástípusok észlelését (például faágak, kamera időbélyegek). A mozgásmaszkokat nagyon megfontoltan kell alkalmazni, mert a túlzott maszkolás megnehezíti az objektumok követését." + }, + "edit": "Mozgási Maszk Módosítása", + "polygonAreaTooLarge": { + "documentation": "Olvassa el a dokumentációt", + "title": "A mozgásmaszk a kamera képkockájának {{polygonArea}}%-át fedi le. Nagy mozgásmaszkok használata nem ajánlott.", + "tips": "A mozgásmaszkok nem akadályozzák meg az objektumok észlelését. Ehelyett ajánlott kötelező zónát használni." + }, + "documentTitle": "Mozgási Maszk Módosítása - Frigate", + "add": "Új Mozgási Maszk", + "point_one": "{{count}} pont", + "point_other": "{{count}} pont", + "label": "Mozgási maszk", + "clickDrawPolygon": "Kattintson a sokszög rajzoláshoz a képre.", + "toast": { + "success": { + "title": "{{polygonName}} neve mentve. Indítsa újra a Frigate-et a módosítások érvényesítéséhez.", + "noName": "A mozgásmaszk elmentésre került. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + } + }, + "restart_required": "Újraindítás szükséges (maszkok/zónák módosultak)", + "filter": { + "all": "Mindem Maszk és Zóna" + }, + "motionMaskLabel": "Mozgási Maszk {{number}}", + "objectMaskLabel": "Tárgy Maszk {{number}} {{label}}", + "toast": { + "success": { + "copyCoordinates": "A {{polyName}} koordinátái vágólapra másolva." + }, + "error": { + "copyCoordinatesFailed": "Nem sikerült a koordinátákat a vágólapra másolni." + } + } + }, + "debug": { + "objectList": "Tárgy lista", + "debugging": "Hibakeresés", + "motion": { + "desc": "Mutasd a dobozokat azon területek körül, ahol mozgás észlelés történt", + "title": "Mozgáskeretek", + "tips": "

    Mozgáskeretek


    Piros keretek jelennek meg a képkocka azon területein, ahol jelenleg mozgást észlelhető.

    " + }, + "zones": { + "title": "Zónák", + "desc": "Mutassa a meghatározott zónák körvonalát" + }, + "objectShapeFilterDrawing": { + "score": "Pontszám", + "document": "Olvassa el a dokumentációt ", + "area": "Terület", + "title": "Objektum alak szűrő rajzolás", + "desc": "Rajzoljon egy téglalapot a képre a terület- és arányadatok megtekintéséhez", + "tips": "Engedélyezze ezt az opciót, hogy téglalapot rajzoljon a kameraképen, amely megmutatja a területét és az arányát. Ezek az értékek ezután felhasználhatók az objektumalak szűrő paramétereinek beállításához a konfigurációban.", + "ratio": "Arány" + }, + "timestamp": { + "title": "Időbélyeg", + "desc": "Időbélyeg megjelenítése a képen" + }, + "title": "Hibakeresés", + "noObjects": "Nincs tárgy", + "detectorDesc": "A Frigate a(z) {{detectors}} érzékelőit használja az objektumok észlelésére a kamera videófolyamában.", + "desc": "A hibakereső nézet valós idejű képet mutat a követett objektumokról és azok statisztikáiról. Az objektumlista késleltetett összefoglalót ad az észlelt objektumokról.", + "boundingBoxes": { + "title": "Határoló keretek", + "desc": "Mutassa a határoló kereteket a követett objektumok körül", + "colors": { + "label": "Határolókeret színek", + "info": "
  • Indításkor minden objektumcímkéhez más-más szín kerül hozzárendelésre
  • A sötétkék vékony vonal azt jelzi, hogy az objektum ebben a pillanatban nincs észlelve
  • A szürke vékony vonal azt jelzi, hogy az objektum állónak van észlelve
  • A vastag vonal azt jelzi, hogy az objektum az automatikus követés tárgya (ha engedélyezve van)
  • " + } + }, + "mask": { + "title": "Mozgásmaszkok", + "desc": "Mozgásmaszk poligonok megjelenítése" + }, + "regions": { + "title": "Régiók", + "desc": "Mutassa a célterület keretét, amelyet az objektumérzékelőhöz küldenek", + "tips": "

    Célterület keretek


    Világoszöld keretek jelennek meg a képkocka azon területein, amelyek az objektumérzékelőnek elküldésre kerülnek.

    " + }, + "paths": { + "title": "Útvonalak", + "desc": "A követett objektum útvonalához tartozó jelentősebb pontok megjelenítése", + "tips": "

    Útvonalak


    A vonalak és körök jelzik a követett objektum életciklusa során érintett jelentősebb pontokat.

    " + }, + "openCameraWebUI": "Nyissa meg a {{camera}} webes felületét", + "audio": { + "title": "Hang", + "noAudioDetections": "Nincs hangérzékelés", + "score": "pontszám", + "currentRMS": "Aktuális effektív érték", + "currentdbFS": "Aktuális dbFS" + } + }, + "motionDetectionTuner": { + "toast": { + "success": "Mozgási beállítások mentve." + }, + "improveContrast": { + "desc": "Növelje a kontrasztot a sötétebb jeleneteknél. Alapbeállítás: BE", + "title": "Kontraszt Növelése" + }, + "Threshold": { + "title": "Határérték", + "desc": "A küszöbérték határozza meg, hogy egy pixel fényességének mekkora változása szükséges ahhoz, hogy mozgásnak minősüljön. Alapértelmezett: 30" + }, + "title": "Mozgásérzékelő hangoló", + "unsavedChanges": "Nem mentett mozgásérzékelő hangolási módosítások ({{camera}})", + "desc": { + "title": "A Frigate a mozgásérzékelést elsődleges ellenőrzésként használja, hogy megállapítsa, van-e a képkockában olyan esemény, amely érdemes az objektumfelismeréssel való vizsgálatra.", + "documentation": "Olvassa el a mozgásérzékelő hangolási útmutatót" + }, + "contourArea": { + "title": "Kontúrterület", + "desc": "A kontúrterület értékét arra használják, hogy eldöntsék, melyik megváltozott pixelekből álló csoport minősül mozgásnak. Alapértelmezett: 10" + } + }, + "camera": { + "streams": { + "title": "Adások", + "desc": "Ideiglenesen tiltsa le a kamerát a Frigate újraindításáig. A kamera teljes letiltása megállítja a Frigate feldolgozását az adott kamera közvetítésénél. Az észlelés, rögzítés és hibakeresés nem lesz elérhető.
    Megjegyzés: Ez nem tiltja le a go2rtc újraközvetítéseket." + }, + "review": { + "title": "Áttekintés", + "alerts": "Riasztások ", + "detections": "Érzékelések ", + "desc": "Ideiglenesen engedélyezze/tiltsa le a riasztásokat és észleléseket ennél a kameránál a Frigate újraindításáig. Letiltás esetén nem keletkeznek új ellenőrzési elemek. " + }, + "reviewClassification": { + "readTheDocumentation": "Olvassa el a Dokumentációt", + "title": "Attekintés Osztályozás", + "noDefinedZones": "Nincs zóna meghatározva ehhez a kamerához.", + "objectAlertsTips": "Minden {{alertsLabels}} tárgy a {{cameraName}} -n Riasztásként fog megjelenni.", + "selectDetectionsZones": "Válassza ki a zónákat az Észleléshez", + "limitDetections": "Korlátozza az észleléseket adott zónákra", + "selectAlertsZones": "Válassza ki a zónákat a Riasztásokhoz", + "zoneObjectAlertsTips": "Minden {{alertsLabels}}tárgy észlelés {{zone}}-ban/-ben a {{cameraName}}-n Riasztásként lesz megjelenítve.", + "objectDetectionsTips": "Minden {{detectionsLabels}} tárgy, ami nincs bekategorizálva a {{cameraName}}-n Észleléskén lesz megjelenítve függetlenül attól, hogy melyik zónában vannak.", + "desc": "A Frigate az ellenőrzési elemeket Riasztásokra és Észlelésekre osztja. Alapértelmezés szerint az összes személy és autó objektum Riasztásnak számít. Az ellenőrzési elemek kategorizálását finomíthatja azzal, hogy megadja az észleléshez szükséges zónákat.", + "zoneObjectDetectionsTips": { + "text": "Az összes {{detectionsLabels}} objektum, amely nincs kategorizálva a(z) {{zone}} zónában a(z) {{cameraName}} kamerán, Észlelésként lesz megjelenítve.", + "notSelectDetections": "Az összes {{detectionsLabels}} objektum, amelyet a(z) {{zone}} zónában észleltek a(z) {{cameraName}} kamerán, és amely nem lett Riasztásként kategorizálva, Észlelésként lesz megjelenítve, függetlenül attól, hogy melyik zónában vannak.", + "regardlessOfZoneObjectDetectionsTips": "Az összes {{detectionsLabels}} objektum, amely nincs kategorizálva a(z) {{cameraName}} kamerán, Észlelésként lesz megjelenítve zónától függetlenül." + }, + "unsavedChanges": "Nem mentett Ellenőrzési Kategorizálási beállítások a(z) {{camera}} kamerához", + "toast": { + "success": "Az Ellenőrzési Kategorizálás beállításai elmentésre kerültek. A módosítások alkalmazásához indítsa újra a Frigate-et." + } + }, + "title": "Kamera Beállítások", + "object_descriptions": { + "title": "Generatív AI Tárgy Leírások", + "desc": "Ideiglenesen engedélyezze/tiltsa le a generatív AI objektumleírásokat ehhez a kamerához. Letiltás esetén a rendszer nem kéri le a mesterséges intelligencia által generált leírásokat a kamerán követett objektumokhoz." + }, + "addCamera": "Új Kamera Hozzáadása", + "editCamera": "Kamera Szerkesztése:", + "selectCamera": "Válasszon ki egy Kamerát", + "backToSettings": "Vissza a Kamera Beállításokhoz", + "cameraConfig": { + "add": "Kamera Hozzáadása", + "edit": "Kamera Szerkesztése", + "name": "Kamera Neve", + "nameRequired": "Kamera nevének megadása szükséges", + "description": "Konfigurálja a kamera beállításait, beleértve a stream bemeneteket és szerepeket.", + "nameInvalid": "A kamera neve csak betűket, számokat, aláhúzásjeleket vagy kötőjeleket tartalmazhat", + "namePlaceholder": "pl: bejarati_ajto", + "enabled": "Engedélyezve", + "ffmpeg": { + "inputs": "Bemeneti Adatfolyamok", + "path": "Adatfolyam útvonal", + "pathRequired": "Adatfolyam útvonal szükséges", + "pathPlaceholder": "rtsp://...", + "roles": "Szerepkörök", + "rolesRequired": "Legalább egy szerepkör megadása kötelező", + "rolesUnique": "Minden szerepkör (hang, érzékelés, rögzítés) csak egy adatfolyamhoz rendelhető hozzá", + "addInput": "Bejövő Adatfolyam Hozzáadása", + "removeInput": "Bejövő Adatfolyam Eltávolítása", + "inputsRequired": "Legalább egy bemeneti adatfolyam szükséges" + }, + "toast": { + "success": "A következő kamera sikeresen mentve: {{cameraName}}" + }, + "nameLength": "A kamera nevének kevesebbnek kell lennie 24 karakternél." + }, + "review_descriptions": { + "title": "Generatív MI Áttekintési Leírások", + "desc": "Ideiglenesen engedélyezze/tiltsa le a generatív mesterséges intelligencia által generált leírásokat ehhez a kamerához. Letiltás esetén a mesterséges intelligencia által generált leírások nem lesznek lekérve a kamerán található elemhez." + } + }, + "triggers": { + "documentTitle": "Trigger-ek", + "management": { + "title": "Triggerek kezelése", + "desc": "A(z) {{camera}} nevű kamera triggereinek kezelése. A bélyegkép típussal a kiválasztott követett objektumhoz hasonló bélyegképekre, a leírás típussal pedig a megadott szöveghez hasonló leírásokra aktiválhatja a funkciót." + }, + "addTrigger": "Trigger hozzáadása", + "table": { + "name": "Név", + "type": "Típus", + "content": "Tartalom", + "threshold": "Határérték", + "actions": "Akciók", + "noTriggers": "Nincsenek konfigurált triggerek ehhez a kamerához.", + "edit": "Szerkesztés", + "deleteTrigger": "Trigger törlése", + "lastTriggered": "Utoljára triggerelve" + }, + "type": { + "thumbnail": "Bélyegkép", + "description": "Leírás" + }, + "actions": { + "alert": "Megjelölés Riasztásként", + "notification": "Értesítés küldése" + }, + "dialog": { + "createTrigger": { + "title": "Trigger létrehozása", + "desc": "Hozz létre egy triggert a(z) {{camera}} kamerához" + }, + "editTrigger": { + "title": "Trigger Szerkesztése", + "desc": "Trigger beállítások szerkesztése a következő kamerán: {{camera}}" + }, + "deleteTrigger": { + "title": "Trigger Törlése", + "desc": "Biztosan törölni szeretné a(z){{triggerName}} triggert? Ez a művelet nem vonható vissza." + }, + "form": { + "name": { + "title": "Név", + "placeholder": "Add meg a trigger nevét", + "error": { + "minLength": "A névnek minimum 2 karakter hosszúnak kell lennie.", + "invalidCharacters": "A név csak betűket, számokat, aláhúzásjeleket és kötőjeleket tartalmazhat.", + "alreadyExists": "Már létezik egy ilyen nevű trigger ehhez a kamerához." + } + }, + "enabled": { + "description": "Engedélyezze vagy tiltsa le ezt a triggert" + }, + "type": { + "title": "Típus", + "placeholder": "Válaszd ki a trigger típusát" + }, + "content": { + "title": "Tartalom", + "imagePlaceholder": "Válassz egy képet", + "textPlaceholder": "Írja be a szöveges tartalmat", + "imageDesc": "Válasszon ki egy képet, amely aktiválja ezt a műveletet, amikor a rendszer hasonló képet észlel.", + "textDesc": "Írjon be egy szöveget, amely aktiválja ezt a műveletet, amikor a rendszer hasonló követett objektumleírást észlel.", + "error": { + "required": "Tartalom megadása kötelező." + } + }, + "threshold": { + "title": "Határérték", + "error": { + "min": "A határértéknek 0-nál nagyobbnak kell lennie", + "max": "A határérték legfeljebb 1 lehet" + } + }, + "actions": { + "title": "Akciók", + "desc": "Alapértelmezés szerint a Frigate minden trigger esetén MQTT üzenetet küld. Válasszon ki egy további műveletet, amelyet a trigger aktiválásakor végre kell hajtani.", + "error": { + "min": "Legalább egy műveletet ki kell választani." + } + }, + "friendly_name": { + "title": "Barátságos név", + "placeholder": "Nevezd meg vagy írd le ezt a triggert", + "description": "Egy opcionális felhasználóbarát név vagy leíró szöveg ehhez az eseményindítóhoz." + } + } + }, + "toast": { + "success": { + "createTrigger": "A trigger sikeresen létrehozva: {{name}}.", + "updateTrigger": "A trigger sikeresen módosítva: {{name}}.", + "deleteTrigger": "A trigger sikeresen törölve: {{name}}." + }, + "error": { + "createTriggerFailed": "A trigger létrehozása sikertelen: {{errorMessage}}", + "updateTriggerFailed": "A trigger módosítása sikertelen: {{errorMessage}}", + "deleteTriggerFailed": "A trigger törlése sikertelen: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Szemantikus keresés le van tiltva", + "desc": "A Triggerek használatához engedélyezni kell a szemantikus keresést." + } + }, + "roles": { + "management": { + "title": "Megtekintői szerepkör-kezelés", + "desc": "Kezelje az egyéni nézői szerepköröket és a kamera-hozzáférési engedélyeiket ehhez a Frigate-példányhoz." + }, + "addRole": "Szerepkör hozzáadása", + "table": { + "role": "Szerepkör", + "cameras": "Kamerák", + "actions": "Akciók", + "noRoles": "Nem találhatók egyéni szerepkörök.", + "editCameras": "Kamerák módosítása", + "deleteRole": "Szerepkör törlése" + }, + "toast": { + "success": { + "createRole": "Szerepkör létrehozva: {{role}}", + "updateCameras": "Kamerák frissítve a szerepkörhöz: {{role}}", + "deleteRole": "Szerepkör sikeresen törölve: {{role}}", + "userRolesUpdated_one": "{{count}} felhasználó, akit ehhez a szerepkörhöz rendeltünk, frissült „néző”-re, amely hozzáféréssel rendelkezik az összes kamerához.", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Nem sikerült létrehozni a szerepkört: {{errorMessage}}", + "updateCamerasFailed": "Nem sikerült frissíteni a kamerákat: {{errorMessage}}", + "deleteRoleFailed": "Nem sikerült törölni a szerepkört: {{errorMessage}}", + "userUpdateFailed": "Nem sikerült frissíteni a felhasználói szerepköröket: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Új szerepkör létrehozása", + "desc": "Adjon hozzá egy új szerepkört, és adja meg a kamera hozzáférési engedélyeit." + }, + "editCameras": { + "title": "Szerepkör kamerák szerkesztése", + "desc": "Frissítse a kamerahozzáférést a(z) {{role}} szerepkörhöz." + }, + "deleteRole": { + "title": "Szerepkör törlése", + "desc": "Ez a művelet nem vonható vissza. Ez véglegesen törli a szerepkört, és az ezzel a szerepkörrel rendelkező összes felhasználót a „megtekintő” szerepkörhöz rendeli, amivel a megtekintő hozzáférhet az összes kamerához.", + "warn": "Biztosan törölni szeretnéd a(z) {{role}} szerepkört?", + "deleting": "Törlés..." + }, + "form": { + "role": { + "title": "Szerepkör neve", + "placeholder": "Adja meg a szerepkör nevét", + "desc": "Csak betűk, számok, pontok és aláhúzásjelek engedélyezettek.", + "roleIsRequired": "A szerepkör nevének megadása kötelező", + "roleOnlyInclude": "A szerepkör neve csak betűket, számokat , . vagy _ karaktereket tartalmazhat", + "roleExists": "Már létezik egy ilyen nevű szerepkör." + }, + "cameras": { + "title": "Kamerák", + "desc": "Válassza ki azokat a kamerákat, amelyekhez ennek a szerepkörnek hozzáférése van. Legalább egy kamera megadása szükséges.", + "required": "Legalább egy kamerát ki kell választani." + } + } + } + }, + "cameraWizard": { + "title": "Kamera hozzáadása", + "description": "Kövesse az alábbi lépéseket, hogy új kamerát adjon hozzá a Frigate telepítéséhez.", + "steps": { + "nameAndConnection": "Név & adatkapcsolat", + "streamConfiguration": "Stream beállítások", + "validationAndTesting": "Validálás és tesztelés" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/hu/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/hu/views/system.json new file mode 100644 index 0000000..fffa798 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/hu/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "Kamera statisztikák - Frigate", + "storage": "Tárhely statisztikák - Frigate", + "general": "Általános Statisztikák - Frigate", + "logs": { + "frigate": "Frigate naplók - Frigate", + "go2rtc": "Go2RTC naplók - Frigate", + "nginx": "Nginx naplók - Frigate" + }, + "enrichments": "Kiegészítés statisztikák - Frigate" + }, + "cameras": { + "label": { + "ffmpeg": "FFmpeg", + "overallSkippedDetectionsPerSecond": "összes kihagyott észlelés per másodperc", + "camera": "kamera", + "detect": "észlel", + "skipped": "kihagyott", + "overallFramesPerSecond": "összes képkocka per másodperc", + "overallDetectionsPerSecond": "összes észlelés per másodperc", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraDetect": "{{camName}} észlelés", + "cameraFramesPerSecond": "{{camName}} képkocka per másodperc", + "cameraDetectionsPerSecond": "{{camName}} észlelés per másodperc", + "cameraSkippedDetectionsPerSecond": "{{camName}} kihagyott észlelés per másodperc", + "cameraCapture": "{{camName}} megszerzése", + "capture": "megszerez" + }, + "title": "Kamerák", + "info": { + "video": "Videó:", + "resolution": "Felbontás:", + "codec": "Codec:", + "error": "Hiba: {{error}}", + "cameraProbeInfo": "{{camera}} Kamera Szonda Infó", + "fetching": "Kamera Adat Begyűjtése", + "aspectRatio": "képarány", + "stream": "Folyam: {{idx}}", + "fps": "FPS:", + "unknown": "Ismeretlen", + "audio": "Hang:", + "tips": { + "title": "Kamera Szonda Infó" + }, + "streamDataFromFFPROBE": "Az adás adat begyűjtése ffprobe-bal." + }, + "overview": "Áttekintés", + "framesAndDetections": "Képek / Észlelések", + "toast": { + "success": { + "copyToClipboard": "Szonda adat másolva a vágólapra." + }, + "error": { + "unableToProbeCamera": "Nem sikerült felderíteni a kamerát: {{errorMessage}}" + } + } + }, + "title": "Rendszer", + "logs": { + "copy": { + "label": "Másolás a Vágólapra", + "error": "Nem sikerült a naplók vágólapra másolása", + "success": "Naplók a vágólapra másolva" + }, + "type": { + "label": "Típus", + "timestamp": "Időbélyeg", + "tag": "Cédula", + "message": "Üzenet" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Hiba a naplók begyűjtése közben: {{errorMessage}}", + "whileStreamingLogs": "Hiba a naplók bekérésekor: {{errorMessage}}" + } + }, + "download": { + "label": "Naplók letöltése" + }, + "tips": "A naplók a szerverről érkeznek" + }, + "general": { + "title": "Általános", + "detector": { + "title": "Érzékelők", + "inferenceSpeed": "Érzékelők Inferencia Sebessége", + "cpuUsage": "Érzékelő CPU Kihasználtság", + "memoryUsage": "Érzékelő Memória Kihasználtság", + "temperature": "Érzékelő Hőmérséklete", + "cpuUsageInformation": "A detektálási modellekbe érkező és onnan távozó bemeneti és kimeneti adatok előkészítéséhez használt CPU. Ez az érték nem méri a következtetési kihasználtságot, még GPU vagy gyorsító használata esetén sem." + }, + "hardwareInfo": { + "title": "Hardver Infó", + "gpuUsage": "GPU Kihasználtság", + "gpuMemory": "GPU Memória", + "gpuInfo": { + "nvidiaSMIOutput": { + "name": "Név: {{name}}", + "title": "Nvidia SMI Kimenet", + "vbios": "VBios Infó: {{vbios}}", + "driver": "Meghajtó: {{driver}}", + "cudaComputerCapability": "CUDA Számítási Képesség: {{cuda_compute}}" + }, + "vainfoOutput": { + "processOutput": "Folyamat Kimenete:", + "processError": "Folyamat Hiba:", + "title": "Vainfo Kimenet", + "returnCode": "Visszatérési Érték: {{code}}" + }, + "copyInfo": { + "label": "GPU infó másolása" + }, + "closeInfo": { + "label": "GPU info bezárása" + }, + "toast": { + "success": "GPU infó a vágólapra másolva" + } + }, + "gpuEncoder": "GPU Enkóder", + "gpuDecoder": "GPU Dekóder", + "npuUsage": "NPU Kihasználtság", + "npuMemory": "NPU Memória" + }, + "otherProcesses": { + "processMemoryUsage": "Folyamat Memória Kihasználtság", + "title": "Egyéb Folyamatok", + "processCpuUsage": "Folyamat CPU Kihasználtság" + } + }, + "storage": { + "recordings": { + "title": "Felvételek", + "tips": "Ez az érték mutatja a Frigate adatbázisában található felvételek teljes tárhely felhasználását. A Frigate nem követi az összes fájl által foglalt tárhelyet.", + "earliestRecording": "Legkorábbi elérhető felvétel:" + }, + "title": "Tárhely", + "overview": "Áttekintés", + "cameraStorage": { + "unusedStorageInformation": "Felhasználatlan Tárhely Információ", + "title": "Kamera Tárhely", + "camera": "Kamera", + "storageUsed": "Tárhely", + "percentageOfTotalUsed": "Teljes Százaléka", + "bandwidth": "Sávszélesség", + "unused": { + "title": "Felhasználatlan", + "tips": "Ez az érték nem feltétlenül tükrözi pontosan a Frigate számára elérhető szabad helyet, ha a meghajtón egyéb fájlok is tárolva vannak a Frigate felvételein kívül. A Frigate nem követi a tárhelyhasználatot a saját felvételein kívül." + } + }, + "shm": { + "title": "SHM (megosztott memória) kiosztás", + "warning": "A jelenlegi SHM mérete ({{total}}MB) túl kicsi. Növeld meg minimum ennyivel: {{min_shm}}MB." + } + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Kép Beágyazás Sebesség", + "face_embedding_speed": "Arc Beágyazás Sebesség", + "face_recognition_speed": "Arcfelismerés Sebesség", + "plate_recognition_speed": "Rendszám Felismerés Sebesség", + "text_embedding_speed": "Szöveg Beágyazás Sebesség", + "image_embedding": "Kép Beágyazása", + "text_embedding": "Szöveg Beágyazása", + "face_recognition": "Arcfelismerés", + "plate_recognition": "Rendszám Felismerés", + "yolov9_plate_detection_speed": "YOLOv9 Rendszám Felismerés Sebesség", + "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés" + }, + "infPerSecond": "Inferencia Per Másodperc", + "title": "Kiegészítések" + }, + "metrics": "Rendszer jellemzők", + "stats": { + "detectIsVerySlow": "{{detect}} nagyon lassú ({{speed}} ms)", + "healthy": "A rendszer egészséges", + "cameraIsOffline": "{{camera}} nem elérhető", + "detectIsSlow": "{{detect}} lassú ({{speed}} ms)", + "reindexingEmbeddings": "Beágyazások újra indexelése ({{processed}}% kész)", + "ffmpegHighCpuUsage": "{{camera}}-nak/-nek magas FFmpeg CPU felhasználása ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "A(z) {{camera}} kameránál magas az észlelési CPU-használat ({{detectAvg}}%)", + "shmTooLow": "A /dev/shm részére foglalt területet ({{total}} MB) legalább {{min}} MB-ra kell növelni." + }, + "lastRefreshed": "Utoljára frissítve: " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/audio.json b/sam2-cpu/frigate-dev/web/public/locales/id/audio.json new file mode 100644 index 0000000..0f759c1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/audio.json @@ -0,0 +1,89 @@ +{ + "yell": "Teriakan", + "speech": "Bahasa", + "babbling": "Ocehan", + "bellow": "Di bawah", + "whoop": "Teriakan", + "whispering": "Bisikan", + "snicker": "Tertawa", + "crying": "Menangis", + "sigh": "Mendesah", + "choir": "Paduan Suara", + "yodeling": "Bernyanyi Yodel", + "chant": "Nyanyian", + "child_singing": "Anak bernyanyi", + "rapping": "Mengetuk", + "humming": "Bersenandung", + "groan": "Mengerang", + "grunt": "Mendengus", + "breathing": "Bernafas", + "laughter": "Tertawa", + "singing": "Nyanyian", + "mantra": "Mantra", + "synthetic_singing": "Nyanyian sintesis", + "whistling": "Siulan", + "car": "Mobil", + "motorcycle": "Motor", + "bicycle": "Sepeda", + "bus": "Bis", + "train": "Kereta", + "boat": "Kapal", + "sneeze": "Bersin", + "run": "Lari", + "footsteps": "Langkah kaki", + "chewing": "Mengunyah", + "biting": "Menggigit", + "stomach_rumble": "Perut Keroncongan", + "burping": "Sendawa", + "hiccup": "Cegukan", + "fart": "Kentut", + "hands": "Tangan", + "heartbeat": "Detak Jantung", + "applause": "Tepuk Tangan", + "chatter": "Obrolan", + "children_playing": "Anak-Anak Bermain", + "animal": "Binatang", + "pets": "Peliharaan", + "dog": "Anjing", + "bark": "Gonggongan", + "howl": "Melolong", + "cat": "Kucing", + "meow": "Meong", + "livestock": "Hewan Ternak", + "horse": "Kuda", + "cattle": "Sapi", + "pig": "Babi", + "goat": "Kambing", + "sheep": "Domba", + "chicken": "Ayam", + "cluck": "Berkokok", + "cock_a_doodle_doo": "Kukuruyuk", + "turkey": "Kalkun", + "duck": "Bebek", + "quack": "Kwek", + "goose": "Angsa", + "wild_animals": "Hewan Liar", + "bird": "Burung", + "pigeon": "Merpati", + "crow": "Gagak", + "owl": "Burung Hantu", + "flapping_wings": "Kepakan Sayap", + "dogs": "Anjing", + "insect": "Serangga", + "cricket": "Jangkrik", + "mosquito": "Nyamuk", + "fly": "Lalat", + "frog": "Katak", + "snake": "Ular", + "music": "Musik", + "musical_instrument": "Alat Musik", + "guitar": "Gitar", + "electric_guitar": "Gitar Elektrik", + "acoustic_guitar": "Gitar Akustik", + "strum": "Genjreng", + "banjo": "Banjo", + "snoring": "Ngorok", + "cough": "Batuk", + "clapping": "Tepukan", + "camera": "Kamera" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/common.json b/sam2-cpu/frigate-dev/web/public/locales/id/common.json new file mode 100644 index 0000000..9072354 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/common.json @@ -0,0 +1,15 @@ +{ + "time": { + "untilForRestart": "Hingga Frigate memulai ulang.", + "untilRestart": "Sampai memulai ulang", + "ago": "{{timeAgo}} Lalu", + "justNow": "Sekarang", + "today": "Hari ini", + "yesterday": "Kemarin", + "untilForTime": "Hingga {{time}}", + "last7": "7 hari terakhir", + "last14": "14 hari terakhir", + "last30": "30 hari terakhir" + }, + "readTheDocumentation": "Baca dokumentasi" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/auth.json new file mode 100644 index 0000000..742e311 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nama pengguna", + "password": "Kata sandi", + "login": "Masuk", + "errors": { + "usernameRequired": "Username diperlukan", + "passwordRequired": "Password diperlukan", + "rateLimit": "Melewati batas permintaan. Coba lagi nanti.", + "loginFailed": "Gagal Masuk", + "unknownError": "Eror tidak diketahui. Mohon lihat log.", + "webUnknownError": "Eror tidak diketahui. Mohon lihat log konsol." + }, + "firstTimeLogin": "Mencoba masuk untuk pertama kali? Kredensial sudah dicetak di dalam riwayat Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/camera.json new file mode 100644 index 0000000..9da7f9f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/camera.json @@ -0,0 +1,23 @@ +{ + "group": { + "label": "Grup Kamera", + "add": "Tambah Grup Kamera", + "edit": "Edit Grup kamera", + "delete": { + "label": "Hapus Grup Kamera", + "confirm": { + "title": "Yakin Hapus", + "desc": "Apakah Anda yakin ingin menghapus grup kamera {{name}}?" + } + }, + "name": { + "label": "Nama", + "placeholder": "Masukkan nama…", + "errorMessage": { + "mustLeastCharacters": "Nama grup kamera minimal harus 2 karakter.", + "exists": "Nama grup kamera sudah ada.", + "nameMustNotPeriod": "Nama grup kamera tidak boleh ada titik." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/dialog.json new file mode 100644 index 0000000..5d5f20f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/dialog.json @@ -0,0 +1,25 @@ +{ + "restart": { + "title": "Apakah kamu yakin ingin memulai ulang Frigate?", + "button": "Mulai ulang", + "restarting": { + "title": "Sedang Merestart Frigate", + "content": "Halaman ini akan memulai ulang dalam {{countdown}} detik.", + "button": "Muat Ulang Sekarang" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Kirim ke Frigate+", + "desc": "Objek di lokasi yang ingin Anda hindari bukanlah deteksi palsu. Mengirimnya sebagai deteksi palsu akan membingungkan model." + }, + "review": { + "question": { + "label": "Konfirmasi label ini untuk Frigate Plus", + "ask_a": "Apakah objek ini adalah sebuah{{label}}?" + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/filter.json new file mode 100644 index 0000000..0ea01e6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/filter.json @@ -0,0 +1,19 @@ +{ + "filter": "Saring", + "labels": { + "label": "Label", + "all": { + "title": "Semua Label", + "short": "Label" + }, + "count_one": "{{count}} Label", + "count_other": "{{count}} Label" + }, + "zones": { + "label": "Zona", + "all": { + "title": "Semua Zona", + "short": "Zona" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/icons.json new file mode 100644 index 0000000..de1a6a2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Pilih ikon", + "search": { + "placeholder": "Cari Icon…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/input.json new file mode 100644 index 0000000..8e0877b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Unduh Video", + "toast": { + "success": "Video yang yang anda lihat sudah mulai di unduh." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/id/components/player.json new file mode 100644 index 0000000..097e50a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/components/player.json @@ -0,0 +1,20 @@ +{ + "noPreviewFound": "Pratinjau Tidak Ditemukan", + "noPreviewFoundFor": "Tidak ada Pratinjau untuk {{cameraName}}", + "submitFrigatePlus": { + "submit": "Kirim", + "title": "Kirim frame ini ke Frigate+?" + }, + "noRecordingsFoundForThisTime": "Tidak ada Rekaman pada waktu ini", + "livePlayerRequiredIOSVersion": "iOS 17.1 atau yang lebih tinggi diperlukan untuk tipe siaran langsung ini.", + "streamOffline": { + "title": "Stream Tidak Aktif", + "desc": "Tidak ada gambar yang diterima dari strim detect {{cameraName}}, mohon lihat log error" + }, + "cameraDisabled": "Kamera dinonaktifkan", + "stats": { + "streamType": { + "title": "Tipe stream:" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/objects.json b/sam2-cpu/frigate-dev/web/public/locales/id/objects.json new file mode 100644 index 0000000..bfeeca8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/objects.json @@ -0,0 +1,20 @@ +{ + "person": "Orang", + "bicycle": "Sepeda", + "car": "Mobil", + "motorcycle": "Motor", + "airplane": "Pesawat", + "bus": "Bis", + "train": "Kereta", + "boat": "Kapal", + "traffic_light": "Lampu Lalu Lintas", + "fire_hydrant": "Hidran Kebakaran", + "animal": "Binatang", + "dog": "Anjing", + "bark": "Gonggongan", + "cat": "Kucing", + "horse": "Kuda", + "goat": "Kambing", + "sheep": "Domba", + "bird": "Burung" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/configEditor.json new file mode 100644 index 0000000..a4d7bae --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor Konfigurasi - Frigate", + "configEditor": "Editor Konfigurasi", + "copyConfig": "Salin Konfigurasi", + "saveAndRestart": "Simpan dan Mulai ulang", + "saveOnly": "Hanya simpan", + "toast": { + "success": { + "copyToClipboard": "Konfigurasi disalin ke papan klip." + }, + "error": { + "savingError": "Gagal menyimpan konfigurasi" + } + }, + "confirm": "Keluar tanpa menyimpan?", + "safeModeDescription": "Frigate sedang dalam mode aman karena kesalahan validasi konfigurasi.", + "safeConfigEditor": "Editor Konfigurasi(Mode Aman)" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/events.json new file mode 100644 index 0000000..94ee3d4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/events.json @@ -0,0 +1,59 @@ +{ + "alerts": "Peringatan", + "detections": "Deteksi", + "motion": { + "label": "Gerakan", + "only": "Hanya Gerakan" + }, + "allCameras": "Semua Kamera", + "empty": { + "detection": "Tidak ada deteksi untuk ditinjau", + "alert": "Tidak ada peringatan untuk ditinjau", + "motion": "Data gerakan tidak ditemukan" + }, + "timeline.aria": "Pilih timeline", + "timeline": "Linimasa", + "zoomIn": "Perbesar", + "zoomOut": "Perkecil", + "events": { + "label": "Peristiwa-Peristiwa", + "aria": "Pilih peristiwa", + "noFoundForTimePeriod": "Tidak ada peristiwa dalam periode waktu berikut." + }, + "detail": { + "label": "Detil", + "noDataFound": "Tidak ada detil data untuk di review", + "aria": "Beralih tampilan detil", + "trackedObject_one": "objek", + "trackedObject_other": "objek-objek", + "noObjectDetailData": "Tidak ada data objek detil tersedia.", + "settings": "Pengaturan Tampilan Detil", + "alwaysExpandActive": { + "title": "Selalu lebarkan yang aktif", + "desc": "Selalu perluas detil objek item tinjauan aktif jika tersedia." + } + }, + "objectTrack": { + "trackedPoint": "Titik terlacak", + "clickToSeek": "Klik untuk mencari waktu ini" + }, + "documentTitle": "Tinjauan - Frigate", + "recordings": { + "documentTitle": "Rekaman - Frigate" + }, + "calendarFilter": { + "last24Hours": "24 Jam Terakhir" + }, + "markAsReviewed": "Tandai sebagai sudah ditinjau", + "markTheseItemsAsReviewed": "Tandai item-item berikut sebagai sudah ditinjau", + "newReviewItems": { + "button": "Item Batu Untuk Ditinjau", + "label": "Lihat item ulasan baru" + }, + "selected_one": "{{count}} terpilih", + "selected_other": "{{count}} terpilih", + "camera": "Kamera", + "detected": "terdeteksi", + "suspiciousActivity": "Aktivitas Mencurigakan", + "threateningActivity": "Aktivitas yang Mengancam" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/explore.json new file mode 100644 index 0000000..de062e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/explore.json @@ -0,0 +1,20 @@ +{ + "documentTitle": "Jelajahi - Frigate", + "generativeAI": "AI Generatif", + "exploreIsUnavailable": { + "title": "Penelusuran tidak tersedia", + "embeddingsReindexing": { + "context": "Jelajahi dapat digunakan setelah embedding objek yang dilacak selesai di-reindex.", + "startingUp": "Sedang memulai…", + "estimatedTime": "Perkiraan waktu tersisa:", + "finishingShortly": "Selesai sesaat lagi", + "step": { + "thumbnailsEmbedded": "Keluku dilampirkan " + } + } + }, + "details": { + "timestamp": "Stempel waktu" + }, + "exploreMore": "Eksplor lebih jauh objek-objek {{label}}" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/exports.json new file mode 100644 index 0000000..043c313 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Expor - Frigate", + "search": "Cari", + "noExports": "Ekspor tidak ditemukan", + "deleteExport": "Hapus Ekspor", + "deleteExport.desc": "Apakah Anda yakin ingin menghapus {{exportName}}?", + "editExport": { + "title": "Ganti Nama Ekspor", + "desc": "Masukkan nama baru untuk ekspor ini.", + "saveExport": "Simpan Ekspor" + }, + "toast": { + "error": { + "renameExportFailed": "Gagal mengganti nama ekspor: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Bagikan Ekspor", + "downloadVideo": "Unduh Video", + "editName": "Ubah nama", + "deleteExport": "Hapus ekspor" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/faceLibrary.json new file mode 100644 index 0000000..bde637f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/faceLibrary.json @@ -0,0 +1,92 @@ +{ + "description": { + "addFace": "Tambah ke koleksi Pustaka Wajah dengan men-upload gambar pertama anda.", + "placeholder": "Masukkan Nama untuk koleksi ini", + "invalidName": "Nama tidak valid. Nama hanya dapat berisi huruf, angka, spasi, apostrof, garis bawah, dan tanda hubung." + }, + "details": { + "person": "Orang", + "subLabelScore": "Skor Sub Label", + "face": "Detail Wajah", + "scoreInfo": "Skor sub label adalah nilai gabungan dari tingkat keyakinan sistem dalam mengenali wajah. Nilai ini bisa berbeda dengan skor yang terlihat pada gambar cuplikan.", + "timestamp": "Stempel waktu", + "unknown": "Tidak diketahui", + "faceDesc": "Detail objek terlacak yang menghasilkan wajah ini" + }, + "documentTitle": "Perpustakaan Wajah - Frigate", + "collections": "Koleksi", + "createFaceLibrary": { + "desc": "Buat koleksi baru", + "title": "Buat Koleksi", + "nextSteps": "Untuk membangun fondasi yang kuat:
  • Gunakan tab Pengenalan Terbaru untuk memilih dan melatih gambar untuk setiap orang yang terdeteksi.
  • Fokus pada gambar langsung untuk hasil terbaik; hindari melatih gambar yang menangkap wajah pada sudut tertentu.
  • ", + "new": "Buat Wajah Baru" + }, + "uploadFaceImage": { + "desc": "Unggah gambar untuk dipindai wajah dan sertakan untuk {{pageToggle}}", + "title": "Unggah Gambar Wajah" + }, + "steps": { + "faceName": "Masukkan Nama Wajah", + "uploadFace": "Unggah Gambar Wajah", + "nextSteps": "Langkah Berikutnya", + "description": { + "uploadFace": "Upload sebuah gambar dari {{name}} yang menunjukkan wajah mereka dari sisi depan. Gambar tidak perlu dipotong ke wajah mereka." + } + }, + "train": { + "title": "Pengenalan Terkini", + "aria": "Pilih pengenalan terkini", + "empty": "Tidak ada percobaan pengenalan wajah baru-baru ini" + }, + "deleteFaceLibrary": { + "title": "Hapus Nama", + "desc": "Apakah anda yakin ingin menghapus koleksi {{name}}? Ini akan menghapus semua wajah terkait secara permanen." + }, + "deleteFaceAttempts": { + "title": "Hapus Wajah-Wajah", + "desc_other": "Apakah anda yakin ingin menghapis {{count}} wajah? Aksi ini tidak dapat diurungkan." + }, + "renameFace": { + "title": "Ganti Nama Wajah", + "desc": "Masukkan nama baru untuk {{name}}" + }, + "button": { + "deleteFaceAttempts": "Hapus Wajah", + "addFace": "Tambah Wajah", + "renameFace": "Ganti Nama Wajah", + "deleteFace": "Hapus Wajah", + "uploadImage": "Unggah Gambar", + "reprocessFace": "Proses Ulang Wajah" + }, + "imageEntry": { + "validation": { + "selectImage": "Silahkan pilih sebuah file gambar." + }, + "dropActive": "Letakkan gambar di sini…", + "dropInstructions": "Seret dan lepaskan atau tempel gambar di sini, atau klik untuk memilih", + "maxSize": "Ukuran maksimum: {{size}}MB" + }, + "nofaces": "Tidak ada wajah tersedia", + "trainFaceAs": "Latih Gambar sebagai:", + "trainFace": "Latih Wajah", + "toast": { + "success": { + "uploadedImage": "Berhasil men unggah gambar.", + "addFaceLibrary": "{{name}} telah berhasil ditambahkan ke Pustaka Wajah!", + "deletedFace_other": "Berhasil menghapus {{count}} wajah.", + "deletedName_other": "{{count}} wajah telah berhasil dihapus.", + "renamedFace": "Berhasil mengganti nama wajah ke {{name}}", + "trainedFace": "Berhasil melatih wajah.", + "updatedFaceScore": "Berhasil memperbaharui nilai wajah." + }, + "error": { + "uploadingImageFailed": "Gagal menunggah gambar: {{errorMessage}}", + "addFaceLibraryFailed": "Gagal mengatur nama wajah: {{errorMessage}}", + "deleteFaceFailed": "Gagal untuk menghapus: {{errorMessage}}", + "deleteNameFailed": "Gagal menghapus nama: {{errorMessage}}", + "renameFaceFailed": "Gagal mengganti nama wajah: {{errorMessage}}", + "trainFailed": "Gagal untuk melatih: {{errorMessage}}", + "updateFaceScoreFailed": "Gagal untuk memperbaharui nilai wajah: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/live.json new file mode 100644 index 0000000..97a7335 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/live.json @@ -0,0 +1,21 @@ +{ + "documentTitle.withCamera": "{{camera}} - Langsung - Frigate", + "documentTitle": "Langsung - Frigate", + "lowBandwidthMode": "Mode Bandwith-Rendah", + "twoWayTalk": { + "enable": "Nyalakan Komunikasi dua arah", + "disable": "Nonaktifkan Komunikasi Dua Arah" + }, + "cameraAudio": { + "enable": "Nyalakan Audio Kamera", + "disable": "Matikan Audio Kamera" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klik kotak ini untuk menengahkan kamera", + "enable": "Aktifkan klik untuk bergerak" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/recording.json new file mode 100644 index 0000000..8d5970f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Saring", + "export": "Expor", + "calendar": "Kalender", + "filters": "Penyaring", + "toast": { + "error": { + "noValidTimeSelected": "Rentan Waktu yang dipilih tidak valid", + "endTimeMustAfterStartTime": "Waktu akhir harus setelah waktu mulai" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/search.json new file mode 100644 index 0000000..c4c5989 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/search.json @@ -0,0 +1,13 @@ +{ + "search": "Cari", + "savedSearches": "Simpan Pencarian", + "searchFor": "Cari untuk {{inputValue}}", + "button": { + "clear": "Bersihkan pencarian", + "save": "Simpan Pencarian", + "delete": "Hapus pencarian yang disimpan", + "filterInformation": "Saring Informasi", + "filterActive": "Filter aktif" + }, + "trackedObjectId": "Tracked Object ID" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/settings.json new file mode 100644 index 0000000..8d1b4de --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/settings.json @@ -0,0 +1,18 @@ +{ + "documentTitle": { + "default": "Pengaturan - Frigate", + "camera": "Pengaturan Kamera - Frigate", + "classification": "Pengaturan Klasifikasi - Frigate", + "authentication": "Pengaturan Autentikasi - Frigate", + "masksAndZones": "Editor Mask dan Zona - Frigate", + "motionTuner": "Penyetel Gerakan - Frigate", + "general": "Frigate - Pengaturan Umum", + "object": "Debug - Frigate", + "enrichments": "Frigate - Pengaturan Pengayaan", + "cameraManagement": "Pengaturan Kamera - Frigate" + }, + "menu": { + "cameraManagement": "Pengaturan", + "notifications": "Notifikasi" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/id/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/id/views/system.json new file mode 100644 index 0000000..183e7ca --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/id/views/system.json @@ -0,0 +1,15 @@ +{ + "documentTitle": { + "cameras": "Status kamera - Frigate", + "storage": "Status Penyimpanan - Frigate", + "general": "Status umum - Frigate", + "enrichments": "Statistik Enrichment - Frigate", + "logs": { + "frigate": "Log Frigate - Frigate", + "go2rtc": "Log Go2RTC - Frigate", + "nginx": "Log NGINX - Frigate" + } + }, + "title": "Sistem", + "metrics": "Metrik sistem" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/audio.json b/sam2-cpu/frigate-dev/web/public/locales/it/audio.json new file mode 100644 index 0000000..17ca12c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/audio.json @@ -0,0 +1,503 @@ +{ + "oink": "Grugnito", + "engine": "Motore", + "keyboard": "Tastiera", + "snake": "Serpente", + "mantra": "Mantra", + "radio": "Radio", + "goat": "Capra", + "bird": "Uccello", + "clock": "Orologio", + "gears": "Ingranaggi", + "fire": "Fuoco", + "hammer": "Martello", + "gunshot": "Sparo", + "bell": "Campana", + "explosion": "Esplosione", + "silence": "Silenzio", + "ambulance": "Ambulanza", + "thunder": "Tuono", + "purr": "Fusa", + "guitar": "Chitarra", + "bass_guitar": "Basso elettrico", + "eruption": "Eruzione", + "sound_effect": "Effetto sonoro", + "bicycle": "Bicicletta", + "vehicle": "Veicolo", + "flute": "Flauto", + "harp": "Arpa", + "steam": "Vapore", + "sailboat": "Barca a vela", + "helicopter": "Elicottero", + "coin": "Moneta", + "scissors": "Forbici", + "electric_shaver": "Rasoio elettrico", + "mechanisms": "Meccanismi", + "printer": "Stampante", + "television": "Televisione", + "environmental_noise": "Rumore ambientale", + "smoke_detector": "Rilevatore di fumo", + "clarinet": "Clarinetto", + "horse": "Cavallo", + "electric_guitar": "Chitarra elettrica", + "meow": "Miao", + "ringtone": "Suoneria", + "boat": "Barca", + "sampler": "Campionatore", + "song": "Canzone", + "clapping": "Applausi", + "cat": "Gatto", + "chicken": "Pollo", + "acoustic_guitar": "Chitarra acustica", + "speech": "Parlato", + "babbling": "Balbettio", + "motorcycle": "Motociclo", + "yell": "Urlo", + "whoop": "Ululato", + "car": "Automobile", + "bellow": "Ruggito", + "whispering": "Bisbiglio", + "snicker": "Risatina", + "crying": "Pianto", + "sigh": "Sospiro", + "singing": "Canto", + "choir": "Coro", + "yodeling": "Gorgheggio", + "chant": "Cantilena", + "child_singing": "Canto di bambino", + "laughter": "Risata", + "synthetic_singing": "Canto sintetico", + "rapping": "Rap", + "humming": "Mormorio", + "groan": "Gemito", + "grunt": "Grugnito", + "whistling": "Fischio", + "breathing": "Respiro", + "wheeze": "Respiro affannoso", + "snoring": "Russare", + "gasp": "Respiro profondo", + "cough": "Tosse", + "snort": "Sbuffare", + "pant": "Ansimare", + "throat_clearing": "Schiarimento della gola", + "sneeze": "Starnuto", + "sniff": "Sniffare", + "footsteps": "Passi", + "run": "Corsa", + "chewing": "Masticare", + "gargling": "Gargarismo", + "stomach_rumble": "Brontolio di stomaco", + "burping": "Rutto", + "fart": "Peto", + "hiccup": "Singhiozzo", + "finger_snapping": "Schiocco di dita", + "hands": "Mani", + "heartbeat": "Battito cardiaco", + "applause": "Applauso", + "heart_murmur": "Soffio cardiaco", + "cheering": "Tifo", + "animal": "Animale", + "pets": "Animali domestici", + "dog": "Cane", + "bark": "Abbaio", + "howl": "Ululato", + "yip": "Guaito", + "crowd": "Folla", + "livestock": "Bestiame", + "cowbell": "Campanaccio", + "turkey": "Tacchino", + "children_playing": "Bambini che giocano", + "chatter": "Chiacchiere", + "bow_wow": "Bau Bau", + "growling": "Ringhio", + "whimper_dog": "Lamento di cane", + "hiss": "Sibilo", + "caterwaul": "Miagolio", + "pig": "Maiale", + "sheep": "Pecora", + "fowl": "Pollame", + "bleat": "Belato", + "cock_a_doodle_doo": "Chicchirichì", + "gobble": "Glu Glu", + "cluck": "Chioccia", + "duck": "Anatra", + "quack": "Qua Qua", + "wild_animals": "Animali selvatici", + "goose": "Oca", + "honk": "Starnazzare", + "roar": "Ruggito", + "roaring_cats": "Ruggito felino", + "chirp": "Cinguettio", + "pigeon": "Piccione", + "crow": "Corvo", + "coo": "Tubare", + "owl": "Gufo", + "caw": "Gracchio", + "dogs": "Cani", + "hoot": "Bubbolio", + "flapping_wings": "Battito d'ali", + "mouse": "Mouse", + "rats": "Ratti", + "insect": "Insetto", + "cricket": "Grillo", + "mosquito": "Zanzara", + "buzz": "Ronzio", + "fly": "Mosca", + "frog": "Rana", + "croak": "Gracidio", + "rattle": "Sonaglio", + "music": "Musica", + "musical_instrument": "Strumento musicale", + "whale_vocalization": "Canto della balena", + "plucked_string_instrument": "Strumento a corda pizzicata", + "tapping": "Tapping (tecnica di chitarra)", + "steel_guitar": "Chitarra d'acciaio", + "strum": "Strimpellio", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolino", + "zither": "Cetra da tavolo", + "ukulele": "Ukulele", + "piano": "Pianoforte", + "electric_piano": "Pianoforte elettrico", + "organ": "Organo", + "electronic_organ": "Organo elettronico", + "synthesizer": "Sintetizzatore", + "hammond_organ": "Organo Hammond", + "drum_kit": "Batteria", + "drum": "Tamburo", + "drum_machine": "Drum Machine", + "tabla": "Tabla", + "cymbal": "Piatto", + "hi_hat": "Charleston", + "wood_block": "Blocco di legno", + "tambourine": "Tamburello", + "maraca": "Maracas", + "gong": "Gong", + "tubular_bells": "Campane tubolari", + "mallet_percussion": "Mazzuola", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafono", + "steelpan": "Tamburo d'acciaio", + "orchestra": "Orchestra", + "brass_instrument": "Ottoni", + "trumpet": "Tromba", + "french_horn": "Corno francese", + "trombone": "Trombone", + "bowed_string_instrument": "Strumento ad arco", + "violin": "Violino", + "string_section": "Sezione d'archi", + "cello": "Violoncello", + "double_bass": "Contrabbasso", + "pizzicato": "Pizzicato", + "wind_instrument": "Strumento a fiato", + "saxophone": "Sassofono", + "church_bell": "Campana della chiesa", + "jingle_bell": "Campanellino", + "bicycle_bell": "Campanello della bici", + "tuning_fork": "Diapason", + "chime": "Carillon", + "wind_chime": "Campane a vento", + "harmonica": "Armonica", + "accordion": "Fisarmonica", + "bagpipes": "Cornamusa", + "didgeridoo": "Didgeridoo", + "pop_music": "Musica pop", + "theremin": "Theremin", + "singing_bowl": "Campana tibetana", + "rock_music": "Musica rock", + "heavy_metal": "Heavy Metal", + "beatboxing": "Beatboxing", + "hip_hop_music": "Musica hip hop", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock psichedelico", + "bus": "Autobus", + "train": "Treno", + "percussion": "Percussioni", + "harpsichord": "Clavicembalo", + "snare_drum": "Rullante", + "rimshot": "Colpi nei bordi del tamburo", + "drum_roll": "Rullo di tamburi", + "bass_drum": "Grancassa", + "timpani": "Timpano", + "progressive_rock": "Rock progressivo", + "scratching": "Graffio", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Musica soul", + "reggae": "Reggae", + "country": "Country", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Musica folk", + "middle_eastern_music": "Musica mediorientale", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Musica classica", + "opera": "Opera", + "electronic_music": "Musica elettronica", + "house_music": "Musica house", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Elettronica", + "camera": "Telecamera", + "shuffle": "Trascinare i piedi", + "biting": "Mordere", + "clip_clop": "Trotto di cavallo", + "neigh": "Nitrire", + "cattle": "Bestiame", + "moo": "Muggire", + "squawk": "Strillo", + "patter": "Ticchettio", + "swing_music": "Musica swing", + "electronic_dance_music": "Musica dance elettronica", + "ambient_music": "Musica ambientale", + "trance_music": "Musica trance", + "blues": "Blues", + "music_for_children": "Musica per bambini", + "new-age_music": "Musica new age", + "vocal_music": "Musica vocale", + "a_capella": "A capella", + "music_of_latin_america": "Musica latinoamericana", + "music_of_africa": "Musica africana", + "afrobeat": "Afrobeat", + "christian_music": "Musica cristiana", + "gospel_music": "Musica gospel", + "music_of_asia": "Musica asiatica", + "carnatic_music": "Musica carnatica", + "music_of_bollywood": "Musica di Bollywood", + "ska": "Ska", + "traditional_music": "Musica tradizionale", + "independent_music": "Musica indipendente", + "background_music": "Musica di sottofondo", + "theme_music": "Musica a tema", + "jingle": "Motivetto", + "soundtrack_music": "Colonna sonora musicale", + "lullaby": "Ninna nanna", + "video_game_music": "Musica per videogiochi", + "christmas_music": "Musica natalizia", + "dance_music": "Musica da ballo", + "wedding_music": "Musica per matrimoni", + "happy_music": "Musica allegra", + "sad_music": "Musica triste", + "tender_music": "Musica dolce", + "salsa_music": "Musica salsa", + "flamenco": "Flamenco", + "angry_music": "Musica arrabbiata", + "scary_music": "Musica spaventosa", + "wind": "Vento", + "rustling_leaves": "Fruscio di foglie", + "wind_noise": "Rumore del vento", + "thunderstorm": "Temporale", + "water": "Acqua", + "rain": "Pioggia", + "raindrop": "Goccia di pioggia", + "stream": "Ruscello", + "waterfall": "Cascata", + "ocean": "Oceano", + "waves": "Onde", + "rain_on_surface": "Pioggia in superficie", + "gurgling": "Gorgoglio", + "crackle": "Crepitio", + "rowboat": "Barca a remi", + "motorboat": "Motoscafo", + "ship": "Nave", + "toot": "Fischio", + "motor_vehicle": "Veicolo a motore", + "car_alarm": "Allarme auto", + "power_windows": "Alzacristalli elettrici", + "skidding": "Sbandare", + "tire_squeal": "Stridio di pneumatici", + "car_passing_by": "Auto che passa", + "race_car": "Auto da corsa", + "air_brake": "Freno ad aria compressa", + "air_horn": "Clacson ad aria", + "reversing_beeps": "Bip di retromarcia", + "ice_cream_truck": "Camioncino dei gelati", + "emergency_vehicle": "Veicolo di emergenza", + "police_car": "Auto della polizia", + "fire_engine": "Camion dei pompieri", + "traffic_noise": "Rumore del traffico", + "rail_transport": "Trasporto ferroviario", + "train_whistle": "Fischio del treno", + "train_horn": "Clacson del treno", + "railroad_car": "Vagone ferroviario", + "train_wheels_squealing": "Ruote del treno che stridono", + "subway": "Metropolitana", + "aircraft": "Aeromobile", + "aircraft_engine": "Motore aeronautico", + "jet_engine": "Motore a reazione", + "propeller": "Elica", + "fixed-wing_aircraft": "Aeromobile ad ala fissa", + "skateboard": "Skateboard", + "light_engine": "Motore leggero", + "dental_drill's_drill": "Trapano dentale", + "lawn_mower": "Taglia erba", + "exciting_music": "Musica emozionante", + "truck": "Camion", + "firecracker": "Petardo", + "chainsaw": "Motosega", + "medium_engine": "Motore medio", + "heavy_engine": "Motore pesante", + "engine_knocking": "Battito del motore", + "engine_starting": "Avviamento del motore", + "idling": "Al minimo", + "accelerating": "Accelerando", + "door": "Porta", + "doorbell": "Campanello", + "ding-dong": "Ding Dong", + "sliding_door": "Porta scorrevole", + "slam": "Sbattere", + "knock": "Bussare", + "tap": "Tocco", + "squeak": "Squittio", + "cupboard_open_or_close": "Apertura o chiusura armadio", + "drawer_open_or_close": "Apertura o chiusura cassetto", + "dishes": "Piatti", + "cutlery": "Posate", + "chopping": "Tritare", + "frying": "Frittura", + "microwave_oven": "Forno a microonde", + "blender": "Miscelatore", + "water_tap": "Rubinetto dell'acqua", + "sink": "Lavello", + "bathtub": "Vasca", + "hair_dryer": "Asciugacapelli", + "toilet_flush": "Scarico del water", + "toothbrush": "Spazzolino da denti", + "electric_toothbrush": "Spazzolino elettrico", + "vacuum_cleaner": "Aspirapolvere", + "zipper": "Cerniera", + "keys_jangling": "Chiavi che tintinnano", + "shuffling_cards": "Mescolare le carte", + "typing": "Digitazione", + "typewriter": "Macchina da scrivere", + "computer_keyboard": "Tastiera del computer", + "writing": "Scrivere", + "alarm": "Allarme", + "telephone": "Telefono", + "telephone_bell_ringing": "Telefono che squilla", + "telephone_dialing": "Composizione telefonica", + "dial_tone": "Tono di linea", + "busy_signal": "Segnale di occupato", + "alarm_clock": "Sveglia", + "siren": "Sirena", + "civil_defense_siren": "Sirena della Protezione Civile", + "buzzer": "Cicalino", + "fire_alarm": "Allarme antincendio", + "foghorn": "Corno da nebbia", + "whistle": "Fischio", + "steam_whistle": "Fischio a vapore", + "ratchet": "Cricchetto", + "tick": "Tic tac", + "tick-tock": "Tic-Tac", + "pulleys": "Pulegge", + "sewing_machine": "Macchina da cucire", + "mechanical_fan": "Ventilatore meccanico", + "air_conditioning": "Aria condizionata", + "cash_register": "Registratore di cassa", + "single-lens_reflex_camera": "Telecamera reflex a obiettivo singolo", + "tools": "Utensili", + "jackhammer": "Martello pneumatico", + "sawing": "Segare", + "filing": "Limare", + "sanding": "Levigatura", + "power_tool": "Utensile elettrico", + "drill": "Trapano", + "fusillade": "Fucilazione", + "machine_gun": "Mitragliatrice", + "artillery_fire": "Fuoco di artiglieria", + "cap_gun": "Pistola a fumogeni", + "fireworks": "Fuochi d'artificio", + "burst": "Esplosione", + "boom": "Scoppio", + "wood": "Legno", + "chop": "Taglio", + "splinter": "Scheggia", + "crack": "Crepa", + "glass": "Vetro", + "chink": "Fessura", + "shatter": "Frantumare", + "static": "Statico", + "white_noise": "Rumore bianco", + "pink_noise": "Rumore rosa", + "field_recording": "Registrazione sul campo", + "scream": "Grido", + "vibration": "Vibrazione", + "sodeling": "Zollatura", + "chird": "Accordo", + "change_ringing": "Cambia suoneria", + "shofar": "Shofar", + "liquid": "Liquido", + "splash": "Schizzo", + "slosh": "Sciabordio", + "squish": "Schiacciare", + "drip": "Gocciolare", + "pour": "Versare", + "trickle": "Gocciolare", + "gush": "Sgorgare", + "fill": "Riempire", + "spray": "Spruzzare", + "pump": "Pompare", + "stir": "Mescolare", + "boiling": "Ebollizione", + "sonar": "Sonar", + "arrow": "Freccia", + "whoosh": "Sibilo", + "thump": "Tonfo", + "thunk": "Tonfo", + "electronic_tuner": "Accordatore elettronico", + "effects_unit": "Unità degli effetti", + "chorus_effect": "Effetto coro", + "basketball_bounce": "Rimbalzo di basket", + "bang": "Botto", + "slap": "Schiaffo", + "whack": "Colpo", + "smash": "Distruggere", + "breaking": "Rottura", + "bouncing": "Rimbalzo", + "whip": "Frusta", + "flap": "Patta", + "scratch": "Graffio", + "scrape": "Graffio", + "rub": "Strofinio", + "roll": "Rotolio", + "crushing": "Schiacciamento", + "crumpling": "Accartocciamento", + "tearing": "Strappo", + "beep": "Segnale acustico", + "ping": "Segnale", + "ding": "Bip", + "clang": "Fragore", + "squeal": "Strillo", + "creak": "Scricchiolio", + "rustle": "Fruscio", + "whir": "Ronzio", + "clatter": "Rumore", + "sizzle": "Sfrigolio", + "clicking": "Cliccando", + "clickety_clack": "Clic-clac", + "rumble": "Rombo", + "plop": "Tonfo", + "hum": "Ronzio", + "zing": "Brio", + "boing": "Balzo", + "crunch": "Scricchiolio", + "sine_wave": "Onda sinusoidale", + "harmonic": "Armonica", + "chirp_tone": "Tono di cinguettio", + "pulse": "Impulso", + "inside": "Dentro", + "outside": "Fuori", + "reverberation": "Riverbero", + "echo": "Eco", + "noise": "Rumore", + "mains_hum": "Ronzio di rete", + "distortion": "Distorsione", + "sidetone": "Effetto laterale", + "cacophony": "Cacofonia", + "throbbing": "Palpitante" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/common.json b/sam2-cpu/frigate-dev/web/public/locales/it/common.json new file mode 100644 index 0000000..7fc7fc2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/common.json @@ -0,0 +1,314 @@ +{ + "time": { + "last14": "Ultimi 14 giorni", + "pm": "pm", + "last30": "Ultimi 30 giorni", + "untilRestart": "Fino al riavvio", + "yesterday": "Ieri", + "am": "am", + "untilForTime": "Fino alle {{time}}", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minuti", + "minute_other": "{{time}} minuti", + "5minutes": "5 minuti", + "24hours": "24 ore", + "second_one": "{{time}} secondo", + "second_many": "{{time}} secondi", + "second_other": "{{time}} secondi", + "untilForRestart": "Fino al riavvio di Frigate.", + "ago": "{{timeAgo}} fa", + "justNow": "Adesso", + "today": "Oggi", + "last7": "Ultimi 7 giorni", + "thisWeek": "Questa settimana", + "lastWeek": "Settimana scorsa", + "thisMonth": "Questo mese", + "lastMonth": "Mese scorso", + "10minutes": "10 minuti", + "30minutes": "30 minuti", + "1hour": "1 ora", + "12hours": "12 ore", + "year_one": "{{time}} anno", + "year_many": "{{time}} anni", + "year_other": "{{time}} anni", + "month_one": "{{time}} mese", + "month_many": "{{time}} mesi", + "month_other": "{{time}} mesi", + "day_one": "{{time}} giorno", + "day_many": "{{time}} giorni", + "day_other": "{{time}} giorni", + "hour_one": "{{time}} ora", + "hour_many": "{{time}} ore", + "hour_other": "{{time}} ore", + "yr": "{{time}}anno", + "mo": "{{time}}mese", + "d": "{{time}}giorno", + "h": "{{time}}ora", + "m": "{{time}}min", + "s": "{{time}}sec", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "24hour": "MM-dd-yy-HH-mm-ss", + "12hour": "MM-dd-yy-h-mm-ss-a" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "In corso", + "invalidStartTime": "Ora di inizio non valida", + "invalidEndTime": "Ora di fine non valida" + }, + "button": { + "cancel": "Annulla", + "yes": "Sì", + "unselect": "Deseleziona", + "disabled": "Disabilitato", + "fullscreen": "A schermo intero", + "save": "Salva", + "no": "No", + "edit": "Modifica", + "export": "Esporta", + "disable": "Disabilita", + "apply": "Applica", + "reset": "Reimposta", + "done": "Fatto", + "enabled": "Abilitato", + "enable": "Abilita", + "saving": "Salvataggio…", + "copy": "Copia", + "history": "Storico", + "exitFullscreen": "Esci da schermo intero", + "on": "ACCESO", + "copyCoordinates": "Copia coordinate", + "download": "Scarica", + "info": "Informazioni", + "suspended": "Sospeso", + "unsuspended": "Riattiva", + "play": "Riproduci", + "deleteNow": "Elimina ora", + "next": "Successivo", + "off": "SPENTO", + "delete": "Elimina", + "close": "Chiudi", + "back": "Indietro", + "pictureInPicture": "Immagine nell'immagine", + "twoWayTalk": "Comunicazione bidirezionale", + "cameraAudio": "Audio della telecamera", + "continue": "Continua" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "miglia/h" + }, + "length": { + "feet": "piedi", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/ora", + "mbph": "MB/ora", + "gbph": "GB/ora" + } + }, + "label": { + "back": "Vai indietro", + "hide": "Nascondi {{item}}", + "show": "Mostra {{item}}", + "ID": "ID", + "none": "Nessuna", + "all": "Tutte" + }, + "menu": { + "configuration": "Configurazione", + "languages": "Lingue", + "appearance": "Aspetto", + "systemMetrics": "Metriche di sistema", + "systemLogs": "Registri di sistema", + "settings": "Impostazioni", + "configurationEditor": "Editor di configurazione", + "language": { + "en": "English (Inglese)", + "zhCN": "简体中文 (Cinese semplificato)", + "withSystem": { + "label": "Usa la lingua di sistema" + }, + "es": "Español (Spagnolo)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francese)", + "ar": "العربية (Arabo)", + "pt": "Português (Portoghese)", + "ru": "Русский (Russo)", + "tr": "Türkçe (Turco)", + "nl": "Nederlands (Olandese)", + "sv": "Svenska (Svedese)", + "cs": "Čeština (Ceco)", + "nb": "Norsk Bokmål (Norvegese)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persiano)", + "ro": "Română (Rumeno)", + "hu": "Magyar (Ungherese)", + "fi": "Suomi (Finlandese)", + "da": "Dansk (Danese)", + "el": "Ελληνικά (Greco)", + "sk": "Slovenčina (Slovacco)", + "ja": "日本語 (Giapponese)", + "uk": "Українська (Ucraino)", + "pl": "Polski (Polacco)", + "de": "Deutsch (Tedesco)", + "he": "עברית (Ebraico)", + "it": "Italiano (Italiano)", + "yue": "粵語 (Cantonese)", + "th": "ไทย (Tailandese)", + "ca": "Català (Catalano)", + "ptBR": "Português brasileiro (Portoghese brasiliano)", + "sr": "Српски (Serbo)", + "sl": "Slovenščina (Sloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Bulgaro)", + "gl": "Galego (Galiziano)", + "id": "Bahasa Indonesia (Indonesiano)", + "ur": "اردو (Urdu)" + }, + "darkMode": { + "label": "Modalità scura", + "light": "Chiara", + "dark": "Scura", + "withSystem": { + "label": "Usa le impostazioni di sistema per le modalità chiara/scura" + } + }, + "system": "Sistema", + "theme": { + "label": "Tema", + "blue": "Blu", + "contrast": "Alto contrasto", + "green": "Verde", + "default": "Predefinito", + "red": "Rosso", + "nord": "Nord", + "highcontrast": "Contrasto elevato" + }, + "live": { + "cameras": { + "title": "Telecamere", + "count_one": "{{count}} Telecamera", + "count_many": "{{count}} Telecamere", + "count_other": "{{count}} Telecamere" + }, + "title": "Dal vivo", + "allCameras": "Tutte le telecamere" + }, + "help": "Aiuto", + "documentation": { + "title": "Documentazione", + "label": "Documentazione di Frigate" + }, + "restart": "Riavvia Frigate", + "review": "Rivedi", + "explore": "Esplora", + "export": "Esporta", + "uiPlayground": "Interfaccia area prove", + "user": { + "account": "Account", + "current": "Utente attuale: {{user}}", + "anonymous": "anonimo", + "logout": "Esci", + "title": "Utente", + "setPassword": "Imposta password" + }, + "withSystem": "Sistema", + "faceLibrary": "Raccolta volti", + "classification": "Classificazione" + }, + "pagination": { + "next": { + "title": "Successiva", + "label": "Vai alla pagina successiva" + }, + "previous": { + "label": "Vai alla pagina precedente", + "title": "Precedente" + }, + "label": "paginazione", + "more": "Altre pagine" + }, + "role": { + "title": "Ruolo", + "admin": "Amministratore", + "viewer": "Spettatore", + "desc": "Gli amministratori hanno accesso completo a tutte le funzionalità dell'interfaccia utente di Frigate. Gli spettatori possono visualizzare solo le telecamere, gli elementi di revisione e i filmati storici nell'interfaccia utente." + }, + "accessDenied": { + "desc": "Non hai i permessi per visualizzare questa pagina.", + "documentTitle": "Accesso vietato - Frigate", + "title": "Accesso vietato" + }, + "notFound": { + "desc": "Pagina non trovata", + "documentTitle": "Non trovato - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "URL copiata negli appunti.", + "save": { + "error": { + "title": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "noMessage": "Impossibile salvare le modifiche alla configurazione" + }, + "title": "Salva" + } + }, + "selectItem": "Seleziona {{item}}", + "readTheDocumentation": "Leggi la documentazione", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} e {{1}}", + "many": "{{items}}, e {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opzionale", + "internalID": "L'ID interno che Frigate utilizza nella configurazione e nel database" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/auth.json new file mode 100644 index 0000000..f743877 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nome utente", + "password": "Password", + "login": "Accedi", + "errors": { + "usernameRequired": "Il nome utente è obbligatorio", + "passwordRequired": "La password è obbligatoria", + "rateLimit": "Superato il limite di tentativi. Riprova più tardi.", + "unknownError": "Errore sconosciuto. Controlla i registri.", + "webUnknownError": "Errore sconosciuto. Controlla i registri della console.", + "loginFailed": "Accesso non riuscito" + }, + "firstTimeLogin": "Stai cercando di accedere per la prima volta? Le credenziali sono scritte nei registri di Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/camera.json new file mode 100644 index 0000000..a681de1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Gruppo di telecamere", + "add": "Aggiungi gruppo di telecamere", + "delete": { + "label": "Elimina gruppo di telecamere", + "confirm": { + "title": "Conferma eliminazione", + "desc": "Sei sicuro di voler eliminare il gruppo di telecamere {{name}}?" + } + }, + "edit": "Modifica gruppo di telecamere", + "name": { + "label": "Nome", + "placeholder": "Inserisci un nome…", + "errorMessage": { + "exists": "Il nome scelto per il gruppo telecamere è già presente.", + "invalid": "Nome del gruppo di telecamere non valido.", + "mustLeastCharacters": "Il nome del gruppo di telecamere deve contenere almeno 2 caratteri.", + "nameMustNotPeriod": "Il nome del gruppo di telecamere non deve contenere punti." + } + }, + "camera": { + "setting": { + "label": "Impostazioni di trasmissione della telecamera", + "title": "Impostazioni di trasmissione di {{cameraName}}", + "audioIsAvailable": "L'audio è disponibile per questo flusso", + "streamMethod": { + "label": "Metodo di trasmissione", + "method": { + "smartStreaming": { + "label": "Trasmissione intelligente (consigliata)", + "desc": "La trasmissione intelligente aggiorna l'immagine della telecamera una volta al minuto quando non si verifica alcuna attività rilevabile, per risparmiare larghezza di banda e risorse. Quando viene rilevata un'attività, l'immagine passa automaticamente alla trasmissione dal vivo." + }, + "continuousStreaming": { + "label": "Trasmissione continua", + "desc": { + "warning": "La trasmissione continua può causare un elevato utilizzo di larghezza di banda e problemi di prestazioni. Da usare con cautela.", + "title": "L'immagine della telecamera sarà sempre trasmessa dal vivo quando è visibile sulla schermata, anche se non viene rilevata alcuna attività." + } + }, + "noStreaming": { + "label": "Nessuna trasmissione", + "desc": "Le immagini delle telecamere verranno aggiornate solo una volta al minuto e non verrà effettuata alcuna trasmissione dal vivo." + } + }, + "placeholder": "Scegli un metodo di trasmissione" + }, + "compatibilityMode": { + "label": "Modalità di compatibilità", + "desc": "Abilita questa opzione solo se la trasmissione dal vivo della tua telecamera mostra artefatti cromatici e presenta una linea diagonale sul lato destro dell'immagine." + }, + "audio": { + "tips": { + "document": "Leggi la documentazione ", + "title": "L'audio deve essere trasmesso dalla tua telecamera e configurato in go2rtc per questo flusso." + } + }, + "audioIsUnavailable": "L'audio non è disponibile per questo flusso", + "desc": "Modifica le opzioni di trasmissione dal vivo per la schermata di questo gruppo di telecamere. Queste impostazioni sono specifiche del dispositivo/browser.", + "stream": "Flusso", + "placeholder": "Scegli un flusso" + }, + "birdseye": "Birdseye" + }, + "cameras": { + "desc": "Seleziona le telecamere per questo gruppo.", + "label": "Telecamere" + }, + "icon": "Icona", + "success": "Il gruppo di telecamere ({{name}}) è stato salvato." + }, + "debug": { + "options": { + "label": "Impostazioni", + "title": "Opzioni", + "showOptions": "Mostra opzioni", + "hideOptions": "Nascondi opzioni" + }, + "boundingBox": "Riquadro di delimitazione", + "timestamp": "Orario", + "zones": "Zone", + "mask": "Maschera", + "motion": "Movimento", + "regions": "Regioni" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/dialog.json new file mode 100644 index 0000000..5e88d1f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/dialog.json @@ -0,0 +1,136 @@ +{ + "restart": { + "title": "Sei sicuro di voler riavviare Frigate?", + "button": "Riavvia", + "restarting": { + "title": "Frigate si sta riavviando", + "content": "Questa pagina si ricaricherà in {{countdown}} secondi.", + "button": "Forza ricarica ora" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Invia a Frigate+", + "desc": "Gli oggetti in posizioni che si desidera evitare non sono falsi positivi. Inviarli come falsi positivi confonderà il modello." + }, + "review": { + "false": { + "label": "Non confermare questa etichetta per Frigate Plus", + "false_one": "Questo non è un {{label}}", + "false_many": "Questi non sono un {{label}}", + "false_other": "Questi non sono un {{label}}" + }, + "true": { + "label": "Conferma questa etichetta per Frigate Plus", + "true_one": "Questo è un {{label}}", + "true_many": "Questi sono {{label}}", + "true_other": "Questi sono {{label}}" + }, + "state": { + "submitted": "Inviato" + }, + "question": { + "label": "Conferma questa etichetta per Frigate Plus", + "ask_a": "Questo oggetto è un {{label}}?", + "ask_an": "Questo oggetto è un {{label}}?", + "ask_full": "Questo oggetto è un {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Visualizza in Storico" + } + }, + "export": { + "time": { + "fromTimeline": "Seleziona dalla cronologia", + "custom": "Personalizzato", + "start": { + "title": "Ora di inizio", + "label": "Seleziona l'ora di inizio" + }, + "end": { + "title": "Ora di fine", + "label": "Seleziona l'ora di fine" + }, + "lastHour_one": "Ultima ora", + "lastHour_many": "Ultime {{count}} ore", + "lastHour_other": "Ultime {{count}} ore" + }, + "export": "Esporta", + "selectOrExport": "Seleziona o esporta", + "toast": { + "success": "Esportazione avviata correttamente. Visualizza il file nella pagina delle esportazioni.", + "error": { + "failed": "Impossibile avviare l'esportazione: {{error}}", + "endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio", + "noVaildTimeSelected": "Nessun intervallo di tempo valido selezionato" + }, + "view": "Visualizzazione" + }, + "fromTimeline": { + "saveExport": "Salva esportazione", + "previewExport": "Anteprima esportazione" + }, + "select": "Seleziona", + "name": { + "placeholder": "Assegna un nome all'esportazione" + } + }, + "streaming": { + "label": "Trasmissione", + "showStats": { + "label": "Mostra statistiche di trasmissione", + "desc": "Abilita questa opzione per visualizzare le statistiche della trasmissione come sovrapposizione sul flusso della telecamera." + }, + "debugView": "Vista correzioni", + "restreaming": { + "disabled": "La ritrasmissione non è abilitata per questa telecamera.", + "desc": { + "title": "Imposta go2rtc per opzioni aggiuntive di visualizzazione dal vivo e audio per questa telecamera.", + "readTheDocumentation": "Leggi la documentazione" + } + } + }, + "search": { + "saveSearch": { + "label": "Salva ricerca", + "overwrite": "{{searchName}} esiste già. Il salvataggio sovrascriverà il valore esistente.", + "desc": "Specifica un nome per questa ricerca salvata.", + "button": { + "save": { + "label": "Salva questa ricerca" + } + }, + "placeholder": "Inserisci un nome per la tua ricerca", + "success": "La ricerca ({{searchName}}) è stata salvata." + } + }, + "recording": { + "button": { + "export": "Esporta", + "markAsReviewed": "Segna come visto", + "deleteNow": "Elimina ora", + "markAsUnreviewed": "Segna come non visto" + }, + "confirmDelete": { + "desc": { + "selected": "Vuoi davvero eliminare tutti i video registrati associati a questo elemento di visto?

    Tieni premuto il tasto Maiusc per ignorare questa finestra di dialogo in futuro." + }, + "title": "Conferma eliminazione", + "toast": { + "success": "Il filmato associato agli elementi di recensione selezionati è stato eliminato correttamente.", + "error": "Impossibile eliminare: {{error}}" + } + } + }, + "imagePicker": { + "selectImage": "Seleziona la miniatura di un oggetto tracciato", + "search": { + "placeholder": "Cerca per etichetta o sottoetichetta..." + }, + "noImages": "Nessuna miniatura trovata per questa telecamera", + "unknownLabel": "Immagine di attivazione salvata" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/filter.json new file mode 100644 index 0000000..22fc520 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtro", + "labels": { + "label": "Etichette", + "all": { + "title": "Tutte le etichette", + "short": "Etichette" + }, + "count_one": "{{count}} Etichetta", + "count_other": "{{count}} Etichette" + }, + "more": "Altri filtri", + "zones": { + "label": "Zone", + "all": { + "title": "Tutte le zone", + "short": "Zone" + } + }, + "dates": { + "all": { + "title": "Tutte le date", + "short": "Date" + }, + "selectPreset": "Seleziona una preimpostazione…" + }, + "reset": { + "label": "Ripristina i filtri ai valori predefiniti" + }, + "features": { + "submittedToFrigatePlus": { + "label": "Inviato a Frigate+", + "tips": "Devi prima filtrare gli oggetti tracciati che hanno un'istantanea.

    Gli oggetti tracciati senza istantanee non possono essere inviati a Frigate+." + }, + "hasSnapshot": "Contiene una istantanea", + "hasVideoClip": "Contiene un filmato video", + "label": "Caratteristiche" + }, + "sort": { + "dateAsc": "Data (crescente)", + "scoreAsc": "Punteggio dell'oggetto (crescente)", + "dateDesc": "Data (discendente)", + "speedAsc": "Velocità stimata (crescente)", + "speedDesc": "Velocità stimata (discendente)", + "relevance": "Rilevanza", + "label": "Ordina", + "scoreDesc": "Punteggio dell'oggetto (discrescente)" + }, + "explore": { + "settings": { + "title": "Impostazioni", + "gridColumns": { + "desc": "Seleziona il numero di colonne nella vista griglia.", + "title": "Colonne della griglia" + }, + "defaultView": { + "title": "Vista predefinita", + "desc": "Se non è selezionato alcun filtro, visualizza un riepilogo degli oggetti tracciati più di recente per etichetta oppure visualizza una griglia non filtrata.", + "summary": "Riepilogo", + "unfilteredGrid": "Griglia non filtrata" + }, + "searchSource": { + "label": "Cerca la fonte", + "desc": "Scegli se cercare nelle miniature o nelle descrizioni degli oggetti tracciati.", + "options": { + "thumbnailImage": "Immagine in miniatura", + "description": "Descrizione" + } + } + }, + "date": { + "selectDateBy": { + "label": "Seleziona una data da filtrare" + } + } + }, + "logSettings": { + "label": "Filtra livello di registro", + "loading": { + "title": "Caricamento", + "desc": "Scorrendo il riquadro dei registri fino in fondo, i nuovi registri vengono automaticamente visualizzati non appena vengono aggiunti." + }, + "filterBySeverity": "Filtra i registri per gravità", + "disableLogStreaming": "Disabilita la trasmissione del registro", + "allLogs": "Tutti i registri" + }, + "trackedObjectDelete": { + "toast": { + "success": "Oggetti tracciati eliminati correttamente.", + "error": "Impossibile eliminare gli oggetti tracciati: {{errorMessage}}" + }, + "desc": "L'eliminazione di questi {{objectLength}} oggetti tracciati rimuove l'istantanea, eventuali incorporamenti salvati e tutte le voci associate al ciclo di vita dell'oggetto. Il filmato registrato di questi oggetti tracciati nella vista Storico NON verrà eliminato.

    Vuoi procedere?

    Tieni premuto il tasto Maiusc per ignorare questa finestra di dialogo in futuro.", + "title": "Conferma eliminazione" + }, + "recognizedLicensePlates": { + "title": "Targhe riconosciute", + "selectPlatesFromList": "Seleziona una o più targhe dall'elenco.", + "loadFailed": "Impossibile caricare le targhe riconosciute.", + "loading": "Caricamento targhe riconosciute…", + "placeholder": "Digita per cercare le targhe…", + "noLicensePlatesFound": "Nessuna targa trovata.", + "selectAll": "Seleziona tutto", + "clearAll": "Cancella tutto" + }, + "timeRange": "Intervallo di tempo", + "subLabels": { + "label": "Sottoetichette", + "all": "Tutte le sottoetichette" + }, + "score": "Punteggio", + "estimatedSpeed": "Velocità stimata ({{unit}})", + "cameras": { + "label": "Filtro telecamere", + "all": { + "title": "Tutte le telecamere", + "short": "Telecamere" + } + }, + "review": { + "showReviewed": "Mostra visti" + }, + "motion": { + "showMotionOnly": "Mostra solo movimento" + }, + "zoneMask": { + "filterBy": "Filtra per maschera di zona" + }, + "classes": { + "label": "Classi", + "all": { + "title": "Tutte le classi" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classi" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/icons.json new file mode 100644 index 0000000..1877886 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Cerca un'icona…" + }, + "selectIcon": "Seleziona un'icona" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/input.json new file mode 100644 index 0000000..fdd42a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "toast": { + "success": "Il video dell'oggetto da te visto ha iniziato a scaricarsi." + }, + "label": "Scarica video" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/it/components/player.json new file mode 100644 index 0000000..2aee1a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Nessuna anteprima trovata", + "noRecordingsFoundForThisTime": "Nessuna registrazione trovata per questo intervallo", + "noPreviewFoundFor": "Nessuna anteprima trovata per {{cameraName}}", + "submitFrigatePlus": { + "title": "Vuoi inviare questo fotogramma a Frigate+?", + "submit": "Invia" + }, + "livePlayerRequiredIOSVersion": "Per questo tipo di trasmissione dal vivo è richiesto iOS 17.1 o versione successiva.", + "stats": { + "streamType": { + "short": "Tipo", + "title": "Tipo di trasmissione:" + }, + "bandwidth": { + "title": "Larghezza di banda:", + "short": "Larghezza di banda" + }, + "latency": { + "title": "Latenza:", + "value": "{{seconds}} secondi", + "short": { + "title": "Latenza", + "value": "{{seconds}} sec" + } + }, + "droppedFrames": { + "title": "Fotogrammi persi:", + "short": { + "title": "Persi", + "value": "{{droppedFrames}} fotogrammi" + } + }, + "decodedFrames": "Fotogrammi decodificati:", + "totalFrames": "Totale fotogrammi:", + "droppedFrameRate": "Percentuali fotogrammi persi:" + }, + "streamOffline": { + "desc": "Nessun fotogramma ricevuto sul flusso detect di {{cameraName}}, controlla i registri degli errori", + "title": "Trasmissione disconnessa" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Fotogramma inviato correttamente a Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Impossibile inviare il fotogramma a Frigate+" + } + }, + "cameraDisabled": "La telecamera è disattivata" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/objects.json b/sam2-cpu/frigate-dev/web/public/locales/it/objects.json new file mode 100644 index 0000000..a512b00 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/objects.json @@ -0,0 +1,120 @@ +{ + "bird": "Uccello", + "clock": "Orologio", + "scissors": "Forbici", + "vehicle": "Veicolo", + "cat": "Gatto", + "boat": "Barca", + "horse": "Cavallo", + "bicycle": "Bicicletta", + "person": "Persona", + "car": "Automobile", + "motorcycle": "Motociclo", + "dog": "Cane", + "bark": "Abbaio", + "animal": "Animale", + "sheep": "Pecora", + "mouse": "Mouse", + "bear": "Orso", + "elephant": "Elefante", + "zebra": "Zebra", + "giraffe": "Giraffa", + "hat": "Cappello", + "umbrella": "Ombrello", + "train": "Treno", + "backpack": "Zaino", + "cow": "Mucca", + "airplane": "Aereo", + "bus": "Autobus", + "shoe": "Scarpa", + "skateboard": "Skateboard", + "kite": "Aquilone", + "oven": "Forno", + "sink": "Lavello", + "stop_sign": "Segnale di stop", + "raccoon": "Procione", + "postnl": "PostNL", + "nzpost": "NZPost", + "book": "Libro", + "frisbee": "Frisbee", + "laptop": "Portatile", + "knife": "Coltello", + "spoon": "Cucchiaio", + "bowl": "Ciotola", + "dhl": "DHL", + "banana": "Banana", + "carrot": "Carota", + "dining_table": "Tavolo da pranzo", + "hot_dog": "Hot Dog", + "mirror": "Specchio", + "microwave": "Microonde", + "toaster": "Tostapane", + "teddy_bear": "Orsacchiotto di peluche", + "hair_brush": "Spazzola per capelli", + "squirrel": "Scoiattolo", + "deer": "Cervo", + "robot_lawnmower": "Robot tagliaerba", + "waste_bin": "Cestino", + "on_demand": "Su richiesta", + "ups": "UPS", + "fedex": "FedEx", + "postnord": "PostNord", + "traffic_light": "Semaforo", + "fire_hydrant": "Idrante antincendio", + "street_sign": "Cartello stradale", + "parking_meter": "Parchimetro", + "bench": "Panca", + "eye_glasses": "Occhiali da vista", + "handbag": "Borsa a mano", + "tie": "Cravatta", + "suitcase": "Valigia", + "skis": "Sci", + "snowboard": "Snowboard", + "sports_ball": "Palla sportiva", + "baseball_bat": "Mazza da baseball", + "baseball_glove": "Guanto da baseball", + "surfboard": "Tavola da surf", + "tennis_racket": "Racchetta da tennis", + "bottle": "Bottiglia", + "plate": "Piatto", + "wine_glass": "Bicchiere da vino", + "cup": "Tazza", + "fork": "Forchetta", + "apple": "Mela", + "sandwich": "Panino", + "orange": "Arancia", + "broccoli": "Broccoli", + "pizza": "Pizza", + "donut": "Ciambella", + "cake": "Torta", + "chair": "Sedia", + "couch": "Divano", + "door": "Porta", + "keyboard": "Tastiera", + "potted_plant": "Pianta in vaso", + "bed": "Letto", + "window": "Finestra", + "desk": "Scrivania", + "toilet": "Toilette", + "tv": "Televisione", + "remote": "Telecomando", + "cell_phone": "Telefono cellulare", + "blender": "Miscelatore", + "refrigerator": "Frigorifero", + "hair_dryer": "Asciugacapelli", + "toothbrush": "Spazzolino da denti", + "vase": "Vaso", + "fox": "Volpe", + "goat": "Capra", + "rabbit": "Coniglio", + "face": "Viso", + "license_plate": "Targa", + "package": "Pacchetto", + "bbq_grill": "Griglia per barbecue", + "amazon": "Amazon", + "usps": "USPS", + "an_post": "An Post", + "purolator": "Purolator", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/classificationModel.json new file mode 100644 index 0000000..84fb129 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/classificationModel.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Modelli di classificazione", + "button": { + "deleteClassificationAttempts": "Elimina immagini di classificazione", + "renameCategory": "Rinomina classe", + "deleteCategory": "Elimina classe", + "deleteImages": "Elimina immagini", + "trainModel": "Modello di addestramento", + "addClassification": "Aggiungi classificazione", + "deleteModels": "Elimina modelli", + "editModel": "Modifica modello" + }, + "toast": { + "success": { + "deletedCategory": "Classe eliminata", + "deletedImage": "Immagini eliminate", + "categorizedImage": "Immagine classificata con successo", + "trainedModel": "Modello addestrato con successo.", + "trainingModel": "Avviato con successo l'addestramento del modello.", + "deletedModel_one": "Eliminato con successo {{count}} modello", + "deletedModel_many": "Eliminati con successo {{count}} modelli", + "deletedModel_other": "Eliminati con successo {{count}} modelli", + "updatedModel": "Configurazione del modello aggiornata correttamente", + "renamedCategory": "Classe rinominata correttamente in {{name}}" + }, + "error": { + "deleteImageFailed": "Impossibile eliminare: {{errorMessage}}", + "deleteCategoryFailed": "Impossibile eliminare la classe: {{errorMessage}}", + "categorizeFailed": "Impossibile categorizzare l'immagine: {{errorMessage}}", + "trainingFailed": "Addestramento del modello fallito. Controlla i registri di Frigate per i dettagli.", + "deleteModelFailed": "Impossibile eliminare il modello: {{errorMessage}}", + "updateModelFailed": "Impossibile aggiornare il modello: {{errorMessage}}", + "trainingFailedToStart": "Impossibile avviare l'addestramento del modello: {{errorMessage}}", + "renameCategoryFailed": "Impossibile rinominare la classe: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Elimina classe", + "desc": "Vuoi davvero eliminare la classe {{name}}? Questa operazione eliminerà definitivamente tutte le immagini associate e richiederà un nuovo addestramento del modello.", + "minClassesTitle": "Impossibile eliminare la classe", + "minClassesDesc": "Un modello di classificazione deve avere almeno 2 classi. Aggiungi un'altra classe prima di eliminare questa." + }, + "deleteDatasetImages": { + "title": "Elimina immagini della base dati", + "desc_one": "Vuoi davvero eliminare {{count}} immagine da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello.", + "desc_many": "Vuoi davvero eliminare {{count}} immagini da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello.", + "desc_other": "Vuoi davvero eliminare {{count}} immagini da {{dataset}}? Questa azione non può essere annullata e richiederà un nuovo addestramento del modello." + }, + "deleteTrainImages": { + "title": "Elimina le immagini di addestramento", + "desc_one": "Vuoi davvero eliminare {{count}} immagine? Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} immagini? Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} immagini? Questa azione non può essere annullata." + }, + "renameCategory": { + "title": "Rinomina classe", + "desc": "Inserisci un nuovo nome per {{name}}. Sarà necessario riaddestrare il modello affinché la modifica del nome abbia effetto." + }, + "description": { + "invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini." + }, + "train": { + "title": "Classificazioni recenti", + "titleShort": "Recente", + "aria": "Seleziona classificazioni recenti" + }, + "categories": "Classi", + "createCategory": { + "new": "Crea nuova classe" + }, + "categorizeImageAs": "Classifica immagine come:", + "categorizeImage": "Classifica immagine", + "noModels": { + "object": { + "title": "Nessun modello di classificazione degli oggetti", + "description": "Crea un modello personalizzato per classificare gli oggetti rilevati.", + "buttonText": "Crea modello oggetto" + }, + "state": { + "title": "Nessun modello di classificazione dello stato", + "description": "Crea un modello personalizzato per monitorare e classificare i cambiamenti di stato in aree specifiche della telecamera.", + "buttonText": "Crea modello di stato" + } + }, + "wizard": { + "title": "Crea nuova classificazione", + "steps": { + "nameAndDefine": "Nome e definizione", + "stateArea": "Area di stato", + "chooseExamples": "Scegli esempi" + }, + "step1": { + "description": "I modelli di stato monitorano le aree fisse delle telecamere per rilevare eventuali cambiamenti (ad esempio, porta aperta/chiusa). I modelli di oggetti aggiungono classificazioni agli oggetti rilevati (ad esempio, animali noti, addetti alle consegne, ecc.).", + "name": "Nome", + "namePlaceholder": "Inserisci il nome del modello...", + "type": "Tipo", + "typeState": "Stato", + "typeObject": "Oggetto", + "objectLabel": "Etichetta oggetto", + "objectLabelPlaceholder": "Seleziona il tipo di oggetto...", + "classificationType": "Tipo di classificazione", + "classificationTypeTip": "Scopri i tipi di classificazione", + "classificationTypeDesc": "Le sottoetichette aggiungono testo aggiuntivo all'etichetta dell'oggetto (ad esempio, \"Persona: UPS\"). Gli attributi sono metadati ricercabili, archiviati separatamente nei metadati dell'oggetto.", + "classificationSubLabel": "Etichetta secondaria", + "classificationAttribute": "Attributo", + "classes": "Classi", + "classesTip": "Scopri di più sulle classi", + "classesStateDesc": "Definisci i diversi stati in cui può trovarsi l'area della tua telecamera. Ad esempio: \"aperto\" e \"chiuso\" per una porta del garage.", + "classesObjectDesc": "Definisci le diverse categorie in cui classificare gli oggetti rilevati. Ad esempio: \"corriere\", \"residente\", \"straniero\" per la classificazione delle persone.", + "classPlaceholder": "Inserisci il nome della classe...", + "errors": { + "nameRequired": "Il nome del modello è obbligatorio", + "nameLength": "Il nome del modello deve contenere al massimo 64 caratteri", + "nameOnlyNumbers": "Il nome del modello non può contenere solo numeri", + "classRequired": "È richiesta almeno 1 classe", + "classesUnique": "I nomi delle classi devono essere univoci", + "stateRequiresTwoClasses": "I modelli di stato richiedono almeno 2 classi", + "objectLabelRequired": "Seleziona un'etichetta per l'oggetto", + "objectTypeRequired": "Seleziona un tipo di classificazione" + }, + "states": "Stati" + }, + "step2": { + "description": "Seleziona le telecamere e definisci l'area da monitorare per ciascuna telecamera. Il modello classificherà lo stato di queste aree.", + "cameras": "Telecamere", + "selectCamera": "Seleziona telecamera", + "noCameras": "Fai clic su + per aggiungere telecamere", + "selectCameraPrompt": "Selezionare una telecamera dall'elenco per definire la sua area di monitoraggio" + }, + "step3": { + "selectImagesPrompt": "Seleziona tutte le immagini con: {{className}}", + "selectImagesDescription": "Clicca sulle immagini per selezionarle. Clicca su Continua quando hai finito con questa classe.", + "generating": { + "title": "Generazione di immagini campione", + "description": "Frigate sta estraendo immagini rappresentative dalle registrazioni. L'operazione potrebbe richiedere qualche istante..." + }, + "training": { + "title": "Modello di addestramento", + "description": "Il tuo modello è in fase di addestramento in sottofondo. Chiudi questa finestra di dialogo e il tuo modello inizierà a funzionare non appena l'addestramento sarà completato." + }, + "retryGenerate": "Riprova generazione", + "noImages": "Nessuna immagine campione generata", + "classifying": "Classificazione e addestramento...", + "trainingStarted": "Addestramento iniziato con successo", + "errors": { + "noCameras": "Nessuna telecamera configurata", + "noObjectLabel": "Nessuna etichetta oggetto selezionata", + "generateFailed": "Impossibile generare esempi: {{error}}", + "generationFailed": "Generazione fallita. Per favore riprova.", + "classifyFailed": "Impossibile classificare le immagini: {{error}}" + }, + "generateSuccess": "Immagini campione generate correttamente", + "allImagesRequired_one": "Classifica tutte le immagini. Rimane {{count}} immagine.", + "allImagesRequired_many": "Classifica tutte le immagini. Rimangono {{count}} immagini.", + "allImagesRequired_other": "Classifica tutte le immagini. Rimangono {{count}} immagini.", + "modelCreated": "Modello creato correttamente. Utilizza la vista Classificazioni recenti per aggiungere immagini per gli stati mancanti, quindi addestrare il modello.", + "missingStatesWarning": { + "title": "Esempi di stati mancanti", + "description": "Non hai selezionato esempi per tutti gli stati. Il modello non verrà addestrato finché tutti gli stati non avranno immagini. Dopo aver continuato, utilizza la vista Classificazioni recenti per classificare le immagini per gli stati mancanti, quindi addestra il modello." + } + } + }, + "deleteModel": { + "title": "Elimina modello di classificazione", + "single": "Vuoi davvero eliminare {{name}}? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di allenamento. Questa azione non può essere annullata.", + "desc_one": "Vuoi davvero eliminare {{count}} modello? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} modelli? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} modelli? Questa operazione eliminerà definitivamente tutti i dati associati, comprese le immagini e i dati di addestramento. Questa azione non può essere annullata." + }, + "menu": { + "objects": "Oggetti", + "states": "Stati" + }, + "details": { + "scoreInfo": "Il punteggio rappresenta la confidenza media della classificazione in tutti i rilevamenti di questo oggetto." + }, + "edit": { + "title": "Modifica modello di classificazione", + "descriptionState": "Modifica le classi per questo modello di classificazione dello stato. Le modifiche richiederanno un nuovo addestramento del modello.", + "descriptionObject": "Modifica il tipo di oggetto e il tipo di classificazione per questo modello di classificazione degli oggetti.", + "stateClassesInfo": "Nota: la modifica delle classi di stato richiede il riaddestramento del modello con le classi aggiornate." + }, + "tooltip": { + "trainingInProgress": "Il modello è attualmente in addestramento", + "modelNotReady": "Il modello non è pronto per l'addestramento", + "noNewImages": "Nessuna nuova immagine da addestrare. Classifica prima più immagini nel database.", + "noChanges": "Nessuna modifica al database dall'ultimo addestramento." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/configEditor.json new file mode 100644 index 0000000..f53aaed --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor di configurazione", + "documentTitle": "Editor di configurazione - Frigate", + "copyConfig": "Copia configurazione", + "saveAndRestart": "Salva e riavvia", + "saveOnly": "Salva soltanto", + "toast": { + "success": { + "copyToClipboard": "Configurazione copiata negli appunti." + }, + "error": { + "savingError": "Errore durante il salvataggio della configurazione" + } + }, + "confirm": "Vuoi uscire senza salvare?", + "safeConfigEditor": "Editor di configurazione (modalità provvisoria)", + "safeModeDescription": "Frigate è in modalità provvisoria a causa di un errore di convalida della configurazione." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/events.json new file mode 100644 index 0000000..d5e861c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/events.json @@ -0,0 +1,63 @@ +{ + "alerts": "Avvisi", + "detections": "Rilevamenti", + "motion": { + "label": "Movimenti", + "only": "Solo movimenti" + }, + "empty": { + "alert": "Non ci sono avvisi da rivedere", + "detection": "Non ci sono rilevamenti da rivedere", + "motion": "Nessun dato di movimento trovato" + }, + "newReviewItems": { + "label": "Visualizza i nuovi elementi da rivedere", + "button": "Nuovi elementi da rivedere" + }, + "markTheseItemsAsReviewed": "Segna questi elementi come visti", + "markAsReviewed": "Segna come visto", + "documentTitle": "Rivedi - Frigate", + "allCameras": "Tutte le camere", + "timeline": "Cronologia", + "timeline.aria": "Seleziona la cronologia", + "events": { + "label": "Eventi", + "aria": "Seleziona eventi", + "noFoundForTimePeriod": "Nessun evento trovato per questo intervallo." + }, + "recordings": { + "documentTitle": "Registrazioni - Frigate" + }, + "calendarFilter": { + "last24Hours": "Ultime 24 ore" + }, + "camera": "Telecamera", + "selected": "{{count}} selezionati", + "selected_one": "{{count}} selezionati", + "selected_other": "{{count}} selezionati", + "detected": "rilevato", + "suspiciousActivity": "Attività sospetta", + "threateningActivity": "Attività minacciosa", + "detail": { + "noDataFound": "Nessun dato dettagliato da rivedere", + "aria": "Attiva/disattiva la visualizzazione dettagliata", + "trackedObject_one": "{{count}} oggetto", + "trackedObject_other": "{{count}} oggetti", + "noObjectDetailData": "Non sono disponibili dati dettagliati sull'oggetto.", + "label": "Dettaglio", + "settings": "Impostazioni di visualizzazione dettagliata", + "alwaysExpandActive": { + "title": "Espandi sempre attivo", + "desc": "Espandere sempre i dettagli dell'oggetto dell'elemento di revisione attivo quando disponibili." + } + }, + "objectTrack": { + "trackedPoint": "Punto tracciato", + "clickToSeek": "Premi per cercare in questo momento" + }, + "zoomIn": "Ingrandisci", + "zoomOut": "Rimpicciolisci", + "normalActivity": "Normale", + "needsReview": "Necessita revisione", + "securityConcern": "Rischio per la sicurezza" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/explore.json new file mode 100644 index 0000000..9277ee9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/explore.json @@ -0,0 +1,291 @@ +{ + "generativeAI": "IA Generativa", + "documentTitle": "Esplora - Frigate", + "exploreIsUnavailable": { + "title": "Esplora non è disponibile", + "embeddingsReindexing": { + "context": "Potrai usare Esplora dopo che l'incorporamento degli oggetti tracciati avrà terminato la ricostruzione dell'indice.", + "step": { + "descriptionsEmbedded": "Descrizioni incorporate: ", + "thumbnailsEmbedded": "Miniature incorporate: ", + "trackedObjectsProcessed": "Oggetti tracciati elaborati: " + }, + "startingUp": "Avvio…", + "estimatedTime": "Tempo rimanente stimato:", + "finishingShortly": "Completamento a breve" + }, + "downloadingModels": { + "setup": { + "textModel": "Modello di testo", + "visionModel": "Modello di visione", + "visionModelFeatureExtractor": "Estrattore di funzionalità del modello di visione", + "textTokenizer": "Tokenizzatore di testo" + }, + "tips": { + "documentation": "Leggi la documentazione", + "context": "Una volta scaricati i modelli, potrebbe essere necessario reindicizzare gli incorporamenti degli oggetti tracciati." + }, + "error": "Si è verificato un errore. Controlla i registri di Frigate.", + "context": "Frigate sta scaricando i modelli di incorporamento necessari per supportare la funzione di Ricerca Semantica. L'operazione potrebbe richiedere diversi minuti, a seconda della velocità della connessione di rete." + } + }, + "details": { + "timestamp": "Orario", + "snapshotScore": { + "label": "Punteggio istantanea" + }, + "regenerateFromSnapshot": "Rigenera da istantanea", + "item": { + "tips": { + "mismatch_one": "E' stato rilevato {{count}} oggetto non disponibile e incluse in questo elemento visto. Tale oggetto non è stato classificato come avviso o rilevamento oppure è già stato ripulito/eliminato.", + "mismatch_many": "Sono stati rilevati {{count}} oggetti non disponibili e inclusi in questo elemento visto. Tali oggetti non sono stati classificati come avvisi o rilevamenti oppure sono già stati ripuliti/eliminati.", + "mismatch_other": "Sono stati rilevati {{count}} oggetti non disponibili e inclusi in questo elemento visto. Tali oggetti non sono stati classificati come avvisi o rilevamenti oppure sono già stati ripuliti/eliminati.", + "hasMissingObjects": "Modifica la configurazione se vuoi che Frigate salvi gli oggetti tracciati per le seguenti etichette: {{objects}}" + }, + "title": "Dettagli dell'elemento visto", + "desc": "Dettagli dell'elemento visto", + "button": { + "share": "Condividi questo elemento visto", + "viewInExplore": "Visualizza in Esplora" + }, + "toast": { + "success": { + "regenerate": "È stata richiesta una nuova descrizione a {{provider}}. A seconda della velocità del tuo provider, la rigenerazione della nuova descrizione potrebbe richiedere del tempo.", + "updatedSublabel": "Sottoetichetta aggiornata correttamente.", + "updatedLPR": "Targa aggiornata con successo.", + "audioTranscription": "Trascrizione audio richiesta con successo. A seconda della velocità del server Frigate, la trascrizione potrebbe richiedere del tempo." + }, + "error": { + "regenerate": "Impossibile chiamare {{provider}} per una nuova descrizione: {{errorMessage}}", + "updatedSublabelFailed": "Impossibile aggiornare la sottoetichetta: {{errorMessage}}", + "updatedLPRFailed": "Impossibile aggiornare la targa: {{errorMessage}}", + "audioTranscription": "Impossibile richiedere la trascrizione audio: {{errorMessage}}" + } + } + }, + "zones": "Zone", + "description": { + "label": "Descrizione", + "placeholder": "Descrizione dell'oggetto tracciato", + "aiTips": "Frigate non richiederà una descrizione al tuo fornitore di Intelligenza Artificiale Generativa finché non sarà terminato il ciclo di vita dell'oggetto tracciato." + }, + "label": "Etichetta", + "editSubLabel": { + "title": "Modifica sottoetichetta", + "desc": "Inserisci una nuova sottoetichetta per questa {{label}}", + "descNoLabel": "Inserisci una nuova sottoetichetta per questo oggetto tracciato" + }, + "editLPR": { + "title": "Modifica targa", + "desc": "Inserisci un nuovo valore della targa per questa {{label}}", + "descNoLabel": "Inserisci un nuovo valore di targa per questo oggetto tracciato" + }, + "topScore": { + "label": "Punteggio massimo", + "info": "Il punteggio massimo è il punteggio mediano più alto per l'oggetto tracciato, quindi potrebbe differire dal punteggio mostrato nella miniatura del risultato della ricerca." + }, + "recognizedLicensePlate": "Targa riconosciuta", + "estimatedSpeed": "Velocità stimata", + "objects": "Oggetti", + "camera": "Telecamera", + "button": { + "findSimilar": "Trova simili", + "regenerate": { + "title": "Rigenera", + "label": "Rigenera la descrizione dell'oggetto tracciato" + } + }, + "expandRegenerationMenu": "Espandi il menu di rigenerazione", + "regenerateFromThumbnails": "Rigenera dalle miniature", + "tips": { + "descriptionSaved": "Descrizione salvata correttamente", + "saveDescriptionFailed": "Impossibile aggiornare la descrizione: {{errorMessage}}" + }, + "score": { + "label": "Punteggio" + } + }, + "objectLifecycle": { + "annotationSettings": { + "offset": { + "tips": "SUGGERIMENTO: immagina un filmato evento con una persona che cammina da sinistra verso destra. Se il riquadro di delimitazione della cronologia dell'evento si trova costantemente a sinistra della persona, il valore dovrebbe essere diminuito. Analogamente, se una persona cammina da sinistra verso destra e il riquadro di delimitazione si trova costantemente davanti alla persona, il valore dovrebbe essere aumentato.", + "desc": "Questi dati provengono dal flusso di rilevamento della telecamera, ma vengono sovrapposti alle immagini del flusso di registrazione. È improbabile che i due flussi siano perfettamente sincronizzati. Di conseguenza, il riquadro di delimitazione e il filmato non saranno perfettamente allineati. Tuttavia, è possibile utilizzare il campo annotation_offset per correggere questo problema.", + "label": "Compensazione di annotazione", + "documentation": "Leggi la documentazione ", + "millisecondsToOffset": "Millisecondi per compensare il rilevamento delle annotazioni. Predefinito: 0", + "toast": { + "success": "La compensazione di annotazione per {{camera}} è stato salvato nel file di configurazione. Riavvia Frigate per applicare le modifiche." + } + }, + "showAllZones": { + "title": "Mostra tutte le zone", + "desc": "Mostra sempre le zone nei fotogrammi in cui gli oggetti sono entrati in una zona." + }, + "title": "Impostazioni di annotazione" + }, + "lifecycleItemDesc": { + "heard": "{{label}} sentito", + "attribute": { + "faceOrLicense_plate": "{{attribute}} rilevato per {{label}}", + "other": "{{label}} riconosciuto come {{attribute}}" + }, + "gone": "{{label}} lasciato", + "external": "{{label}} rilevato", + "visible": "{{label}} rilevato", + "entered_zone": "{{label}} è entrato in {{zones}}", + "active": "{{label}} è diventato attivo", + "stationary": "{{label}} è diventato stazionario", + "header": { + "ratio": "Rapporto", + "area": "Area", + "zones": "Zone" + } + }, + "title": "Ciclo di vita dell'oggetto", + "createObjectMask": "Crea maschera oggetto", + "noImageFound": "Nessuna immagine trovata per questo orario.", + "adjustAnnotationSettings": "Regola le impostazioni di annotazione", + "scrollViewTips": "Scorri per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.", + "autoTrackingTips": "Le posizioni dei riquadri di delimitazione non saranno precise per le telecamere con tracciamento automatico.", + "carousel": { + "previous": "Diapositiva precedente", + "next": "Diapositiva successiva" + }, + "count": "{{first}} di {{second}}", + "trackedPoint": "Punto tracciato" + }, + "type": { + "snapshot": "istantanea", + "object_lifecycle": "ciclo di vita dell'oggetto", + "details": "dettagli", + "video": "video", + "thumbnail": "miniatura", + "tracking_details": "dettagli di tracciamento" + }, + "itemMenu": { + "downloadSnapshot": { + "label": "Scarica istantanea", + "aria": "Scarica istantanea" + }, + "viewInHistory": { + "label": "Visualizza in Storico", + "aria": "Visualizza in Storico" + }, + "deleteTrackedObject": { + "label": "Elimina questo oggetto tracciato" + }, + "downloadVideo": { + "label": "Scarica video", + "aria": "Scarica video" + }, + "viewObjectLifecycle": { + "label": "Visualizza il ciclo di vita dell'oggetto", + "aria": "Mostra il ciclo di vita dell'oggetto" + }, + "findSimilar": { + "label": "Trova simili", + "aria": "Trova oggetti tracciati simili" + }, + "submitToPlus": { + "label": "Invia a Frigate+", + "aria": "Invia a Frigate Plus" + }, + "addTrigger": { + "label": "Aggiungi innesco", + "aria": "Aggiungi un innesco per questo oggetto tracciato" + }, + "audioTranscription": { + "label": "Trascrivere", + "aria": "Richiedi la trascrizione audio" + }, + "showObjectDetails": { + "label": "Mostra il percorso dell'oggetto" + }, + "hideObjectDetails": { + "label": "Nascondi il percorso dell'oggetto" + }, + "viewTrackingDetails": { + "label": "Visualizza i dettagli di tracciamento", + "aria": "Mostra i dettagli di tracciamento" + } + }, + "dialog": { + "confirmDelete": { + "desc": "L'eliminazione di questo oggetto tracciato rimuove l'istantanea, eventuali incorporamenti salvati e tutte le voci associate ai dettagli di tracciamento. Il filmato registrato di questo oggetto tracciato nella vista Storico NON verrà eliminato.

    Vuoi davvero procedere?", + "title": "Conferma eliminazione" + } + }, + "trackedObjectDetails": "Dettagli dell'oggetto tracciato", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Impossibile eliminare l'oggetto tracciato: {{errorMessage}}", + "success": "Oggetto tracciato eliminato correttamente." + } + }, + "tooltip": "Corrispondenza {{type}} al {{confidence}}%", + "previousTrackedObject": "Oggetto tracciato in precedenza", + "nextTrackedObject": "Prossimo oggetto tracciato" + }, + "trackedObjectsCount_one": "{{count}} oggetto tracciato ", + "trackedObjectsCount_many": "{{count}} oggetti tracciati ", + "trackedObjectsCount_other": "{{count}} oggetti tracciati ", + "fetchingTrackedObjectsFailed": "Errore durante il recupero degli oggetti tracciati: {{errorMessage}}", + "noTrackedObjects": "Nessun oggetto tracciato trovato", + "exploreMore": "Esplora altri oggetti {{label}}", + "aiAnalysis": { + "title": "Analisi IA" + }, + "concerns": { + "label": "Preoccupazioni" + }, + "trackingDetails": { + "title": "Dettagli di tracciamento", + "noImageFound": "Nessuna immagine trovata per questo orario.", + "createObjectMask": "Crea maschera oggetto", + "adjustAnnotationSettings": "Regola le impostazioni di annotazione", + "scrollViewTips": "Clicca per visualizzare i momenti più significativi del ciclo di vita di questo oggetto.", + "autoTrackingTips": "Le posizioni dei riquadri di delimitazione saranno imprecise per le telecamere con tracciamento automatico.", + "count": "{{first}} di {{second}}", + "trackedPoint": "Punto tracciato", + "lifecycleItemDesc": { + "visible": "{{label}} rilevato", + "entered_zone": "{{label}} è entrato in {{zones}}", + "active": "{{label}} è diventato attivo", + "stationary": "{{label}} è diventato stazionario", + "attribute": { + "faceOrLicense_plate": "{{attribute}} rilevato per {{label}}", + "other": "{{label}} riconosciuto come {{attribute}}" + }, + "gone": "{{label}} lasciato", + "heard": "{{label}} sentito", + "external": "{{label}} rilevato", + "header": { + "zones": "Zone", + "ratio": "Rapporto", + "area": "Area", + "score": "Punteggio" + } + }, + "annotationSettings": { + "title": "Impostazioni di annotazione", + "showAllZones": { + "title": "Mostra tutte le zone", + "desc": "Mostra sempre le zone nei fotogrammi in cui gli oggetti sono entrati in una zona." + }, + "offset": { + "label": "Differenza annotazione", + "desc": "Questi dati provengono dal flusso di rilevamento della telecamera, ma vengono sovrapposti alle immagini del flusso di registrazione. È improbabile che i due flussi siano perfettamente sincronizzati. Di conseguenza, il riquadro di delimitazione e il filmato non saranno perfettamente allineati. È possibile utilizzare questa impostazione per spostare le annotazioni in avanti o indietro nel tempo per allinearle meglio al filmato registrato.", + "millisecondsToOffset": "Millisecondi per compensare il rilevamento delle annotazioni. Predefinito: 0", + "tips": "Ridurre il valore se la riproduzione video è in anticipo rispetto ai riquadri e ai punti del percorso, e aumentarlo se la riproduzione video è in ritardo rispetto ad essi. Questo valore può essere negativo.", + "toast": { + "success": "La differenza dell'annotazione per {{camera}} è stato salvato nel file di configurazione. Riavvia Frigate per applicare le modifiche." + } + } + }, + "carousel": { + "previous": "Diapositiva precedente", + "next": "Diapositiva successiva" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/exports.json new file mode 100644 index 0000000..1866475 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Esporta - Frigate", + "search": "Cerca", + "noExports": "Nessuna esportazione trovata", + "deleteExport": "Elimina esportazione", + "deleteExport.desc": "Sei sicuro di voler eliminare {{exportName}}?", + "editExport": { + "desc": "Inserisci un nuovo nome per questa esportazione.", + "title": "Rinomina esportazione", + "saveExport": "Salva esportazione" + }, + "toast": { + "error": { + "renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Condividi esportazione", + "downloadVideo": "Scarica video", + "editName": "Modifica nome", + "deleteExport": "Elimina esportazione" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/faceLibrary.json new file mode 100644 index 0000000..ad47e7b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "selectItem": "Seleziona {{item}}", + "description": { + "addFace": "Aggiungi una nuova raccolta alla Libreria dei Volti caricando la tua prima immagine.", + "placeholder": "Inserisci un nome per questa raccolta", + "invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini." + }, + "details": { + "confidence": "Fiducia", + "person": "Persona", + "face": "Dettagli del volto", + "faceDesc": "Dettagli dell'oggetto tracciato che ha generato questo volto", + "timestamp": "Orario", + "subLabelScore": "Punteggio dell'etichetta secondaria", + "scoreInfo": "Il punteggio dell'etichetta secondaria è il punteggio ponderato per tutte le confidenze dei volti riconosciuti, quindi potrebbe differire dal punteggio mostrato nell'istantanea.", + "unknown": "Sconosciuto" + }, + "train": { + "title": "Riconoscimenti recenti", + "aria": "Seleziona i riconoscimenti recenti", + "empty": "Non ci sono recenti tentativi di riconoscimento facciale" + }, + "button": { + "addFace": "Aggiungi volto", + "deleteFaceAttempts": "Elimina volti", + "uploadImage": "Carica immagine", + "reprocessFace": "Rielabora il volto", + "deleteFace": "Elimina volto", + "renameFace": "Rinomina volto" + }, + "trainFace": "Addestra il volto", + "toast": { + "success": { + "deletedName_one": "{{count}} volto è stato eliminato con successo.", + "deletedName_many": "{{count}} volti sono stati eliminati con successo.", + "deletedName_other": "{{count}} volti sono stati eliminati con successo.", + "trainedFace": "Volto addestrato con successo.", + "deletedFace_one": "Eliminato con successo {{count}} volto.", + "deletedFace_many": "Eliminati con successo {{count}} volti.", + "deletedFace_other": "Eliminati con successo {{count}} volti.", + "updatedFaceScore": "Punteggio del volto aggiornato con successo a {{name}} ({{score}}).", + "uploadedImage": "Immagine caricata correttamente.", + "addFaceLibrary": "{{name}} è stato aggiunto con successo alla Libreria dei Volti!", + "renamedFace": "Rinominato correttamente il volto in {{name}}" + }, + "error": { + "addFaceLibraryFailed": "Impossibile impostare il nome del volto: {{errorMessage}}", + "uploadingImageFailed": "Impossibile caricare l'immagine: {{errorMessage}}", + "deleteFaceFailed": "Impossibile eliminare: {{errorMessage}}", + "trainFailed": "Impossibile addestrare: {{errorMessage}}", + "updateFaceScoreFailed": "Impossibile aggiornare il punteggio del volto: {{errorMessage}}", + "deleteNameFailed": "Impossibile eliminare il nome: {{errorMessage}}", + "renameFaceFailed": "Impossibile rinominare il volto: {{errorMessage}}" + } + }, + "imageEntry": { + "dropActive": "Rilascia l'immagine qui…", + "dropInstructions": "Trascina e rilascia o incolla un'immagine qui oppure fai clic per selezionarla", + "maxSize": "Dimensione massima: {{size}} MB", + "validation": { + "selectImage": "Seleziona un file immagine." + } + }, + "createFaceLibrary": { + "title": "Crea raccolta", + "nextSteps": "Per costruire una base solida:
  • Usa la scheda \"Riconoscimenti recenti\" per selezionare e addestrare le immagini per ogni persona rilevata.
  • Concentrati sulle immagini dritte per ottenere risultati migliori; evita di addestrare immagini che catturano i volti da un'angolazione.
  • ", + "desc": "Crea una nuova raccolta", + "new": "Crea nuovo volto" + }, + "readTheDocs": "Leggi la documentazione", + "selectFace": "Seleziona volto", + "documentTitle": "Libreria dei Volti - Frigate", + "uploadFaceImage": { + "desc": "Carica un'immagine per scansionare i volti e includerla in {{pageToggle}}", + "title": "Carica l'immagine del volto" + }, + "deleteFaceLibrary": { + "title": "Elimina nome", + "desc": "Vuoi davvero eliminare la raccolta {{name}}? Questa operazione eliminerà definitivamente tutti i volti associati." + }, + "trainFaceAs": "Addestra il volto come:", + "steps": { + "faceName": "Inserisci il nome del volto", + "nextSteps": "Prossimi passi", + "uploadFace": "Carica l'immagine del volto", + "description": { + "uploadFace": "Carica un'immagine di {{name}} che mostri il suo viso da un'angolazione frontale. Non è necessario ritagliare l'immagine in modo da mostrare solo il viso." + } + }, + "renameFace": { + "title": "Rinomina volto", + "desc": "Inserisci un nuovo nome per {{name}}" + }, + "collections": "Collezioni", + "deleteFaceAttempts": { + "title": "Elimina volti", + "desc_one": "Vuoi davvero eliminare {{count}} volto? Questa azione non può essere annullata.", + "desc_many": "Vuoi davvero eliminare {{count}} volti? Questa azione non può essere annullata.", + "desc_other": "Vuoi davvero eliminare {{count}} volti? Questa azione non può essere annullata." + }, + "nofaces": "Nessun volto disponibile", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/live.json new file mode 100644 index 0000000..c32113e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Dal vivo - Frigate", + "documentTitle.withCamera": "{{camera}} - Dal vivo - Frigate", + "lowBandwidthMode": "Modalità a bassa larghezza di banda", + "twoWayTalk": { + "enable": "Abilita audio bidirezionale", + "disable": "Disabilita audio bidirezionale" + }, + "snapshots": { + "enable": "Abilita istantanee", + "disable": "Disabilita istantanee" + }, + "manualRecording": { + "recordDisabledTips": "Poiché la registrazione è disabilitata o limitata nella configurazione di questa telecamera, verrà salvata solo un'istantanea.", + "title": "Su richiesta", + "tips": "Scarica un'istantanea attuale o avvia un evento manuale in base alle impostazioni di conservazione della registrazione di questa telecamera.", + "playInBackground": { + "label": "Riproduci in sottofondo", + "desc": "Abilita questa opzione per continuare la trasmissione quando il lettore è nascosto." + }, + "showStats": { + "label": "Mostra statistiche", + "desc": "Abilita questa opzione per visualizzare le statistiche della trasmissione come sovrapposizione sul flusso della telecamera." + }, + "debugView": "Vista correzioni", + "start": "Avvia la registrazione su richiesta", + "started": "Registrazione manuale su richiesta avviata.", + "failedToStart": "Impossibile avviare la registrazione manuale su richiesta.", + "end": "Termina la registrazione su richiesta", + "ended": "Registrazione manuale su richiesta terminata.", + "failedToEnd": "Impossibile terminare la registrazione manuale su richiesta." + }, + "cameraSettings": { + "snapshots": "Istantanee", + "autotracking": "Tracciamento automatico", + "title": "Impostazioni di {{camera}}", + "cameraEnabled": "Telecamera abilitata", + "objectDetection": "Rilevamento di oggetti", + "recording": "Registrazione", + "audioDetection": "Rilevamento audio", + "transcription": "Trascrizione audio" + }, + "history": { + "label": "Mostra filmati storici" + }, + "notifications": "Notifiche", + "streamingSettings": "Impostazioni di trasmissione", + "audio": "Audio", + "cameraAudio": { + "enable": "Abilita audio della telecamera", + "disable": "Disabilita audio della telecamera" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Abilita clic per spostare", + "disable": "Disabilita il clic per spostare", + "label": "Fai clic nella cornice per centrare la telecamera" + }, + "left": { + "label": "Sposta la telecamera PTZ a sinistra" + }, + "up": { + "label": "Sposta la telecamera PTZ verso l'alto" + }, + "down": { + "label": "Sposta la telecamera PTZ verso il basso" + }, + "right": { + "label": "Sposta la telecamera PTZ a destra" + } + }, + "zoom": { + "in": { + "label": "Ingrandisci la telecamera PTZ" + }, + "out": { + "label": "Riduci la telecamera PTZ" + } + }, + "frame": { + "center": { + "label": "Fai clic nella cornice per centrare la telecamera PTZ" + } + }, + "presets": "Preimpostazioni della telecamera PTZ", + "focus": { + "in": { + "label": "Aumenta fuoco della telecamera PTZ" + }, + "out": { + "label": "Diminuisci fuoco della telecamera PTZ" + } + } + }, + "camera": { + "enable": "Abilita telecamera", + "disable": "Disabilita telecamera" + }, + "muteCameras": { + "enable": "Muta tutte le telecamere", + "disable": "Attiva audio di tutte le telecamere" + }, + "detect": { + "enable": "Abilita rilevamento", + "disable": "Disabilita rilevamento" + }, + "recording": { + "enable": "Abilita registrazione", + "disable": "Disabilita registrazione" + }, + "audioDetect": { + "enable": "Abilita rilevamento audio", + "disable": "Disabilita rilevamento audio" + }, + "autotracking": { + "enable": "Abilita il tracciamento automatico", + "disable": "Disabilita il tracciamento automatico" + }, + "streamStats": { + "enable": "Mostra statistiche di trasmissione", + "disable": "Nascondi statistiche di trasmissione" + }, + "suspend": { + "forTime": "Sospendi per: " + }, + "stream": { + "title": "Trasmissione", + "audio": { + "tips": { + "title": "L'audio deve essere trasmesso dalla tua telecamera e configurato in go2rtc per questa trasmissione.", + "documentation": "Leggi la documentazione " + }, + "unavailable": "L'audio non è disponibile per questo flusso", + "available": "L'audio è disponibile per questo flusso" + }, + "twoWayTalk": { + "tips": "Il dispositivo deve supportare la funzionalità e WebRTC deve essere configurato per la comunicazione bidirezionale.", + "tips.documentation": "Leggi la documentazione ", + "unavailable": "La comunicazione bidirezionale non è disponibile per questo flusso", + "available": "La comunicazione bidirezionale è disponibile per questo flusso" + }, + "playInBackground": { + "tips": "Abilita questa opzione per continuare la trasmissione quando il lettore è nascosto.", + "label": "Riproduci in sottofondo" + }, + "lowBandwidth": { + "tips": "La visualizzazione dal vivo è in modalità a bassa larghezza di banda a causa di errori di caricamento o di trasmissione.", + "resetStream": "Reimposta flusso" + }, + "debug": { + "picker": "Selezione del flusso non disponibile in modalità correzioni. La visualizzazione correzioni utilizza sempre il flusso a cui è assegnato il ruolo di rilevamento." + } + }, + "effectiveRetainMode": { + "modes": { + "all": "Tutto", + "motion": "Movimento", + "active_objects": "Oggetti attivi" + }, + "notAllTips": "La configurazione di conservazione della registrazione di {{source}} è impostata su mode: {{effectiveRetainMode}}, quindi questa registrazione su richiesta conserverà solo i segmenti con {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Modifica formato", + "group": { + "label": "Modifica gruppo telecamere" + }, + "exitEdit": "Esci dalla modifica" + }, + "transcription": { + "enable": "Abilita la trascrizione audio in tempo reale", + "disable": "Disabilita la trascrizione audio in tempo reale" + }, + "noCameras": { + "buttonText": "Aggiungi telecamera", + "title": "Nessuna telecamera configurata", + "description": "Per iniziare, collega una telecamera a Frigate.", + "restricted": { + "title": "Nessuna telecamera disponibile", + "description": "Non hai l'autorizzazione per visualizzare alcuna telecamera in questo gruppo." + } + }, + "snapshot": { + "takeSnapshot": "Scarica l'istantanea attuale", + "noVideoSource": "Nessuna sorgente video disponibile per l'istantanea.", + "captureFailed": "Impossibile catturare l'istantanea.", + "downloadStarted": "Scaricamento istantanea avviato." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/recording.json new file mode 100644 index 0000000..411070b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/recording.json @@ -0,0 +1,12 @@ +{ + "calendar": "Calendario", + "export": "Esporta", + "filter": "Filtro", + "filters": "Filtri", + "toast": { + "error": { + "noValidTimeSelected": "Nessun intervallo di tempo valido selezionato", + "endTimeMustAfterStartTime": "L'ora di fine deve essere successiva all'ora di inizio" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/search.json new file mode 100644 index 0000000..873ef00 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Cerca", + "savedSearches": "Ricerche salvate", + "searchFor": "Cerca {{inputValue}}", + "button": { + "clear": "Cancella ricerca", + "save": "Salva ricerca", + "delete": "Elimina la ricerca salvata", + "filterInformation": "Informazioni sul filtro", + "filterActive": "Filtri attivi" + }, + "filter": { + "label": { + "has_snapshot": "Contiene istantanea", + "cameras": "Telecamere", + "search_type": "Tipo di ricerca", + "has_clip": "Contiene video", + "before": "Prima", + "labels": "Etichette", + "min_score": "Punteggio minimo", + "zones": "Zone", + "max_score": "Punteggio massimo", + "min_speed": "Velocità minima", + "time_range": "Intervallo di tempo", + "after": "Dopo", + "max_speed": "Velocità massima", + "recognized_license_plate": "Targa riconosciuta", + "sub_labels": "Sottoetichette" + }, + "tips": { + "desc": { + "step3": "Puoi utilizzare più filtri aggiungendoli uno dopo l'altro, lasciando uno spazio in mezzo.", + "step5": "Il filtro intervallo di tempo utilizza il formato {{exampleTime}}.", + "exampleLabel": "Esempio:", + "step1": "Digita un nome chiave di filtro seguito da due punti (ad esempio, \"telecamere:\").", + "step2": "Seleziona un valore dai suggerimenti oppure digita il tuo valore.", + "step4": "I filtri data (prima: e dopo:) utilizzano il formato {{DateFormat}}.", + "step6": "Rimuovi i filtri cliccando sulla \"x\" accanto ad essi.", + "text": "I filtri ti aiutano a restringere i risultati della ricerca. Ecco come usarli nel campo di inserimento:" + }, + "title": "Come utilizzare i filtri di testo" + }, + "toast": { + "error": { + "minScoreMustBeLessOrEqualMaxScore": "Il 'punteggio minimo' deve essere minore o uguale del 'punteggio massimo'.", + "beforeDateBeLaterAfter": "La data 'prima' deve essere successiva alla data 'dopo'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "La 'velocità minima' deve essere minore o uguale della 'velocità massima'.", + "afterDatebeEarlierBefore": "La data 'dopo' deve essere precedente alla data 'prima'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Il 'punteggio massimo' deve essere maggiore o uguale del 'punteggio minimo'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "La 'velocità massima' deve essere maggiore o uguale della 'velocità minima'." + } + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "header": { + "noFilters": "Filtri", + "activeFilters": "Filtri attivi", + "currentFilterType": "Valori del filtro" + } + }, + "similaritySearch": { + "clear": "Cancella ricerca di somiglianza", + "title": "Ricerca di somiglianza", + "active": "Ricerca di somiglianza attiva" + }, + "placeholder": { + "search": "Ricerca…" + }, + "trackedObjectId": "ID oggetto tracciato" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/settings.json new file mode 100644 index 0000000..afaf310 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/settings.json @@ -0,0 +1,1294 @@ +{ + "documentTitle": { + "authentication": "Impostazioni di autenticazione - Frigate", + "default": "Impostazioni - Frigate", + "classification": "Impostazioni di classificazione - Frigate", + "camera": "Impostazioni telecamera - Frigate", + "masksAndZones": "Editor di maschere e zone - Frigate", + "motionTuner": "Regolatore di movimento - Frigate", + "object": "Correzioni - Frigate", + "general": "Impostazioni interfaccia - Frigate", + "frigatePlus": "Impostazioni Frigate+ - Frigate", + "notifications": "Impostazioni di notifiche - Frigate", + "enrichments": "Impostazioni di miglioramento - Frigate", + "cameraManagement": "Gestisci telecamere - Frigate", + "cameraReview": "Impostazioni revisione telecamera - Frigate" + }, + "frigatePlus": { + "snapshotConfig": { + "cleanCopyWarning": "Alcune telecamere hanno le istantanee abilitate ma la copia pulita disabilitata. È necessario abilitare clean_copy nella configurazione delle istantanee per poter inviare le immagini da queste telecamere a Frigate+.", + "table": { + "snapshots": "Istantanee", + "camera": "Telecamera", + "cleanCopySnapshots": "Istantanee clean_copy" + }, + "desc": "Per inviare a Frigate+ è necessario che nella configurazione siano abilitate sia le istantanee che le istantanee clean_copy.", + "documentation": "Leggi la documentazione", + "title": "Configurazione istantanee" + }, + "apiKey": { + "title": "Chiave API Frigate+", + "validated": "La chiave API Frigate+ è stata rilevata e convalidata", + "notValidated": "La chiave API Frigate+ non è stata rilevata o non è stata convalidata", + "desc": "La chiave API Frigate+ consente l'integrazione con il servizio Frigate+.", + "plusLink": "Scopri di più su Frigate+" + }, + "modelInfo": { + "trainDate": "Data addestramento", + "baseModel": "Modello base", + "cameras": "Telecamere", + "plusModelType": { + "userModel": "Messa a punto fine", + "baseModel": "Modello base" + }, + "availableModels": "Modelli disponibili", + "loadingAvailableModels": "Caricamento dei modelli disponibili…", + "supportedDetectors": "Rilevatori supportati", + "error": "Impossibile caricare le informazioni sul modello", + "modelType": "Tipo di modello", + "modelSelect": "Qui puoi selezionare i modelli disponibili su Frigate+. Nota: puoi selezionare solo i modelli compatibili con la configurazione attuale del tuo rilevatore.", + "title": "Informazioni sul modello", + "loading": "Caricamento informazioni sul modello…" + }, + "toast": { + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "success": "Le impostazioni di Frigate+ sono state salvate. Riavvia Frigate per applicare le modifiche." + }, + "title": "Impostazioni Frigate+", + "restart_required": "Riavvio richiesto (modello Frigate+ modificato)", + "unsavedChanges": "Modifiche alle impostazioni di Frigate+ non salvate" + }, + "debug": { + "timestamp": { + "desc": "Sovrapponi l'orario all'immagine", + "title": "Orario" + }, + "objectShapeFilterDrawing": { + "tips": "Abilita questa opzione per disegnare un rettangolo sull'immagine della telecamera per mostrarne l'area e il rapporto. Questi valori possono quindi essere utilizzati per impostare i parametri del filtro forma oggetto nella configurazione.", + "area": "Area", + "title": "Disegno del filtro della forma dell'oggetto", + "desc": "Disegna un rettangolo sull'immagine per visualizzare i dettagli dell'area e del rapporto", + "document": "Leggi la documentazione ", + "score": "Punteggio", + "ratio": "Rapporto" + }, + "detectorDesc": "Frigate utilizza i tuoi rilevatori ({{detectors}}) per rilevare oggetti nel flusso video della tua telecamera.", + "boundingBoxes": { + "colors": { + "info": "
  • All'avvio, a ciascuna etichetta dell'oggetto verranno assegnati colori diversi
  • Una linea sottile blu scuro indica che l'oggetto non è stato rilevato in questo momento
  • Una linea sottile grigia indica che l'oggetto è stato rilevato come stazionario
  • Una linea spessa indica che l'oggetto è oggetto di tracciamento automatico (se abilitato)
  • ", + "label": "Colori del riquadro di delimitazione dell'oggetto" + }, + "desc": "Mostra i riquadri di delimitazione attorno agli oggetti tracciati", + "title": "Riquadri di delimitazione" + }, + "regions": { + "tips": "

    Riquadri di regione


    I riquadri di colore verde brillante verranno sovrapposte alle aree di interesse nel fotogramma che viene inviato al rilevatore di oggetti.

    ", + "title": "Regioni", + "desc": "Mostra un riquadro della regione di interesse inviata al rilevatore di oggetti" + }, + "noObjects": "Nessun oggetto", + "title": "Correzioni", + "desc": "La vista di correzione mostra una vista in tempo reale degli oggetti tracciati e delle relative statistiche. L'elenco degli oggetti mostra un riepilogo ritardato degli oggetti rilevati.", + "debugging": "Correzioni", + "objectList": "Elenco degli oggetti", + "mask": { + "desc": "Mostra i poligoni della maschera di movimento", + "title": "Maschere di movimento" + }, + "motion": { + "title": "Riquadri di movimento", + "desc": "Mostra i riquadri attorno alle aree in cui viene rilevato il movimento", + "tips": "

    Riquadri di movimento


    I riquadri rossi verranno sovrapposti alle aree dell'inquadratura in cui viene attualmente rilevato un movimento

    " + }, + "zones": { + "title": "Zone", + "desc": "Mostra un contorno di tutte le zone definite" + }, + "paths": { + "title": "Percorsi", + "desc": "Mostra i punti significativi del percorso dell'oggetto tracciato", + "tips": "

    Percorsi


    Linee e cerchi indicheranno i punti significativi in cui l'oggetto tracciato si è spostato durante il suo ciclo di vita.

    " + }, + "audio": { + "title": "Audio", + "currentdbFS": "dbFS correnti", + "noAudioDetections": "Nessun rilevamento audio", + "score": "punteggio", + "currentRMS": "RMS attuale" + }, + "openCameraWebUI": "Apri l'interfaccia utente Web di {{camera}}" + }, + "masksAndZones": { + "motionMasks": { + "context": { + "title": "Le maschere di movimento vengono utilizzate per impedire che tipi di movimento indesiderati attivino il rilevamento (ad esempio: rami di alberi, orari di telecamere). Le maschere di movimento dovrebbero essere utilizzate con molta parsimonia: un uso eccessivo renderebbe più difficile il tracciamento degli oggetti.", + "documentation": "Leggi la documentazione" + }, + "desc": { + "title": "Le maschere di movimento vengono utilizzate per impedire che movimenti indesiderati attivino il rilevamento. Un mascheramento eccessivo renderà più difficile il tracciamento degli oggetti.", + "documentation": "Documentazione" + }, + "label": "Maschera di movimento", + "edit": "Modifica maschera di movimento", + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "polygonAreaTooLarge": { + "title": "La maschera di movimento copre il {{polygonArea}}% dell'inquadratura. Si sconsiglia di utilizzare maschere di movimento di grandi dimensioni.", + "documentation": "Leggi la documentazione", + "tips": "Le maschere di movimento non impediscono il rilevamento degli oggetti. È consigliabile utilizzare una zona specifica." + }, + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "documentTitle": "Modifica maschera movimento - Frigate", + "add": "Nuova maschera di movimento", + "toast": { + "success": { + "title": "{{polygonName}} è stato salvato. Riavvia Frigate per applicare le modifiche.", + "noName": "La maschera di movimento è stata salvata. Riavvia Frigate per applicare le modifiche." + } + } + }, + "form": { + "zoneName": { + "error": { + "hasIllegalCharacter": "Il nome della zona contiene caratteri non validi.", + "mustNotBeSameWithCamera": "Il nome della zona non deve essere uguale al nome della telecamera.", + "mustBeAtLeastTwoCharacters": "Il nome della zona deve essere composto da almeno 2 caratteri.", + "alreadyExists": "Per questa telecamera esiste già una zona con questo nome.", + "mustNotContainPeriod": "Il nome della zona non deve contenere punti.", + "mustHaveAtLeastOneLetter": "Il nome della zona deve contenere almeno una lettera." + } + }, + "distance": { + "error": { + "text": "La distanza deve essere maggiore o uguale a 0.1.", + "mustBeFilled": "Per utilizzare la stima della velocità è necessario compilare tutti i campi relativi alla distanza." + } + }, + "polygonDrawing": { + "delete": { + "title": "Conferma eliminazione", + "desc": "Sei sicuro di voler eliminare {{type}} {{name}}?", + "success": "{{name}} è stato eliminato." + }, + "removeLastPoint": "Rimuovi l'ultimo punto", + "reset": { + "label": "Cancella tutti i punti" + }, + "snapPoints": { + "true": "Aggancia punti", + "false": "Non agganciare punti" + }, + "error": { + "mustBeFinished": "Prima di salvare, è necessario terminare il disegno del poligono." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "L'inerzia deve essere superiore a 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Il tempo di permanenza deve essere maggiore o uguale a 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "La soglia di velocità deve essere maggiore o uguale a 0,1." + } + } + }, + "filter": { + "all": "Tutte le maschere e zone" + }, + "toast": { + "success": { + "copyCoordinates": "Coordinate per {{polyName}} copiate negli appunti." + }, + "error": { + "copyCoordinatesFailed": "Impossibile copiare le coordinate negli appunti." + } + }, + "zones": { + "speedEstimation": { + "desc": "Abilita la stima della velocità per gli oggetti in questa zona. La zona deve avere esattamente 4 punti.", + "title": "Stima della velocità", + "docs": "Leggi la documentazione", + "lineCDistance": "Distanza della linea C ({{unit}})", + "lineDDistance": "Distanza della linea D ({{unit}})", + "lineADistance": "Distanza della linea A ({{unit}})", + "lineBDistance": "Distanza della linea B ({{unit}})" + }, + "add": "Aggiungi zona", + "speedThreshold": { + "desc": "Specifica una velocità minima affinché gli oggetti vengano presi in considerazione in questa zona.", + "toast": { + "error": { + "pointLengthError": "La stima della velocità è stata disattivata per questa zona. Le zone con stima della velocità devono avere esattamente 4 punti.", + "loiteringTimeError": "Le zone con tempi di permanenza superiori a 0 non devono essere utilizzate per la stima della velocità." + } + }, + "title": "Soglia di velocità ({{unit}})" + }, + "desc": { + "title": "Le zone consentono di definire un'area specifica dell'inquadratura, in modo da poter determinare se un oggetto si trova o meno all'interno di un'area particolare.", + "documentation": "Documentazione" + }, + "edit": "Modifica zona", + "name": { + "inputPlaceHolder": "Inserisci un nome…", + "title": "Nome", + "tips": "Il nome deve essere composto da almeno 2 caratteri, contenere almeno una lettera e non deve essere il nome di una telecamera o di un'altra zona." + }, + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "label": "Zone", + "documentTitle": "Modifica zona - Frigate", + "inertia": { + "title": "Inerzia", + "desc": "Specifica quanti fotogrammi deve avere un oggetto in una zona prima di essere considerato nella zona. Predefinito: 3" + }, + "loiteringTime": { + "title": "Tempo di permanenza", + "desc": "Imposta un intervallo di tempo minimo in secondi per cui l'oggetto deve trovarsi nella zona affinché venga attivato. Predefinito: 0" + }, + "objects": { + "title": "Oggetti", + "desc": "Elenco degli oggetti che si applicano a questa zona." + }, + "allObjects": "Tutti gli oggetti", + "toast": { + "success": "La zona ({{zoneName}}) è stata salvata. Riavvia Frigate per applicare le modifiche." + } + }, + "objectMasks": { + "desc": { + "title": "Le maschere di filtro degli oggetti vengono utilizzate per filtrare i falsi positivi per un determinato tipo di oggetto in base alla posizione.", + "documentation": "Documentazione" + }, + "context": "Le maschere di filtro degli oggetti vengono utilizzate per filtrare i falsi positivi per un determinato tipo di oggetto in base alla posizione.", + "point_one": "{{count}} punto", + "point_many": "{{count}} punti", + "point_other": "{{count}} punti", + "add": "Aggiungi maschera oggetti", + "clickDrawPolygon": "Fai clic per disegnare un poligono sull'immagine.", + "edit": "Modifica maschera oggetti", + "objects": { + "desc": "Tipo di oggetto applicabile a questa maschera oggetto.", + "title": "Oggetti", + "allObjectTypes": "Tutti i tipi di oggetti" + }, + "toast": { + "success": { + "noName": "La maschera oggetto è stata salvata. Riavvia Frigate per applicare le modifiche.", + "title": "{{polygonName}} è stato salvato. Riavvia Frigate per applicare le modifiche." + } + }, + "label": "Maschere di oggetti", + "documentTitle": "Modifica maschera oggetti - Frigate" + }, + "restart_required": "Riavvio richiesto (maschere/zone modificate)", + "motionMaskLabel": "Maschera di movimento {{number}}", + "objectMaskLabel": "Maschera di oggetto {{number}} ({{label}})" + }, + "cameraSetting": { + "camera": "Telecamera", + "noCamera": "Nessuna telecamera" + }, + "camera": { + "reviewClassification": { + "limitDetections": "Limita i rilevamenti a zone specifiche", + "objectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "zoneObjectDetectionsTips": { + "text": "Tutti gli oggetti {{detectionsLabels}} non categorizzati in {{zone}} su {{cameraName}} verranno mostrati come Rilevamenti.", + "notSelectDetections": "Tutti gli oggetti {{detectionsLabels}} rilevati in {{zone}} su {{cameraName}} non classificati come Avvisi verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "regardlessOfZoneObjectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano." + }, + "title": "Classificazione della revisione", + "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e automobile sono considerati Avvisi. Puoi perfezionare la categorizzazione degli elementi di revisione configurando le zone desiderate.", + "objectAlertsTips": "Tutti gli oggetti {{alertsLabels}} su {{cameraName}} verranno mostrati come Avvisi.", + "toast": { + "success": "La configurazione della classificazione di revisione è stata salvata. Riavvia Frigate per applicare le modifiche." + }, + "readTheDocumentation": "Leggi la documentazione", + "noDefinedZones": "Per questa telecamera non sono definite zone.", + "zoneObjectAlertsTips": "Tutti gli oggetti {{alertsLabels}} rilevati in {{zone}} su {{cameraName}} verranno visualizzati come Avvisi.", + "selectAlertsZones": "Seleziona le zone per gli Avvisi", + "selectDetectionsZones": "Seleziona le zone per i Rilevamenti", + "unsavedChanges": "Impostazioni di classificazione delle revisioni non salvate per {{camera}}" + }, + "streams": { + "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi da parte di Frigate. Rilevamenti, registrazioni e correzioni non saranno disponibili.
    Nota: questa operazione non disabilita le ritrasmissioni di go2rtc.", + "title": "Flussi" + }, + "title": "Impostazioni telecamera", + "review": { + "title": "Rivedi", + "desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitati, non verranno generati nuovi elementi di revisione. ", + "alerts": "Avvisi ", + "detections": "Rilevamenti " + }, + "object_descriptions": { + "title": "Descrizioni di oggetti di IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera." + }, + "review_descriptions": { + "title": "Descrizioni delle revisioni dell'IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni delle revisioni dell'IA generativa per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli elementi da recensire su questa telecamera." + }, + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "cameraConfig": { + "add": "Aggiungi telecamera", + "edit": "Modifica telecamera", + "description": "Configura le impostazioni della telecamera, inclusi gli ingressi ed i ruoli della trasmissione.", + "name": "Nome della telecamera", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameInvalid": "Il nome della telecamera deve contenere solo lettere, numeri, caratteri di sottolineatura o trattini", + "namePlaceholder": "ad esempio: porta_principale", + "enabled": "Abilitata", + "ffmpeg": { + "inputs": "Flussi di ingresso", + "path": "Percorso del flusso", + "pathRequired": "Il percorso del flusso è obbligatorio", + "pathPlaceholder": "rtsp://...", + "roles": "Ruoli", + "rolesRequired": "È richiesto almeno un ruolo", + "rolesUnique": "Ogni ruolo (audio, rilevamento, registrazione) può essere assegnato solo ad un flusso", + "addInput": "Aggiungi flusso di ingresso", + "removeInput": "Rimuovi flusso di ingresso", + "inputsRequired": "È richiesto almeno un flusso di ingresso" + }, + "toast": { + "success": "La telecamera {{cameraName}} è stata salvata correttamente" + }, + "nameLength": "Il nome della telecamera deve contenere meno di 24 caratteri." + } + }, + "menu": { + "motionTuner": "Regolatore di movimento", + "notifications": "Notifiche", + "ui": "Interfaccia utente", + "classification": "Classificazione", + "cameras": "Impostazioni telecamera", + "masksAndZones": "Maschere / Zone", + "debug": "Correzioni", + "users": "Utenti", + "frigateplus": "Frigate+", + "enrichments": "Miglioramenti", + "triggers": "Inneschi", + "roles": "Ruoli", + "cameraManagement": "Gestione", + "cameraReview": "Rivedi" + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "viewerDesc": "Limitato solo alle schermate dal vivo, alle revisioni, alle esplorazioni e alle esportazioni.", + "intro": "Seleziona il ruolo appropriato per questo utente:", + "admin": "Amministratore", + "adminDesc": "Accesso completo a tutte le funzionalità.", + "viewer": "Spettatore", + "customDesc": "Ruolo personalizzato con accesso specifico alla telecamera." + }, + "title": "Cambia ruolo utente", + "desc": "Aggiorna i permessi per {{username}}", + "select": "Seleziona un ruolo" + }, + "deleteUser": { + "warn": "Sei sicuro di voler eliminare {{username}}?", + "title": "Elimina utente", + "desc": "Questa azione non può essere annullata. L'account utente verrà eliminato definitivamente e tutti i dati associati verranno rimossi." + }, + "form": { + "user": { + "placeholder": "Inserisci il nome utente", + "title": "Nome utente", + "desc": "Sono consentiti solo lettere, numeri, punti e caratteri di sottolineatura." + }, + "password": { + "confirm": { + "title": "Conferma password", + "placeholder": "Conferma password" + }, + "strength": { + "title": "Forza della password: ", + "weak": "Debole", + "medium": "Media", + "strong": "Forte", + "veryStrong": "Molto forte" + }, + "title": "Password", + "placeholder": "Inserisci la password", + "match": "Le password corrispondono", + "notMatch": "Le password non corrispondono" + }, + "newPassword": { + "title": "Nuova password", + "placeholder": "Inserisci la nuova password", + "confirm": { + "placeholder": "Reinserisci la nuova password" + } + }, + "usernameIsRequired": "Il nome utente è obbligatorio", + "passwordIsRequired": "La password è obbligatoria" + }, + "createUser": { + "desc": "Aggiungi un nuovo account utente e specifica un ruolo per l'accesso alle aree dell'interfaccia utente di Frigate.", + "title": "Crea nuovo utente", + "usernameOnlyInclude": "Il nome utente può contenere solo lettere, numeri, . o _", + "confirmPassword": "Conferma la password" + }, + "passwordSetting": { + "updatePassword": "Aggiorna la password per {{username}}", + "setPassword": "Imposta password", + "desc": "Crea una password complessa per proteggere questo account.", + "cannotBeEmpty": "La password non può essere vuota", + "doNotMatch": "Le password non corrispondono" + } + }, + "table": { + "password": "Password", + "username": "Nome utente", + "actions": "Azioni", + "role": "Ruolo", + "noUsers": "Nessun utente trovato.", + "changeRole": "Cambia ruolo utente", + "deleteUser": "Elimina utente" + }, + "toast": { + "error": { + "roleUpdateFailed": "Impossibile aggiornare il ruolo: {{errorMessage}}", + "setPasswordFailed": "Impossibile salvare la password: {{errorMessage}}", + "createUserFailed": "Impossibile creare l'utente: {{errorMessage}}", + "deleteUserFailed": "Impossibile eliminare l'utente: {{errorMessage}}" + }, + "success": { + "createUser": "Utente {{user}} creato con successo", + "updatePassword": "Password aggiornata con successo.", + "deleteUser": "Utente {{user}} eliminato con successo", + "roleUpdated": "Ruolo aggiornato per {{user}}" + } + }, + "title": "Utenti", + "management": { + "title": "Gestione utenti", + "desc": "Gestisci gli account utente di questa istanza Frigate." + }, + "addUser": "Aggiungi utente", + "updatePassword": "Aggiorna password" + }, + "general": { + "liveDashboard": { + "automaticLiveView": { + "desc": "Passa automaticamente alla visualizzazione dal vivo di una telecamera quando viene rilevata attività. Disattivando questa opzione, le immagini statiche della telecamera nella schermata dal vivo verranno aggiornate solo una volta al minuto.", + "label": "Visualizzazione automatica dal vivo" + }, + "playAlertVideos": { + "label": "Riproduci video di avvisi", + "desc": "Per impostazione predefinita, gli avvisi recenti nella schermata dal vivo vengono riprodotti come brevi video in ciclo. Disattiva questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser." + }, + "title": "Schermata dal vivo", + "displayCameraNames": { + "label": "Mostra sempre i nomi delle telecamere", + "desc": "Mostra sempre i nomi delle telecamere in una scheda nel cruscotto della visualizzazione dal vivo multi telecamera." + }, + "liveFallbackTimeout": { + "label": "Scadenza attesa lettore dal vivo", + "desc": "Quando la trasmissione dal vivo ad alta qualità di una telecamera non è disponibile, dopo questo numero di secondi torna alla modalità a bassa larghezza di banda. Valore predefinito: 3." + } + }, + "title": "Impostazioni interfaccia", + "storedLayouts": { + "title": "Formati memorizzati", + "desc": "La disposizione delle telecamere in un gruppo può essere trascinata/ridimensionata. Le posizioni vengono salvate nella memoria locale del browser.", + "clearAll": "Cancella tutti i formati" + }, + "cameraGroupStreaming": { + "title": "Impostazioni di trasmissione del gruppo di telecamere", + "desc": "Le impostazioni di trasmissione per ciascun gruppo di telecamere vengono salvate nella memoria locale del browser.", + "clearAll": "Cancella tutte le impostazioni di trasmissione" + }, + "recordingsViewer": { + "title": "Visualizzatore di registrazioni", + "defaultPlaybackRate": { + "label": "Velocità di riproduzione predefinita", + "desc": "Velocità di riproduzione predefinita per la riproduzione delle registrazioni." + } + }, + "calendar": { + "title": "Calendario", + "firstWeekday": { + "label": "Primo giorno della settimana", + "desc": "Giorno in cui iniziano le settimane del calendario di revisione.", + "sunday": "Domenica", + "monday": "Lunedi" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Formato memorizzato cancellato per {{cameraName}}", + "clearStreamingSettings": "Cancellate le impostazioni di trasmissione per tutti i gruppi di telecamere." + }, + "error": { + "clearStoredLayoutFailed": "Impossibile cancellare il formato memorizzato: {{errorMessage}}", + "clearStreamingSettingsFailed": "Impossibile cancellare le impostazioni di trasmissione: {{errorMessage}}" + } + } + }, + "classification": { + "licensePlateRecognition": { + "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo \"automobile\". Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", + "title": "Riconoscimento della targa", + "readTheDocumentation": "Leggi la documentazione" + }, + "title": "Impostazioni di classificazione", + "semanticSearch": { + "title": "Ricerca semantica", + "desc": "La ricerca semantica in Frigate consente di trovare gli oggetti tracciati all'interno degli elementi della recensione utilizzando l'immagine stessa, una descrizione testuale definita dall'utente o una generata automaticamente.", + "readTheDocumentation": "Leggi la documentazione", + "modelSize": { + "large": { + "title": "grande", + "desc": "L'utilizzo di large sfrutta il modello Jina completo e, se applicabile, verrà eseguito automaticamente sulla GPU." + }, + "small": { + "desc": "L'utilizzo di small sfrutta una versione quantizzata del modello che utilizza meno RAM ed è più veloce sulla CPU con una differenza davvero trascurabile nella qualità di incorporamento.", + "title": "piccolo" + }, + "label": "Dimensioni del modello", + "desc": "Dimensioni del modello utilizzato per gli incorporamenti della ricerca semantica." + }, + "reindexNow": { + "label": "Reindicizza ora", + "desc": "La reindicizzazione rigenererà gli incorporamenti per tutti gli oggetti tracciati. Questo processo viene eseguito in sottofondo e potrebbe impegnare al massimo la CPU e richiedere un tempo considerevole, a seconda del numero di oggetti tracciati.", + "confirmTitle": "Conferma reindicizzazione", + "confirmDesc": "Vuoi davvero reindicizzare tutti gli incorporamenti di oggetti tracciati? Questo processo verrà eseguito in sottofondo, ma potrebbe impegnare al massimo la CPU e richiedere molto tempo. Puoi monitorare l'avanzamento nella pagina Esplora.", + "confirmButton": "Reindicizza", + "success": "Reindicizzazione avviata con successo.", + "alreadyInProgress": "La reindicizzazione è già in corso.", + "error": "Impossibile avviare la reindicizzazione: {{errorMessage}}" + } + }, + "faceRecognition": { + "title": "Riconoscimento facciale", + "desc": "Il riconoscimento facciale consente di assegnare un nome alle persone e, quando il volto viene riconosciuto, Frigate assegnerà il nome della persona come sottoetichetta. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "modelSize": { + "desc": "Dimensioni del modello utilizzato per il riconoscimento facciale.", + "small": { + "title": "piccolo", + "desc": "L'utilizzo di small sfrutta un modello di incorporamento di volti FaceNet che funziona in modo efficiente sulla maggior parte delle CPU." + }, + "large": { + "title": "grande", + "desc": "L'utilizzo di large sfrutta un modello di incorporamento di volti ArcFace e verrà eseguito automaticamente sulla GPU, se applicabile." + }, + "label": "Dimensioni del modello" + }, + "readTheDocumentation": "Leggi la documentazione" + }, + "toast": { + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", + "success": "Le impostazioni di classificazione sono state salvate. Riavvia Frigate per applicare le modifiche." + }, + "birdClassification": { + "desc": "La classificazione degli uccelli identifica gli uccelli noti utilizzando un modello Tensorflow quantizzato. Quando un uccello noto viene riconosciuto, il suo nome comune viene aggiunto come sub_label. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Classificazione degli uccelli" + }, + "restart_required": "Riavvio richiesto (impostazioni di classificazione modificate)", + "unsavedChanges": "Modifiche alle impostazioni di classificazione non salvate" + }, + "dialog": { + "unsavedChanges": { + "title": "Ci sono modifiche non salvate.", + "desc": "Vuoi salvare le modifiche prima di continuare?" + } + }, + "motionDetectionTuner": { + "improveContrast": { + "title": "Migliora il contrasto", + "desc": "Migliora il contrasto nelle scene più scure. Predefinito: ATTIVO" + }, + "desc": { + "title": "Frigate utilizza il rilevamento del movimento come primo controllo per verificare se nell'inquadratura si verifica qualcosa che valga la pena verificare tramite il rilevamento degli oggetti.", + "documentation": "Leggi la Guida alla Regolazione del Movimento" + }, + "title": "Regolatore di rilevamento del movimento", + "contourArea": { + "title": "Area di contorno", + "desc": "Il valore dell'area di contorno viene utilizzato per decidere quali gruppi di pixel modificati possono essere considerati movimento. Predefinito: 10" + }, + "Threshold": { + "title": "Soglia", + "desc": "Il valore di soglia determina l'entità della variazione di luminanza di un pixel necessaria per essere considerato movimento. Predefinito: 30" + }, + "toast": { + "success": "Le impostazioni di movimento sono state salvate." + }, + "unsavedChanges": "Modifiche non salvate del regolatore di movimento ({{camera}})" + }, + "notification": { + "email": { + "placeholder": "es. esempio@email.com", + "desc": "È richiesto un indirizzo email valido che verrà utilizzato per avvisarti in caso di problemi con il servizio push.", + "title": "E-mail" + }, + "cameras": { + "title": "Telecamere", + "noCameras": "Nessuna telecamera disponibile", + "desc": "Seleziona per quali telecamere abilitare le notifiche." + }, + "unregisterDevice": "Annulla la registrazione di questo dispositivo", + "sendTestNotification": "Invia una notifica di prova", + "active": "Notifiche attive", + "suspended": "Notifiche sospese {{time}}", + "suspendTime": { + "5minutes": "Sospendi per 5 minuti", + "30minutes": "Sospendi per 30 minuti", + "10minutes": "Sospendi per 10 minuti", + "1hour": "Sospendi per 1 ora", + "12hours": "Sospendi per 12 ore", + "24hours": "Sospendi per 24 ore", + "untilRestart": "Sospendi fino al riavvio", + "suspend": "Sospendi" + }, + "globalSettings": { + "desc": "Sospendi temporaneamente le notifiche per telecamere specifiche su tutti i dispositivi registrati.", + "title": "Impostazioni globali" + }, + "registerDevice": "Registra questo dispositivo", + "notificationUnavailable": { + "desc": "Le notifiche push web richiedono un contesto sicuro (https://...). Questa è una limitazione del browser. Accedi a Frigate in modo sicuro per utilizzare le notifiche.", + "documentation": "Leggi la documentazione", + "title": "Notifiche non disponibili" + }, + "deviceSpecific": "Impostazioni specifiche del dispositivo", + "toast": { + "success": { + "registered": "Registrazione per le notifiche completata con successo. È necessario riavviare Frigate prima di poter inviare qualsiasi notifica (inclusa una notifica di prova).", + "settingSaved": "Le impostazioni di notifica sono state salvate." + }, + "error": { + "registerFailed": "Impossibile salvare la registrazione della notifica." + } + }, + "title": "Notifiche", + "notificationSettings": { + "title": "Impostazioni notifiche", + "desc": "Frigate può inviare notifiche push in modo nativo al tuo dispositivo quando è in esecuzione nel browser o installato come PWA.", + "documentation": "Leggi la documentazione" + }, + "cancelSuspension": "Annulla sospensione", + "unsavedRegistrations": "Registrazioni di notifiche non salvate", + "unsavedChanges": "Modifiche alle notifiche non salvate" + }, + "enrichments": { + "toast": { + "success": "Le impostazioni di miglioramento sono state salvate. Riavvia Frigate per applicare le modifiche.", + "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}" + }, + "title": "Impostazioni di miglioramento", + "semanticSearch": { + "reindexNow": { + "desc": "La reindicizzazione rigenererà gli incorporamenti per tutti gli oggetti tracciati. Questo processo viene eseguito in sottofondo e potrebbe impegnare al massimo la CPU e richiedere un tempo considerevole, a seconda del numero di oggetti tracciati.", + "success": "Reindicizzazione avviata con successo.", + "label": "Reindicizza ora", + "confirmTitle": "Conferma reindicizzazione", + "confirmDesc": "Vuoi davvero reindicizzare tutti gli incorporamenti di oggetti tracciati? Questo processo verrà eseguito in sottofondo, ma potrebbe impegnare al massimo la CPU e richiedere molto tempo. Puoi monitorare l'avanzamento nella pagina Esplora.", + "confirmButton": "Reindicizza", + "alreadyInProgress": "La reindicizzazione è già in corso.", + "error": "Impossibile avviare la reindicizzazione: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Utilizzando piccolo si utilizza una versione quantizzata del modello che utilizza meno RAM ed è più veloce sulla CPU con una differenza davvero trascurabile nella qualità di incorporamento.", + "title": "piccolo" + }, + "label": "Dimensioni del modello", + "desc": "Dimensione del modello utilizzato per gli incorporamenti della ricerca semantica.", + "large": { + "title": "grande", + "desc": "L'utilizzo di grande utilizza il modello Jina completo e, se applicabile, verrà eseguito automaticamente sulla GPU." + } + }, + "title": "Ricerca semantica", + "desc": "La ricerca semantica in Frigate consente di trovare gli oggetti tracciati all'interno degli elementi della recensione utilizzando l'immagine stessa, una descrizione testuale definita dall'utente o una generata automaticamente.", + "readTheDocumentation": "Leggi la documentazione" + }, + "faceRecognition": { + "desc": "Il riconoscimento facciale consente di assegnare un nome alle persone e, quando il volto viene riconosciuto, Frigate assegnerà il nome della persona come sottoetichetta. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Riconoscimento facciale", + "readTheDocumentation": "Leggi la documentazione", + "modelSize": { + "label": "Dimensioni del modello", + "desc": "Dimensioni del modello utilizzato per il riconoscimento facciale.", + "small": { + "title": "piccolo", + "desc": "L'utilizzo di piccolo sfrutta un modello di incorporamento di volti FaceNet che funziona in modo efficiente sulla maggior parte delle CPU." + }, + "large": { + "title": "grande", + "desc": "L'utilizzo di grande impiega un modello di incorporamento di volti ArcFace e verrà eseguito automaticamente sulla GPU, se applicabile." + } + } + }, + "birdClassification": { + "desc": "La classificazione degli uccelli identifica gli uccelli noti utilizzando un modello Tensorflow quantizzato. Quando un uccello noto viene riconosciuto, il suo nome comune viene aggiunto come sub_label. Queste informazioni sono incluse nell'interfaccia utente, nei filtri e nelle notifiche.", + "title": "Classificazione degli uccelli" + }, + "licensePlateRecognition": { + "desc": "Frigate può riconoscere le targhe dei veicoli e aggiungere automaticamente i caratteri rilevati al campo recognized_license_plate o un nome noto come sub_label agli oggetti di tipo automobile (car). Un caso d'uso comune potrebbe essere la lettura delle targhe delle auto che entrano in un vialetto o che transitano lungo una strada.", + "title": "Riconoscimento della targa", + "readTheDocumentation": "Leggi la documentazione" + }, + "unsavedChanges": "Modifiche alle impostazioni di miglioramento non salvate", + "restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)" + }, + "triggers": { + "documentTitle": "Inneschi", + "management": { + "title": "Inneschi", + "desc": "Gestisci gli inneschi per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." + }, + "addTrigger": "Aggiungi innesco", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Contenuto", + "threshold": "Soglia", + "actions": "Azioni", + "noTriggers": "Nessun innesco configurato per questa telecamera.", + "edit": "Modifica", + "deleteTrigger": "Elimina innesco", + "lastTriggered": "Ultimo innesco" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrizione" + }, + "actions": { + "alert": "Contrassegna come avviso", + "notification": "Invia notifica", + "sub_label": "Aggiungi sottoetichetta", + "attribute": "Aggiungi attributo" + }, + "dialog": { + "createTrigger": { + "title": "Crea innesco", + "desc": "Crea un innesco per la telecamera {{camera}}" + }, + "editTrigger": { + "title": "Modifica innesco", + "desc": "Modifica le impostazioni per l'innesco della telecamera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimina innesco", + "desc": "Vuoi davvero eliminare l'innesco {{triggerName}}? Questa azione non può essere annullata." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Assegna un nome a questo innesco", + "error": { + "minLength": "Il campo deve contenere almeno 2 caratteri.", + "invalidCharacters": "Il campo può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", + "alreadyExists": "Per questa telecamera esiste già un innesco con questo nome." + }, + "description": "Inserisci un nome o una descrizione univoca per identificare questo innesco" + }, + "enabled": { + "description": "Abilita o disabilita questo innesco" + }, + "type": { + "title": "Tipo", + "placeholder": "Seleziona il tipo di innesco", + "description": "Si attiva quando viene rilevata una descrizione di un oggetto simile tracciato", + "thumbnail": "Attiva quando viene rilevata una miniatura di un oggetto simile tracciato" + }, + "content": { + "title": "Contenuto", + "imagePlaceholder": "Seleziona una miniatura", + "textPlaceholder": "Inserisci il contenuto del testo", + "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un innesco dal menu.", + "textDesc": "Inserisci il testo per attivare questa azione quando viene rilevata una descrizione simile dell'oggetto tracciato.", + "error": { + "required": "Il contenuto è obbligatorio." + } + }, + "threshold": { + "title": "Soglia", + "error": { + "min": "La soglia deve essere almeno 0", + "max": "La soglia deve essere al massimo 1" + }, + "desc": "Imposta la soglia di similarità per questo innesco. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'innesco." + }, + "actions": { + "title": "Azioni", + "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli inneschi. Le sottoetichette aggiungono il nome dell'innesco all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.", + "error": { + "min": "È necessario selezionare almeno un'azione." + } + }, + "friendly_name": { + "title": "Nome semplice", + "placeholder": "Assegna un nome o descrivi questo innesco", + "description": "Un nome semplice o un testo descrittivo facoltativo per questo innesco." + } + } + }, + "toast": { + "success": { + "createTrigger": "L'innesco {{name}} è stato creato correttamente.", + "updateTrigger": "L'innesco {{name}} è stato aggiornato correttamente.", + "deleteTrigger": "L'innesco {{name}} è stato eliminato correttamente." + }, + "error": { + "createTriggerFailed": "Impossibile creare l'innesco: {{errorMessage}}", + "updateTriggerFailed": "Impossibile aggiornare l'innesco: {{errorMessage}}", + "deleteTriggerFailed": "Impossibile eliminare l'innesco: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "La ricerca semantica è disabilitata", + "desc": "Per utilizzare gli inneschi, è necessario abilitare la ricerca semantica." + }, + "wizard": { + "title": "Crea innesco", + "step1": { + "description": "Configura le impostazioni di base per il tuo innesco." + }, + "step2": { + "description": "Imposta il contenuto che attiverà questa azione." + }, + "step3": { + "description": "Configura la soglia e le azioni per questo innesco." + }, + "steps": { + "nameAndType": "Nome e tipo", + "configureData": "Configurare i dati", + "thresholdAndActions": "Soglia e azioni" + } + } + }, + "roles": { + "management": { + "title": "Gestione del ruolo di spettatore", + "desc": "Gestisci i ruoli di spettatori personalizzati e le relative autorizzazioni di accesso alla telecamera per questa istanza Frigate." + }, + "addRole": "Aggiungi ruolo", + "table": { + "role": "Ruolo", + "cameras": "Telecamere", + "actions": "Azioni", + "noRoles": "Nessun ruolo personalizzato trovato.", + "editCameras": "Modifica telecamere", + "deleteRole": "Elimina ruolo" + }, + "toast": { + "success": { + "createRole": "Ruolo {{role}} creato con successo", + "updateCameras": "Telecamere aggiornate per il ruolo {{role}}", + "deleteRole": "Ruolo {{role}} eliminato con successo", + "userRolesUpdated_one": "{{count}} utente assegnato a questo ruolo è stato aggiornato a \"spettatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_many": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere.", + "userRolesUpdated_other": "{{count}} utenti assegnati a questo ruolo sono stati aggiornati a \"spettatore\", che ha accesso a tutte le telecamere." + }, + "error": { + "createRoleFailed": "Impossibile creare il ruolo: {{errorMessage}}", + "updateCamerasFailed": "Impossibile aggiornare le telecamere: {{errorMessage}}", + "deleteRoleFailed": "Impossibile eliminare il ruolo: {{errorMessage}}", + "userUpdateFailed": "Impossibile aggiornare i ruoli utente: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Crea nuovo ruolo", + "desc": "Aggiungi un nuovo ruolo e specifica le autorizzazioni di accesso alla telecamera." + }, + "editCameras": { + "title": "Modifica telecamere di ruolo", + "desc": "Aggiorna l'accesso alla telecamera per il ruolo {{role}}." + }, + "deleteRole": { + "title": "Elimina ruolo", + "desc": "Questa azione non può essere annullata. Ciò eliminerà definitivamente il ruolo e assegnerà a tutti gli utenti il ruolo di 'spettatore', che darà loro accesso a tutte le telecamere.", + "warn": "Sei sicuro di voler eliminare {{role}}?", + "deleting": "Eliminazione in corso..." + }, + "form": { + "role": { + "title": "Nome del ruolo", + "placeholder": "Inserisci il nome del ruolo", + "desc": "Sono consentiti solo lettere, numeri, punti e caratteri di sottolineatura.", + "roleIsRequired": "Il nome del ruolo è obbligatorio", + "roleOnlyInclude": "Il nome del ruolo può includere solo lettere, numeri, . o _", + "roleExists": "Esiste già un ruolo con questo nome." + }, + "cameras": { + "title": "Telecamere", + "desc": "Seleziona le telecamere a cui questo ruolo ha accesso. È richiesta almeno una telecamera.", + "required": "È necessario selezionare almeno una telecamera." + } + } + } + }, + "cameraReview": { + "title": "Impostazioni revisione telecamera", + "object_descriptions": { + "title": "Descrizioni oggetti IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni degli oggetti generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli oggetti tracciati su questa telecamera." + }, + "review_descriptions": { + "title": "Descrizioni revisioni IA generativa", + "desc": "Abilita/disabilita temporaneamente le descrizioni delle revisioni generate dall'IA per questa telecamera. Se disabilitate, le descrizioni generate dall'IA non verranno richieste per gli elementi da rivedere su questa telecamera." + }, + "review": { + "title": "Rivedi", + "desc": "Abilita/disabilita temporaneamente avvisi e rilevamenti per questa telecamera fino al riavvio di Frigate. Se disabilitato, non verranno generati nuovi elementi di revisione. ", + "alerts": "Avvisi ", + "detections": "Rilevamenti " + }, + "reviewClassification": { + "title": "Classificazione revisione", + "desc": "Frigate categorizza gli elementi di revisione come Avvisi e Rilevamenti. Per impostazione predefinita, tutti gli oggetti persona e auto sono considerati Avvisi. È possibile perfezionare la categorizzazione degli elementi di revisione configurando le zone richieste per ciascuno di essi.", + "noDefinedZones": "Per questa telecamera non sono definite zone.", + "objectAlertsTips": "Tutti gli oggetti {{alertsLabels}} su {{cameraName}} verranno mostrati come Avvisi.", + "zoneObjectAlertsTips": "Tutti gli oggetti {{alertsLabels}} rilevati in {{zone}} su {{cameraName}} verranno mostrati come Avvisi.", + "objectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "zoneObjectDetectionsTips": { + "text": "Tutti gli oggetti {{detectionsLabels}} non categorizzati in {{zone}} su {{cameraName}} verranno mostrati come Rilevamenti.", + "notSelectDetections": "Tutti gli oggetti {{detectionsLabels}} rilevati in {{zone}} su {{cameraName}} non classificati come Avvisi verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano.", + "regardlessOfZoneObjectDetectionsTips": "Tutti gli oggetti {{detectionsLabels}} non categorizzati su {{cameraName}} verranno mostrati come Rilevamenti, indipendentemente dalla zona in cui si trovano." + }, + "unsavedChanges": "Impostazioni di classificazione delle revisioni non salvate per {{camera}}", + "selectAlertsZones": "Seleziona le zone per gli Avvisi", + "selectDetectionsZones": "Seleziona le zone per i Rilevamenti", + "limitDetections": "Limita i rilevamenti a zone specifiche", + "toast": { + "success": "La configurazione della classificazione di revisione è stata salvata. Riavvia Frigate per applicare le modifiche." + } + } + }, + "cameraWizard": { + "step3": { + "streamUnavailable": "Anteprima trasmissione non disponibile", + "description": "Configura i ruoli del flusso e aggiungi altri flussi alla tua telecamera.", + "validationTitle": "Convalida del flusso", + "connectAllStreams": "Connetti tutti i flussi", + "reconnectionSuccess": "Riconnessione riuscita.", + "reconnectionPartial": "Alcuni flussi non sono riusciti a riconnettersi.", + "reload": "Ricarica", + "connecting": "Connessione...", + "streamTitle": "Flusso {{number}}", + "valid": "Convalida", + "failed": "Fallito", + "notTested": "Non verificata", + "connectStream": "Connetti", + "connectingStream": "Connessione", + "disconnectStream": "Disconnetti", + "estimatedBandwidth": "Larghezza di banda stimata", + "roles": "Ruoli", + "none": "Nessuno", + "error": "Errore", + "streamValidated": "Flusso {{number}} convalidato con successo", + "streamValidationFailed": "Convalida del flusso {{number}} non riuscita", + "saveAndApply": "Salva nuova telecamera", + "saveError": "Configurazione non valida. Controlla le impostazioni.", + "issues": { + "title": "Convalida del flusso", + "videoCodecGood": "Il codec video è {{codec}}.", + "audioCodecGood": "Il codec audio è {{codec}}.", + "noAudioWarning": "Nessun audio rilevato per questo flusso, le registrazioni non avranno audio.", + "audioCodecRecordError": "Per supportare l'audio nelle registrazioni è necessario il codec audio AAC.", + "audioCodecRequired": "Per supportare il rilevamento audio è necessario un flusso audio.", + "restreamingWarning": "Riducendo le connessioni alla telecamera per il flusso di registrazione l'utilizzo della CPU potrebbe aumentare leggermente.", + "dahua": { + "substreamWarning": "Il flusso 1 è bloccato a bassa risoluzione. Molte telecamere Dahua/Amcrest/EmpireTech supportano flussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "hikvision": { + "substreamWarning": "Il flusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano flussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "resolutionHigh": "Una risoluzione di {{resolution}} potrebbe causare un aumento dell'utilizzo delle risorse.", + "resolutionLow": "Una risoluzione di {{resolution}} potrebbe essere troppo bassa per un rilevamento affidabile di oggetti di piccole dimensioni." + }, + "ffmpegModule": "Utilizza la modalità di compatibilità della trasmissione", + "ffmpegModuleDescription": "Se il flusso non si carica dopo diversi tentativi, prova ad abilitare questa opzione. Se abilitata, Frigate utilizzerà il modulo ffmpeg con go2rtc. Questo potrebbe garantire una migliore compatibilità con alcuni flussi di telecamere.", + "streamsTitle": "Flussi della telecamera", + "addStream": "Aggiungi flusso", + "addAnotherStream": "Aggiungi un altro flusso", + "streamUrl": "URL del flusso", + "streamUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "selectStream": "Seleziona un flusso", + "searchCandidates": "Ricerca candidati in corso...", + "noStreamFound": "Nessun flusso trovato", + "url": "URL", + "resolution": "Risoluzione", + "selectResolution": "Seleziona la risoluzione", + "quality": "Qualità", + "selectQuality": "Seleziona la qualità", + "roleLabels": { + "detect": "Rilevamento di oggetti", + "record": "Registrazione", + "audio": "Audio" + }, + "testStream": "Prova di connessione", + "testSuccess": "Prova del flusso riuscita!", + "testFailed": "Prova del flusso fallita", + "testFailedTitle": "Prova fallita", + "connected": "Connesso", + "notConnected": "Non connesso", + "featuresTitle": "Caratteristiche", + "go2rtc": "Riduci le connessioni alla telecamera", + "detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"rilevamento\".", + "rolesPopover": { + "title": "Ruoli del flusso", + "detect": "Flusso principale per il rilevamento degli oggetti.", + "record": "Salva segmenti del flusso video in base alle impostazioni di configurazione.", + "audio": "Flusso per il rilevamento basato sull'audio." + }, + "featuresPopover": { + "title": "Caratteristiche del flusso", + "description": "Utilizza la ritrasmissione go2rtc per ridurre le connessioni alla tua telecamera." + } + }, + "title": "Aggiungi telecamera", + "description": "Per aggiungere una nuova telecamera alla tua installazione Frigate, segui i passaggi indicati di seguito.", + "steps": { + "nameAndConnection": "Nome e connessione", + "streamConfiguration": "Configurazione flusso", + "validationAndTesting": "Validazione e prova", + "probeOrSnapshot": "Analisi o istantanea" + }, + "save": { + "success": "Nuova telecamera {{cameraName}} salvata correttamente.", + "failure": "Errore durante il salvataggio di {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Risoluzione", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Fornisci un URL di flusso valido", + "testFailed": "Prova del flusso fallita: {{error}}" + }, + "step1": { + "description": "Inserisci i dettagli della tua telecamera e scegli se analizzarla o selezionarne manualmente la marca.", + "cameraName": "Nome telecamera", + "cameraNamePlaceholder": "ad esempio, porta_anteriore o Panoramica cortile", + "host": "Indirizzo sistema/IP", + "port": "Porta", + "username": "Nome utente", + "usernamePlaceholder": "Opzionale", + "password": "Password", + "passwordPlaceholder": "Opzionale", + "selectTransport": "Seleziona il protocollo di trasmissione", + "cameraBrand": "Marca telecamera", + "selectBrand": "Seleziona la marca della telecamera per il modello URL", + "customUrl": "URL del flusso personalizzato", + "brandInformation": "Informazioni sul marchio", + "brandUrlFormat": "Per le telecamere con formato URL RTSP come: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "testConnection": "Prova connessione", + "testSuccess": "Prova di connessione riuscita!", + "testFailed": "Prova di connessione fallita. Controlla i dati immessi e riprova.", + "streamDetails": "Dettagli del flusso", + "warnings": { + "noSnapshot": "Impossibile recuperare un'immagine dal flusso configurato." + }, + "errors": { + "brandOrCustomUrlRequired": "Seleziona una marca di telecamera con sistema/IP oppure scegli \"Altro\" con un URL personalizzato", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameLength": "Il nome della telecamera deve contenere al massimo 64 caratteri", + "invalidCharacters": "Il nome della telecamera contiene caratteri non validi", + "nameExists": "Il nome della telecamera esiste già", + "brands": { + "reolink-rtsp": "Reolink RTSP non è consigliato. Abilita HTTP nelle impostazioni del firmware della telecamera e riavvia la procedura guidata." + }, + "customUrlRtspRequired": "Gli URL personalizzati devono iniziare con \"rtsp://\". Per i flussi di telecamere non RTSP è richiesta la configurazione manuale." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Analisi dei metadati della telecamera in corso...", + "fetchingSnapshot": "Recupero istantanea della telecamera in corso..." + }, + "probeMode": "Analisi telecamera", + "detectionMethodDescription": "Analizza la telecamera con ONVIF (se supportato) per trovare gli URL dei flussi video della telecamera oppure seleziona manualmente la marca della telecamera per utilizzare URL predefiniti. Per inserire un URL RTSP personalizzato, scegli il metodo manuale e seleziona \"Altro\".", + "connectionSettings": "Impostazioni di connessione", + "detectionMethod": "Metodo di rilevamento del flusso", + "onvifPort": "Porta ONVIF", + "manualMode": "Selezione manuale", + "onvifPortDescription": "Per le telecamere che supportano ONVIF, in genere è 80 o 8080.", + "useDigestAuth": "Utilizza l'autenticazione digest", + "useDigestAuthDescription": "Utilizza l'autenticazione HTTP digest per ONVIF. Alcune telecamere potrebbero richiedere un nome utente e una password ONVIF dedicati, anziché l'utente amministratore classico." + }, + "step2": { + "description": "Analizza la telecamera per individuare i flussi disponibili oppure configura le impostazioni manuali in base al metodo di rilevamento selezionato.", + "streamsTitle": "Flussi della telecamera", + "addStream": "Aggiungi flusso", + "addAnotherStream": "Aggiungi un altro flusso", + "streamTitle": "Flusso {{number}}", + "streamUrl": "URL del flusso", + "streamUrlPlaceholder": "rtsp://nomeutente:password@sistema:porta/percorso", + "url": "URL", + "resolution": "Risoluzione", + "selectResolution": "Seleziona la risoluzione", + "quality": "Qualità", + "selectQuality": "Seleziona la qualità", + "roles": "Ruoli", + "roleLabels": { + "detect": "Rilevamento oggetti", + "record": "Registrazione", + "audio": "Audio" + }, + "testStream": "Prova connessione", + "testSuccess": "Prova di connessione riuscita!", + "testFailed": "Prova di connessione fallita. Controlla i dati inseriti e riprova.", + "testFailedTitle": "Prova fallita", + "connected": "Connessa", + "notConnected": "Non connessa", + "featuresTitle": "Caratteristiche", + "go2rtc": "Riduci le connessioni alla telecamera", + "detectRoleWarning": "Per procedere, almeno un flusso deve avere il ruolo \"rileva\".", + "rolesPopover": { + "title": "Ruoli del flusso", + "detect": "Flusso principale per il rilevamento degli oggetti.", + "record": "Salva segmenti del flusso video in base alle impostazioni di configurazione.", + "audio": "Flusso per il rilevamento basato sull'audio." + }, + "featuresPopover": { + "title": "Caratteristiche del flusso", + "description": "Utilizza la ritrasmissione go2rtc per ridurre le connessioni alla tua telecamera." + }, + "probeFailed": "Impossibile analizzare la telecamera: {{error}}", + "probeSuccessful": "Analisi riuscita", + "probeError": "Errore analisi", + "probeNoSuccess": "Analisi non riuscita", + "rtspCandidatesDescription": "I seguenti URL RTSP sono stati trovati dall'analisi della telecamera. Prova la connessione per visualizzare i metadati della trasmissione.", + "streamDetails": "Dettagli del flusso", + "probing": "Analisi telecamera in corso...", + "retry": "Riprova", + "testing": { + "probingMetadata": "Analisi dei metadati della telecamera in corso...", + "fetchingSnapshot": "Recupero dell'istantanea della telecamera in corso..." + }, + "probingDevice": "Analisi del dispositivo in corso...", + "deviceInfo": "Informazioni sul dispositivo", + "manufacturer": "Produttore", + "model": "Modello", + "firmware": "Firmware", + "profiles": "Profili", + "ptzSupport": "Supporto PTZ", + "autotrackingSupport": "Supporto per il tracciamento automatico", + "presets": "Preimpostazioni", + "rtspCandidates": "Candidati RTSP", + "noRtspCandidates": "Nessun URL RTSP trovato dalla telecamera. Le credenziali potrebbero essere errate oppure la telecamera potrebbe non supportare ONVIF o il metodo utilizzato per recuperare gli URL RTSP. Torna indietro e inserisci manualmente l'URL RTSP.", + "candidateStreamTitle": "Candidato {{number}}}}", + "useCandidate": "Utilizza", + "uriCopy": "Copia", + "uriCopied": "URI copiato negli appunti", + "testConnection": "Prova di connessione", + "toggleUriView": "Fai clic per attivare/disattivare la visualizzazione completa dell'URI", + "errors": { + "hostRequired": "È richiesto il nome sistema/indirizzo IP" + } + }, + "step4": { + "description": "Convalida e analisi finale prima di salvare la nuova telecamera. Collega ogni flusso prima di salvare.", + "validationTitle": "Validazione del flusso", + "connectAllStreams": "Connetti tutti i flussi", + "reconnectionSuccess": "Riconnessione riuscita.", + "reconnectionPartial": "Alcuni flussi non sono riusciti a riconnettersi.", + "streamUnavailable": "Anteprima del flusso non disponibile", + "reload": "Ricarica", + "connecting": "Connessione in corso...", + "streamTitle": "Flusso {{number}}", + "valid": "Valida", + "failed": "Fallito", + "notTested": "Non verificato", + "connectStream": "Connetti", + "connectingStream": "Connessione in corso", + "disconnectStream": "Disconnetti", + "estimatedBandwidth": "Larghezza di banda stimata", + "roles": "Ruoli", + "ffmpegModule": "Utilizza la modalità di compatibilità del flusso", + "ffmpegModuleDescription": "Se il flusso non si carica dopo diversi tentativi, prova ad abilitare questa opzione. Se abilitata, Frigate utilizzerà il modulo ffmpeg con go2rtc. Questo potrebbe garantire una migliore compatibilità con alcuni flussi di telecamere.", + "none": "Nessuno", + "error": "Errore", + "streamValidated": "Flusso {{number}} convalidato con successo", + "streamValidationFailed": "Convalida del flusso {{number}} non riuscita", + "saveAndApply": "Salva nuova telecamera", + "saveError": "Configurazione non valida. Controlla le impostazioni.", + "issues": { + "title": "Validazione del flusso", + "videoCodecGood": "Il codec video è {{codec}}.", + "audioCodecGood": "Il codec audio è {{codec}}.", + "resolutionHigh": "Una risoluzione di {{resolution}} potrebbe causare un aumento dell'utilizzo delle risorse.", + "resolutionLow": "Una risoluzione di {{resolution}} potrebbe essere troppo bassa per un rilevamento affidabile di oggetti di piccole dimensioni.", + "noAudioWarning": "Nessun audio rilevato per questo flusso, le registrazioni non avranno audio.", + "audioCodecRecordError": "Per supportare l'audio nelle registrazioni è necessario il codec audio AAC.", + "audioCodecRequired": "Per supportare il rilevamento audio è necessario un flusso audio.", + "restreamingWarning": "Riducendo le connessioni alla telecamera per il flusso di registrazione l'utilizzo della CPU potrebbe aumentare leggermente.", + "brands": { + "reolink-rtsp": "Reolink RTSP non è consigliato. Abilita HTTP nelle impostazioni del firmware della telecamera e riavvia la procedura guidata." + }, + "dahua": { + "substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Dahua/Amcrest/EmpireTech supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + }, + "hikvision": { + "substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." + } + } + } + }, + "cameraManagement": { + "title": "Gestisci telecamere", + "addCamera": "Aggiungi nuova telecamera", + "editCamera": "Modifica telecamera:", + "selectCamera": "Seleziona una telecamera", + "backToSettings": "Torna alle impostazioni della telecamera", + "streams": { + "title": "Abilita/Disabilita telecamere", + "desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili.
    Nota: questa operazione non disattiva le ritrasmissioni di go2rtc." + }, + "cameraConfig": { + "add": "Aggiungi telecamera", + "edit": "Modifica telecamera", + "description": "Configura le impostazioni della telecamera, inclusi gli ingressi ed i ruoli dei flussi.", + "name": "Nome telecamera", + "nameRequired": "Il nome della telecamera è obbligatorio", + "nameLength": "Il nome della telecamera deve contenere al massimo 64 caratteri.", + "namePlaceholder": "ad esempio, porta_anteriore o Panoramica cortile", + "toast": { + "success": "La telecamera {{cameraName}} è stata salvata correttamente" + }, + "enabled": "Abilitata", + "ffmpeg": { + "inputs": "Flussi di ingresso", + "path": "Percorso del flusso", + "pathRequired": "Il percorso del flusso è obbligatorio", + "pathPlaceholder": "rtsp://...", + "roles": "Ruoli", + "rolesRequired": "È richiesto almeno un ruolo", + "rolesUnique": "Ogni ruolo (audio, rilevamento, registrazione) può essere assegnato solo ad un flusso", + "addInput": "Aggiungi flusso di ingresso", + "removeInput": "Rimuovi flusso di ingresso", + "inputsRequired": "È richiesto almeno un flusso di ingresso" + }, + "go2rtcStreams": "Flussi go2rtc", + "streamUrls": "URL dei flussi", + "addUrl": "Aggiungi URL", + "addGo2rtcStream": "Aggiungi flusso go2rtc" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/it/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/it/views/system.json new file mode 100644 index 0000000..b031828 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/it/views/system.json @@ -0,0 +1,199 @@ +{ + "documentTitle": { + "cameras": "Statistiche telecamere - Frigate", + "enrichments": "Statistiche di miglioramento - Frigate", + "storage": "Statistiche archiviazione - Frigate", + "general": "Statistiche generali - Frigate", + "logs": { + "frigate": "Registri Frigate - Frigate", + "go2rtc": "Registri Go2RTC - Frigate", + "nginx": "Registri Nginx - Frigate" + } + }, + "logs": { + "type": { + "timestamp": "Orario", + "label": "Tipo", + "tag": "Etichetta", + "message": "Messaggio" + }, + "tips": "I registri vengono trasmessi dal server", + "toast": { + "error": { + "whileStreamingLogs": "Errore durante la trasmissione dei registri: {{errorMessage}}", + "fetchingLogsFailed": "Errore durante il recupero dei registri: {{errorMessage}}" + } + }, + "download": { + "label": "Scarica registri" + }, + "copy": { + "label": "Copia negli appunti", + "success": "Registri copiati negli appunti", + "error": "Impossibile copiare i registri negli appunti" + } + }, + "general": { + "hardwareInfo": { + "gpuInfo": { + "nvidiaSMIOutput": { + "cudaComputerCapability": "Capacità di elaborazione CUDA: {{cuda_compute}}", + "title": "Risultati Nvidia SMI", + "name": "Nome: {{name}}", + "driver": "Driver: {{driver}}", + "vbios": "Informazioni VBios: {{vbios}}" + }, + "vainfoOutput": { + "title": "Risultati Vainfo", + "returnCode": "Codice di ritorno: {{code}}", + "processOutput": "Risultati del processo:", + "processError": "Errore del processo:" + }, + "closeInfo": { + "label": "Chiudi informazioni GPU" + }, + "copyInfo": { + "label": "Copia informazioni GPU" + }, + "toast": { + "success": "Informazioni GPU copiate negli appunti" + } + }, + "title": "Informazioni hardware", + "gpuDecoder": "Decodificatore GPU", + "gpuEncoder": "Codificatore GPU", + "gpuUsage": "Utilizzo GPU", + "gpuMemory": "Memoria GPU", + "npuUsage": "Utilizzo NPU", + "npuMemory": "Memoria NPU", + "intelGpuWarning": { + "title": "Avviso statistiche GPU Intel", + "message": "Statistiche GPU non disponibili", + "description": "Si tratta di un problema noto negli strumenti di reportistica delle statistiche GPU di Intel (intel_gpu_top), che si interrompe e restituisce ripetutamente un utilizzo della GPU pari a 0% anche nei casi in cui l'accelerazione hardware e il rilevamento degli oggetti funzionano correttamente sulla (i)GPU. Non si tratta di un problema di Frigate. È possibile riavviare il sistema per risolvere temporaneamente il problema e verificare che la GPU funzioni correttamente. Ciò non influisce sulle prestazioni." + } + }, + "detector": { + "inferenceSpeed": "Velocità inferenza rilevatore", + "title": "Rilevatori", + "cpuUsage": "Utilizzo CPU rilevatore", + "memoryUsage": "Utilizzo memoria rilevatore", + "temperature": "Temperatura del rilevatore", + "cpuUsageInformation": "CPU utilizzata nella preparazione dei dati di ingresso e uscita da/verso i modelli di rilevamento. Questo valore non misura l'utilizzo dell'inferenza, anche se si utilizza una GPU o un acceleratore." + }, + "title": "Generale", + "otherProcesses": { + "title": "Altri processi", + "processCpuUsage": "Utilizzo CPU processo", + "processMemoryUsage": "Utilizzo memoria processo" + } + }, + "enrichments": { + "embeddings": { + "face_embedding_speed": "Velocità incorporamento volti", + "plate_recognition_speed": "Velocità riconoscimento targhe", + "image_embedding_speed": "Velocità incorporamento immagini", + "text_embedding_speed": "Velocità incorporamento testo", + "face_recognition_speed": "Velocità di riconoscimento facciale", + "face_recognition": "Riconoscimento facciale", + "plate_recognition": "Riconoscimento delle targhe", + "yolov9_plate_detection_speed": "Velocità di rilevamento della targa con YOLOv9", + "yolov9_plate_detection": "Rilevamento della targa con YOLOv9", + "image_embedding": "Incorporamento di immagini", + "text_embedding": "Incorporamento di testo", + "review_description": "Descrizione della revisione", + "review_description_speed": "Velocità della descrizione di revisione", + "review_description_events_per_second": "Descrizione della revisione", + "object_description": "Descrizione dell'oggetto", + "object_description_speed": "Velocità della descrizione dell'oggetto", + "object_description_events_per_second": "Descrizione dell'oggetto" + }, + "title": "Miglioramenti", + "infPerSecond": "Inferenze al secondo", + "averageInf": "Tempo medio di inferenza" + }, + "cameras": { + "info": { + "fetching": "Recupero dati della telecamera", + "streamDataFromFFPROBE": "I dati del flusso vengono ottenuti con ffprobe.", + "cameraProbeInfo": "Informazioni analisi telecamera {{camera}}", + "stream": "Flusso {{idx}}", + "video": "Video:", + "codec": "Codec:", + "resolution": "Risoluzione:", + "fps": "FPS:", + "unknown": "Sconosciuto", + "audio": "Audio:", + "error": "Errore: {{error}}", + "tips": { + "title": "Informazioni analisi telecamera" + }, + "aspectRatio": "rapporto d'aspetto" + }, + "title": "Telecamere", + "overview": "Sommario", + "framesAndDetections": "Fotogrammi / Rilevamenti", + "label": { + "camera": "telecamera", + "detect": "rilevamento", + "skipped": "saltati", + "ffmpeg": "FFmpeg", + "capture": "cattura", + "overallFramesPerSecond": "fotogrammi totali al secondo", + "overallDetectionsPerSecond": "rilevamenti totali al secondo", + "overallSkippedDetectionsPerSecond": "rilevamenti totali saltati al secondo", + "cameraCapture": "{{camName}} cattura", + "cameraDetect": "{{camName}} rilevamento", + "cameraFramesPerSecond": "{{camName}} fotogrammi al secondo", + "cameraDetectionsPerSecond": "{{camName}} rilevamenti al secondo", + "cameraSkippedDetectionsPerSecond": "{{camName}} rilevamenti saltati al secondo", + "cameraFfmpeg": "{{camName}} FFmpeg" + }, + "toast": { + "success": { + "copyToClipboard": "Dati di analisi copiati negli appunti." + }, + "error": { + "unableToProbeCamera": "Impossibile analizzare la telecamera: {{errorMessage}}" + } + } + }, + "stats": { + "detectHighCpuUsage": "{{camera}} ha un utilizzo elevato della CPU con il rilevamento ({{detectAvg}}%)", + "ffmpegHighCpuUsage": "{{camera}} ha un elevato utilizzo della CPU con FFmpeg ({{ffmpegAvg}}%)", + "healthy": "Il sistema è integro", + "reindexingEmbeddings": "Reindicizzazione degli incorporamenti (completata al {{processed}}%)", + "cameraIsOffline": "{{camera}} è disconnessa", + "detectIsSlow": "{{detect}} è lento ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} è molto lento ({{speed}} ms)", + "shmTooLow": "L'allocazione /dev/shm ({{total}} MB) dovrebbe essere aumentata almeno a {{min}} MB." + }, + "title": "Sistema", + "metrics": "Metriche di sistema", + "storage": { + "title": "Archiviazione", + "overview": "Sommario", + "recordings": { + "title": "Registrazioni", + "tips": "Questo valore rappresenta lo spazio di archiviazione totale utilizzato dalle registrazioni nel database di Frigate. Frigate non tiene traccia dell'utilizzo dello spazio di archiviazione per tutti i file presenti sul disco.", + "earliestRecording": "Prima registrazione disponibile:" + }, + "cameraStorage": { + "title": "Archiviazione della telecamera", + "camera": "Telecamera", + "unusedStorageInformation": "Informazioni spazio di archiviazione non utilizzato", + "storageUsed": "Archiviazione", + "percentageOfTotalUsed": "Percentuale del totale", + "bandwidth": "Larghezza di banda", + "unused": { + "title": "Liberi", + "tips": "Questo valore potrebbe non rappresentare accuratamente lo spazio libero disponibile per Frigate se nel disco sono archiviati altri file oltre alle registrazioni di Frigate. Frigate non tiene traccia dell'utilizzo dello spazio di archiviazione al di fuori delle sue registrazioni." + } + }, + "shm": { + "title": "Allocazione SHM (memoria condivisa)", + "warning": "La dimensione SHM attuale di {{total}} MB è troppo piccola. Aumentarla ad almeno {{min_shm}} MB.", + "readTheDocumentation": "Leggi la documentazione" + } + }, + "lastRefreshed": "Ultimo aggiornamento: " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ja/audio.json new file mode 100644 index 0000000..c546c09 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "話し声", + "car": "車", + "bicycle": "自転車", + "yell": "叫び声", + "motorcycle": "オートバイ", + "babbling": "赤ちゃんの喃語", + "bellow": "怒鳴り声", + "whoop": "歓声", + "whispering": "ささやき声", + "laughter": "笑い声", + "snicker": "くすくす笑い", + "crying": "泣き声", + "sigh": "ため息", + "singing": "歌声", + "choir": "合唱", + "yodeling": "ヨーデル", + "chant": "詠唱", + "mantra": "マントラ", + "child_singing": "子供の歌声", + "synthetic_singing": "合成音声の歌", + "rapping": "ラップ", + "humming": "ハミング", + "groan": "うめき声", + "grunt": "うなり声", + "whistling": "口笛", + "breathing": "呼吸音", + "wheeze": "ぜいぜい声", + "snoring": "いびき", + "gasp": "はっと息をのむ音", + "pant": "荒い息", + "snort": "鼻を鳴らす音", + "cough": "咳", + "throat_clearing": "咳払い", + "sneeze": "くしゃみ", + "sniff": "鼻をすする音", + "run": "走る音", + "shuffle": "足を引きずる音", + "footsteps": "足音", + "chewing": "咀嚼音", + "biting": "かみつく音", + "gargling": "うがい", + "stomach_rumble": "お腹の音", + "burping": "げっぷ", + "hiccup": "しゃっくり", + "fart": "おなら", + "hands": "手の音", + "finger_snapping": "指を鳴らす音", + "clapping": "拍手", + "heartbeat": "心臓の鼓動", + "heart_murmur": "心雑音", + "cheering": "歓声", + "applause": "拍手喝采", + "chatter": "おしゃべり", + "crowd": "群衆", + "children_playing": "子供の遊ぶ声", + "animal": "動物", + "pets": "ペット", + "dog": "犬", + "bark": "樹皮", + "yip": "キャンキャン鳴く声", + "howl": "遠吠え", + "bow_wow": "ワンワン", + "growling": "うなり声", + "whimper_dog": "犬の鳴き声(クンクン)", + "cat": "猫", + "purr": "ゴロゴロ音", + "meow": "ニャー", + "hiss": "シャー", + "caterwaul": "猫のけんか声", + "livestock": "家畜", + "horse": "馬", + "clip_clop": "カツカツ音", + "neigh": "いななき", + "cattle": "牛", + "moo": "モー", + "cowbell": "カウベル", + "pig": "豚", + "oink": "ブーブー", + "goat": "ヤギ", + "bleat": "メェー", + "sheep": "羊", + "fowl": "家禽", + "chicken": "鶏", + "cluck": "コッコッ", + "cock_a_doodle_doo": "コケコッコー", + "turkey": "七面鳥", + "gobble": "グルル", + "duck": "アヒル", + "quack": "ガーガー", + "goose": "ガチョウ", + "honk": "ホンク", + "wild_animals": "野生動物", + "roaring_cats": "猛獣の鳴き声", + "roar": "咆哮", + "bird": "鳥", + "chirp": "さえずり", + "squawk": "ギャーギャー", + "pigeon": "ハト", + "coo": "クークー", + "crow": "カラス", + "caw": "カーカー", + "owl": "フクロウ", + "hoot": "ホーホー", + "flapping_wings": "羽ばたき", + "dogs": "犬たち", + "rats": "ネズミ", + "mouse": "マウス", + "patter": "パタパタ音", + "insect": "昆虫", + "cricket": "コオロギ", + "mosquito": "蚊", + "fly": "ハエ", + "buzz": "ブーン", + "frog": "カエル", + "croak": "ゲロゲロ", + "snake": "ヘビ", + "rattle": "ガラガラ音", + "whale_vocalization": "クジラの鳴き声", + "music": "音楽", + "musical_instrument": "楽器", + "plucked_string_instrument": "撥弦楽器", + "guitar": "ギター", + "electric_guitar": "エレキギター", + "bass_guitar": "ベースギター", + "acoustic_guitar": "アコースティックギター", + "steel_guitar": "スティールギター", + "tapping": "タッピング", + "strum": "ストローク", + "banjo": "バンジョー", + "sitar": "シタール", + "mandolin": "マンドリン", + "zither": "ツィター", + "ukulele": "ウクレレ", + "keyboard": "キーボード", + "piano": "ピアノ", + "electric_piano": "エレクトリックピアノ", + "organ": "オルガン", + "electronic_organ": "電子オルガン", + "hammond_organ": "ハモンドオルガン", + "synthesizer": "シンセサイザー", + "sampler": "サンプラー", + "harpsichord": "チェンバロ", + "percussion": "打楽器", + "drum_kit": "ドラムセット", + "drum_machine": "ドラムマシン", + "drum": "ドラム", + "snare_drum": "スネアドラム", + "rimshot": "リムショット", + "drum_roll": "ドラムロール", + "bass_drum": "バスドラム", + "timpani": "ティンパニ", + "tabla": "タブラ", + "cymbal": "シンバル", + "hi_hat": "ハイハット", + "wood_block": "ウッドブロック", + "tambourine": "タンバリン", + "maraca": "マラカス", + "gong": "ゴング", + "tubular_bells": "チューブラーベル", + "mallet_percussion": "マレット打楽器", + "marimba": "マリンバ", + "glockenspiel": "グロッケンシュピール", + "vibraphone": "ビブラフォン", + "steelpan": "スティールパン", + "orchestra": "オーケストラ", + "brass_instrument": "金管楽器", + "french_horn": "フレンチホルン", + "trumpet": "トランペット", + "trombone": "トロンボーン", + "bowed_string_instrument": "擦弦楽器", + "string_section": "弦楽セクション", + "violin": "バイオリン", + "pizzicato": "ピチカート", + "cello": "チェロ", + "double_bass": "コントラバス", + "wind_instrument": "木管楽器", + "flute": "フルート", + "saxophone": "サックス", + "clarinet": "クラリネット", + "harp": "ハープ", + "bell": "鐘", + "church_bell": "教会の鐘", + "jingle_bell": "ジングルベル", + "bicycle_bell": "自転車ベル", + "tuning_fork": "音叉", + "chime": "チャイム", + "wind_chime": "風鈴", + "harmonica": "ハーモニカ", + "accordion": "アコーディオン", + "bagpipes": "バグパイプ", + "didgeridoo": "ディジュリドゥ", + "theremin": "テルミン", + "singing_bowl": "シンギングボウル", + "scratching": "スクラッチ音", + "pop_music": "ポップ音楽", + "hip_hop_music": "ヒップホップ音楽", + "beatboxing": "ボイスパーカッション", + "rock_music": "ロック音楽", + "heavy_metal": "ヘビーメタル", + "punk_rock": "パンクロック", + "grunge": "グランジ", + "progressive_rock": "プログレッシブロック", + "rock_and_roll": "ロックンロール", + "psychedelic_rock": "サイケデリックロック", + "rhythm_and_blues": "リズム・アンド・ブルース", + "soul_music": "ソウル音楽", + "reggae": "レゲエ", + "country": "カントリー", + "swing_music": "スウィング音楽", + "bluegrass": "ブルーグラス", + "funk": "ファンク", + "folk_music": "フォーク音楽", + "middle_eastern_music": "中東音楽", + "jazz": "ジャズ", + "disco": "ディスコ", + "classical_music": "クラシック音楽", + "opera": "オペラ", + "electronic_music": "電子音楽", + "house_music": "ハウス", + "techno": "テクノ", + "dubstep": "ダブステップ", + "drum_and_bass": "ドラムンベース", + "electronica": "エレクトロニカ", + "electronic_dance_music": "EDM", + "ambient_music": "アンビエント", + "trance_music": "トランス", + "music_of_latin_america": "ラテン音楽", + "salsa_music": "サルサ", + "flamenco": "フラメンコ", + "blues": "ブルース", + "music_for_children": "子供向け音楽", + "new-age_music": "ニューエイジ音楽", + "vocal_music": "声楽", + "a_capella": "アカペラ", + "music_of_africa": "アフリカ音楽", + "afrobeat": "アフロビート", + "christian_music": "キリスト教音楽", + "gospel_music": "ゴスペル", + "music_of_asia": "アジア音楽", + "carnatic_music": "カルナータカ音楽", + "music_of_bollywood": "ボリウッド音楽", + "ska": "スカ", + "traditional_music": "伝統音楽", + "independent_music": "インディーズ音楽", + "song": "歌", + "background_music": "BGM", + "theme_music": "テーマ音楽", + "jingle": "ジングル", + "soundtrack_music": "サウンドトラック", + "lullaby": "子守唄", + "video_game_music": "ゲーム音楽", + "christmas_music": "クリスマス音楽", + "dance_music": "ダンス音楽", + "wedding_music": "結婚式音楽", + "happy_music": "明るい音楽", + "sad_music": "悲しい音楽", + "tender_music": "優しい音楽", + "exciting_music": "ワクワクする音楽", + "angry_music": "怒りの音楽", + "scary_music": "怖い音楽", + "wind": "風", + "rustling_leaves": "木の葉のざわめき", + "wind_noise": "風の音", + "thunderstorm": "雷雨", + "thunder": "雷鳴", + "water": "水", + "rain": "雨", + "raindrop": "雨粒", + "rain_on_surface": "雨が当たる音", + "stream": "小川", + "waterfall": "滝", + "ocean": "海", + "waves": "波", + "steam": "蒸気", + "gurgling": "ゴボゴボ音", + "fire": "火", + "crackle": "パチパチ音", + "vehicle": "車両", + "boat": "ボート", + "sailboat": "帆船", + "rowboat": "手漕ぎボート", + "motorboat": "モーターボート", + "ship": "船", + "motor_vehicle": "自動車", + "toot": "クラクション", + "car_alarm": "車のアラーム", + "power_windows": "パワーウィンドウ", + "skidding": "スリップ音", + "tire_squeal": "タイヤの悲鳴", + "car_passing_by": "車が通る音", + "race_car": "レーシングカー", + "truck": "トラック", + "air_brake": "エアブレーキ", + "air_horn": "エアホーン", + "reversing_beeps": "バック警告音", + "ice_cream_truck": "アイスクリームトラック", + "bus": "バス", + "emergency_vehicle": "緊急車両", + "police_car": "パトカー", + "ambulance": "救急車", + "fire_engine": "消防車", + "traffic_noise": "交通騒音", + "rail_transport": "鉄道輸送", + "train": "電車", + "train_whistle": "汽笛", + "train_horn": "列車のホーン", + "railroad_car": "鉄道車両", + "train_wheels_squealing": "車輪のきしむ音", + "subway": "地下鉄", + "aircraft": "航空機", + "aircraft_engine": "航空機エンジン", + "jet_engine": "ジェットエンジン", + "propeller": "プロペラ", + "helicopter": "ヘリコプター", + "fixed-wing_aircraft": "固定翼機", + "skateboard": "スケートボード", + "engine": "エンジン", + "light_engine": "小型エンジン", + "dental_drill's_drill": "歯科用ドリル", + "lawn_mower": "芝刈り機", + "chainsaw": "チェーンソー", + "medium_engine": "中型エンジン", + "heavy_engine": "大型エンジン", + "engine_knocking": "ノッキング音", + "engine_starting": "エンジン始動", + "idling": "アイドリング", + "accelerating": "加速音", + "door": "ドア", + "doorbell": "ドアベル", + "ding-dong": "ピンポン", + "sliding_door": "引き戸", + "slam": "ドアをバタンと閉める音", + "knock": "ノック", + "tap": "トントン音", + "squeak": "きしみ音", + "cupboard_open_or_close": "戸棚の開閉", + "drawer_open_or_close": "引き出しの開閉", + "dishes": "食器", + "cutlery": "カトラリー", + "chopping": "包丁で切る音", + "frying": "揚げ物の音", + "microwave_oven": "電子レンジ", + "blender": "ミキサー", + "water_tap": "水道の蛇口", + "sink": "流し台", + "bathtub": "浴槽", + "hair_dryer": "ヘアドライヤー", + "toilet_flush": "トイレの水流", + "toothbrush": "歯ブラシ", + "electric_toothbrush": "電動歯ブラシ", + "vacuum_cleaner": "掃除機", + "zipper": "ファスナー", + "keys_jangling": "鍵のジャラジャラ音", + "coin": "コイン", + "scissors": "はさみ", + "electric_shaver": "電気シェーバー", + "shuffling_cards": "カードを切る音", + "typing": "タイピング音", + "typewriter": "タイプライター", + "computer_keyboard": "コンピュータキーボード", + "writing": "書く音", + "alarm": "アラーム", + "telephone": "電話", + "telephone_bell_ringing": "電話のベル音", + "ringtone": "着信音", + "telephone_dialing": "ダイヤル音", + "dial_tone": "発信音", + "busy_signal": "話中音", + "alarm_clock": "目覚まし時計", + "siren": "サイレン", + "civil_defense_siren": "防災サイレン", + "buzzer": "ブザー", + "smoke_detector": "火災警報器", + "fire_alarm": "火災報知器", + "foghorn": "霧笛", + "whistle": "ホイッスル", + "steam_whistle": "蒸気笛", + "mechanisms": "機械仕掛け", + "ratchet": "ラチェット", + "clock": "時計", + "tick": "カチカチ音", + "tick-tock": "チクタク音", + "gears": "歯車", + "pulleys": "滑車", + "sewing_machine": "ミシン", + "mechanical_fan": "扇風機", + "air_conditioning": "エアコン", + "cash_register": "レジ", + "printer": "プリンター", + "camera": "カメラ", + "single-lens_reflex_camera": "一眼レフカメラ", + "tools": "工具", + "hammer": "ハンマー", + "jackhammer": "削岩機", + "sawing": "のこぎり", + "filing": "やすりがけ", + "sanding": "研磨", + "power_tool": "電動工具", + "drill": "ドリル", + "explosion": "爆発", + "gunshot": "銃声", + "machine_gun": "機関銃", + "fusillade": "一斉射撃", + "artillery_fire": "砲撃", + "cap_gun": "おもちゃのピストル", + "fireworks": "花火", + "firecracker": "爆竹", + "burst": "破裂音", + "eruption": "噴火", + "boom": "ドカン", + "wood": "木材", + "chop": "伐採音", + "splinter": "裂ける音", + "crack": "割れる音", + "glass": "ガラス", + "chink": "チリン音", + "shatter": "粉々に割れる音", + "silence": "静寂", + "sound_effect": "効果音", + "environmental_noise": "環境音", + "static": "ノイズ", + "white_noise": "ホワイトノイズ", + "pink_noise": "ピンクノイズ", + "television": "テレビ", + "radio": "ラジオ", + "field_recording": "フィールド録音", + "scream": "悲鳴" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/common.json b/sam2-cpu/frigate-dev/web/public/locales/ja/common.json new file mode 100644 index 0000000..ba84f3e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/common.json @@ -0,0 +1,271 @@ +{ + "time": { + "untilForRestart": "Frigate が再起動するまで。", + "untilRestart": "再起動まで", + "untilForTime": "{{time}} まで", + "ago": "{{timeAgo}} 前", + "justNow": "今", + "today": "本日", + "yesterday": "昨日", + "last7": "7日間", + "last14": "14日間", + "last30": "30日間", + "thisWeek": "今週", + "lastWeek": "先週", + "thisMonth": "今月", + "lastMonth": "先月", + "5minutes": "5 分", + "10minutes": "10 分", + "30minutes": "30 分", + "1hour": "1 時間", + "12hours": "12 時間", + "24hours": "24 時間", + "pm": "午後", + "am": "午前", + "yr": "{{time}}年", + "year_other": "{{time}} 年", + "mo": "{{time}}ヶ月", + "month_other": "{{time}} ヶ月", + "d": "{{time}}日", + "day_other": "{{time}} 日", + "h": "{{time}}時間", + "hour_other": "{{time}} 時間", + "m": "{{time}}分", + "minute_other": "{{time}} 分", + "s": "{{time}}秒", + "second_other": "{{time}} 秒", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } + }, + "readTheDocumentation": "ドキュメントを見る", + "unit": { + "speed": { + "mph": "mph", + "kph": "Km/h" + }, + "length": { + "feet": "フィート", + "meters": "メートル" + }, + "data": { + "gbph": "GB/hour", + "gbps": "GB/s", + "kbph": "kB/hour", + "kbps": "kB/s", + "mbph": "MB/hour", + "mbps": "MB/s" + } + }, + "label": { + "back": "戻る" + }, + "button": { + "apply": "適用", + "reset": "リセット", + "done": "完了", + "enabled": "有効", + "enable": "有効にする", + "disabled": "無効", + "disable": "無効にする", + "save": "保存", + "saving": "保存中…", + "cancel": "キャンセル", + "close": "閉じる", + "copy": "コピー", + "back": "戻る", + "history": "履歴", + "fullscreen": "全画面", + "exitFullscreen": "全画面解除", + "pictureInPicture": "ピクチャーインピクチャー", + "twoWayTalk": "双方向通話", + "cameraAudio": "カメラ音声", + "on": "オン", + "off": "オフ", + "edit": "編集", + "copyCoordinates": "座標をコピー", + "delete": "削除", + "yes": "はい", + "no": "いいえ", + "download": "ダウンロード", + "info": "情報", + "suspended": "一時停止", + "unsuspended": "再開", + "play": "再生", + "unselect": "選択解除", + "export": "書き出し", + "deleteNow": "今すぐ削除", + "next": "次へ" + }, + "menu": { + "system": "システム", + "systemMetrics": "システムモニター", + "configuration": "設定", + "systemLogs": "システムログ", + "settings": "設定", + "configurationEditor": "設定エディタ", + "languages": "言語", + "appearance": "外観", + "darkMode": { + "label": "ダークモード", + "light": "ライト", + "dark": "ダーク", + "withSystem": { + "label": "システム設定に従う" + } + }, + "withSystem": "システム", + "theme": { + "label": "テーマ", + "blue": "青", + "green": "緑", + "nord": "ノルド", + "red": "赤", + "highcontrast": "ハイコントラスト", + "default": "デフォルト" + }, + "help": "ヘルプ", + "documentation": { + "title": "ドキュメント", + "label": "Frigate ドキュメント" + }, + "restart": "Frigate を再起動", + "live": { + "title": "ライブ", + "allCameras": "全カメラ", + "cameras": { + "title": "カメラ", + "count_other": "{{count}} 台のカメラ" + } + }, + "review": "レビュー", + "explore": "ブラウズ", + "export": "書き出し", + "uiPlayground": "UI テスト環境", + "faceLibrary": "顔データベース", + "user": { + "title": "ユーザー", + "account": "アカウント", + "current": "現在のユーザー: {{user}}", + "anonymous": "未ログイン", + "logout": "ログアウト", + "setPassword": "パスワードを設定" + }, + "language": { + "en": "English (英語)", + "es": "Español (スペイン語)", + "zhCN": "简体中文 (簡体字中国語)", + "hi": "हिन्दी (ヒンディー語)", + "fr": "Français (フランス語)", + "ar": "العربية (アラビア語)", + "pt": "Português (ポルトガル語)", + "ptBR": "Português brasileiro (ブラジルポルトガル語)", + "ru": "Русский (ロシア語)", + "de": "Deutsch (ドイツ語)", + "ja": "日本語 (日本語)", + "tr": "Türkçe (トルコ語)", + "it": "Italiano (イタリア語)", + "nl": "Nederlands (オランダ語)", + "sv": "Svenska (スウェーデン語)", + "cs": "Čeština (チェコ語)", + "nb": "Norsk Bokmål (ノルウェー語)", + "ko": "한국어 (韓国語)", + "vi": "Tiếng Việt (ベトナム語)", + "fa": "فارسی (ペルシア語)", + "pl": "Polski (ポーランド語)", + "uk": "Українська (ウクライナ語)", + "he": "עברית (ヘブライ語)", + "el": "Ελληνικά (ギリシャ語)", + "ro": "Română (ルーマニア語)", + "hu": "Magyar (ハンガリー語)", + "fi": "Suomi (フィンランド語)", + "da": "Dansk (デンマーク語)", + "sk": "Slovenčina (スロバキア語)", + "yue": "粵語 (広東語)", + "th": "ไทย (タイ語)", + "ca": "Català (カタルーニャ語)", + "sr": "Српски (セルビア語)", + "sl": "Slovenščina (スロベニア語)", + "lt": "Lietuvių (リトアニア語)", + "bg": "Български (ブルガリア語)", + "gl": "Galego (ガリシア語)", + "id": "Bahasa Indonesia (インドネシア語)", + "ur": "اردو (ウルドゥー語)", + "withSystem": { + "label": "システム設定に従う" + } + } + }, + "toast": { + "copyUrlToClipboard": "URLをクリップボードにコピーしました。", + "save": { + "title": "保存", + "error": { + "title": "設定変更の保存に失敗しました: {{errorMessage}}", + "noMessage": "設定変更の保存に失敗しました" + } + } + }, + "role": { + "title": "役割", + "admin": "管理者", + "viewer": "閲覧者", + "desc": "管理者はFrigate UIのすべての機能に完全にアクセスできます。閲覧者はカメラ、レビュー項目、履歴映像の閲覧に制限されます。" + }, + "pagination": { + "label": "ページ移動", + "previous": { + "title": "前へ", + "label": "前のページへ" + }, + "next": { + "title": "次へ", + "label": "次のページへ" + }, + "more": "さらにページ" + }, + "accessDenied": { + "documentTitle": "アクセス拒否 - Frigate", + "title": "アクセス拒否", + "desc": "このページを表示する権限がありません。" + }, + "notFound": { + "documentTitle": "ページが見つかりません - Frigate", + "title": "404", + "desc": "ページが見つかりません" + }, + "selectItem": "{{item}} を選択", + "information": { + "pixels": "{{area}}ピクセル" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/auth.json new file mode 100644 index 0000000..b9ff983 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "ユーザー名", + "password": "パスワード", + "login": "ログイン", + "errors": { + "usernameRequired": "ユーザー名が必要です", + "passwordRequired": "パスワードが必要です", + "rateLimit": "リクエスト制限を超えました。後でもう一度お試しください。", + "loginFailed": "ログインに失敗しました", + "unknownError": "不明なエラー。ログを確認してください。", + "webUnknownError": "不明なエラー。コンソールログを確認してください。" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/camera.json new file mode 100644 index 0000000..4491d0a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "カメラグループ", + "add": "カメラグループを追加", + "edit": "カメラグループを編集", + "delete": { + "label": "カメラグループを削除", + "confirm": { + "title": "削除の確認", + "desc": "カメラグループ {{name}} を削除してもよろしいですか?" + } + }, + "name": { + "label": "名前", + "placeholder": "名前を入力…", + "errorMessage": { + "mustLeastCharacters": "カメラグループ名は2文字以上である必要があります。", + "exists": "このカメラグループ名は既に存在します。", + "nameMustNotPeriod": "カメラグループ名にピリオドは使用できません。", + "invalid": "無効なカメラグループ名です。" + } + }, + "cameras": { + "label": "カメラ", + "desc": "このグループに含めるカメラを選択します。" + }, + "icon": "アイコン", + "success": "カメラグループ({{name}})を保存しました。", + "camera": { + "birdseye": "バードアイ", + "setting": { + "label": "カメラのストリーミング設定", + "title": "{{cameraName}} のストリーミング設定", + "desc": "このカメラグループのダッシュボードでのライブストリーミングオプションを変更します。これらの設定はデバイス/ブラウザごとに異なります。", + "audioIsAvailable": "このストリームでは音声が利用可能です", + "audioIsUnavailable": "このストリームでは音声は利用できません", + "audio": { + "tips": { + "title": "このストリームで音声を使用するには、カメラから音声が出力され、go2rtc で設定されている必要があります。" + } + }, + "stream": "ストリーム", + "placeholder": "ストリームを選択", + "streamMethod": { + "label": "ストリーミング方式", + "placeholder": "方式を選択", + "method": { + "noStreaming": { + "label": "ストリーミングなし", + "desc": "カメラ画像は1分に1回のみ更新され、ライブストリーミングは行われません。" + }, + "smartStreaming": { + "label": "スマートストリーミング(推奨)", + "desc": "検知可能なアクティビティがない場合は、帯域とリソース節約のため画像を1分に1回更新します。アクティビティが検知されると、画像はシームレスにライブストリームへ切り替わります。" + }, + "continuousStreaming": { + "label": "常時ストリーミング", + "desc": { + "title": "ダッシュボードで表示されている間は、アクティビティが検知されていなくても常にライブストリームになります。", + "warning": "常時ストリーミングは高い帯域幅使用やパフォーマンス問題の原因となる場合があります。注意して使用してください。" + } + } + } + }, + "compatibilityMode": { + "label": "互換モード", + "desc": "このオプションは、ライブストリームに色のアーティファクトが表示され、画像右側に斜めの線が出る場合にのみ有効にしてください。" + } + } + } + }, + "debug": { + "options": { + "label": "設定", + "title": "オプション", + "showOptions": "オプションを表示", + "hideOptions": "オプションを非表示" + }, + "boundingBox": "バウンディングボックス", + "timestamp": "タイムスタンプ", + "zones": "ゾーン", + "mask": "マスク", + "motion": "モーション", + "regions": "領域" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/dialog.json new file mode 100644 index 0000000..2c5f5e0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/dialog.json @@ -0,0 +1,119 @@ +{ + "restart": { + "title": "Frigate を再起動してもよろしいですか?", + "restarting": { + "title": "Frigate を再起動中", + "content": "このページは {{countdown}} 秒後に再読み込みされます。", + "button": "今すぐ強制再読み込み" + }, + "button": "再起動" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Frigate+ に送信", + "desc": "回避したい場所でのオブジェクトは誤検出ではありません。誤検出として送信するとモデルが混乱します。" + }, + "review": { + "question": { + "label": "Frigate Plus 用ラベルの確認", + "ask_a": "このオブジェクトは {{label}} ですか?", + "ask_an": "このオブジェクトは {{label}} ですか?", + "ask_full": "このオブジェクトは {{untranslatedLabel}}({{translatedLabel}})ですか?" + }, + "state": { + "submitted": "送信済み" + } + } + }, + "video": { + "viewInHistory": "履歴で表示" + } + }, + "export": { + "time": { + "fromTimeline": "タイムラインから選択", + "lastHour_other": "直近{{count}}時間", + "custom": "カスタム", + "start": { + "title": "開始時刻", + "label": "開始時刻を選択" + }, + "end": { + "title": "終了時刻", + "label": "終了時刻を選択" + } + }, + "name": { + "placeholder": "書き出しに名前を付ける" + }, + "select": "選択", + "export": "書き出し", + "selectOrExport": "選択または書き出し", + "toast": { + "success": "書き出しを開始しました。/exports フォルダでファイルを確認できます。", + "error": { + "failed": "書き出しの開始に失敗しました: {{error}}", + "endTimeMustAfterStartTime": "終了時間は開始時間より後である必要があります", + "noVaildTimeSelected": "有効な時間範囲が選択されていません" + } + }, + "fromTimeline": { + "saveExport": "書き出しを保存", + "previewExport": "書き出しをプレビュー" + } + }, + "streaming": { + "label": "ストリーム", + "restreaming": { + "disabled": "このカメラではリストリーミングは有効になっていません。", + "desc": { + "title": "このカメラで追加のライブビューと音声を利用するには go2rtc をセットアップしてください。" + } + }, + "showStats": { + "label": "ストリーム統計を表示", + "desc": "有効にすると、カメラ映像に統計情報をオーバーレイ表示します。" + }, + "debugView": "デバッグビュー" + }, + "search": { + "saveSearch": { + "label": "検索を保存", + "desc": "この保存済み検索の名前を入力してください。", + "placeholder": "検索名を入力", + "overwrite": "{{searchName}} は既に存在します。保存すると上書きされます。", + "success": "検索({{searchName}})を保存しました。", + "button": { + "save": { + "label": "この検索を保存" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "削除の確認", + "desc": { + "selected": "このレビュー項目に関連付けられた録画動画をすべて削除してもよろしいですか?

    今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。" + }, + "toast": { + "success": "選択したレビュー項目に関連する動画を削除しました。", + "error": "削除に失敗しました: {{error}}" + } + }, + "button": { + "export": "書き出し", + "markAsReviewed": "レビュー済みにする", + "deleteNow": "今すぐ削除", + "markAsUnreviewed": "未レビューに戻す" + } + }, + "imagePicker": { + "selectImage": "追跡オブジェクトのサムネイルを選択", + "search": { + "placeholder": "ラベルまたはサブラベルで検索…" + }, + "noImages": "このカメラのサムネイルは見つかりません" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/filter.json new file mode 100644 index 0000000..66a52a2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/filter.json @@ -0,0 +1,136 @@ +{ + "labels": { + "label": "ラベル", + "all": { + "title": "すべてのラベル", + "short": "ラベル" + }, + "count_one": "{{count}} ラベル", + "count_other": "{{count}} ラベル" + }, + "filter": "フィルター", + "classes": { + "label": "クラス", + "all": { + "title": "すべてのクラス" + }, + "count_one": "{{count}} クラス", + "count_other": "{{count}} クラス" + }, + "zones": { + "label": "ゾーン", + "all": { + "title": "すべてのゾーン", + "short": "ゾーン" + } + }, + "dates": { + "selectPreset": "プリセットを選択…", + "all": { + "title": "すべての日付", + "short": "日付" + } + }, + "more": "その他のフィルター", + "reset": { + "label": "フィルターを既定値にリセット" + }, + "timeRange": "期間", + "subLabels": { + "label": "サブラベル", + "all": "すべてのサブラベル" + }, + "score": "スコア", + "estimatedSpeed": "推定速度({{unit}})", + "features": { + "label": "機能", + "hasSnapshot": "スナップショットあり", + "hasVideoClip": "ビデオクリップあり", + "submittedToFrigatePlus": { + "label": "Frigate+ に送信済み", + "tips": "まずスナップショットのある追跡オブジェクトでフィルターしてください。

    スナップショットのない追跡オブジェクトは Frigate+ に送信できません。" + } + }, + "sort": { + "label": "並び替え", + "dateAsc": "日付(昇順)", + "dateDesc": "日付(降順)", + "scoreAsc": "オブジェクトスコア(昇順)", + "scoreDesc": "オブジェクトスコア(降順)", + "speedAsc": "推定速度(昇順)", + "speedDesc": "推定速度(降順)", + "relevance": "関連度" + }, + "cameras": { + "label": "カメラフィルター", + "all": { + "title": "すべてのカメラ", + "short": "カメラ" + } + }, + "review": { + "showReviewed": "レビュー済みを表示" + }, + "motion": { + "showMotionOnly": "モーションのみ表示" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "title": "既定の表示", + "desc": "フィルター未選択時、ラベルごとの最新追跡オブジェクトの概要を表示するか、未フィルタのグリッドを表示するかを選びます。", + "summary": "概要", + "unfilteredGrid": "未フィルタグリッド" + }, + "gridColumns": { + "title": "グリッド列数", + "desc": "グリッド表示の列数を選択します。" + }, + "searchSource": { + "label": "検索対象", + "desc": "追跡オブジェクトのサムネイル画像と説明文のどちらを検索するかを選択します。", + "options": { + "thumbnailImage": "サムネイル画像", + "description": "説明" + } + } + }, + "date": { + "selectDateBy": { + "label": "フィルターする日付を選択" + } + } + }, + "logSettings": { + "label": "ログレベルでフィルター", + "filterBySeverity": "重大度でログをフィルター", + "loading": { + "title": "読み込み中", + "desc": "ログペインが最下部にあると、新しいログが追加され次第自動でストリーミング表示されます。" + }, + "disableLogStreaming": "ログのストリーミングを無効化", + "allLogs": "すべてのログ" + }, + "trackedObjectDelete": { + "title": "削除の確認", + "desc": "これら {{objectLength}} 件の追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、関連するオブジェクトのライフサイクル項目が削除されます。履歴ビューの録画映像は削除されません

    続行してもよろしいですか?

    今後このダイアログを表示しない場合は Shift キーを押しながら操作してください。", + "toast": { + "success": "追跡オブジェクトを削除しました。", + "error": "追跡オブジェクトの削除に失敗しました: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "ゾーンマスクでフィルター" + }, + "recognizedLicensePlates": { + "title": "認識されたナンバープレート", + "loadFailed": "認識済みナンバープレートの読み込みに失敗しました。", + "loading": "認識済みナンバープレートを読み込み中…", + "placeholder": "ナンバープレートを入力して検索…", + "noLicensePlatesFound": "ナンバープレートが見つかりません。", + "selectPlatesFromList": "リストから1件以上選択してください。", + "selectAll": "すべて選択", + "clearAll": "すべてクリア" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/icons.json new file mode 100644 index 0000000..2307ca4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "アイコンを検索…" + }, + "selectIcon": "アイコンを選択" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/input.json new file mode 100644 index 0000000..2272574 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "動画をダウンロード", + "toast": { + "success": "レビュー項目の動画のダウンロードを開始しました。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ja/components/player.json new file mode 100644 index 0000000..93befd9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "プレビューが見つかりません", + "noRecordingsFoundForThisTime": "この時間の録画は見つかりません", + "noPreviewFoundFor": "{{cameraName}} のプレビューが見つかりません", + "streamOffline": { + "title": "ストリームオフライン", + "desc": "{{cameraName}} の detect ストリームでフレームが受信されていません。エラーログを確認してください" + }, + "submitFrigatePlus": { + "title": "このフレームを Frigate+ に送信しますか?", + "submit": "送信" + }, + "livePlayerRequiredIOSVersion": "このライブストリームタイプには iOS 17.1 以上が必要です。", + "cameraDisabled": "カメラは無効です", + "stats": { + "streamType": { + "title": "ストリームタイプ:", + "short": "タイプ" + }, + "bandwidth": { + "title": "帯域:", + "short": "帯域" + }, + "latency": { + "title": "遅延:", + "value": "{{seconds}} 秒", + "short": { + "title": "遅延", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "総フレーム:", + "droppedFrames": { + "title": "ドロップしたフレーム:", + "short": { + "title": "ドロップ", + "value": "{{droppedFrames}} フレーム" + } + }, + "decodedFrames": "デコードしたフレーム:", + "droppedFrameRate": "ドロップしたフレームレート:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "フレームを Frigate+ に送信しました" + }, + "error": { + "submitFrigatePlusFailed": "フレームの Frigate+ への送信に失敗しました" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ja/objects.json new file mode 100644 index 0000000..c8b24e8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/objects.json @@ -0,0 +1,120 @@ +{ + "bicycle": "自転車", + "car": "車", + "person": "人", + "motorcycle": "オートバイ", + "airplane": "飛行機", + "animal": "動物", + "dog": "犬", + "bark": "樹皮", + "cat": "猫", + "horse": "馬", + "goat": "ヤギ", + "sheep": "羊", + "bird": "鳥", + "mouse": "マウス", + "keyboard": "キーボード", + "vehicle": "車両", + "boat": "ボート", + "bus": "バス", + "train": "電車", + "skateboard": "スケートボード", + "door": "ドア", + "blender": "ミキサー", + "sink": "流し台", + "hair_dryer": "ヘアドライヤー", + "toothbrush": "歯ブラシ", + "scissors": "はさみ", + "clock": "時計", + "traffic_light": "信号機", + "fire_hydrant": "消火栓", + "street_sign": "道路標識", + "stop_sign": "一時停止標識", + "parking_meter": "駐車メーター", + "bench": "ベンチ", + "cow": "牛", + "elephant": "象", + "bear": "クマ", + "zebra": "シマウマ", + "giraffe": "キリン", + "hat": "帽子", + "backpack": "バックパック", + "umbrella": "傘", + "shoe": "靴", + "eye_glasses": "メガネ", + "handbag": "ハンドバッグ", + "tie": "ネクタイ", + "suitcase": "スーツケース", + "frisbee": "フリスビー", + "skis": "スキー板", + "snowboard": "スノーボード", + "sports_ball": "スポーツボール", + "kite": "凧", + "baseball_bat": "野球バット", + "baseball_glove": "野球グローブ", + "surfboard": "サーフボード", + "tennis_racket": "テニスラケット", + "bottle": "ボトル", + "plate": "皿", + "wine_glass": "ワイングラス", + "cup": "コップ", + "fork": "フォーク", + "knife": "ナイフ", + "spoon": "スプーン", + "bowl": "ボウル", + "banana": "バナナ", + "apple": "リンゴ", + "sandwich": "サンドイッチ", + "orange": "オレンジ", + "broccoli": "ブロッコリー", + "carrot": "ニンジン", + "hot_dog": "ホットドッグ", + "pizza": "ピザ", + "donut": "ドーナツ", + "cake": "ケーキ", + "chair": "椅子", + "couch": "ソファ", + "potted_plant": "鉢植え", + "bed": "ベッド", + "mirror": "鏡", + "dining_table": "ダイニングテーブル", + "window": "窓", + "desk": "机", + "toilet": "トイレ", + "tv": "テレビ", + "laptop": "ノートパソコン", + "remote": "リモコン", + "cell_phone": "携帯電話", + "microwave": "電子レンジ", + "oven": "オーブン", + "toaster": "トースター", + "refrigerator": "冷蔵庫", + "book": "本", + "vase": "花瓶", + "teddy_bear": "テディベア", + "hair_brush": "ヘアブラシ", + "squirrel": "リス", + "deer": "シカ", + "fox": "キツネ", + "rabbit": "ウサギ", + "raccoon": "アライグマ", + "robot_lawnmower": "ロボット芝刈り機", + "waste_bin": "ゴミ箱", + "on_demand": "オンデマンド", + "face": "顔", + "license_plate": "ナンバープレート", + "package": "荷物", + "bbq_grill": "バーベキューグリル", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/classificationModel.json new file mode 100644 index 0000000..54710f9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/classificationModel.json @@ -0,0 +1,14 @@ +{ + "documentTitle": "分類モデル", + "button": { + "deleteImages": "画像を削除" + }, + "toast": { + "success": { + "deletedImage": "削除された画像", + "categorizedImage": "画像の分類に成功しました", + "trainedModel": "モデルを正常に学習させました。", + "trainingModel": "モデルのトレーニングを正常に開始しました。" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/configEditor.json new file mode 100644 index 0000000..704c83d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "設定をコピー", + "configEditor": "設定エディタ", + "saveAndRestart": "保存後再起動", + "saveOnly": "保存", + "confirm": "保存せずに終了しますか?", + "documentTitle": "設定エディタ - Frigate", + "safeConfigEditor": "設定エディタ (セーフモード)", + "safeModeDescription": "Frigate は config の検証エラーによるセーフモードです.", + "toast": { + "success": { + "copyToClipboard": "コンフィグをクリップボードにコピー。" + }, + "error": { + "savingError": "設定の保存に失敗しました" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/events.json new file mode 100644 index 0000000..b19ad95 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/events.json @@ -0,0 +1,40 @@ +{ + "detections": "検出", + "motion": { + "label": "モーション", + "only": "モーションのみ" + }, + "alerts": "アラート", + "empty": { + "detection": "レビューする検出はありません", + "alert": "レビューするアラートはありません", + "motion": "モーションデータは見つかりません" + }, + "camera": "カメラ", + "allCameras": "全カメラ", + "timeline": "タイムライン", + "timeline.aria": "タイムラインを選択", + "events": { + "label": "イベント", + "aria": "イベントを選択", + "noFoundForTimePeriod": "この期間のイベントは見つかりません。" + }, + "documentTitle": "レビュー - Frigate", + "recordings": { + "documentTitle": "録画 - Frigate" + }, + "calendarFilter": { + "last24Hours": "直近24時間" + }, + "markAsReviewed": "レビュー済みにする", + "markTheseItemsAsReviewed": "これらの項目をレビュー済みにする", + "newReviewItems": { + "label": "新しいレビュー項目を表示", + "button": "レビューすべき新規項目" + }, + "selected_one": "{{count}} 件選択", + "selected_other": "{{count}} 件選択", + "detected": "検出", + "suspiciousActivity": "不審なアクティビティ", + "threateningActivity": "脅威となるアクティビティ" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/explore.json new file mode 100644 index 0000000..3e782f9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/explore.json @@ -0,0 +1,222 @@ +{ + "generativeAI": "生成AI", + "documentTitle": "探索 - Frigate", + "details": { + "timestamp": "タイムスタンプ", + "item": { + "title": "レビュー項目の詳細", + "desc": "レビュー項目の詳細", + "button": { + "share": "このレビュー項目を共有", + "viewInExplore": "探索で表示" + }, + "tips": { + "mismatch_other": "利用不可のオブジェクトが {{count}} 件、このレビュー項目に含まれています。これらはアラートまたは検出の条件を満たしていないか、既にクリーンアップ/削除されています。", + "hasMissingObjects": "次のラベルの追跡オブジェクトを保存したい場合は設定を調整してください: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "{{provider}} に新しい説明をリクエストしました。プロバイダの速度により再生成に時間がかかる場合があります。", + "updatedSublabel": "サブラベルを更新しました。", + "updatedLPR": "ナンバープレートを更新しました。", + "audioTranscription": "音声文字起こしをリクエストしました。" + }, + "error": { + "regenerate": "{{provider}} への新しい説明の呼び出しに失敗しました: {{errorMessage}}", + "updatedSublabelFailed": "サブラベルの更新に失敗しました: {{errorMessage}}", + "updatedLPRFailed": "ナンバープレートの更新に失敗しました: {{errorMessage}}", + "audioTranscription": "音声文字起こしのリクエストに失敗しました: {{errorMessage}}" + } + } + }, + "label": "ラベル", + "editSubLabel": { + "title": "サブラベルを編集", + "desc": "この {{label}} の新しいサブラベルを入力", + "descNoLabel": "この追跡オブジェクトの新しいサブラベルを入力" + }, + "editLPR": { + "title": "ナンバープレートを編集", + "desc": "この {{label}} の新しいナンバープレート値を入力", + "descNoLabel": "この追跡オブジェクトの新しいナンバープレート値を入力" + }, + "snapshotScore": { + "label": "スナップショットスコア" + }, + "topScore": { + "label": "トップスコア", + "info": "トップスコアは追跡オブジェクトの最高中央値スコアであり、検索結果のサムネイルに表示されるスコアとは異なる場合があります。" + }, + "score": { + "label": "スコア" + }, + "recognizedLicensePlate": "認識されたナンバープレート", + "estimatedSpeed": "推定速度", + "objects": "オブジェクト", + "camera": "カメラ", + "zones": "ゾーン", + "button": { + "findSimilar": "類似を検索", + "regenerate": { + "title": "再生成", + "label": "追跡オブジェクトの説明を再生成" + } + }, + "description": { + "label": "説明", + "placeholder": "追跡オブジェクトの説明", + "aiTips": "追跡オブジェクトのライフサイクルが終了するまで、生成AIプロバイダに説明はリクエストされません。" + }, + "expandRegenerationMenu": "再生成メニューを展開", + "regenerateFromSnapshot": "スナップショットから再生成", + "regenerateFromThumbnails": "サムネイルから再生成", + "tips": { + "descriptionSaved": "説明を保存しました", + "saveDescriptionFailed": "説明の更新に失敗しました: {{errorMessage}}" + } + }, + "exploreMore": "{{label}} のオブジェクトをさらに探索", + "exploreIsUnavailable": { + "title": "探索は利用できません", + "embeddingsReindexing": { + "context": "追跡オブジェクトの埋め込みの再インデックスが完了すると「探索」を使用できます。", + "startingUp": "起動中…", + "estimatedTime": "残りの推定時間:", + "finishingShortly": "まもなく完了", + "step": { + "thumbnailsEmbedded": "埋め込み済みサムネイル: ", + "descriptionsEmbedded": "埋め込み済み説明: ", + "trackedObjectsProcessed": "処理済み追跡オブジェクト: " + } + }, + "downloadingModels": { + "context": "Frigate はセマンティック検索(意味理解型画像検索)をサポートするために必要な埋め込みモデルをダウンロードしています。ネットワーク速度により数分かかる場合があります。", + "setup": { + "visionModel": "ビジョンモデル", + "visionModelFeatureExtractor": "ビジョンモデル特徴抽出器", + "textModel": "テキストモデル", + "textTokenizer": "テキストトークナイザー" + }, + "tips": { + "context": "モデルのダウンロード後、追跡オブジェクトの埋め込みを再インデックスすることを検討してください。" + }, + "error": "エラーが発生しました。Frigate のログを確認してください。" + } + }, + "trackedObjectDetails": "追跡オブジェクトの詳細", + "type": { + "details": "詳細", + "snapshot": "スナップショット", + "video": "動画", + "object_lifecycle": "オブジェクトのライフサイクル" + }, + "objectLifecycle": { + "title": "オブジェクトのライフサイクル", + "noImageFound": "このタイムスタンプの画像は見つかりません。", + "createObjectMask": "オブジェクトマスクを作成", + "adjustAnnotationSettings": "アノテーション設定を調整", + "scrollViewTips": "スクロールしてこのオブジェクトのライフサイクルの重要な瞬間を表示します。", + "autoTrackingTips": "オートトラッキングカメラではバウンディングボックスの位置が正確でない場合があります。", + "count": "{{first}} / {{second}}", + "trackedPoint": "追跡ポイント", + "lifecycleItemDesc": { + "visible": "{{label}} を検出", + "entered_zone": "{{label}} が {{zones}} に進入", + "active": "{{label}} がアクティブになりました", + "stationary": "{{label}} が静止しました", + "attribute": { + "faceOrLicense_plate": "{{label}} の {{attribute}} を検出", + "other": "{{label}} を {{attribute}} として認識" + }, + "gone": "{{label}} が離脱", + "heard": "{{label}} を検知(音声)", + "external": "{{label}} を検出", + "header": { + "zones": "ゾーン", + "ratio": "比率", + "area": "面積" + } + }, + "annotationSettings": { + "title": "アノテーション設定", + "showAllZones": { + "title": "すべてのゾーンを表示", + "desc": "オブジェクトがゾーンに入ったフレームでは常にゾーンを表示します。" + }, + "offset": { + "label": "アノテーションオフセット", + "desc": "このデータはカメラの detect フィードから来ていますが、record フィードの画像に重ねて表示されます。2つのストリームが完全に同期していない可能性があるため、バウンディングボックスと映像が完全には一致しないことがあります。annotation_offset フィールドで調整できます。", + "millisecondsToOffset": "detect のアノテーションをオフセットするミリ秒数。既定: 0", + "tips": "ヒント: 左から右へ歩く人物のイベントクリップを想像してください。タイムラインのバウンディングボックスが人物より常に左側にあるなら値を小さく、常に先行しているなら値を大きくします。", + "toast": { + "success": "{{camera}} のアノテーションオフセットを設定ファイルに保存しました。変更を適用するには Frigate を再起動してください。" + } + } + }, + "carousel": { + "previous": "前のスライド", + "next": "次のスライド" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "動画をダウンロード", + "aria": "動画をダウンロード" + }, + "downloadSnapshot": { + "label": "スナップショットをダウンロード", + "aria": "スナップショットをダウンロード" + }, + "viewObjectLifecycle": { + "label": "オブジェクトのライフサイクルを表示", + "aria": "オブジェクトのライフサイクルを表示" + }, + "findSimilar": { + "label": "類似を検索", + "aria": "類似する追跡オブジェクトを検索" + }, + "addTrigger": { + "label": "トリガーを追加", + "aria": "この追跡オブジェクトのトリガーを追加" + }, + "audioTranscription": { + "label": "文字起こし", + "aria": "音声文字起こしをリクエスト" + }, + "submitToPlus": { + "label": "Frigate+ に送信", + "aria": "Frigate Plus に送信" + }, + "viewInHistory": { + "label": "履歴で表示", + "aria": "履歴で表示" + }, + "deleteTrackedObject": { + "label": "この追跡オブジェクトを削除" + } + }, + "dialog": { + "confirmDelete": { + "title": "削除の確認", + "desc": "この追跡オブジェクトを削除すると、スナップショット、保存された埋め込み、および関連するライフサイクル項目が削除されます。履歴ビューの録画映像は削除されません

    続行してもよろしいですか?" + } + }, + "noTrackedObjects": "追跡オブジェクトは見つかりませんでした", + "fetchingTrackedObjectsFailed": "追跡オブジェクトの取得エラー: {{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 件の追跡オブジェクト ", + "searchResult": { + "tooltip": "{{type}} と一致({{confidence}}%)", + "deleteTrackedObject": { + "toast": { + "success": "追跡オブジェクトを削除しました。", + "error": "追跡オブジェクトの削除に失敗しました: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "AI 解析" + }, + "concerns": { + "label": "懸念" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/exports.json new file mode 100644 index 0000000..b5107f4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "書き出し - Frigate", + "noExports": "書き出しは見つかりません", + "search": "検索", + "deleteExport": "書き出しを削除", + "deleteExport.desc": "{{exportName}} を削除してもよろしいですか?", + "editExport": { + "title": "書き出し名を変更", + "desc": "この書き出しの新しい名前を入力してください。", + "saveExport": "書き出しを保存" + }, + "toast": { + "error": { + "renameExportFailed": "書き出し名の変更に失敗しました: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/faceLibrary.json new file mode 100644 index 0000000..f82b4e7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/faceLibrary.json @@ -0,0 +1,95 @@ +{ + "description": { + "placeholder": "このコレクションの名前を入力", + "addFace": "最初の画像をアップロードして、フェイスライブラリに新しいコレクションを追加してください。", + "invalidName": "無効な名前です。名前に使用できるのは英数字、スペース、アポストロフィ、アンダースコア、ハイフンのみです。" + }, + "details": { + "person": "人物", + "face": "顔の詳細", + "timestamp": "タイムスタンプ", + "unknown": "不明", + "subLabelScore": "サブラベルスコア", + "scoreInfo": "サブラベルスコアは、認識された顔の信頼度の加重スコアです。スナップショットに表示されるスコアとは異なる場合があります。", + "faceDesc": "この顔を生成した追跡オブジェクトの詳細" + }, + "documentTitle": "顔データベース - Frigate", + "uploadFaceImage": { + "title": "顔画像をアップロード", + "desc": "顔を検出するために画像をアップロードし、{{pageToggle}} に追加します" + }, + "collections": "コレクション", + "createFaceLibrary": { + "title": "コレクションを作成", + "desc": "新しいコレクションを作成", + "new": "新しい顔を作成", + "nextSteps": "強固な基盤を作るために:
  • [学習]タブで各人物に対して画像を選択し学習させてください。
  • 最良の結果のため、正面を向いた画像に集中し、斜めからの顔画像は学習に使わないでください。
  • " + }, + "selectItem": "{{item}} を選択", + "steps": { + "faceName": "顔の名前を入力", + "uploadFace": "顔画像をアップロード", + "nextSteps": "次のステップ", + "description": { + "uploadFace": "{{name}} の正面を向いた顔が写っている画像をアップロードしてください。顔部分だけにトリミングする必要はありません。" + } + }, + "train": { + "title": "学習", + "aria": "学習を選択", + "empty": "最近の顔認識の試行はありません" + }, + "selectFace": "顔を選択", + "deleteFaceLibrary": { + "title": "名前を削除", + "desc": "コレクション {{name}} を削除してもよろしいですか?関連する顔はすべて完全に削除されます。" + }, + "deleteFaceAttempts": { + "title": "顔を削除", + "desc_other": "{{count}} 件の顔を削除してもよろしいですか?この操作は元に戻せません。" + }, + "renameFace": { + "title": "顔の名前を変更", + "desc": "{{name}} の新しい名前を入力" + }, + "button": { + "deleteFaceAttempts": "顔を削除", + "addFace": "顔を追加", + "renameFace": "顔の名前を変更", + "deleteFace": "顔を削除", + "uploadImage": "画像をアップロード", + "reprocessFace": "顔を再処理" + }, + "imageEntry": { + "validation": { + "selectImage": "画像ファイルを選択してください。" + }, + "dropActive": "ここに画像をドロップ…", + "dropInstructions": "画像をここにドラッグ&ドロップ、ペースト、またはクリックして選択", + "maxSize": "最大サイズ: {{size}}MB" + }, + "nofaces": "顔はありません", + "pixels": "{{area}}px", + "trainFaceAs": "顔を次として学習:", + "trainFace": "顔を学習", + "toast": { + "success": { + "uploadedImage": "画像をアップロードしました。", + "addFaceLibrary": "{{name}} を顔データベースに追加しました!", + "deletedFace_other": "{{count}} 件の顔を削除しました。", + "deletedName_other": "{{count}} 件の顔を削除しました。", + "renamedFace": "顔の名前を {{name}} に変更しました", + "trainedFace": "顔の学習が完了しました。", + "updatedFaceScore": "顔のスコアを更新しました。" + }, + "error": { + "uploadingImageFailed": "画像のアップロードに失敗しました: {{errorMessage}}", + "addFaceLibraryFailed": "顔名の設定に失敗しました: {{errorMessage}}", + "deleteFaceFailed": "削除に失敗しました: {{errorMessage}}", + "deleteNameFailed": "名前の削除に失敗しました: {{errorMessage}}", + "renameFaceFailed": "顔の名前変更に失敗しました: {{errorMessage}}", + "trainFailed": "学習に失敗しました: {{errorMessage}}", + "updateFaceScoreFailed": "顔スコアの更新に失敗しました: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/live.json new file mode 100644 index 0000000..cfcd573 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/live.json @@ -0,0 +1,183 @@ +{ + "documentTitle": "ライブ - Frigate", + "documentTitle.withCamera": "{{camera}} - ライブ - Frigate", + "lowBandwidthMode": "低帯域モード", + "twoWayTalk": { + "enable": "双方向通話を有効化", + "disable": "双方向通話を無効化" + }, + "cameraAudio": { + "enable": "カメラ音声を有効化", + "disable": "カメラ音声を無効化" + }, + "ptz": { + "move": { + "clickMove": { + "label": "フレーム内をクリックしてカメラを中央に移動", + "enable": "クリック移動を有効化", + "disable": "クリック移動を無効化" + }, + "left": { + "label": "PTZ カメラを左へ移動" + }, + "up": { + "label": "PTZ カメラを上へ移動" + }, + "down": { + "label": "PTZ カメラを下へ移動" + }, + "right": { + "label": "PTZ カメラを右へ移動" + } + }, + "zoom": { + "in": { + "label": "PTZ カメラをズームイン" + }, + "out": { + "label": "PTZ カメラをズームアウト" + } + }, + "focus": { + "in": { + "label": "PTZ カメラをフォーカスイン" + }, + "out": { + "label": "PTZ カメラをフォーカスアウト" + } + }, + "frame": { + "center": { + "label": "フレーム内をクリックして PTZ カメラを中央へ" + } + }, + "presets": "PTZ カメラのプリセット" + }, + "camera": { + "enable": "カメラを有効化", + "disable": "カメラを無効化" + }, + "muteCameras": { + "enable": "全カメラをミュート", + "disable": "全カメラのミュートを解除" + }, + "detect": { + "enable": "検出を有効化", + "disable": "検出を無効化" + }, + "recording": { + "enable": "録画を有効化", + "disable": "録画を無効化" + }, + "snapshots": { + "enable": "スナップショットを有効化", + "disable": "スナップショットを無効化" + }, + "audioDetect": { + "enable": "音声検出を有効化", + "disable": "音声検出を無効化" + }, + "transcription": { + "enable": "ライブ音声文字起こしを有効化", + "disable": "ライブ音声文字起こしを無効化" + }, + "autotracking": { + "enable": "オートトラッキングを有効化", + "disable": "オートトラッキングを無効化" + }, + "streamStats": { + "enable": "ストリーム統計を表示", + "disable": "ストリーム統計を非表示" + }, + "manualRecording": { + "title": "オンデマンド録画", + "tips": "このカメラの録画保持設定に基づいて、即時スナップショットをダウンロードするか、手動イベントを開始してください。", + "playInBackground": { + "label": "バックグラウンドで再生", + "desc": "プレーヤーが非表示の場合でもストリーミングを継続するにはこのオプションを有効にします。" + }, + "showStats": { + "label": "統計を表示", + "desc": "カメラ映像にストリーム統計をオーバーレイ表示するにはこのオプションを有効にします。" + }, + "debugView": "デバッグビュー", + "start": "オンデマンド録画を開始", + "started": "手動のオンデマンド録画を開始しました。", + "failedToStart": "手動のオンデマンド録画の開始に失敗しました。", + "recordDisabledTips": "このカメラは設定で録画が無効または制限されているため、スナップショットのみ保存されます。", + "end": "オンデマンド録画を終了", + "ended": "手動のオンデマンド録画を終了しました。", + "failedToEnd": "手動のオンデマンド録画の終了に失敗しました。" + }, + "streamingSettings": "ストリーミング設定", + "notifications": "通知", + "audio": "音声", + "suspend": { + "forTime": "一時停止: " + }, + "stream": { + "title": "ストリーム", + "audio": { + "tips": { + "title": "このストリームで音声を使用するには、カメラから音声が出力され、go2rtc で設定されている必要があります。" + }, + "available": "このストリームでは音声を利用できます", + "unavailable": "このストリームでは音声は利用できません" + }, + "twoWayTalk": { + "tips": "端末が機能をサポートし、双方向通話に WebRTC が設定されている必要があります。", + "available": "このストリームで双方向通話を利用できます", + "unavailable": "このストリームで双方向通話は利用できません" + }, + "lowBandwidth": { + "tips": "バッファリングやストリームエラーのため、ライブビューは低帯域モードになっています。", + "resetStream": "ストリームをリセット" + }, + "playInBackground": { + "label": "バックグラウンドで再生", + "tips": "プレーヤーが非表示でもストリーミングを継続するにはこのオプションを有効にします。" + }, + "debug": { + "picker": "デバッグモードではストリームの選択はできません。デバッグビューは常に 検出ロールに割り当てられたストリームを使用します。" + } + }, + "cameraSettings": { + "title": "{{camera}} の設定", + "cameraEnabled": "カメラ有効", + "objectDetection": "物体検出", + "recording": "録画", + "snapshots": "スナップショット", + "audioDetection": "音声検出", + "transcription": "音声文字起こし", + "autotracking": "オートトラッキング" + }, + "history": { + "label": "履歴映像を表示" + }, + "effectiveRetainMode": { + "modes": { + "all": "すべて", + "motion": "モーション", + "active_objects": "アクティブなオブジェクト" + }, + "notAllTips": "{{source}} の録画保持設定は mode: {{effectiveRetainMode}} になっているため、このオンデマンド録画では {{effectiveRetainModeName}} を含むセグメントのみが保持されます。" + }, + "editLayout": { + "label": "レイアウトを編集", + "group": { + "label": "カメラグループを編集" + }, + "exitEdit": "編集を終了" + }, + "noCameras": { + "title": "カメラが設定されていません", + "buttonText": "カメラを追加", + "description": "開始するには、カメラを接続してください。" + }, + "snapshot": { + "takeSnapshot": "即時スナップショットをダウンロード", + "noVideoSource": "スナップショットに使用できる映像ソースがありません。", + "captureFailed": "スナップショットの取得に失敗しました。", + "downloadStarted": "スナップショットのダウンロードを開始しました。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/recording.json new file mode 100644 index 0000000..7d76d19 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "フィルター", + "calendar": "カレンダー", + "export": "書き出し", + "filters": "フィルター", + "toast": { + "error": { + "noValidTimeSelected": "適切な時刻の範囲が選択されていません", + "endTimeMustAfterStartTime": "終了時刻は開始時刻より後である必要があります" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/search.json new file mode 100644 index 0000000..d5be5ed --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/search.json @@ -0,0 +1,72 @@ +{ + "searchFor": "「{{inputValue}}」を検索", + "button": { + "save": "検索を保存", + "delete": "保存済み検索を削除", + "filterInformation": "フィルター情報", + "clear": "検索をクリア", + "filterActive": "フィルターが有効" + }, + "search": "検索", + "savedSearches": "保存済み検索", + "trackedObjectId": "追跡オブジェクトID", + "filter": { + "label": { + "cameras": "カメラ", + "labels": "ラベル", + "zones": "ゾーン", + "sub_labels": "サブラベル", + "search_type": "検索タイプ", + "time_range": "期間", + "before": "以前", + "after": "以後", + "min_score": "最小スコア", + "max_score": "最大スコア", + "min_speed": "最小速度", + "max_speed": "最大速度", + "recognized_license_plate": "認識されたナンバープレート", + "has_clip": "クリップあり", + "has_snapshot": "スナップショットあり" + }, + "searchType": { + "thumbnail": "サムネイル", + "description": "説明" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「以前」日付は「以後」日付より後である必要があります。", + "afterDatebeEarlierBefore": "「以後」日付は「以前」日付より前である必要があります。", + "minScoreMustBeLessOrEqualMaxScore": "「最小スコア」は「最大スコア」以下である必要があります。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最大スコア」は「最小スコア」以上である必要があります。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最小速度」は「最大速度」以下である必要があります。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最大速度」は「最小速度」以上である必要があります。" + } + }, + "tips": { + "title": "テキストフィルターの使い方", + "desc": { + "text": "フィルターを使うと検索結果を絞り込めます。入力欄での使い方は次の通りです。", + "step1": "フィルターのキー名の後にコロンを付けて入力します(例: \"cameras:\")。", + "step2": "候補から値を選ぶか、自分で入力します。", + "step3": "複数のフィルターは、間にスペースを入れて続けて追加できます。", + "step4": "日付フィルター(before: と after:)は {{DateFormat}} 形式を使用します。", + "step5": "期間フィルターは {{exampleTime}} 形式を使用します。", + "step6": "フィルターは隣の 'x' をクリックして削除できます。", + "exampleLabel": "例:" + } + }, + "header": { + "currentFilterType": "フィルター値", + "noFilters": "フィルター", + "activeFilters": "有効なフィルター" + } + }, + "similaritySearch": { + "title": "類似検索", + "active": "類似検索を実行中", + "clear": "類似検索をクリア" + }, + "placeholder": { + "search": "検索…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/settings.json new file mode 100644 index 0000000..000aac8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/settings.json @@ -0,0 +1,1043 @@ +{ + "documentTitle": { + "authentication": "認証設定 - Frigate", + "camera": "カメラ設定 - Frigate", + "default": "設定 - Frigate", + "enrichments": "高度解析設定 - Frigate", + "masksAndZones": "マスク/ゾーンエディタ - Frigate", + "motionTuner": "モーションチューナー - Frigate", + "object": "デバッグ - Frigate", + "general": "一般設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "cameraManagement": "カメラ設定 - Frigate", + "cameraReview": "カメラレビュー設定 - Frigate" + }, + "menu": { + "ui": "UI", + "enrichments": "高度解析", + "cameras": "カメラ設定", + "masksAndZones": "マスク/ゾーン", + "motionTuner": "モーションチューナー", + "triggers": "トリガー", + "debug": "デバッグ", + "users": "ユーザー", + "notifications": "通知", + "frigateplus": "Frigate+", + "cameraManagement": "管理", + "cameraReview": "レビュー", + "roles": "区分" + }, + "dialog": { + "unsavedChanges": { + "title": "未保存の変更があります。", + "desc": "続行する前に変更を保存しますか?" + } + }, + "cameraSetting": { + "camera": "カメラ", + "noCamera": "カメラなし" + }, + "general": { + "title": "一般設定", + "liveDashboard": { + "title": "ライブダッシュボード", + "automaticLiveView": { + "label": "自動ライブビュー", + "desc": "アクティビティ検知時に自動でそのカメラのライブビューへ切り替えます。無効にすると、ライブダッシュボード上の静止画像は1分に1回のみ更新されます。" + }, + "playAlertVideos": { + "label": "アラート動画を再生", + "desc": "既定では、ライブダッシュボードの最近のアラートは小さなループ動画として再生されます。無効にすると、最近のアラートはこのデバイス/ブラウザでは静止画像のみ表示されます。" + } + }, + "storedLayouts": { + "title": "保存済みレイアウト", + "desc": "カメラグループ内のレイアウトはドラッグ/リサイズできます。位置情報はブラウザのローカルストレージに保存されます。", + "clearAll": "すべてのレイアウトをクリア" + }, + "cameraGroupStreaming": { + "title": "カメラグループのストリーミング設定", + "desc": "各カメラグループのストリーミング設定はブラウザのローカルストレージに保存されます。", + "clearAll": "すべてのストリーミング設定をクリア" + }, + "recordingsViewer": { + "title": "録画ビューア", + "defaultPlaybackRate": { + "label": "既定の再生速度", + "desc": "録画再生の既定の再生速度です。" + } + }, + "calendar": { + "title": "カレンダー", + "firstWeekday": { + "label": "週の開始曜日", + "desc": "レビューカレンダーで週が始まる曜日。", + "sunday": "日曜日", + "monday": "月曜日" + } + }, + "toast": { + "success": { + "clearStoredLayout": "{{cameraName}} の保存済みレイアウトをクリアしました", + "clearStreamingSettings": "すべてのカメラグループのストリーミング設定をクリアしました。" + }, + "error": { + "clearStoredLayoutFailed": "保存済みレイアウトのクリアに失敗しました: {{errorMessage}}", + "clearStreamingSettingsFailed": "ストリーミング設定のクリアに失敗しました: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "高度解析設定", + "unsavedChanges": "未保存の高度解析設定の変更", + "birdClassification": { + "title": "鳥類分類", + "desc": "量子化された TensorFlow モデルを使って既知の鳥を識別します。既知の鳥を認識した場合、その一般名を sub_label として追加します。この情報は UI、フィルタ、通知に含まれます。" + }, + "semanticSearch": { + "title": "セマンティック検索", + "desc": "Frigate のセマンティック検索では、画像そのもの、ユーザー定義のテキスト説明、または自動生成された説明を用いて、レビュー項目内の追跡オブジェクトを検索できます。", + "reindexNow": { + "label": "今すぐ再インデックス", + "desc": "再インデックスは、すべての追跡オブジェクトの埋め込みを再生成します。バックグラウンドで実行され、追跡オブジェクト数によっては CPU を使い切り、相応の時間がかかる場合があります。", + "confirmTitle": "再インデックスの確認", + "confirmDesc": "すべての追跡オブジェクトの埋め込みを再インデックスしますか?この処理はバックグラウンドで実行されますが、CPU を使い切り、時間がかかる場合があります。進行状況は[探索]ページで確認できます。", + "confirmButton": "再インデックス", + "success": "再インデックスを開始しました。", + "alreadyInProgress": "再インデックスはすでに進行中です。", + "error": "再インデックスの開始に失敗しました: {{errorMessage}}" + }, + "modelSize": { + "label": "モデルサイズ", + "desc": "セマンティック検索の埋め込みに使用するモデルのサイズです。", + "small": { + "title": "スモール", + "desc": "small を使用すると、量子化モデルにより RAM 使用量が少なく、CPU 上で高速に動作します。埋め込み品質の差はごく僅かです。" + }, + "large": { + "title": "ラージ", + "desc": "large を使用すると、完全な Jina モデルを用い、可能であれば自動的に GPU で動作します。" + } + } + }, + "faceRecognition": { + "title": "顔認識", + "desc": "顔認識により、人に名前を割り当て、顔を認識した際にその人名をサブラベルとして付与します。この情報は UI、フィルタ、通知に含まれます。", + "modelSize": { + "label": "モデルサイズ", + "desc": "顔認識に使用するモデルのサイズです。", + "small": { + "title": "スモール", + "desc": "small は FaceNet ベースの顔埋め込みモデルを使用し、多くの CPU で効率よく動作します。" + }, + "large": { + "title": "ラージ", + "desc": "large は ArcFace ベースの顔埋め込みモデルを使用し、可能であれば自動的に GPU で動作します。" + } + } + }, + "licensePlateRecognition": { + "title": "ナンバープレート認識", + "desc": "車両のナンバープレートを認識し、検出文字列を recognized_license_plate フィールドへ、または既知の名称を car タイプのオブジェクトの sub_label として自動追加できます。一般的な用途として、私道に入ってくる車や道路を通過する車のナンバー読み取りがあります。" + }, + "restart_required": "再起動が必要です(高度解析設定を変更)", + "toast": { + "success": "高度解析設定を保存しました。変更を適用するには Frigate を再起動してください。", + "error": "設定変更の保存に失敗しました: {{errorMessage}}" + } + }, + "camera": { + "title": "カメラ設定", + "streams": { + "title": "ストリーム", + "desc": "Frigate の再起動まで、カメラを一時的に無効化します。無効化すると、このカメラのストリーム処理は完全に停止します。検出、録画、デバッグは利用できません。
    注: これは go2rtc のリストリームは無効化しません。" + }, + "object_descriptions": { + "title": "生成 AI オブジェクト説明", + "desc": "このカメラの生成 AI によるオブジェクト説明を一時的に有効/無効にします。無効にすると、追跡オブジェクトに対して説明はリクエストされません。" + }, + "review_descriptions": { + "title": "生成 AI レビュー説明", + "desc": "このカメラの生成 AI によるレビュー説明を一時的に有効/無効にします。無効にすると、レビュー項目に対して説明はリクエストされません。" + }, + "review": { + "title": "レビュー", + "desc": "Frigate の再起動まで、このカメラのアラートと検出を一時的に有効/無効にします。無効時は新しいレビュー項目は生成されません。 ", + "alerts": "アラート ", + "detections": "検出 " + }, + "reviewClassification": { + "title": "レビュー分類", + "desc": "Frigate はレビュー項目をアラートと検出に分類します。既定では personcar はアラートです。必要ゾーンを設定することで分類を細かく調整できます。", + "noDefinedZones": "このカメラにはゾーンが定義されていません。", + "objectAlertsTips": "{{cameraName}} 上の {{alertsLabels}} はすべてアラートとして表示されます。", + "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検出された {{alertsLabels}} はすべてアラートとして表示されます。", + "objectDetectionsTips": "{{cameraName}} で未分類の {{detectionsLabels}} は、ゾーンに関わらず検出として表示されます。", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} の {{zone}} で未分類の {{detectionsLabels}} は検出として表示されます。", + "notSelectDetections": "{{cameraName}} の {{zone}} で検出された {{detectionsLabels}} のうちアラートに分類されないものは、ゾーンに関わらず検出として表示されます。", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で未分類の {{detectionsLabels}} は、ゾーンに関わらず検出として表示されます。" + }, + "unsavedChanges": "{{camera}} のレビュー分類設定に未保存の変更があります", + "selectAlertsZones": "アラートのゾーンを選択", + "selectDetectionsZones": "検出のゾーンを選択", + "limitDetections": "検出を特定ゾーンに制限", + "toast": { + "success": "レビュー分類設定を保存しました。変更を適用するには Frigate を再起動してください。" + } + }, + "addCamera": "新しいカメラを追加", + "editCamera": "カメラを編集:", + "selectCamera": "カメラを選択", + "backToSettings": "カメラ設定に戻る", + "cameraConfig": { + "add": "カメラを追加", + "edit": "カメラを編集", + "description": "ストリーム入力とロールを含むカメラ設定を構成します。", + "name": "カメラ名", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は24文字未満である必要があります。", + "namePlaceholder": "例: front_door", + "enabled": "有効", + "ffmpeg": { + "inputs": "入力ストリーム", + "path": "ストリームパス", + "pathRequired": "ストリームパスは必須です", + "pathPlaceholder": "rtsp://...", + "roles": "ロール", + "rolesRequired": "少なくとも1つのロールが必要です", + "rolesUnique": "各ロール(audio, detect, record)は1つのストリームにのみ割り当て可能です", + "addInput": "入力ストリームを追加", + "removeInput": "入力ストリームを削除", + "inputsRequired": "少なくとも1つの入力ストリームが必要です" + }, + "toast": { + "success": "カメラ {{cameraName}} を保存しました" + } + } + }, + "masksAndZones": { + "filter": { + "all": "すべてのマスクとゾーン" + }, + "restart_required": "再起動が必要です(マスク/ゾーンを変更)", + "toast": { + "success": { + "copyCoordinates": "{{polyName}} の座標をクリップボードにコピーしました。" + }, + "error": { + "copyCoordinatesFailed": "座標をクリップボードにコピーできませんでした。" + } + }, + "motionMaskLabel": "モーションマスク {{number}}", + "objectMaskLabel": "オブジェクトマスク {{number}}({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "ゾーン名は2文字以上である必要があります。", + "mustNotBeSameWithCamera": "ゾーン名はカメラ名と同一にできません。", + "alreadyExists": "この名前のゾーンはこのカメラに既に存在します。", + "mustNotContainPeriod": "ゾーン名にピリオドは使用できません。", + "hasIllegalCharacter": "ゾーン名に不正な文字が含まれています。" + } + }, + "distance": { + "error": { + "text": "距離は 0.1 以上である必要があります。", + "mustBeFilled": "速度推定を使用するには、すべての距離フィールドを入力してください。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性は 0 より大きい必要があります。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "滞留時間は 0 以上である必要があります。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度しきい値は 0.1 以上である必要があります。" + } + }, + "polygonDrawing": { + "removeLastPoint": "最後の点を削除", + "reset": { + "label": "すべての点をクリア" + }, + "snapPoints": { + "true": "点をスナップ", + "false": "点をスナップしない" + }, + "delete": { + "title": "削除の確認", + "desc": "{{type}} {{name}} を削除してもよろしいですか?", + "success": "{{name}} を削除しました。" + }, + "error": { + "mustBeFinished": "保存する前に多角形の作図を完了してください。" + } + } + }, + "zones": { + "label": "ゾーン", + "documentTitle": "ゾーンを編集 - Frigate", + "desc": { + "title": "ゾーンを使うと、フレーム内の特定領域を定義し、オブジェクトがその領域内にいるかどうかを判断できます。", + "documentation": "ドキュメント" + }, + "add": "ゾーンを追加", + "edit": "ゾーンを編集", + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "name": { + "title": "名称", + "inputPlaceHolder": "名前を入力…", + "tips": "名前は2文字以上、かつカメラ名や他のゾーン名と重複しない必要があります。" + }, + "inertia": { + "title": "慣性", + "desc": "オブジェクトがゾーン内にいるとみなすまでに必要なフレーム数を指定します。既定: 3" + }, + "loiteringTime": { + "title": "滞留時間", + "desc": "ゾーンが有効化されるまでに、オブジェクトがゾーン内に留まる必要がある最小秒数です。既定: 0" + }, + "objects": { + "title": "オブジェクト", + "desc": "このゾーンに適用するオブジェクトの一覧。" + }, + "allObjects": "すべてのオブジェクト", + "speedEstimation": { + "title": "速度推定", + "desc": "このゾーン内のオブジェクトに対して速度推定を有効にします。ゾーンはちょうど4点である必要があります。", + "lineADistance": "A 線の距離({{unit}})", + "lineBDistance": "B 線の距離({{unit}})", + "lineCDistance": "C 線の距離({{unit}})", + "lineDDistance": "D 線の距離({{unit}})" + }, + "speedThreshold": { + "title": "速度しきい値({{unit}})", + "desc": "このゾーンで考慮するオブジェクトの最小速度を指定します。", + "toast": { + "error": { + "pointLengthError": "このゾーンの速度推定を無効化しました。速度推定を使うゾーンは4点である必要があります。", + "loiteringTimeError": "滞留時間が 0 より大きいゾーンでは速度推定は使用しないでください。" + } + } + }, + "toast": { + "success": "ゾーン({{zoneName}})を保存しました。変更を適用するには Frigate を再起動してください。" + } + }, + "motionMasks": { + "label": "モーションマスク", + "documentTitle": "モーションマスクを編集 - Frigate", + "desc": { + "title": "モーションマスクは、望ましくない種類の動きで検出がトリガーされるのを防ぎます。過度なマスクはオブジェクト追跡を困難にします。", + "documentation": "ドキュメント" + }, + "add": "新しいモーションマスク", + "edit": "モーションマスクを編集", + "context": { + "title": "モーションマスクは、望ましくない動き(例: 木の枝、カメラのタイムスタンプ)で検出がトリガーされるのを防ぐために使用します。ごく控えめに使用してください。過度なマスクはオブジェクト追跡を困難にします。" + }, + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "polygonAreaTooLarge": { + "title": "モーションマスクがカメラフレームの {{polygonArea}}% を覆っています。大きなモーションマスクは推奨されません。", + "tips": "モーションマスクはオブジェクトの検出自体を防ぎません。代わりに必須ゾーンを使用してください。" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。変更を適用するには Frigate を再起動してください。", + "noName": "モーションマスクを保存しました。変更を適用するには Frigate を再起動してください。" + } + } + }, + "objectMasks": { + "label": "オブジェクトマスク", + "documentTitle": "オブジェクトマスクを編集 - Frigate", + "desc": { + "title": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。", + "documentation": "ドキュメント" + }, + "add": "オブジェクトマスクを追加", + "edit": "オブジェクトマスクを編集", + "context": "オブジェクトフィルタマスクは、位置に基づいて特定のオブジェクトタイプの誤検出を除外するために使用します。", + "point_other": "{{count}} 点", + "clickDrawPolygon": "画像上をクリックして多角形を描画します。", + "objects": { + "title": "オブジェクト", + "desc": "このオブジェクトマスクに適用するオブジェクトタイプ。", + "allObjectTypes": "すべてのオブジェクトタイプ" + }, + "toast": { + "success": { + "title": "{{polygonName}} を保存しました。変更を適用するには Frigate を再起動してください。", + "noName": "オブジェクトマスクを保存しました。変更を適用するには Frigate を再起動してください。" + } + } + } + }, + "motionDetectionTuner": { + "title": "モーション検出チューナー", + "unsavedChanges": "未保存のモーションチューナーの変更({{camera}})", + "desc": { + "title": "Frigate は、フレーム内に物体検出で確認すべき動きがあるかの一次チェックとしてモーション検出を使用します。", + "documentation": "モーション調整ガイドを読む" + }, + "Threshold": { + "title": "しきい値", + "desc": "しきい値は、ピクセルの輝度変化がモーションとみなされるために必要な変化量を決定します。既定: 30" + }, + "contourArea": { + "title": "輪郭面積", + "desc": "どの変化ピクセルのグループをモーションとして扱うかを決める値です。既定: 10" + }, + "improveContrast": { + "title": "コントラスト改善", + "desc": "暗いシーンのコントラストを改善します。既定: ON" + }, + "toast": { + "success": "モーション設定を保存しました。" + } + }, + "debug": { + "title": "デバッグ", + "detectorDesc": "Frigate は検出器({{detectors}})を使用して、カメラの映像ストリーム内のオブジェクトを検出します。", + "desc": "デバッグビューは、追跡オブジェクトとその統計をリアルタイムに表示します。オブジェクト一覧には、検出オブジェクトの時差サマリが表示されます。", + "openCameraWebUI": "{{camera}} の Web UI を開く", + "debugging": "デバッグ", + "objectList": "オブジェクト一覧", + "noObjects": "オブジェクトなし", + "audio": { + "title": "音声", + "noAudioDetections": "音声検出なし", + "score": "スコア", + "currentRMS": "現在の RMS", + "currentdbFS": "現在の dBFS" + }, + "boundingBoxes": { + "title": "バウンディングボックス", + "desc": "追跡オブジェクトの周囲にバウンディングボックスを表示します", + "colors": { + "label": "オブジェクトのボックス色", + "info": "
  • 起動時に、各オブジェクトラベルへ異なる色が割り当てられます
  • 細い濃青線は、現在時点では未検出であることを示します
  • 細い灰線は、静止していると検出されたことを示します
  • 太線は、(有効時)オートトラッキングの対象であることを示します
  • " + } + }, + "timestamp": { + "title": "タイムスタンプ", + "desc": "画像にタイムスタンプを重ねて表示します" + }, + "zones": { + "title": "ゾーン", + "desc": "定義済みゾーンのアウトラインを表示します" + }, + "mask": { + "title": "モーションマスク", + "desc": "モーションマスクの多角形を表示します" + }, + "motion": { + "title": "モーションボックス", + "desc": "モーションが検出された領域のボックスを表示します", + "tips": "

    モーションボックス


    現在モーションが検出されている領域に赤いボックスが重ねて表示されます

    " + }, + "regions": { + "title": "領域", + "desc": "物体検出器へ送られる関心領域のボックスを表示します", + "tips": "

    領域ボックス


    物体検出器へ送られるフレーム内の関心領域に明るい緑のボックスが重ねて表示されます。

    " + }, + "paths": { + "title": "軌跡", + "desc": "追跡オブジェクトの重要ポイントを表示します", + "tips": "

    軌跡


    線や円で、オブジェクトのライフサイクル中に移動した重要ポイントを示します。

    " + }, + "objectShapeFilterDrawing": { + "title": "オブジェクト形状フィルタの作図", + "desc": "画像上に矩形を描いて面積と比率の詳細を表示します", + "tips": "このオプションを有効にすると、カメラ画像上に矩形を描いてその面積と比率を表示できます。これらの値は設定ファイルのオブジェクト形状フィルタのパラメータ設定に利用できます。", + "score": "スコア", + "ratio": "比率", + "area": "面積" + } + }, + "users": { + "title": "ユーザー", + "management": { + "title": "ユーザー管理", + "desc": "この Frigate インスタンスのユーザーアカウントを管理します。" + }, + "addUser": "ユーザーを追加", + "updatePassword": "パスワードを更新", + "toast": { + "success": { + "createUser": "ユーザー {{user}} を作成しました", + "deleteUser": "ユーザー {{user}} を削除しました", + "updatePassword": "パスワードを更新しました。", + "roleUpdated": "{{user}} のロールを更新しました" + }, + "error": { + "setPasswordFailed": "パスワードの保存に失敗しました: {{errorMessage}}", + "createUserFailed": "ユーザーの作成に失敗しました: {{errorMessage}}", + "deleteUserFailed": "ユーザーの削除に失敗しました: {{errorMessage}}", + "roleUpdateFailed": "ロールの更新に失敗しました: {{errorMessage}}" + } + }, + "table": { + "username": "ユーザー名", + "actions": "操作", + "role": "ロール", + "noUsers": "ユーザーが見つかりません。", + "changeRole": "ユーザーロールを変更", + "password": "パスワード", + "deleteUser": "ユーザーを削除" + }, + "dialog": { + "form": { + "user": { + "title": "ユーザー名", + "desc": "使用できるのは英数字、ピリオド、アンダースコアのみです。", + "placeholder": "ユーザー名を入力" + }, + "password": { + "title": "パスワード", + "placeholder": "パスワードを入力", + "confirm": { + "title": "パスワードの確認", + "placeholder": "パスワードを再入力" + }, + "strength": { + "title": "パスワード強度: ", + "weak": "弱い", + "medium": "普通", + "strong": "強い", + "veryStrong": "非常に強い" + }, + "match": "パスワードが一致しています", + "notMatch": "パスワードが一致しません" + }, + "newPassword": { + "title": "新しいパスワード", + "placeholder": "新しいパスワードを入力", + "confirm": { + "placeholder": "新しいパスワードを再入力" + } + }, + "usernameIsRequired": "ユーザー名は必須です", + "passwordIsRequired": "パスワードは必須です" + }, + "createUser": { + "title": "新規ユーザーを作成", + "desc": "新しいユーザーアカウントを追加し、Frigate UI へのアクセスロールを指定します。", + "usernameOnlyInclude": "ユーザー名に使用できるのは英数字、.、_ のみです", + "confirmPassword": "パスワードを確認してください" + }, + "deleteUser": { + "title": "ユーザーを削除", + "desc": "この操作は元に戻せません。ユーザーアカウントおよび関連データは完全に削除されます。", + "warn": "{{username}} を削除してもよろしいですか?" + }, + "passwordSetting": { + "cannotBeEmpty": "パスワードを空にはできません", + "doNotMatch": "パスワードが一致しません", + "updatePassword": "{{username}} のパスワードを更新", + "setPassword": "パスワードを設定", + "desc": "強力なパスワードを作成して、このアカウントを保護してください。" + }, + "changeRole": { + "title": "ユーザーロールを変更", + "select": "ロールを選択", + "desc": "{{username}} の権限を更新します", + "roleInfo": { + "intro": "このユーザーに適切なロールを選択してください:", + "admin": "管理者", + "adminDesc": "すべての機能にフルアクセス。", + "viewer": "閲覧者", + "viewerDesc": "ライブ、レビュー、探索、書き出しに限定。", + "customDesc": "特定のカメラアクセスを持つカスタムロール。" + } + } + } + }, + "roles": { + "management": { + "title": "閲覧者ロール管理", + "desc": "この Frigate インスタンスのカスタム閲覧者ロールと、そのカメラアクセス権を管理します。" + }, + "addRole": "ロールを追加", + "table": { + "role": "ロール", + "cameras": "カメラ", + "actions": "操作", + "noRoles": "カスタムロールが見つかりません。", + "editCameras": "カメラを編集", + "deleteRole": "ロールを削除" + }, + "toast": { + "success": { + "createRole": "ロール {{role}} を作成しました", + "updateCameras": "ロール {{role}} のカメラを更新しました", + "deleteRole": "ロール {{role}} を削除しました", + "userRolesUpdated_other": "このロールに割り当てられていた {{count}} ユーザーは「viewer」に更新され、すべてのカメラへの閲覧アクセスが付与されました。" + }, + "error": { + "createRoleFailed": "ロールの作成に失敗しました: {{errorMessage}}", + "updateCamerasFailed": "カメラの更新に失敗しました: {{errorMessage}}", + "deleteRoleFailed": "ロールの削除に失敗しました: {{errorMessage}}", + "userUpdateFailed": "ユーザーロールの更新に失敗しました: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "新しいロールを作成", + "desc": "新しいロールを追加し、カメラアクセス権を指定します。" + }, + "editCameras": { + "title": "ロールのカメラを編集", + "desc": "ロール {{role}} のカメラアクセスを更新します。" + }, + "deleteRole": { + "title": "ロールを削除", + "desc": "この操作は元に戻せません。ロールは完全に削除され、このロールを持っていたユーザーは「viewer」ロールに再割り当てされ、すべてのカメラへの閲覧アクセスが付与されます。", + "warn": "{{role}} を削除してもよろしいですか?", + "deleting": "削除中…" + }, + "form": { + "role": { + "title": "ロール名", + "placeholder": "ロール名を入力", + "desc": "使用できるのは英数字、ピリオド、アンダースコアのみです。", + "roleIsRequired": "ロール名は必須です", + "roleOnlyInclude": "ロール名に使用できるのは英数字、.、_ のみです", + "roleExists": "この名前のロールは既に存在します。" + }, + "cameras": { + "title": "カメラ", + "desc": "このロールでアクセス可能なカメラを選択します。少なくとも1台が必要です。", + "required": "少なくとも1台のカメラを選択してください。" + } + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate はブラウザで実行中、または PWA としてインストールされている場合に、端末へネイティブのプッシュ通知を送信できます。" + }, + "notificationUnavailable": { + "title": "通知は利用できません", + "desc": "Web プッシュ通知にはセキュアコンテキスト(https://…)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。" + }, + "globalSettings": { + "title": "グローバル設定", + "desc": "登録済みのすべてのデバイスで、特定のカメラの通知を一時停止します。" + }, + "email": { + "title": "メール", + "placeholder": "例: example@email.com", + "desc": "有効なメールが必要です。プッシュサービスに問題がある場合の通知に使用します。" + }, + "cameras": { + "title": "カメラ", + "noCameras": "利用可能なカメラがありません", + "desc": "通知を有効にするカメラを選択します。" + }, + "deviceSpecific": "デバイス固有の設定", + "registerDevice": "このデバイスを登録", + "unregisterDevice": "このデバイスの登録を解除", + "sendTestNotification": "テスト通知を送信", + "unsavedRegistrations": "未保存の通知登録", + "unsavedChanges": "未保存の通知設定の変更", + "active": "通知は有効", + "suspended": "通知は一時停止中 {{time}}", + "suspendTime": { + "suspend": "一時停止", + "5minutes": "5分間一時停止", + "10minutes": "10分間一時停止", + "30minutes": "30分間一時停止", + "1hour": "1時間一時停止", + "12hours": "12時間一時停止", + "24hours": "24時間一時停止", + "untilRestart": "再起動まで一時停止" + }, + "cancelSuspension": "一時停止を解除", + "toast": { + "success": { + "registered": "通知の登録に成功しました。通知(テスト通知を含む)を送信するには Frigate の再起動が必要です。", + "settingSaved": "通知設定を保存しました。" + }, + "error": { + "registerFailed": "通知登録の保存に失敗しました。" + } + } + }, + "frigatePlus": { + "title": "Frigate+ 設定", + "apiKey": { + "title": "Frigate+ API キー", + "validated": "Frigate+ API キーが検出され、検証されました", + "notValidated": "Frigate+ API キーが検出されないか、検証されていません", + "desc": "Frigate+ API キーは Frigate+ サービスとの統合を有効にします。", + "plusLink": "Frigate+ の詳細を読む" + }, + "snapshotConfig": { + "title": "スナップショット設定", + "desc": "Frigate+ への送信には、設定でスナップショットと clean_copy スナップショットの両方を有効にする必要があります。", + "cleanCopyWarning": "一部のカメラではスナップショットは有効ですが、クリーンコピーが無効です。これらのカメラから Frigate+ へ画像を送信するには、スナップショット設定で clean_copy を有効にしてください。", + "table": { + "camera": "カメラ", + "snapshots": "スナップショット", + "cleanCopySnapshots": "clean_copy スナップショット" + } + }, + "modelInfo": { + "title": "モデル情報", + "modelType": "モデルタイプ", + "trainDate": "学習日", + "baseModel": "ベースモデル", + "plusModelType": { + "baseModel": "ベースモデル", + "userModel": "ファインチューニング済み" + }, + "supportedDetectors": "対応検出器", + "cameras": "カメラ", + "loading": "モデル情報を読み込み中…", + "error": "モデル情報の読み込みに失敗しました", + "availableModels": "利用可能なモデル", + "loadingAvailableModels": "利用可能なモデルを読み込み中…", + "modelSelect": "ここで Frigate+ 上の利用可能なモデルを選択できます。現在の検出器構成と互換性のあるモデルのみ選択可能です。" + }, + "unsavedChanges": "未保存の Frigate+ 設定の変更", + "restart_required": "再起動が必要です(Frigate+ モデルを変更)", + "toast": { + "success": "Frigate+ 設定を保存しました。変更を適用するには Frigate を再起動してください。", + "error": "設定変更の保存に失敗しました: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "トリガー", + "management": { + "title": "トリガー管理", + "desc": "{{camera}} のトリガーを管理します。サムネイルタイプでは、選択した追跡オブジェクトに類似するサムネイルでトリガーし、説明タイプでは、指定したテキストに類似する説明でトリガーします。" + }, + "addTrigger": "トリガーを追加", + "table": { + "name": "名称", + "type": "タイプ", + "content": "コンテンツ", + "threshold": "しきい値", + "actions": "操作", + "noTriggers": "このカメラに設定されたトリガーはありません。", + "edit": "編集", + "deleteTrigger": "トリガーを削除", + "lastTriggered": "最終トリガー時刻" + }, + "type": { + "thumbnail": "サムネイル", + "description": "説明" + }, + "actions": { + "alert": "アラートとしてマーク", + "notification": "通知を送信" + }, + "dialog": { + "createTrigger": { + "title": "トリガーを作成", + "desc": "カメラ {{camera}} のトリガーを作成します" + }, + "editTrigger": { + "title": "トリガーを編集", + "desc": "カメラ {{camera}} のトリガー設定を編集します" + }, + "deleteTrigger": { + "title": "トリガーを削除", + "desc": "トリガー {{triggerName}} を削除してもよろしいですか?この操作は元に戻せません。" + }, + "form": { + "name": { + "title": "名称", + "placeholder": "トリガー名を入力", + "error": { + "minLength": "名称は2文字以上である必要があります。", + "invalidCharacters": "名称に使用できるのは英数字、アンダースコア、ハイフンのみです。", + "alreadyExists": "このカメラには同名のトリガーが既に存在します。" + } + }, + "enabled": { + "description": "このトリガーを有効/無効にする" + }, + "type": { + "title": "タイプ", + "placeholder": "トリガータイプを選択" + }, + "content": { + "title": "コンテンツ", + "imagePlaceholder": "画像を選択", + "textPlaceholder": "テキストを入力", + "imageDesc": "類似画像が検出されたときにこのアクションをトリガーするための画像を選択します。", + "textDesc": "類似する追跡オブジェクトの説明が検出されたときにこのアクションをトリガーするためのテキストを入力します。", + "error": { + "required": "コンテンツは必須です。" + } + }, + "threshold": { + "title": "しきい値", + "error": { + "min": "しきい値は 0 以上である必要があります", + "max": "しきい値は 1 以下である必要があります" + } + }, + "actions": { + "title": "アクション", + "desc": "既定では、すべてのトリガーに対して MQTT メッセージが送信されます。必要に応じて、トリガー時に実行する追加アクションを選択してください。", + "error": { + "min": "少なくとも1つのアクションを選択してください。" + } + }, + "friendly_name": { + "title": "表示名", + "placeholder": "このトリガーの名前または説明", + "description": "このトリガーの表示名または説明文" + } + } + }, + "toast": { + "success": { + "createTrigger": "トリガー {{name}} を作成しました。", + "updateTrigger": "トリガー {{name}} を更新しました。", + "deleteTrigger": "トリガー {{name}} を削除しました。" + }, + "error": { + "createTriggerFailed": "トリガーの作成に失敗しました: {{errorMessage}}", + "updateTriggerFailed": "トリガーの更新に失敗しました: {{errorMessage}}", + "deleteTriggerFailed": "トリガーの削除に失敗しました: {{errorMessage}}" + } + }, + "semanticSearch": { + "desc": "トリガーを使用するにはセマンティック検索を有効にする必要があります。", + "title": "セマンティック検索が無効です" + } + }, + "cameraWizard": { + "step3": { + "saveAndApply": "新しいカメラを保存", + "description": "保存前の最終検証と解析。保存する前に各ストリームを接続してください。", + "validationTitle": "ストリーム検証", + "connectAllStreams": "すべてのストリームを接続", + "reconnectionSuccess": "再接続に成功しました。", + "reconnectionPartial": "一部のストリームの再接続に失敗しました。", + "streamUnavailable": "ストリームプレビューは利用できません", + "reload": "再読み込み", + "connecting": "接続中…", + "streamTitle": "ストリーム {{number}}", + "valid": "有効", + "failed": "失敗", + "notTested": "未テスト", + "connectStream": "接続", + "connectingStream": "接続中", + "disconnectStream": "切断", + "estimatedBandwidth": "推定帯域幅", + "roles": "ロール", + "none": "なし", + "error": "エラー", + "streamValidated": "ストリーム {{number}} の検証に成功しました", + "streamValidationFailed": "ストリーム {{number}} の検証に失敗しました", + "saveError": "無効な構成です。設定を確認してください。", + "issues": { + "title": "ストリーム検証", + "videoCodecGood": "ビデオコーデックは {{codec}} です。", + "audioCodecGood": "オーディオコーデックは {{codec}} です。", + "noAudioWarning": "このストリームでは音声が検出されません。録画には音声が含まれません。", + "audioCodecRecordError": "録画に音声を含めるには AAC オーディオコーデックが必要です。", + "audioCodecRequired": "音声検出を有効にするには音声ストリームが必要です。", + "restreamingWarning": "録画ストリームでカメラへの接続数を減らすと、CPU 使用率がわずかに増加する場合があります。", + "hikvision": { + "substreamWarning": "サブストリーム1は低解像度に固定されています。多くの Hikvision 製カメラでは、追加のサブストリームが利用可能であり、カメラ本体の設定で有効化する必要があります。使用できる場合は、それらのストリームを確認して活用することを推奨します。" + }, + "dahua": { + "substreamWarning": "サブストリーム1は低解像度に固定されています。多くの Dahua/Amcrest/EmpireTech 製カメラでは、追加のサブストリームが利用可能であり、カメラ本体の設定で有効化する必要があります。使用できる場合は、それらのストリームを確認して活用することを推奨します。" + } + } + }, + "title": "カメラを追加", + "description": "以下の手順に従って、Frigate に新しいカメラを追加します。", + "steps": { + "nameAndConnection": "名称と接続", + "streamConfiguration": "ストリーム設定", + "validationAndTesting": "検証とテスト" + }, + "save": { + "success": "新しいカメラ {{cameraName}} を保存しました。", + "failure": "保存エラー: {{cameraName}}。" + }, + "testResultLabels": { + "resolution": "解像度", + "video": "ビデオ", + "audio": "オーディオ", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "有効なストリーム URL を入力してください", + "testFailed": "ストリームテストに失敗しました: {{error}}" + }, + "step1": { + "description": "カメラの詳細を入力し、接続テストを実行します。", + "cameraName": "カメラ名", + "cameraNamePlaceholder": "例: front_door または Back Yard Overview", + "host": "ホスト/IP アドレス", + "port": "ポート", + "username": "ユーザー名", + "usernamePlaceholder": "任意", + "password": "パスワード", + "passwordPlaceholder": "任意", + "selectTransport": "トランスポートプロトコルを選択", + "cameraBrand": "カメラブランド", + "selectBrand": "URL テンプレート用のカメラブランドを選択", + "customUrl": "カスタムストリーム URL", + "brandInformation": "ブランド情報", + "brandUrlFormat": "RTSP URL 形式が {{exampleUrl}} のカメラ向け", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "接続テスト", + "testSuccess": "接続テストに成功しました!", + "testFailed": "接続テストに失敗しました。入力内容を確認して再試行してください。", + "streamDetails": "ストリーム詳細", + "warnings": { + "noSnapshot": "設定されたストリームからスナップショットを取得できません。" + }, + "errors": { + "brandOrCustomUrlRequired": "ホスト/IP とブランドを選択するか、「その他」を選んでカスタム URL を指定してください", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は64文字以下である必要があります", + "invalidCharacters": "カメラ名に無効な文字が含まれています", + "nameExists": "このカメラ名は既に存在します", + "brands": { + "reolink-rtsp": "Reolink の RTSP は推奨されません。カメラ設定で http を有効にし、カメラウィザードを再起動することを推奨します。" + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "step2": { + "description": "ストリームのロールを設定し、必要に応じて追加ストリームを登録します。", + "streamsTitle": "カメラストリーム", + "addStream": "ストリームを追加", + "addAnotherStream": "ストリームをさらに追加", + "streamTitle": "ストリーム {{number}}", + "streamUrl": "ストリーム URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "解像度", + "selectResolution": "解像度を選択", + "quality": "品質", + "selectQuality": "品質を選択", + "roles": "ロール", + "roleLabels": { + "detect": "物体検出", + "record": "録画", + "audio": "音声" + }, + "testStream": "接続テスト", + "testSuccess": "ストリームテストに成功しました!", + "testFailed": "ストリームテストに失敗しました", + "testFailedTitle": "テスト失敗", + "connected": "接続済み", + "notConnected": "未接続", + "featuresTitle": "機能", + "go2rtc": "カメラへの接続数を削減", + "detectRoleWarning": "\"detect\" ロールを持つストリームが少なくとも1つ必要です。", + "rolesPopover": { + "title": "ストリームロール", + "detect": "物体検出のメインフィード。", + "record": "設定に基づいて映像フィードのセグメントを保存します。", + "audio": "音声検出用のフィード。" + }, + "featuresPopover": { + "title": "ストリーム機能", + "description": "go2rtc のリストリーミングを使用してカメラへの接続数を削減します。" + } + } + }, + "cameraManagement": { + "title": "カメラ管理", + "addCamera": "新しいカメラを追加", + "editCamera": "カメラを編集:", + "selectCamera": "カメラを選択", + "backToSettings": "カメラ設定に戻る", + "streams": { + "title": "カメラの有効化/無効化", + "desc": "Frigate を再起動するまで一時的にカメラを無効化します。無効化すると、このカメラのストリーム処理は完全に停止し、検出・録画・デバッグは利用できません。
    注: これは go2rtc のリストリームを無効にはしません。" + }, + "cameraConfig": { + "add": "カメラを追加", + "edit": "カメラを編集", + "description": "ストリーム入力とロールを含むカメラ設定を構成します。", + "name": "カメラ名", + "nameRequired": "カメラ名は必須です", + "nameLength": "カメラ名は64文字未満である必要があります。", + "namePlaceholder": "例: front_door または Back Yard Overview", + "enabled": "有効", + "ffmpeg": { + "inputs": "入力ストリーム", + "path": "ストリームパス", + "pathRequired": "ストリームパスは必須です", + "pathPlaceholder": "rtsp://...", + "roles": "ロール", + "rolesRequired": "少なくとも1つのロールが必要です", + "rolesUnique": "各ロール(audio、detect、record)は1つのストリームにのみ割り当て可能です", + "addInput": "入力ストリームを追加", + "removeInput": "入力ストリームを削除", + "inputsRequired": "少なくとも1つの入力ストリームが必要です" + }, + "go2rtcStreams": "go2rtc ストリーム", + "streamUrls": "ストリーム URL", + "addUrl": "URL を追加", + "addGo2rtcStream": "go2rtc ストリームを追加", + "toast": { + "success": "カメラ {{cameraName}} を保存しました" + } + } + }, + "cameraReview": { + "title": "カメラレビュー設定", + "object_descriptions": { + "title": "生成AIによるオブジェクト説明", + "desc": "このカメラに対する生成AIのオブジェクト説明を一時的に有効/無効にします。無効にすると、このカメラの追跡オブジェクトについてAI生成の説明は要求されません。" + }, + "review_descriptions": { + "title": "生成AIによるレビュー説明", + "desc": "このカメラに対する生成AIのレビュー説明を一時的に有効/無効にします。無効にすると、このカメラのレビュー項目についてAI生成の説明は要求されません。" + }, + "review": { + "title": "レビュー", + "desc": "Frigate を再起動するまで、このカメラのアラートと検出を一時的に有効/無効にします。無効にすると、新しいレビュー項目は生成されません。 ", + "alerts": "アラート ", + "detections": "検出 " + }, + "reviewClassification": { + "title": "レビュー分類", + "desc": "Frigate はレビュー項目をアラートと検出に分類します。既定では、すべての personcar オブジェクトはアラートとして扱われます。必須ゾーンを設定することで、分類をより細かく調整できます。", + "noDefinedZones": "このカメラにはゾーンが定義されていません。", + "objectAlertsTips": "すべての {{alertsLabels}} オブジェクトは {{cameraName}} でアラートとして表示されます。", + "zoneObjectAlertsTips": "{{cameraName}} の {{zone}} で検出されたすべての {{alertsLabels}} オブジェクトはアラートとして表示されます。", + "objectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検出として表示されます。", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} の {{zone}} で分類されていないすべての {{detectionsLabels}} オブジェクトは検出として表示されます。", + "notSelectDetections": "{{cameraName}} の {{zone}} で検出され、アラートに分類されなかったすべての {{detectionsLabels}} オブジェクトは、ゾーンに関係なく検出として表示されます。", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} で分類されていないすべての {{detectionsLabels}} オブジェクトは、どのゾーンにあっても検出として表示されます。" + }, + "unsavedChanges": "未保存のレビュー分類設定({{camera}})", + "selectAlertsZones": "アラート用のゾーンを選択", + "selectDetectionsZones": "検出用のゾーンを選択", + "limitDetections": "特定のゾーンに検出を限定する", + "toast": { + "success": "レビュー分類の設定を保存しました。変更を適用するには Frigate を再起動してください。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ja/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ja/views/system.json new file mode 100644 index 0000000..da57fa7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ja/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "カメラ統計 - Frigate", + "general": "一般統計 - Frigate", + "storage": "ストレージ統計 - Frigate", + "enrichments": "高度解析統計 - Frigate", + "logs": { + "frigate": "Frigate ログ - Frigate", + "go2rtc": "Go2RTC ログ - Frigate", + "nginx": "Nginx ログ - Frigate" + } + }, + "title": "システム", + "metrics": "システムメトリクス", + "logs": { + "download": { + "label": "ログをダウンロード" + }, + "copy": { + "label": "クリップボードにコピー", + "success": "ログをクリップボードにコピーしました", + "error": "ログをクリップボードにコピーできませんでした" + }, + "type": { + "label": "種類", + "timestamp": "タイムスタンプ", + "tag": "タグ", + "message": "メッセージ" + }, + "tips": "ログはサーバーからストリーミングされています", + "toast": { + "error": { + "fetchingLogsFailed": "ログの取得エラー: {{errorMessage}}", + "whileStreamingLogs": "ログのストリーミング中にエラー: {{errorMessage}}" + } + } + }, + "general": { + "title": "全般", + "detector": { + "title": "検出器", + "inferenceSpeed": "ディテクタ推論速度", + "temperature": "ディテクタ温度", + "cpuUsage": "ディテクタの CPU 使用率", + "cpuUsageInformation": "検出モデルへの入力/出力データの準備に使用される CPU。GPU やアクセラレータを使用していても、この値は推論の使用量を測定しません。", + "memoryUsage": "ディテクタのメモリ使用量" + }, + "hardwareInfo": { + "title": "ハードウェア情報", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU メモリ", + "gpuEncoder": "GPU エンコーダー", + "gpuDecoder": "GPU デコーダー", + "gpuInfo": { + "vainfoOutput": { + "title": "vainfo 出力", + "returnCode": "戻りコード: {{code}}", + "processOutput": "プロセス出力:", + "processError": "プロセスエラー:" + }, + "nvidiaSMIOutput": { + "title": "NVIDIA SMI 出力", + "name": "名前: {{name}}", + "driver": "ドライバー: {{driver}}", + "cudaComputerCapability": "CUDA 計算能力: {{cuda_compute}}", + "vbios": "VBIOS 情報: {{vbios}}" + }, + "closeInfo": { + "label": "GPU 情報を閉じる" + }, + "copyInfo": { + "label": "GPU 情報をコピー" + }, + "toast": { + "success": "GPU 情報をクリップボードにコピーしました" + } + }, + "npuUsage": "NPU 使用率", + "npuMemory": "NPU メモリ" + }, + "otherProcesses": { + "title": "その他のプロセス", + "processCpuUsage": "プロセスの CPU 使用率", + "processMemoryUsage": "プロセスのメモリ使用量" + } + }, + "storage": { + "title": "ストレージ", + "overview": "概要", + "recordings": { + "title": "録画", + "tips": "この値は Frigate のデータベースで録画が使用している総ストレージ量を表します。Frigate はディスク上のすべてのファイルの使用量を追跡しているわけではありません。", + "earliestRecording": "利用可能な最古の録画:" + }, + "shm": { + "title": "SHM(共有メモリ)の割り当て", + "warning": "現在の SHM サイズ {{total}}MB は小さすぎます。少なくとも {{min_shm}}MB に増やしてください。" + }, + "cameraStorage": { + "title": "カメラストレージ", + "camera": "カメラ", + "unusedStorageInformation": "未使用ストレージ情報", + "storageUsed": "ストレージ使用量", + "percentageOfTotalUsed": "総使用量に占める割合", + "bandwidth": "帯域幅", + "unused": { + "title": "未使用", + "tips": "Frigate の録画以外にドライブへ保存しているファイルがある場合、この値は Frigate が利用できる空き容量を正確に表さないことがあります。Frigate は録画以外のストレージ使用量を追跡しません。" + } + } + }, + "cameras": { + "title": "カメラ", + "overview": "概要", + "info": { + "aspectRatio": "アスペクト比", + "cameraProbeInfo": "{{camera}} カメラプローブ情報", + "streamDataFromFFPROBE": "ストリームデータは ffprobe で取得しています。", + "fetching": "カメラデータを取得中", + "stream": "ストリーム {{idx}}", + "video": "動画:", + "codec": "コーデック:", + "resolution": "解像度:", + "fps": "FPS:", + "unknown": "不明", + "audio": "音声:", + "error": "エラー: {{error}}", + "tips": { + "title": "カメラプローブ情報" + } + }, + "framesAndDetections": "フレーム / 検出", + "label": { + "camera": "カメラ", + "detect": "検出", + "skipped": "スキップ", + "ffmpeg": "FFmpeg", + "capture": "キャプチャ", + "overallFramesPerSecond": "全体フレーム/秒", + "overallDetectionsPerSecond": "全体検出/秒", + "overallSkippedDetectionsPerSecond": "全体スキップ検出/秒", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} キャプチャ", + "cameraDetect": "{{camName}} 検出", + "cameraFramesPerSecond": "{{camName}} フレーム/秒", + "cameraDetectionsPerSecond": "{{camName}} 検出/秒", + "cameraSkippedDetectionsPerSecond": "{{camName}} スキップ検出/秒" + }, + "toast": { + "success": { + "copyToClipboard": "プローブデータをクリップボードにコピーしました。" + }, + "error": { + "unableToProbeCamera": "カメラをプローブできません: {{errorMessage}}" + } + } + }, + "lastRefreshed": "最終更新: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} の FFmpeg の CPU 使用率が高い({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} の検出の CPU 使用率が高い({{detectAvg}}%)", + "healthy": "システムは正常です", + "reindexingEmbeddings": "埋め込みを再インデックス中({{processed}}% 完了)", + "cameraIsOffline": "{{camera}} はオフラインです", + "detectIsSlow": "{{detect}} が遅い({{speed}} ms)", + "detectIsVerySlow": "{{detect}} が非常に遅い({{speed}} ms)", + "shmTooLow": "/dev/shm の割り当て({{total}} MB)は少なくとも {{min}} MB に増やす必要があります。" + }, + "enrichments": { + "title": "高度解析", + "infPerSecond": "毎秒推論回数", + "embeddings": { + "image_embedding": "画像埋め込み", + "text_embedding": "テキスト埋め込み", + "face_recognition": "顔認識", + "plate_recognition": "ナンバープレート認識", + "image_embedding_speed": "画像埋め込み速度", + "face_embedding_speed": "顔埋め込み速度", + "face_recognition_speed": "顔認識速度", + "plate_recognition_speed": "ナンバープレート認識速度", + "text_embedding_speed": "テキスト埋め込み速度", + "yolov9_plate_detection_speed": "YOLOv9 ナンバープレート検出速度", + "yolov9_plate_detection": "YOLOv9 ナンバープレート検出" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ko/audio.json new file mode 100644 index 0000000..d9db04e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/audio.json @@ -0,0 +1,72 @@ +{ + "crying": "울음", + "snoring": "코골이", + "singing": "노래", + "yell": "비명", + "speech": "말소리", + "babbling": "옹알이", + "bicycle": "자전거", + "a_capella": "아카펠라", + "accelerating": "가속", + "accordion": "아코디언", + "acoustic_guitar": "어쿠스틱 기타", + "car": "차량", + "motorcycle": "원동기", + "bus": "버스", + "train": "기차", + "boat": "보트", + "bird": "새", + "cat": "고양이", + "dog": "강아지", + "horse": "말", + "sheep": "양", + "skateboard": "스케이트보드", + "door": "문", + "mouse": "마우스", + "keyboard": "키보드", + "sink": "싱크대", + "blender": "블렌더", + "clock": "벽시계", + "scissors": "가위", + "hair_dryer": "헤어 드라이어", + "toothbrush": "칫솔", + "vehicle": "탈 것", + "animal": "동물", + "bark": "개", + "goat": "염소", + "bellow": "포효", + "whoop": "환성", + "whispering": "속삭임", + "laughter": "웃음", + "snicker": "낄낄 웃음", + "sigh": "한숨", + "choir": "합창", + "yodeling": "요들링", + "chant": "성가", + "mantra": "만트라", + "child_singing": "어린이 노래", + "synthetic_singing": "Synthetic Singing", + "rapping": "랩", + "humming": "허밍", + "groan": "신음", + "grunt": "으르렁", + "whistling": "휘파람", + "breathing": "숨쉬는 소리", + "wheeze": "헐떡임", + "gasp": "헐떡임", + "pant": "거친숨", + "snort": "코골이", + "cough": "기침", + "throat_clearing": "목 긁는 소리", + "sneeze": "재채기", + "sniff": "훌쩍", + "run": "달리기", + "shuffle": "Shuffle", + "footsteps": "발소리", + "chewing": "씹는 소리", + "biting": "치는 소리", + "gargling": "가글", + "stomach_rumble": "배 꼬르륵", + "burping": "트림", + "camera": "카메라" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/common.json b/sam2-cpu/frigate-dev/web/public/locales/ko/common.json new file mode 100644 index 0000000..e5c8ef9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/common.json @@ -0,0 +1,271 @@ +{ + "readTheDocumentation": "문서 읽기", + "time": { + "untilForTime": "{{time}}까지", + "untilForRestart": "Frigate가 재시작될 때 까지.", + "10minutes": "10분", + "12hours": "12시간", + "1hour": "1시간", + "24hours": "24시간", + "30minutes": "30분", + "5minutes": "5분", + "untilRestart": "재시작 될 때까지", + "ago": "{{timeAgo}} 전", + "justNow": "지금 막", + "today": "오늘", + "yesterday": "어제", + "last7": "최근 7일", + "last14": "최근 14일", + "last30": "최근 30일", + "thisWeek": "이번 주", + "lastWeek": "저번 주", + "thisMonth": "이번 달", + "lastMonth": "저번 달", + "pm": "오후", + "am": "오전", + "yr": "{{time}}년", + "year_other": "{{time}} 년", + "mo": "{{time}}월", + "month_other": "{{time}} 월", + "d": "{{time}}일", + "day_other": "{{time}} 일", + "h": "{{time}}시", + "hour_other": "{{time}} 시", + "m": "{{time}}분", + "minute_other": "{{time}} 분", + "s": "{{time}}초", + "second_other": "{{time}} 초", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } + }, + "notFound": { + "title": "404", + "documentTitle": "찾을 수 없음 - Frigate", + "desc": "페이지 찾을 수 없음" + }, + "accessDenied": { + "title": "접근 거부", + "documentTitle": "접근 거부 - Frigate", + "desc": "이 페이지 접근 권한이 없습니다." + }, + "menu": { + "user": { + "account": "계정", + "title": "사용자", + "current": "현재 사용자:{{user}}", + "anonymous": "익명", + "logout": "로그아웃", + "setPassword": "비밀번호 설정" + }, + "system": "시스템", + "systemMetrics": "시스템 지표", + "configuration": "설정", + "systemLogs": "시스템 로그", + "settings": "설정", + "configurationEditor": "설정 편집기", + "languages": "언어", + "language": { + "en": "English (English)", + "es": "Español (Spanish)", + "zhCN": "简体中文 (Simplified Chinese)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (French)", + "ar": "العربية (Arabic)", + "pt": "Português (Portuguese)", + "ptBR": "Português brasileiro (Brazilian Portuguese)", + "ru": "Русский (Russian)", + "de": "Deutsch (German)", + "ja": "日本語 (Japanese)", + "tr": "Türkçe (Turkish)", + "it": "Italiano (Italian)", + "nl": "Nederlands (Dutch)", + "sv": "Svenska (Swedish)", + "cs": "Čeština (Czech)", + "nb": "Norsk Bokmål (Norwegian Bokmål)", + "ko": "한국어 (Korean)", + "vi": "Tiếng Việt (Vietnamese)", + "fa": "فارسی (Persian)", + "pl": "Polski (Polish)", + "uk": "Українська (Ukrainian)", + "he": "עברית (Hebrew)", + "el": "Ελληνικά (Greek)", + "ro": "Română (Romanian)", + "hu": "Magyar (Hungarian)", + "fi": "Suomi (Finnish)", + "da": "Dansk (Danish)", + "sk": "Slovenčina (Slovak)", + "yue": "粵語 (Cantonese)", + "th": "ไทย (Thai)", + "ca": "Català (Catalan)", + "sr": "Српски (Serbian)", + "sl": "Slovenščina (Slovenian)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "시스템 설정 언어 사용" + } + }, + "appearance": "화면 설정", + "darkMode": { + "label": "다크 모드", + "light": "라이트", + "dark": "다크", + "withSystem": { + "label": "시스템 설정에 따라 설정" + } + }, + "withSystem": "시스템", + "theme": { + "label": "테마", + "blue": "파랑", + "green": "녹색", + "nord": "노드 (Nord)", + "red": "빨강", + "highcontrast": "고 대비", + "default": "기본값" + }, + "help": "도움말", + "documentation": { + "title": "문서", + "label": "Frigate 문서" + }, + "restart": "Frigate 재시작", + "live": { + "title": "실시간", + "allCameras": "모든 카메라", + "cameras": { + "title": "카메라", + "count_other": "{{count}} 카메라" + } + }, + "review": "다시보기", + "explore": "탐색", + "export": "내보내기", + "uiPlayground": "UI 실험장", + "faceLibrary": "얼굴 라이브러리" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "피트", + "meters": "미터" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "label": { + "back": "뒤로" + }, + "button": { + "apply": "적용", + "reset": "리셋", + "done": "완료", + "enabled": "활성화됨", + "enable": "활성화", + "disabled": "비활성화됨", + "disable": "비활성화", + "save": "저장", + "saving": "저장 중…", + "cancel": "취소", + "close": "닫기", + "copy": "복사", + "back": "뒤로", + "history": "히스토리", + "fullscreen": "전체화면", + "exitFullscreen": "전체화면 나가기", + "pictureInPicture": "Picture in Picture", + "twoWayTalk": "양방향 말하기", + "cameraAudio": "카메라 오디오", + "on": "켜기", + "off": "끄기", + "edit": "편집", + "copyCoordinates": "코디네이트 복사", + "delete": "삭제", + "yes": "예", + "no": "아니오", + "download": "다운로드", + "info": "정보", + "suspended": "일시 정지됨", + "unsuspended": "재개", + "play": "재생", + "unselect": "선택 해제", + "export": "내보내기", + "deleteNow": "바로 삭제하기", + "next": "다음" + }, + "toast": { + "copyUrlToClipboard": "클립보드에 URL이 복사되었습니다.", + "save": { + "title": "저장", + "error": { + "title": "설정 저장 실패: {{errorMessage}}", + "noMessage": "설정 저장이 실패했습니다" + } + } + }, + "role": { + "title": "역할", + "admin": "관리자", + "viewer": "감시자", + "desc": "관리자는 Frigate UI에 모든 접근 권한이 있습니다. 감시자는 카메라 감시, 돌아보기, 과거 영상 조회만 가능합니다." + }, + "pagination": { + "label": "나눠보기", + "previous": { + "title": "이전", + "label": "이전 페이지" + }, + "next": { + "title": "다음", + "label": "다음 페이지" + }, + "more": "더 많은 페이지" + }, + "selectItem": "{{item}} 선택", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/auth.json new file mode 100644 index 0000000..65df51e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "사용자명", + "password": "비밀번호", + "login": "로그인", + "errors": { + "usernameRequired": "사용자명은 필수입니다", + "passwordRequired": "비밀번호는 필수입니다", + "rateLimit": "너무 많이 시도했습니다. 다음에 다시 시도하세요.", + "loginFailed": "로그인 실패", + "unknownError": "알려지지 않은 에러. 로그를 확인하세요.", + "webUnknownError": "알려지지 않은 에러. 콘솔 로그를 확인하세요." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/camera.json new file mode 100644 index 0000000..67b1a2e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "label": "카메라 그룹", + "add": "카메라 그룹 추가", + "edit": "카메라 그룹 편집", + "delete": { + "label": "카메라 그룹 삭제", + "confirm": { + "title": "삭제 확인", + "desc": "정말로 카메라 그룹을 삭제하시겠습니까 {{name}}?" + } + }, + "name": { + "label": "이름", + "placeholder": "이름을 입력하세요…", + "errorMessage": { + "mustLeastCharacters": "카메라 그룹 이름은 최소 2자 이상 써야합니다.", + "exists": "이미 존재하는 카메라 그룹 이름입니다.", + "nameMustNotPeriod": "카메라 그룹 이름에 마침표(.)를 넣을 수 없습니다.", + "invalid": "설정 불가능한 카메라 그룹 이름." + } + }, + "cameras": { + "label": "카메라", + "desc": "이 그룹에 넣을 카메라 선택하기." + }, + "icon": "아이콘", + "success": "카메라 그룹 {{name}} 저장되었습니다.", + "camera": { + "birdseye": "버드아이", + "setting": { + "label": "카메라 스트리밍 설정", + "title": "{{cameraName}} 스트리밍 설정", + "desc": "카메라 그룹 대시보드의 실시간 스트리밍 옵션을 변경하세요. 이 설정은 기기/브라우저에 따라 다릅니다.", + "audioIsAvailable": "이 카메라는 오디오 기능을 사용할 수 있습니다", + "audioIsUnavailable": "이 카메라는 오디오 기능을 사용할 수 없습니다", + "audio": { + "tips": { + "title": "오디오를 출력하려면 카메라가 지원하거나 go2rtc에서 설정해야합니다." + } + }, + "stream": "스트림", + "placeholder": "스트림 선택", + "streamMethod": { + "label": "스트리밍 방식", + "placeholder": "스트리밍 방식 선택", + "method": { + "noStreaming": { + "label": "스트리밍 없음", + "desc": "카메라 이미지는 1분에 한 번만 보여지며 라이브 스트리밍은 되지 않습니다." + }, + "smartStreaming": { + "label": "스마트 스트리밍 (추천함)", + "desc": "스마트 스트리밍은 감지되는 활동이 없을 때 대역폭과 자원을 절약하기 위해 1분마다 한 번 카메라 이미지를 업데이트합니다. 활동이 감지되면, 이미지는 자동으로 라이브 스트림으로 원활하게 전환됩니다." + }, + "continuousStreaming": { + "label": "지속적인 스트리밍", + "desc": { + "title": "활동이 감지되지 않더라도 카메라 이미지가 대시보드에서 항상 실시간 스트림됩니다.", + "warning": "지속적인 스트리밍은 높은 대역폭 사용과 퍼포먼스 이슈를 발생할 수 있습니다. 사용에 주의해주세요." + } + } + } + }, + "compatibilityMode": { + "label": "호환 모드", + "desc": "이 옵션은 카메라 라이브 스트림 화면의 색상이 왜곡 되었거나 이미지 오른쪽에 대각선이 나타날때만 사용하세요." + } + } + } + }, + "debug": { + "options": { + "label": "설정", + "title": "옵션", + "showOptions": "옵션 보기", + "hideOptions": "옵션 숨기기" + }, + "boundingBox": "감지 영역 상자", + "timestamp": "시간 기록", + "zones": "구역 (Zones)", + "mask": "마스크", + "motion": "움직임", + "regions": "영역 (Regions)" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/dialog.json new file mode 100644 index 0000000..f701526 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/dialog.json @@ -0,0 +1,92 @@ +{ + "restart": { + "title": "Frigate을 정말로 다시 시작할까요?", + "button": "재시작", + "restarting": { + "title": "Frigate이 재시작 중입니다", + "content": "이 페이지는 {{countdown}} 뒤에 새로 고침 됩니다.", + "button": "강제 재시작" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Frigate+에 등록하기" + }, + "review": { + "question": { + "label": "Frigate +에 이 레이블 등록하기" + } + } + }, + "video": { + "viewInHistory": "히스토리 보기" + } + }, + "export": { + "time": { + "fromTimeline": "타임라인에서 선택하기", + "lastHour_other": "지난 시간", + "custom": "커스텀", + "start": { + "title": "시작 시간", + "label": "시작 시간 선택" + }, + "end": { + "title": "종료 시간", + "label": "종료 시간 선택" + } + }, + "name": { + "placeholder": "내보내기 이름" + }, + "select": "선택", + "export": "내보내기", + "selectOrExport": "선택 또는 내보내기", + "toast": { + "success": "내보내기가 성공적으로 시작되었습니다. /exports 폴더에서 파일을 보실 수 있습니다.", + "error": { + "failed": "내보내기 시작 실패:{{error}}", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다", + "noVaildTimeSelected": "유효한 시간 범위가 선택되지 않았습니다" + } + }, + "fromTimeline": { + "saveExport": "내보내기 저장", + "previewExport": "내보내기 미리보기" + } + }, + "streaming": { + "label": "스트림", + "restreaming": { + "disabled": "이 카메라는 재 스트리밍이 되지 않습니다.", + "desc": { + "title": "이 카메라를 위해 추가적인 라이브 뷰 옵션과 오디오를 go2rtc에서 설정하세요." + } + }, + "showStats": { + "label": "스트림 통계 보기", + "desc": "이 옵션을 활성화하면 스트림 통계가 카메라 피드에 나타납니다." + }, + "debugView": "디버그 뷰" + }, + "search": { + "saveSearch": { + "label": "검색 저장", + "desc": "저장된 검색에 이름을 지정해주세요.", + "placeholder": "검색에 이름 입력하기", + "overwrite": "{{searchName}} (이/가) 이미 존재합니다. 값을 덮어 씌웁니다.", + "success": "{{searchName}} 검색이 저장되었습니다.", + "button": { + "save": { + "label": "이 검색 저장하기" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "삭제 확인" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/filter.json new file mode 100644 index 0000000..942b97c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/filter.json @@ -0,0 +1,39 @@ +{ + "filter": "필터", + "labels": { + "label": "레이블", + "all": { + "title": "모든 레이블", + "short": "레이블" + } + }, + "zones": { + "label": "구역", + "all": { + "title": "모든 구역", + "short": "구역" + } + }, + "dates": { + "selectPreset": "프리셋 선택", + "all": { + "title": "모든 날짜", + "short": "날짜" + } + }, + "timeRange": "시간 구역", + "subLabels": { + "label": "서브 레이블", + "all": "모든 서브 레이블" + }, + "more": "더 많은 필터", + "classes": { + "label": "분류", + "all": { + "title": "모든 분류" + } + }, + "reset": { + "label": "기본값으로 필터 초기화" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/icons.json new file mode 100644 index 0000000..fb1b47c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "아이콘을 선택해주세요", + "search": { + "placeholder": "아이콘 검색 중…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/input.json new file mode 100644 index 0000000..00a19b7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "비디오 다운로드", + "toast": { + "success": "다시보기 항목 다운로드가 시작되었습니다." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ko/components/player.json new file mode 100644 index 0000000..38ef7da --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/components/player.json @@ -0,0 +1,51 @@ +{ + "submitFrigatePlus": { + "submit": "제출", + "title": "이 프레임을 Frigate+에 제출하시겠습니까?" + }, + "stats": { + "bandwidth": { + "short": "대역폭", + "title": "대역폭:" + }, + "latency": { + "short": { + "title": "지연", + "value": "{{seconds}} 초" + }, + "title": "지연:", + "value": "{{seconds}} 초" + }, + "streamType": { + "short": "종류", + "title": "스트림 종류:" + }, + "totalFrames": "총 프레임:", + "droppedFrames": { + "title": "손실 프레임:", + "short": { + "title": "손실됨", + "value": "{{droppedFrames}} 프레임" + } + }, + "decodedFrames": "복원된 프레임:", + "droppedFrameRate": "프레임 손실률:" + }, + "noRecordingsFoundForThisTime": "이 시간대에는 녹화본이 없습니다", + "noPreviewFound": "미리 보기를 찾을 수 없습니다", + "noPreviewFoundFor": "{{cameraName}}에 미리보기를 찾을 수 없습니다", + "livePlayerRequiredIOSVersion": "이 라이브 스트림 방식은 iOS 17.1 이거나 높은 버전이 필요합니다.", + "streamOffline": { + "title": "스트림 오프라인", + "desc": "{{cameraName}} 카메라에 감지(detect) 스트림의 프레임을 얻지 못했습니다. 에러 로그를 확인하세요" + }, + "cameraDisabled": "카메라를 이용할 수 없습니다", + "toast": { + "success": { + "submittedFrigatePlus": "Frigate+에 프레임이 성공적으로 제출됐습니다" + }, + "error": { + "submitFrigatePlusFailed": "Frigate+에 프레임을 보내지 못했습니다" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ko/objects.json new file mode 100644 index 0000000..e3506b1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/objects.json @@ -0,0 +1,120 @@ +{ + "person": "사람", + "bicycle": "자전거", + "car": "차량", + "motorcycle": "원동기", + "airplane": "비행기", + "bus": "버스", + "train": "기차", + "boat": "보트", + "traffic_light": "신호등", + "fire_hydrant": "소화전", + "street_sign": "도로 표지판", + "stop_sign": "정지 표지판", + "parking_meter": "주차 요금 정산기", + "bench": "벤치", + "bird": "새", + "cat": "고양이", + "dog": "강아지", + "horse": "말", + "sheep": "양", + "cow": "소", + "elephant": "코끼리", + "bear": "곰", + "zebra": "얼룩말", + "giraffe": "기린", + "hat": "모자", + "backpack": "백팩", + "umbrella": "우산", + "shoe": "신발", + "eye_glasses": "안경", + "handbag": "핸드백", + "tie": "타이", + "suitcase": "슈트케이스", + "frisbee": "프리스비", + "skis": "스키", + "snowboard": "스노우보드", + "sports_ball": "스포츠 볼", + "kite": "연", + "baseball_bat": "야구 방망이", + "baseball_glove": "야구 글로브", + "skateboard": "스케이트보드", + "surfboard": "서핑보드", + "tennis_racket": "테니스 라켓", + "bottle": "병", + "plate": "번호판", + "wine_glass": "와인잔", + "cup": "컵", + "fork": "포크", + "knife": "칼", + "spoon": "숟가락", + "bowl": "보울", + "banana": "바나나", + "apple": "사과", + "sandwich": "샌드위치", + "orange": "오렌지", + "broccoli": "브로콜리", + "carrot": "당근", + "hot_dog": "핫도그", + "pizza": "피자", + "donut": "도넛", + "cake": "케이크", + "chair": "의자", + "couch": "소파", + "potted_plant": "화분", + "bed": "침대", + "mirror": "거울", + "dining_table": "식탁", + "window": "창문", + "desk": "책상", + "toilet": "화장실", + "door": "문", + "tv": "TV", + "laptop": "랩탑", + "mouse": "마우스", + "remote": "리모콘", + "keyboard": "키보드", + "cell_phone": "휴대폰", + "microwave": "전자레인지", + "oven": "오븐", + "toaster": "토스터기", + "sink": "싱크대", + "refrigerator": "냉장고", + "blender": "블렌더", + "book": "책", + "clock": "벽시계", + "vase": "꽃병", + "scissors": "가위", + "teddy_bear": "테디베어", + "hair_dryer": "헤어 드라이어", + "toothbrush": "칫솔", + "hair_brush": "빗", + "vehicle": "탈 것", + "squirrel": "다람쥐", + "deer": "사슴", + "animal": "동물", + "bark": "개", + "fox": "여우", + "goat": "염소", + "rabbit": "토끼", + "raccoon": "라쿤", + "robot_lawnmower": "로봇 잔디깎기", + "waste_bin": "쓰레기통", + "on_demand": "수동", + "face": "얼굴", + "license_plate": "차량 번호판", + "package": "패키지", + "bbq_grill": "바베큐 그릴", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/configEditor.json new file mode 100644 index 0000000..bb8a84c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "설정 편집기 - Frigate", + "configEditor": "설정 편집기", + "safeConfigEditor": "설정 편집기 (안전 모드)", + "safeModeDescription": "설정 오류로 인해 Frigate가 안전 모드로 전환되었습니다.", + "copyConfig": "설정 복사", + "saveAndRestart": "저장 & 재시작", + "saveOnly": "저장만 하기", + "confirm": "저장 없이 나갈까요?", + "toast": { + "success": { + "copyToClipboard": "설정이 클립보드에 저장되었습니다." + }, + "error": { + "savingError": "설정 저장 오류" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/events.json new file mode 100644 index 0000000..971494a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/events.json @@ -0,0 +1,51 @@ +{ + "alerts": "경보", + "detections": "대상 감지", + "motion": { + "label": "움직임 감지", + "only": "움직임 감지만" + }, + "allCameras": "모든 카메라", + "empty": { + "alert": "다시 볼 '경보' 영상이 없습니다", + "detection": "다시 볼 '대상 감지' 영상이 없습니다", + "motion": "움직임 감지 데이터가 없습니다" + }, + "timeline": "타임라인", + "timeline.aria": "타임라인 선택", + "events": { + "label": "이벤트", + "aria": "이벤트 선택", + "noFoundForTimePeriod": "이 시간대에 이벤트가 없습니다." + }, + "detail": { + "noDataFound": "다시 볼 상세 데이터가 없습니다", + "aria": "상세 보기", + "trackedObject_one": "추적 대상", + "trackedObject_other": "추적 대상", + "noObjectDetailData": "상세 보기 데이터가 없습니다." + }, + "objectTrack": { + "trackedPoint": "추적 포인트", + "clickToSeek": "이 시점으로 이동" + }, + "documentTitle": "다시 보기 - Frigate", + "recordings": { + "documentTitle": "녹화 - Frigate" + }, + "calendarFilter": { + "last24Hours": "최근 24시간" + }, + "markAsReviewed": "'다시 봤음'으로 표시", + "markTheseItemsAsReviewed": "이 영상들을 '다시 봤음'으로 표시", + "newReviewItems": { + "label": "새로운 '다시 보기' 영상 보기", + "button": "새로운 '다시 보기' 영상" + }, + "selected_one": "{{count}} 선택됨", + "selected_other": "{{count}} 선택됨", + "camera": "카메라", + "detected": "감지됨", + "suspiciousActivity": "수상한 행동", + "threateningActivity": "위협적인 행동" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/explore.json new file mode 100644 index 0000000..231eade --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/explore.json @@ -0,0 +1,31 @@ +{ + "documentTitle": "탐색 - Frigate", + "generativeAI": "생성형 AI", + "exploreMore": "{{label}} 더 많은 감지 대상 탐색하기", + "exploreIsUnavailable": { + "title": "탐색을 사용할 수 없습니다", + "embeddingsReindexing": { + "context": "감지 정보 재처리가 완료되면 탐색할 수 있습니다.", + "startingUp": "시작 중…", + "estimatedTime": "예상 남은시간:", + "finishingShortly": "곧 완료됩니다", + "step": { + "thumbnailsEmbedded": "처리된 썸네일: ", + "descriptionsEmbedded": "처리된 설명: ", + "trackedObjectsProcessed": "처리된 추적 감지: " + } + }, + "downloadingModels": { + "context": "Frigate가 시맨틱 검색 기능을 지원하기 위해 필요한 임베딩 모델을 다운로드하고 있습니다. 네트워크 연결 속도에 따라 몇 분 정도 소요될 수 있습니다.", + "setup": { + "visionModel": "Vision model", + "visionModelFeatureExtractor": "Vision model feature extractor", + "textModel": "Text model", + "textTokenizer": "Text tokenizer" + } + } + }, + "details": { + "timestamp": "시간 기록" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/exports.json new file mode 100644 index 0000000..f4c9026 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "내보내기 - Frigate", + "search": "검색", + "noExports": "내보내기가 없습니다", + "deleteExport": "내보내기 삭제", + "deleteExport.desc": "{{exportName}}을 지우시겠습니까?", + "editExport": { + "title": "내보내기 이름 변경", + "desc": "이 내보내기의 새 이름을 입력하세요.", + "saveExport": "내보내기 저장" + }, + "toast": { + "error": { + "renameExportFailed": "내보내기 이름 변경에 실패했습니다: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/faceLibrary.json new file mode 100644 index 0000000..e1204d8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/faceLibrary.json @@ -0,0 +1,84 @@ +{ + "description": { + "placeholder": "이 모음집의 이름을 입력해주세요", + "addFace": "얼굴 라이브러리에 새 모음집 추가하는 방법을 단계별로 알아보세요.", + "invalidName": "잘못된 이름입니다. 이름은 문자, 숫자, 공백, 따옴표 ('), 밑줄 (_), 그리고 붙임표 (-)만 포함이 가능합니다." + }, + "details": { + "person": "사람", + "subLabelScore": "보조 레이블 신뢰도", + "face": "얼굴 상세정보", + "timestamp": "시간 기록", + "unknown": "알 수 없음" + }, + "selectItem": "{{item}} 선택", + "documentTitle": "얼굴 라이브러리 - Frigate", + "uploadFaceImage": { + "title": "얼굴 사진 올리기" + }, + "collections": "모음집", + "createFaceLibrary": { + "title": "모음집 만들기", + "desc": "새로운 모음집 만들기", + "new": "새 얼굴 만들기" + }, + "steps": { + "faceName": "얼굴 이름 입력", + "uploadFace": "얼굴 사진 올리기", + "nextSteps": "다음 단계" + }, + "train": { + "title": "학습", + "aria": "학습 선택" + }, + "selectFace": "얼굴 선택", + "deleteFaceLibrary": { + "title": "이름 삭제" + }, + "deleteFaceAttempts": { + "title": "얼굴 삭제" + }, + "renameFace": { + "title": "얼굴 이름 바꾸기", + "desc": "{{name}}의 새 이름을 입력하세요" + }, + "button": { + "deleteFaceAttempts": "얼굴 삭제", + "addFace": "얼굴 추가", + "renameFace": "얼굴 이름 바꾸기", + "deleteFace": "얼굴 삭제", + "uploadImage": "이미지 올리기", + "reprocessFace": "얼굴 재조정" + }, + "imageEntry": { + "validation": { + "selectImage": "이미지 파일을 선택해주세요." + }, + "dropActive": "여기에 이미지 놓기…", + "dropInstructions": "이미지를 끌어다 놓거나 여기에 붙여넣으세요. 선택할 수도 있습니다.", + "maxSize": "최대 용량: {{size}}MB" + }, + "nofaces": "얼굴을 찾을 수 없습니다", + "pixels": "{{area}}px", + "trainFaceAs": "얼굴을 다음과 같이 훈련하기:", + "trainFace": "얼굴 훈련하기", + "toast": { + "success": { + "uploadedImage": "이미지 업로드에 성공했습니다.", + "addFaceLibrary": "{{name}} 을 성공적으로 얼굴 라이브러리에 추가했습니다!", + "deletedFace_other": "{{count}} 얼굴을 성공적으로 삭제했습니다.", + "renamedFace": "얼굴 이름을 {{name}} 으로 성공적으로 바꿨습니다", + "trainedFace": "얼굴 훈련을 성공적으로 마쳤습니다.", + "updatedFaceScore": "얼굴 신뢰도를 성공적으로 업데이트 했습니다." + }, + "error": { + "uploadingImageFailed": "이미지 업로드 실패:{{errorMessage}}", + "addFaceLibraryFailed": "얼굴 이름 설정 실패:{{errorMessage}}", + "deleteFaceFailed": "삭제 실패:{{errorMessage}}", + "deleteNameFailed": "이름 삭제 실패:{{errorMessage}}", + "renameFaceFailed": "이름 바꾸기 실패:{{errorMessage}}", + "trainFailed": "훈련 실패:{{errorMessage}}", + "updateFaceScoreFailed": "얼굴 신뢰도 업데이트 실패:{{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/live.json new file mode 100644 index 0000000..bfc44d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/live.json @@ -0,0 +1,183 @@ +{ + "documentTitle": "실시간 보기 - Frigate", + "documentTitle.withCamera": "{{camera}} - 실시간 보기 - Frigate", + "lowBandwidthMode": "저대역폭 모드", + "twoWayTalk": { + "enable": "양방향 말하기 활성화", + "disable": "양방향 말하기 비활성화" + }, + "cameraAudio": { + "enable": "카메라 오디오 활성화", + "disable": "카메라 오디오 비활성화" + }, + "ptz": { + "move": { + "clickMove": { + "label": "클릭해서 카메라 중앙 배치", + "enable": "클릭해서 움직이기 기능 활성화", + "disable": "클릭해서 움직이기 기능 비활성화" + }, + "left": { + "label": "PTZ 카메라 왼쪽으로 이동" + }, + "up": { + "label": "PTZ 카메라 위로 이동" + }, + "down": { + "label": "PTZ 카메라 아래로 이동" + }, + "right": { + "label": "PTZ 카메라 오른쪽으로 이동" + } + }, + "zoom": { + "in": { + "label": "PTZ 카메라 확대" + }, + "out": { + "label": "PTZ 카메라 축소" + } + }, + "focus": { + "in": { + "label": "PTZ 카메라 포커스 인" + }, + "out": { + "label": "PTZ 카메라 포커스 아웃" + } + }, + "frame": { + "center": { + "label": "클릭해서 PTZ 카메라 중앙 배치" + } + }, + "presets": "PTZ 카메라 프리셋" + }, + "camera": { + "enable": "카메라 활성화", + "disable": "카메라 비활성화" + }, + "muteCameras": { + "enable": "모든 카메라 음소거", + "disable": "모든 카메라 음소거 해제" + }, + "detect": { + "enable": "감지 활성화", + "disable": "감지 비활성화" + }, + "recording": { + "enable": "녹화 활성화", + "disable": "녹화 비활성화" + }, + "snapshots": { + "enable": "스냅샷 활성화", + "disable": "스냅샷 비활성화" + }, + "audioDetect": { + "enable": "오디오 감지 활성화", + "disable": "오디오 감지 비활성화" + }, + "transcription": { + "enable": "실시간 오디오 자막 활성화", + "disable": "실시간 오디오 자막 비활성화" + }, + "autotracking": { + "enable": "자동 추적 활성화", + "disable": "자동 추적 비활성화" + }, + "streamStats": { + "enable": "스트림 통계 보기", + "disable": "스트림 통계 숨기기" + }, + "manualRecording": { + "title": "수동 녹화", + "tips": "이 카메라의 녹화 보관 설정에 따라 인스턴트 스냅샷을 다운로드하거나 수동 녹화를 시작할 수 있습니다.", + "playInBackground": { + "label": "백그라운드에서 재생", + "desc": "이 옵션을 활성화하면 플레이어가 숨겨져도 계속 스트리밍됩니다." + }, + "showStats": { + "label": "통계 보기", + "desc": "이 옵션을 활성화하면 카메라 피드에 스트림 통계가 나타납니다." + }, + "debugView": "디버그 보기", + "start": "수동 녹화 시작", + "started": "수동 녹화 시작되었습니다.", + "failedToStart": "수동 녹화 시작이 실패했습니다.", + "recordDisabledTips": "이 카메라 설정에서 녹화가 비활성화 되었거나 제한되어 있어 스냅샷만 저장됩니다.", + "end": "수동 녹화 종료", + "ended": "수동 녹화가 종료되었습니다.", + "failedToEnd": "수동 녹화 종료가 실패했습니다." + }, + "streamingSettings": "스트리밍 설정", + "notifications": "알림", + "audio": "오디오", + "suspend": { + "forTime": "일시정지 시간: " + }, + "stream": { + "title": "스트림", + "audio": { + "available": "이 스트림에서 오디오를 사용할 수 있습니다", + "unavailable": "이 스트림에서 오디오를 사용할 수 없습니다", + "tips": { + "title": "이 스트림에서 오디오를 사용하려면 카메라에서 오디오를 출력하고 go2rtc에서 설정해야 합니다." + } + }, + "debug": { + "picker": "디버그 모드에선 스트림 모드를 선택할 수 없습니다. 디버그 뷰에서는 항상 감지(Detect) 역할로 설정한 스트림을 사용합니다." + }, + "twoWayTalk": { + "tips": "양방향 말하기 기능을 사용하려면 기기에서 기능을 지원해야하며 WebRTC를 설정해야합니다.", + "available": "이 기기는 양방향 말하기 기능을 사용할 수 있습니다", + "unavailable": "이 기기는 양방향 말하기 기능을 사용할 수 없습니다" + }, + "lowBandwidth": { + "tips": "버퍼링 또는 스트림 오류로 실시간 화면을 저대역폭 모드로 변경되었습니다.", + "resetStream": "스트림 리셋" + }, + "playInBackground": { + "label": "백그라운드에서 재생", + "tips": "이 옵션을 활성화하면 플레이어가 숨겨져도 스트리밍이 지속됩니다." + } + }, + "cameraSettings": { + "title": "{{camera}} 설정", + "cameraEnabled": "카메라 활성화", + "objectDetection": "대상 감지", + "recording": "녹화", + "snapshots": "스냅샷", + "audioDetection": "오디오 감지", + "transcription": "오디오 자막", + "autotracking": "자동 추적" + }, + "history": { + "label": "이전 영상 보기" + }, + "effectiveRetainMode": { + "modes": { + "all": "전체", + "motion": "움직임 감지", + "active_objects": "활성 대상" + }, + "notAllTips": "{{source}} 녹화 보관 설정이 mode: {{effectiveRetainMode}}로 되어 있어, 이 수동 녹화는 {{effectiveRetainModeName}}이(가) 있는 구간만 저장됩니다." + }, + "editLayout": { + "label": "레이아웃 편집", + "group": { + "label": "카메라 그룹 편집" + }, + "exitEdit": "편집 종료" + }, + "noCameras": { + "title": "설정된 카메라 없음", + "description": "카메라를 연결해 시작하세요.", + "buttonText": "카메라 추가" + }, + "snapshot": { + "takeSnapshot": "인스턴트 스냅샷 다운로드", + "noVideoSource": "스냅샷 찍을 비디오 소스가 없습니다.", + "captureFailed": "스냅샷 캡쳐를 하지 못했습니다.", + "downloadStarted": "스냅샷 다운로드가 시작됐습니다." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/recording.json new file mode 100644 index 0000000..2aa9934 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "필터", + "export": "내보내기", + "calendar": "날짜", + "filters": "필터", + "toast": { + "error": { + "noValidTimeSelected": "올바른 시간 범위를 선택하세요", + "endTimeMustAfterStartTime": "종료 시간은 시작 시간보다 뒤에 있어야합니다" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/search.json new file mode 100644 index 0000000..f7a6cfd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/search.json @@ -0,0 +1,7 @@ +{ + "search": "검색", + "savedSearches": "저장된 검색들", + "button": { + "clear": "검색 초기화" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/settings.json new file mode 100644 index 0000000..a5b1d55 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/settings.json @@ -0,0 +1,122 @@ +{ + "triggers": { + "dialog": { + "form": { + "threshold": { + "title": "임계치" + }, + "name": { + "title": "이름" + }, + "type": { + "title": "종류", + "placeholder": "트리거 종류 선택" + } + }, + "createTrigger": { + "title": "트리거 생성" + } + }, + "actions": { + "notification": "알림 전송" + } + }, + "documentTitle": { + "default": "설정 - Frigate", + "authentication": "인증 설정 - Frigate", + "camera": "카메라 설정 - Frigate", + "enrichments": "고급 설정 - Frigate", + "masksAndZones": "마스크와 구역 편집기 - Frigate", + "motionTuner": "움직임 감지 조정 - Frigate", + "object": "디버그 - Frigate", + "general": "일반 설정 - Frigate", + "frigatePlus": "Frigate+ 설정 - Frigate", + "notifications": "알림 설정 - Frigate", + "cameraManagement": "카메라 관리 - Frigate", + "cameraReview": "카메라 다시보기 설정 - Frigate" + }, + "users": { + "table": { + "actions": "액션" + } + }, + "menu": { + "ui": "UI", + "enrichments": "고급", + "cameras": "카메라 설정", + "masksAndZones": "마스크 / 구역", + "motionTuner": "움직임 감지 조정", + "triggers": "트리거", + "debug": "디버그", + "users": "사용자", + "roles": "역할", + "notifications": "알림", + "frigateplus": "Frigate+", + "cameraManagement": "관리", + "cameraReview": "다시보기" + }, + "dialog": { + "unsavedChanges": { + "title": "저장되지 않은 변경 사항이 있습니다.", + "desc": "계속하기 전에 변경 사항을 저장하시겠습니까?" + } + }, + "cameraSetting": { + "camera": "카메라", + "noCamera": "카메라 없음" + }, + "general": { + "title": "일반 세팅", + "liveDashboard": { + "title": "실시간 보기 대시보드", + "automaticLiveView": { + "label": "자동으로 실시간 보기 전환", + "desc": "활동이 감지되면 자동으로 실시간 보기로 전환합니다. 이 옵션을 끄면 대시보드의 카메라 화면은 1분마다 한 번만 갱신됩니다." + }, + "playAlertVideos": { + "label": "경보 영상 보기", + "desc": "기본적으로 실시간 보기 대시보드의 최근 경보 영상을 작은 반복 영상으로 재생됩니다. 이 옵션을 끄면 이 기기(또는 브라우저)에서는 정적 이미지로만 표시됩니다." + } + }, + "storedLayouts": { + "title": "저장된 레이아웃", + "desc": "카메라 그룹의 화면 배치는 드래그하거나 크기를 조정할 수 있습니다. 변경된 위치는 브라우저의 로컬 저장소에 저장됩니다.", + "clearAll": "레이아웃 지우기" + }, + "cameraGroupStreaming": { + "title": "카메라 그룹 스트리밍 설정", + "desc": "각각의 카메라 그룹의 스트리밍 설정은 브라우저의 로컬 저장소에 저장됩니다.", + "clearAll": "스트리밍 설정 모두 지우기" + }, + "recordingsViewer": { + "title": "녹화 영상 보기", + "defaultPlaybackRate": { + "label": "기본으로 설정된 다시보기 배속", + "desc": "다시보기 영상 재생할 때 기본 배속을 설정합니다." + } + }, + "calendar": { + "title": "캘린더", + "firstWeekday": { + "label": "주 첫째날", + "desc": "다시보기 캘린더에서 주가 시작되는 첫째날을 설정합니다.", + "sunday": "일요일", + "monday": "월요일" + } + }, + "toast": { + "success": { + "clearStoredLayout": "{{cameraName}}의 레이아웃을 지웠습니다", + "clearStreamingSettings": "모든 카메라 그룹 스트리밍 설정을 지웠습니다." + }, + "error": { + "clearStoredLayoutFailed": "레이아웃 지우기에 실패했습니다:{{errorMessage}}", + "clearStreamingSettingsFailed": "카메라 스트리밍 설정 지우기에 실패했습니다:{{errorMessage}}" + } + } + }, + "enrichments": { + "title": "고급 설정", + "unsavedChanges": "변경된 고급 설정을 저장하지 않았습니다" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ko/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ko/views/system.json new file mode 100644 index 0000000..4ed89d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ko/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "카메라 통계 - Frigate", + "storage": "저장소 통계 - Frigate", + "general": "기본 통계 - Frigate", + "enrichments": "고급 통계 - Frigate", + "logs": { + "frigate": "Frigate 로그 -Frigate", + "go2rtc": "Go2RTC 로그 - Frigate", + "nginx": "Nginx 로그 - Frigate" + } + }, + "title": "시스템", + "metrics": "시스템 통계", + "logs": { + "download": { + "label": "다운로드 로그" + }, + "copy": { + "label": "클립보드에 복사하기", + "success": "클립보드에 로그가 복사되었습니다", + "error": "클립보드에 로그를 저장할 수 없습니다" + }, + "type": { + "label": "타입", + "timestamp": "시간 기록", + "tag": "태그", + "message": "메시지" + }, + "tips": "서버에서 로그 스트리밍 중", + "toast": { + "error": { + "fetchingLogsFailed": "로그 가져오기 오류: {{errorMessage}}", + "whileStreamingLogs": "스크리밍 로그 중 오류: {{errorMessage}}" + } + } + }, + "general": { + "title": "기본", + "detector": { + "title": "감지기", + "inferenceSpeed": "감지 추론 속도", + "temperature": "감지기 온도", + "cpuUsage": "감지기 CPU 사용률", + "memoryUsage": "감지기 메모리 사용률", + "cpuUsageInformation": "감지 모델로 데이터를 입력/출력하기 위한 전처리 과정에서 사용되는 CPU 사용량입니다. GPU나 가속기를 사용하는 경우에도 추론 자체의 사용량은 포함되지 않습니다." + }, + "hardwareInfo": { + "title": "하드웨어 정보", + "gpuUsage": "GPU 사용률", + "gpuMemory": "GPU 메모리", + "gpuEncoder": "GPU 인코더", + "gpuDecoder": "GPU 디코더", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 출력", + "processOutput": "프로세스 출력:", + "processError": "프로세스 오류:", + "returnCode": "리턴 코드:{{code}}" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 출력", + "name": "이름:{{name}}", + "driver": "드라이버:{{driver}}", + "cudaComputerCapability": "CUDA Compute Capability:{{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "copyInfo": { + "label": "GPU 정보 복사" + }, + "toast": { + "success": "GPU 정보가 클립보드에 복사되었습니다" + }, + "closeInfo": { + "label": "GPU 정보 닫기" + } + }, + "npuUsage": "NPU 사용률", + "npuMemory": "NPU 메모리" + }, + "otherProcesses": { + "title": "다른 프로세스들", + "processCpuUsage": "사용중인 CPU 사용률", + "processMemoryUsage": "사용중인 메모리 사용률" + } + }, + "storage": { + "title": "스토리지", + "overview": "전체 현황", + "recordings": { + "title": "녹화", + "tips": "이 값은 Frigate 데이터베이스의 녹화 영상이 사용 중인 전체 저장 공간입니다. Frigate는 디스크 내 다른 파일들의 저장 공간은 추적하지 않습니다.", + "earliestRecording": "가장 오래된 녹화 영상:" + }, + "cameraStorage": { + "title": "카메라 저장소", + "camera": "카메라", + "unusedStorageInformation": "미사용 저장소 정보", + "storageUsed": "용량", + "percentageOfTotalUsed": "전체 대비 비율", + "bandwidth": "대역폭", + "unused": { + "title": "미사용", + "tips": "드라이브에 Frigate 녹화 영상 외에 다른 파일이 저장되어 있는 경우, 이 값은 Frigate에서 실제 사용 가능한 여유 공간을 정확히 나타내지 않을 수 있습니다. Frigate는 녹화 영상 외의 저장 공간 사용량을 추적하지 않습니다." + } + }, + "shm": { + "title": "SHM (공유 메모리) 할당량", + "warning": "현재 SHM 사이즈가 {{total}}MB로 너무 적습니다. 최소 {{min_shm}}MB 이상 올려주세요." + } + }, + "cameras": { + "title": "카메라", + "overview": "전체 현황", + "info": { + "aspectRatio": "종횡비", + "fetching": "카메라 데이터 수집 중", + "stream": "스트림 {{idx}}", + "streamDataFromFFPROBE": "스트림 데이터는 ffprobe에서 받습니다.", + "video": "비디오:", + "codec": "코덱:", + "resolution": "해상도:", + "fps": "FPS:", + "unknown": "알 수 없음", + "audio": "오디오:", + "error": "오류:{{error}}", + "cameraProbeInfo": "{{camera}} 카메라 장치 정보", + "tips": { + "title": "카메라 장치 정보" + } + }, + "framesAndDetections": "프레임 / 감지 (Detections)", + "label": { + "camera": "카메라", + "detect": "감지", + "skipped": "건너뜀", + "ffmpeg": "FFmpeg", + "capture": "캡쳐", + "overallFramesPerSecond": "전체 초당 프레임", + "overallDetectionsPerSecond": "전체 초당 감지", + "overallSkippedDetectionsPerSecond": "전체 초당 건너뛴 감지", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} 캡쳐", + "cameraDetect": "{{camName}} 감지", + "cameraFramesPerSecond": "{{camName}} 초당 프레임", + "cameraDetectionsPerSecond": "{{camName}} 초당 감지", + "cameraSkippedDetectionsPerSecond": "{{camName}} 초당 건너뛴 감지" + }, + "toast": { + "success": { + "copyToClipboard": "데이터 정보가 클립보드에 복사되었습니다." + }, + "error": { + "unableToProbeCamera": "카메라 정보 알 수 없음: {{errorMessage}}" + } + } + }, + "lastRefreshed": "마지막 새로고침: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} FFmpeg CPU 사용량이 높습니다 ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 감지 CPU 사용량이 높습니다 ({{detectAvg}}%)", + "healthy": "시스템 정상", + "reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)", + "cameraIsOffline": "{{camera}} 오프라인입니다", + "detectIsSlow": "{{detect}} (이/가) 느립니다 ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} (이/가) 매우 느립니다 ({{speed}} ms)", + "shmTooLow": "/dev/shm 할당량을 ({{total}} MB) 최소 {{min}} MB 이상 증가시켜야합니다." + }, + "enrichments": { + "title": "추가 분석 정보", + "infPerSecond": "초당 추론 속도", + "embeddings": { + "image_embedding": "이미지 임베딩", + "text_embedding": "텍스트 임베딩", + "face_recognition": "얼굴 인식", + "plate_recognition": "번호판 인식", + "image_embedding_speed": "이미지 임베딩 속도", + "face_embedding_speed": "얼굴 임베딩 속도", + "face_recognition_speed": "얼굴 인식 속도", + "plate_recognition_speed": "번호판 인식 속도", + "text_embedding_speed": "텍스트 임베딩 속도", + "yolov9_plate_detection_speed": "YOLOv9 플레이트 감지 속도", + "yolov9_plate_detection": "YOLOv9 플레이트 감지" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/audio.json b/sam2-cpu/frigate-dev/web/public/locales/lt/audio.json new file mode 100644 index 0000000..2e8d481 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/audio.json @@ -0,0 +1,429 @@ +{ + "camera": "Kamera", + "speech": "Kalbėjimas", + "bicycle": "Dviratis", + "car": "Automobilis", + "motorcycle": "Motociklas", + "bus": "Autobusas", + "train": "Traukinys", + "boat": "Valtis", + "bird": "Paukštis", + "cat": "Katė", + "dog": "Šuo", + "horse": "Arklys", + "sheep": "Avis", + "babbling": "Burbėjimas", + "yell": "Šūksnis", + "skateboard": "Riedlentė", + "door": "Durys", + "mouse": "Pelė", + "keyboard": "Klaviatūra", + "sink": "Kriauklė", + "blender": "Plakiklis", + "clock": "Laikrodis", + "scissors": "Žirklės", + "hair_dryer": "Plaukų Džiovintuvas", + "toothbrush": "Dantų šepetėlis", + "vehicle": "Mašina", + "animal": "Gyvūnas", + "bark": "Lojimas", + "goat": "Ožka", + "bellow": "Apačioje", + "whoop": "Rėkavimas", + "whispering": "Šnabždėjimas", + "laughter": "Juokas", + "snicker": "Kikenimas", + "crying": "Verkimas", + "singing": "Dainavimas", + "sigh": "Atodūsis", + "choir": "Choras", + "yodeling": "Jodliavimas", + "chant": "Giedojimas", + "mantra": "Mantra", + "child_singing": "Dainuoja Vaikas", + "synthetic_singing": "Netikras Dainavimas", + "rapping": "Repavimas", + "humming": "Dūzgimas", + "groan": "Dejuoti", + "grunt": "Niurzgėti", + "whistling": "Švilpti", + "breathing": "Kvepavimas", + "wheeze": "Švokštimas", + "snoring": "Knarkimas", + "gasp": "Aiktelėti", + "pant": "Kelnės", + "snort": "Knarkti", + "cough": "Kosėti", + "throat_clearing": "Atsikrenkšti", + "sneeze": "Čiaudėti", + "sniff": "Uostyti", + "run": "Bėgti", + "shuffle": "Maišyti", + "footsteps": "Žingsniai", + "chewing": "Kramtymas", + "biting": "Kandžiojimas", + "gargling": "Skalavimas", + "stomach_rumble": "Pilvo gurguliavimas", + "burping": "Atsirūgimas", + "hiccup": "Žaksėjimas", + "fart": "Bezdėjimas", + "hands": "Rankos", + "finger_snapping": "Spragsėjimas", + "clapping": "Plojimas", + "heartbeat": "Širdies plakimas", + "heart_murmur": "Širdies Ūžesys", + "cheering": "Džiūgavimas", + "applause": "Aplodismentai", + "chatter": "Plepėti", + "crowd": "Minia", + "children_playing": "Žaidžiantys Vaikai", + "pets": "Gyvūnai", + "yip": "Cyptelėjimas", + "howl": "Kaukimas", + "whimper_dog": "Šuns inkštimas", + "growling": "Urzgimas", + "bow_wow": "Au au", + "purr": "Murkimas", + "meow": "Miaukimas", + "hiss": "Šnypštimas", + "livestock": "Gyvuliai", + "caterwaul": "Kniaukimas", + "clip_clop": "Kanopų Kaukšėjimas", + "neigh": "Prunkštimas", + "moo": "Mūkimas", + "cattle": "Galvijai", + "cowbell": "Karvutės Varpelis", + "pig": "Kiaulė", + "oink": "Kriuksėjimas", + "bleat": "Bliovimas", + "chicken": "Višta", + "cock_a_doodle_doo": "Kakuriakuoti", + "cluck": "Kudakavimas", + "fowl": "Paukščiai", + "turkey": "Kalakutas", + "gobble": "Gargaliavimas", + "duck": "Antis", + "quack": "Kreksėjimas", + "goose": "Žąsis", + "wild_animals": "Laukiniai Gyvūnai", + "honk": "Gagenimas", + "roar": "Riaumoti", + "roaring_cats": "Riaumojančios Katės", + "pigeon": "Balandis", + "chirp": "Čiulbėti", + "crow": "Varna", + "squawk": "Klykimas", + "coo": "Ku", + "owl": "Pelėda", + "caw": "Kranksėjimas", + "hoot": "Ūkti", + "flapping_wings": "Sparnų plazdėjimas", + "dogs": "Šunys", + "rats": "Žiurkės", + "insect": "Vabzdžiai", + "cricket": "Svirpliai", + "mosquito": "Uodai", + "fly": "Musės", + "buzz": "Užėsys", + "patter": "Tekšėjimas", + "frog": "Varlė", + "snake": "Gyvatė", + "croak": "Kvarksėti", + "rattle": "Barškėti", + "whale_vocalization": "Banginio Įgarsinimas", + "music": "Muzika", + "musical_instrument": "Muzikinis Instrumentas", + "plucked_string_instrument": "Sugedęs Styginis Instrumentas", + "guitar": "Gitara", + "electric_guitar": "Elektrinė Gitara", + "bass_guitar": "Bosinė Gitara", + "acoustic_guitar": "Akustinė Gitara", + "steel_guitar": "Metalinė Gitara", + "sitar": "Sitara", + "mandolin": "Mandolina", + "ukulele": "Ukulėle", + "piano": "Pianinas", + "electric_piano": "Elektrinis pianinas", + "organ": "Vargonai", + "banjo": "Bandžia", + "scream": "Rėkti", + "field_recording": "Įrašinėjimas lauke", + "radio": "Radijas", + "television": "Televizija", + "white_noise": "Baltasis triukšmas", + "pink_noise": "Rožinis triukšmas", + "silence": "Tyla", + "shatter": "Dūžimas", + "glass": "Stiklas", + "crack": "Trūkimas", + "wood": "Medis", + "chop": "Kapojimas", + "boom": "Bumtėlti", + "eruption": "Išsiveržimas", + "fireworks": "Fejerverkai", + "artillery_fire": "Artilerinė ugnis", + "explosion": "Sprogimas", + "drill": "Grežimas", + "sanding": "Šveisti", + "power_tool": "Elektriniai įrankiai", + "machine_gun": "Kulkosvaidis", + "filing": "Dildinti", + "sawing": "Pjauti", + "jackhammer": "Kūjis", + "hammer": "Plaktukas", + "tools": "Įrankiai", + "printer": "Spausdintuvas", + "cash_register": "Kasos Aparatas", + "air_conditioning": "Oro Kondicionavimas", + "sewing_machine": "Siuvimo Mašina", + "pulleys": "Skriemulys", + "gears": "Dantračiai", + "tick-tock": "Tiksėjimas", + "tick": "Tik", + "mechanisms": "Mechanizmas", + "whistle": "Švilpimas", + "steam_whistle": "Garinis Švilpimas", + "fire_alarm": "Gaistro Signalas", + "smoke_detector": "Dūmų detektorius", + "siren": "Sirena", + "alarm_clock": "Žadintuvas", + "telephone": "Telefonas", + "writing": "Rašymas", + "shuffling_cards": "Kortų Maišymas", + "zipper": "Užtrauktukas", + "electric_toothbrush": "Elektrinis Dantų Šepetėlis", + "tapping": "Tapsėjimas", + "strum": "Brazdėjimas", + "electronic_organ": "Elektriniai Vargonai", + "hammond_organ": "Hammond Vargonai", + "synthesizer": "Sintezatorius", + "sampler": "Sampleris", + "harpsichord": "Fortepionas", + "percussion": "Perkusija", + "drum_kit": "Būgnų Rinkinys", + "drum_machine": "Būgnų Mašina", + "drum": "Būgnas", + "snare_drum": "Snare Būgnas", + "timpani": "Timpanas", + "tabla": "Tabla", + "cymbal": "Cimbala", + "hi_hat": "Lėkštės", + "wood_block": "Medienos Lentgalis", + "tambourine": "Tamburinas", + "maraca": "Maraka", + "gong": "Gongas", + "tubular_bells": "Vamzdiniai Varpeliai", + "mallet_percussion": "Malet Perkusija", + "vibraphone": "Vibrafonas", + "steelpan": "Metalinė Lėkštė", + "orchestra": "Orkestras", + "brass_instrument": "Variniai Instrumentai", + "trombone": "Trombonas", + "string_section": "Stygų Sekcija", + "violin": "Smuikas", + "double_bass": "Dvigubas Bosas", + "wind_instrument": "Vėjo Instrumentas", + "flute": "Fleita", + "saxophone": "Saksofonas", + "clarinet": "Klarnetas", + "bell": "Varpas", + "church_bell": "Bažnyčios Varpas", + "jingle_bell": "Kalėdinis Varpelis", + "bicycle_bell": "Dviračio Skambutis", + "tuning_fork": "Derinimo Šakutė", + "chime": "Skambesys", + "wind_chime": "Vėjo Skambesys", + "harmonica": "Lūpinė armonika", + "accordion": "Akordionas", + "bagpipes": "Dūdmaišis", + "pop_music": "Pop Muzika", + "hip_hop_music": "Hip-Hop Muzika", + "beatboxing": "Beatboksingas", + "rock_music": "Roko Muzika", + "heavy_metal": "Sunkusis Metalas", + "punk_rock": "Pank Rokas", + "progressive_rock": "Progresyvus Rokas", + "rock_and_roll": "Rokenrolas", + "psychedelic_rock": "Psichodelinis Rokas", + "rhythm_and_blues": "Ritmbliuzas", + "reggae": "Regis", + "swing_music": "Swingas", + "folk_music": "Liaudies Muzikas", + "middle_eastern_music": "Viduriniųjų Rytų Muzika", + "jazz": "Jazas", + "disco": "Disko", + "classical_music": "Klasikinė Muzika", + "opera": "Opera", + "electronic_music": "Elektroninė Muzika", + "house_music": "House Muzika", + "techno": "Techno", + "dubstep": "Dubstepas", + "electronica": "Elektroninė", + "electronic_dance_music": "Elektroninė Šokių Muzika", + "trance_music": "Transo Muzika", + "music_of_latin_america": "Lotynų Amerikos Muzika", + "salsa_music": "Salsa Muzika", + "flamenco": "Flamenko", + "blues": "Bliuzas", + "music_for_children": "Vaikų Muzika", + "vocal_music": "Vokalinė Muzika", + "a_capella": "Akapela", + "music_of_africa": "Afrikietiška Muzika", + "christian_music": "Krikščioniška Muzika", + "gospel_music": "Gospelo Muzika", + "music_of_asia": "Azijietiška Muzika", + "music_of_bollywood": "Bolivudo Muzika", + "traditional_music": "Tradicinė Muzika", + "song": "Daina", + "background_music": "Foninė Muzika", + "theme_music": "Teminė Muzika", + "jingle": "Džinglas", + "soundtrack_music": "Garsotakelio Muzika", + "lullaby": "Lopšinė", + "video_game_music": "Video Žaidimų Muzika", + "christmas_music": "Kalėdinė Muzika", + "dance_music": "Šokių Muzika", + "wedding_music": "Vestuvinė Muzika", + "sad_music": "Liūdna Muzika", + "happy_music": "Laiminga Muzika", + "angry_music": "Pikta Muzika", + "scary_music": "Gązdinanti Muzika", + "wind": "Vėjas", + "rustling_leaves": "Šlamantys Lapai", + "wind_noise": "Vėjo Švilpimas", + "thunderstorm": "Perkūnija", + "thunder": "Griaustinis", + "water": "Vanduo", + "rain": "Lietus", + "raindrop": "Lietaus Lašai", + "rain_on_surface": "Lija ant Paviršiaus", + "stream": "Srovė", + "waterfall": "Krioklys", + "ocean": "Okeanas", + "waves": "Bangos", + "steam": "Garai", + "gurgling": "Gurguliavimas", + "fire": "Ugnis", + "crackle": "Spragėjimas", + "sailboat": "Burlaivis", + "rowboat": "Irklinė valtis", + "motorboat": "Motorinė Valtis", + "ship": "Laivas", + "motor_vehicle": "Motorinis Transportas", + "car_alarm": "Mašinos Signalizacija", + "power_windows": "Elektriniai Langai", + "tire_squeal": "Padangų cypimas", + "car_passing_by": "Pravažiuojanti Mašina", + "race_car": "Lenktyninė Mašina", + "truck": "Sunkvežimis", + "air_brake": "Oro Stabdis", + "reversing_beeps": "Atbulinės Eigos Signalas", + "ice_cream_truck": "Ledų Mašina", + "emergency_vehicle": "Pagalbos Transportas", + "police_car": "Policijos Mašina", + "ambulance": "Greitoji", + "fire_engine": "Užvesti Variklis", + "traffic_noise": "Esimo Triukšmas", + "train_whistle": "Traukinio Švilpimas", + "train_horn": "Traukinio Pypsėjimas", + "subway": "Metro", + "aircraft": "Orlaivis", + "aircraft_engine": "Orlaivio Variklis", + "jet_engine": "Reaktyvinis Variklis", + "propeller": "Propeleris", + "helicopter": "Malūnsparnis", + "fixed-wing_aircraft": "Fiksuotų Sparnų Orlaivis", + "engine": "Variklis", + "light_engine": "Mažas Variklis", + "dental_drill's_drill": "Dantų Gręžimas", + "lawn_mower": "Žoliapjovė", + "chainsaw": "Grandininis Pjūklas", + "medium_engine": "Vidutinis Variklis", + "heavy_engine": "Didelis Variklis", + "engine_knocking": "Variklio Kalimas", + "engine_starting": "Užsikuriantis Variklis", + "idling": "Laisvai Dirbantis", + "accelerating": "Įsibegėjantis", + "doorbell": "Dūrų Skambutis", + "sliding_door": "Slankiojančios Durys", + "slam": "Trenkti", + "knock": "Stuksėti", + "tap": "Tapšnoti", + "cupboard_open_or_close": "Spintelė Atidaryti ar Užsidaryti", + "drawer_open_or_close": "Stalčių Atidaryti ar Uždaryti", + "dishes": "Indai", + "cutlery": "Stalo Įrankiai", + "chopping": "Kapoti", + "static": "Statinis", + "environmental_noise": "Aplinkos Triukšmas", + "sound_effect": "Garso efektai", + "firecracker": "Ugnies Spragėjimas", + "gunshot": "Ginklo Šūvis", + "single-lens_reflex_camera": "Veidrodinis Fotoparatas", + "mechanical_fan": "Mechaninis Fenas", + "ratchet": "Raketė", + "civil_defense_siren": "Civilinės Saugos Sirena", + "busy_signal": "Užimtas Signalas", + "dial_tone": "Numerio Rinkimo Tonas", + "telephone_dialing": "Telefono Rinkimas", + "ringtone": "Skambėjimo Tonas", + "telephone_bell_ringing": "Skamba Telefonas", + "alarm": "Signalizacija", + "computer_keyboard": "Kopiuterio Klaviatūra", + "typewriter": "Spausdinimo Mašina", + "typing": "Spausdinti", + "electric_shaver": "Barzdaskutė", + "coin": "Moneta", + "keys_jangling": "Žvangantys Raktai", + "vacuum_cleaner": "Siurblys", + "toilet_flush": "Tualeto Nuleidimas", + "bathtub": "Vonia", + "water_tap": "Vandens Kranas", + "microwave_oven": "Mikorbangų Krosnelė", + "frying": "Gruzdinimas", + "zither": "Citara", + "rimshot": "Mušimas per kraštą", + "drum_roll": "Būgno dundesys", + "bass_drum": "Bosinis Būgnas", + "marimba": "Marimba", + "glockenspiel": "Varpelis", + "french_horn": "Prancūzų Ragas", + "trumpet": "Trimitas", + "bowed_string_instrument": "Styginiai Instrumentai", + "pizzicato": "Pizikatas", + "cello": "Violončelė", + "harp": "Arfa", + "didgeridoo": "Didžeridū", + "theremin": "Tereminas", + "singing_bowl": "Dainuojantis Dubuo", + "scratching": "Skrečavimas", + "grunge": "Grandžas", + "soul_music": "Soul Muzika", + "country": "Country Muzika", + "bluegrass": "Bluegrass", + "funk": "Funk", + "drum_and_bass": "Drum & Bass", + "ambient_music": "Ambient Muzika", + "new-age_music": "Naujojo Amžiaus Muzika", + "afrobeat": "Afrikietiški Ritmai", + "carnatic_music": "Karnatietiška Muzika", + "ska": "Ska", + "independent_music": "Nepriklausoma Muzika", + "tender_music": "Švelni Muzika", + "exciting_music": "Jaudinanti Muzika", + "toot": "Pyptelėjimas", + "skidding": "Slydimas", + "air_horn": "Klaksonas", + "rail_transport": "Bėginis Transportas", + "railroad_car": "Geležinkelio Vagonas", + "train_wheels_squealing": "Cypiantys Traukino Ratai", + "ding-dong": "Ding-Dong", + "squeak": "Cypimas", + "buzzer": "Skambutis", + "foghorn": "Rūko Sirena", + "fusillade": "Šaudymas", + "cap_gun": "Kapsulinis Pistoletas", + "burst": "Sprogimas", + "splinter": "Skeveldra", + "chink": "Skambėjimas" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/common.json b/sam2-cpu/frigate-dev/web/public/locales/lt/common.json new file mode 100644 index 0000000..0930c68 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/common.json @@ -0,0 +1,285 @@ +{ + "time": { + "untilForTime": "Iki {{time}}", + "untilForRestart": "Iki kol Frigate persikraus.", + "untilRestart": "Iki perkrovimo", + "ago": "prieš {{timeAgo}}", + "justNow": "Ką tik", + "today": "Šiandien", + "yesterday": "Vakar", + "last7": "Paskutinės 7 dienos", + "last14": "Paskutinės 14 dienų", + "last30": "Paskutinės 30 dienų", + "thisWeek": "Šią Savaitę", + "lastWeek": "Praeitą Savaitę", + "thisMonth": "Šį Mėnesį", + "lastMonth": "Praeitą Mėnesį", + "5minutes": "5 minutės", + "10minutes": "10 minučių", + "30minutes": "30 minučių", + "1hour": "1 valandą", + "12hours": "12 valandų", + "24hours": "24 valandos", + "pm": "pm", + "am": "am", + "yr": "{{time}}m", + "year_one": "{{time}} metai", + "year_few": "{{time}} metai", + "year_other": "{{time}} metų", + "mo": "{{time}}mėn", + "month_one": "{{time}} mėnuo", + "month_few": "{{time}} mėnesiai", + "month_other": "{{time}} mėnesių", + "d": "{{time}}d", + "day_one": "{{time}} diena", + "day_few": "{{time}} dienos", + "day_other": "{{time}} dienų", + "h": "{{time}}v", + "hour_one": "{{time}} valanda", + "hour_few": "{{time}} valandos", + "hour_other": "{{time}} valandų", + "m": "{{time}}min", + "minute_one": "{{time}} minutė", + "minute_few": "{{time}} minutės", + "minute_other": "{{time}} minučių", + "s": "{{time}}s", + "second_one": "{{time}} sekundė", + "second_few": "{{time}} sekundės", + "second_other": "{{time}} sekundžių", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + } + }, + "unit": { + "speed": { + "kph": "kmh", + "mph": "mph" + }, + "length": { + "feet": "pėdos", + "meters": "metrai" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/val", + "mbph": "MB/val", + "gbph": "GB/val" + } + }, + "label": { + "back": "Eiti atgal" + }, + "button": { + "apply": "Pritaikyti", + "reset": "Atstatyti", + "done": "Baigta", + "enabled": "Įjungta", + "enable": "Įjungti", + "disabled": "Išjungta", + "disable": "Išjungti", + "save": "Išsaugoti", + "saving": "Saugoma…", + "cancel": "Atšaukti", + "close": "Užverti", + "copy": "Kopijuoti", + "back": "Atgal", + "history": "Istorija", + "fullscreen": "Pilnas Ekranas", + "exitFullscreen": "Išeiti iš Pilno Ekrano", + "pictureInPicture": "Paveikslėlis Paveiksle", + "twoWayTalk": "Dvikryptis Kalbėjimas", + "cameraAudio": "Kameros Garsas", + "on": "ON", + "edit": "Redaguoti", + "copyCoordinates": "Kopijuoti koordinates", + "delete": "Ištrinti", + "yes": "Taip", + "no": "Ne", + "download": "Atsisiųsti", + "info": "Info", + "suspended": "Pristatbdytas", + "unsuspended": "Atnaujinti", + "play": "Groti", + "unselect": "Atžymėti", + "export": "Eksportuoti", + "deleteNow": "Trinti Dabar", + "next": "Kitas", + "off": "OFF" + }, + "menu": { + "system": "Sistema", + "systemMetrics": "Sistemos duomenys", + "configuration": "Konfiguracija", + "systemLogs": "Sistemos įrašai", + "settings": "Nustatymai", + "configurationEditor": "Konfiguracijos Redaktorius", + "languages": "Kalbos", + "language": { + "en": "Anglų", + "es": "Ispanų", + "zhCN": "Kinų (supaprastinta)", + "fr": "Prancūzų", + "ar": "Arabų", + "pt": "Portugalų", + "ru": "Rusų", + "de": "Vokiečių", + "ja": "Japonų", + "tr": "Turkų", + "it": "Italų", + "nl": "Olandų", + "sv": "Švedų", + "cs": "Čekų", + "nb": "Norvegų", + "vi": "Vietnamiečių", + "fa": "Persų", + "pl": "Lenkų", + "uk": "Ukrainos", + "el": "Graikų", + "ro": "Romūnijos", + "hu": "Vengrų", + "fi": "Suomių", + "da": "Danų", + "sk": "Slovakų", + "withSystem": { + "label": "Kalbai naudoti sistemos nustatymus" + }, + "hi": "Hindi", + "ptBR": "Brazilietiška Portugalų", + "ko": "Korėjiečių", + "he": "Hebrajų", + "yue": "Kantoniečių", + "th": "Tailandiečių", + "ca": "Kataloniečių", + "sr": "Serbų", + "sl": "Slovėnų", + "lt": "Lietuvių", + "bg": "Bulgarų", + "gl": "Galician", + "id": "Indonesian", + "ur": "Urdu" + }, + "appearance": "Išvaizda", + "darkMode": { + "label": "Tamsusis Rėžimas", + "light": "Šviesus", + "dark": "Tamsus", + "withSystem": { + "label": "Šviesiam ar tamsiam rėžimui naudoti sistemos nustatymus" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Mėlyna", + "green": "Žalia", + "nord": "Šiaurietiška", + "red": "Raudona", + "highcontrast": "Didelio Kontrasto", + "default": "Numatyta" + }, + "help": "Pagalba", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Perkrauti Frigate", + "live": { + "title": "Tiesiogiai", + "allCameras": "Visos Kameros", + "cameras": { + "title": "Kameros", + "count_one": "{{count}} Kamera", + "count_few": "{{count}} Kameros", + "count_other": "{{count}} Kamerų" + } + }, + "review": "Peržiūros", + "explore": "Iškoti", + "export": "Eksportuoti", + "faceLibrary": "Veidų Biblioteka", + "user": { + "title": "Vartotojas", + "account": "Paskyra", + "current": "Esamas vartotojas: {{user}}", + "anonymous": "neidentifikuotas", + "logout": "atsijungti", + "setPassword": "Nustatyti Slaptažodi" + }, + "uiPlayground": "UI Playground" + }, + "toast": { + "copyUrlToClipboard": "URL nukopijuotas į atmintį.", + "save": { + "title": "Išsaugoti", + "error": { + "title": "Nepavyko išsaugoti konfiguracijos pakeitimų: {{errorMessage}}", + "noMessage": "Nepavyko išsaugoti konfiguracijos pakeitimų" + } + } + }, + "role": { + "title": "Rolė", + "admin": "Adminas", + "viewer": "Žiūrėtojas", + "desc": "Adminai turi pilną prieigą prie visų Frigate vartotojo sąsajos fukncijų. Žiūrėtojai yra apriboti peržiūrėti kameras, peržiūrų įrašus ir istorinius įrašus." + }, + "pagination": { + "label": "puslapiavimas", + "previous": { + "title": "Ankstesnis", + "label": "Eiti į ankstesnį puslapį" + }, + "next": { + "title": "Sekantis", + "label": "Eiti į sekantį puslapį" + }, + "more": "Daugiau puslapių" + }, + "accessDenied": { + "documentTitle": "Priegai Nesuteikta - Frigate", + "title": "Prieiga Nesuteikta", + "desc": "Jūs neturite leidimo žiūrėti šį puslapį." + }, + "notFound": { + "documentTitle": "Nerasta - Frigate", + "title": "404", + "desc": "Puslapis nerastas" + }, + "selectItem": "Pasirinkti {{item}}", + "readTheDocumentation": "Skaityti dokumentaciją", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/auth.json new file mode 100644 index 0000000..3ba7d10 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Vartotojo vardas", + "password": "Slaptažodis", + "login": "Prisijungti", + "errors": { + "usernameRequired": "Vartotojo vardas yra privalomas", + "passwordRequired": "Slaptažodis yra privalomas", + "rateLimit": "Viršytos nustatytos ribos. Pabandykite vėliau.", + "loginFailed": "Prisijungti nepavyko", + "unknownError": "Nežinoma klaida. Patikrinkite įrašus.", + "webUnknownError": "Nežinoma klaida. Patikrinkite konsolės įrašus." + }, + "firstTimeLogin": "Bandote prisijungti pirmą kartą? Prisijungimo informaciją rasite Frigate loguose." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/camera.json new file mode 100644 index 0000000..7f4f5d8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kamerų Grupės", + "add": "Sukurti Kamerų Grupę", + "edit": "Modifikuoti Kamerų Grupę", + "delete": { + "label": "Ištrinti Kamerų Grupę", + "confirm": { + "title": "Patvirtinti ištrynimą", + "desc": "Esate įsitikinę, kad norite ištrinti šią kamerų grupę {{name}}?" + } + }, + "name": { + "label": "Pavadinimas", + "placeholder": "Įveskite pavadinimą…", + "errorMessage": { + "mustLeastCharacters": "Kamerų grupės pavadinimas turi būti bent 2 simbolių.", + "exists": "Kamerų grupės pavadinimas jau egzistuoja.", + "nameMustNotPeriod": "Kamerų grupės pavadinime negali būti taško.", + "invalid": "Nepriimtinas kamera grupės pavadinimas." + } + }, + "cameras": { + "label": "Kameros", + "desc": "Pasirinkite kameras šiai grupei." + }, + "icon": "Ikona", + "success": "Kameraų grupė {{name}} išsaugota.", + "camera": { + "setting": { + "label": "Kamerų Transliacijos Nustatymai", + "title": "{{cameraName}} Transliavimo Nustatymai", + "desc": "Keisti tiesioginės tranliacijos nustatymus šiai kamerų grupės valdymo lentai. Šie nustatymai yra specifiniai įrenginiui/ naršyklei.", + "audioIsAvailable": "Šiai transliacijai yra garso takelis", + "audioIsUnavailable": "Šiai transliacijai nėra garso takelio", + "audio": { + "tips": { + "title": "Šiai transliacijai garsas turi būti teikiamas iš kameros ir konfiguruojamas naudojant go2rtc.", + "document": "Skaityti dokumentaciją " + } + }, + "stream": "Transliacija", + "placeholder": "Pasirinkti transliaciją", + "streamMethod": { + "label": "Transliacijos Metodas", + "placeholder": "Pasirinkti transliacijos metodą", + "method": { + "noStreaming": { + "label": "Nėra transliacijos", + "desc": "Kameros vaizdas atsinaujins tik kartą per mintuę ir nebus tiesioginės transliacijos." + }, + "smartStreaming": { + "label": "Išmanus Transliavimas (rekomenduojama)", + "desc": "Išmanus transliavimas atnaujins jūsų kameros vaizdą kartą per minutę jei nebus aptinkama jokia veikla tam kad saugoti tinklo pralaiduma ir kitus resursus. Aptikus veiklą atvaizdavimas nepertraukiamai persijungs į tiesioginę transliaciją." + }, + "continuousStreaming": { + "label": "Nuolatinė Transliacija", + "desc": { + "title": "Kameros vaizdas visada bus tiesioginė transliacija, jei jis bus matomas valdymo lentoje, net jei jokia veikla nėra aptinkama.", + "warning": "Nepertraukiama transliacija gali naudoti daug tinklo duomenų bei sukelti našumo problemų. Naudoti su atsarga." + } + } + } + }, + "compatibilityMode": { + "desc": "Šį nustatymą naudoti tik jei jūsų kameros tiesioginėje transliacijoje matomi spalvų neatitikimai arba matoma įstriža linija dešinėje vaizdo pusėje.", + "label": "Suderinamumo rėžimas" + } + }, + "birdseye": "Birdseye" + } + }, + "debug": { + "options": { + "label": "Nustatymai", + "title": "Pasirinkimai", + "showOptions": "Rodyti Pasirinkimus", + "hideOptions": "Slėpti Pasirinkimus" + }, + "boundingBox": "Ribojantis Kvadratas", + "timestamp": "Laiko žymė", + "zones": "Zonos", + "mask": "Maskuotė", + "motion": "Judesys", + "regions": "Regionas" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/dialog.json new file mode 100644 index 0000000..28069cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/dialog.json @@ -0,0 +1,121 @@ +{ + "restart": { + "title": "Esate įsitikinę, kad norite perkrauti Frigate?", + "button": "Perkrauti", + "restarting": { + "title": "Frigate Persikrauna", + "content": "Šis puslapis persikraus už {{countdown}} sekundžių.", + "button": "Priverstinai Perkrauti Dabar" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Ar šis objektas yra {{label}}?", + "ask_an": "Ar šis objektas yra {{label}}?", + "label": "Patvirtinti šią etiketę į Frigate Plus", + "ask_full": "Ar šis objektas yra {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Pateikta" + } + }, + "submitToPlus": { + "label": "Pateiktį į Frigate+", + "desc": "Objektai vietose kurių norite vengti nėra klaidingai teigiami. Pateikiant juos kaip klaidingai teigiamus įneš neatitikimų į modelį." + } + }, + "video": { + "viewInHistory": "Pažiūrėti Istorijoje" + } + }, + "streaming": { + "restreaming": { + "disabled": "Šiai kamerai pertransliavimas nėra įjungtas.", + "desc": { + "title": "Nustatyti go2rtc papildomoms tiesioginės transliacijos galimybėms ir šios kameros garsui." + } + }, + "label": "Srautas", + "showStats": { + "label": "Rodyti transliacijos statistiką", + "desc": "Įjungti šią galimybę rodyti transliacijos statistiką kaip pridėtinę informaciją kameros vaizde." + }, + "debugView": "Debug Vaizdas" + }, + "export": { + "time": { + "lastHour_one": "Paskutinė {{count}} Valanda", + "lastHour_few": "Paskutinės {{count}} Valandos", + "lastHour_other": "Paskutinės {{count}} Valandų", + "fromTimeline": "Pasirinkit iš Laiko juostos", + "custom": "Pasirinkimas", + "start": { + "title": "Pradžios Laikas", + "label": "Pasirinkti Pradžios Laiką" + }, + "end": { + "title": "Pabaigos Laikas", + "label": "Pasirinkti Pabaigos Laiką" + } + }, + "fromTimeline": { + "previewExport": "Peržiūrėti Eksportuotus", + "saveExport": "Išsaugoti Exportuojamą" + }, + "name": { + "placeholder": "Pavadinti eksportuojamą įrašą" + }, + "select": "Pasirinkti", + "export": "Eksportuoti", + "selectOrExport": "Pasirinkti ar Eksportuoti", + "toast": { + "success": "Sėkmingai pradėtas eksportavimas. Įrašą galima peržiūrėti /exports kataloge.", + "error": { + "failed": "Nepavyko pradėti eksportavimo: {{error}}", + "endTimeMustAfterStartTime": "Pabaigos Laikas privalo būti vėliau nei pradžios laikas", + "noVaildTimeSelected": "Nėra pasirinkto tinkamo laikotarpio" + } + } + }, + "recording": { + "button": { + "markAsReviewed": "Žymėti kaip peržiūrėtą", + "export": "Eksportuoti", + "deleteNow": "Ištrinti Dabar", + "markAsUnreviewed": "Pažymėti kaip nematytą" + }, + "confirmDelete": { + "desc": { + "selected": "Ar esate įsitikinę, kad norite ištrinti visus įrašytus vaizdo įrašus susijusius su šiuo peržiūros elementu?

    LaikykiteShift norint ateityje praleisti šį pranešimą." + }, + "title": "Patvirtinti Ištrynimą", + "toast": { + "success": "Vaizdo įrašas susijęs su pasirinkta peržiūra buvo sėkmingai ištrintas.", + "error": "Nepavyko ištrinti: {{error}}" + } + } + }, + "search": { + "saveSearch": { + "label": "Išsaugoti Paiešką", + "desc": "Suteikite vardą šiai išsaugotai paieškai.", + "placeholder": "Įveskite pavadinima savo paieškai", + "overwrite": "{{searchName}} jau egzistuoja. Jei išsaugosite esamas įrašas bus perrašytas.", + "success": "Paieška ({{searchName}}) buvo išsaugota.", + "button": { + "save": { + "label": "Išsaugoti šią paiešką" + } + } + } + }, + "imagePicker": { + "selectImage": "Pasirinkti miniatiūrą sekamam objektui", + "search": { + "placeholder": "Ieškoti pagal etiketę arba sub etiketę..." + }, + "noImages": "Šiai kamerai miniatiūrų nerasta" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/filter.json new file mode 100644 index 0000000..f5beaf8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtras", + "labels": { + "label": "Etiketės", + "all": { + "title": "Visos Etiketės", + "short": "Etiketės" + }, + "count_one": "{{count}} Etiketė", + "count_other": "{{count}} Etiketės" + }, + "zones": { + "label": "Zonos", + "all": { + "title": "Visos Zonos", + "short": "Zonos" + } + }, + "review": { + "showReviewed": "Rodyti Peržiūrėtus" + }, + "trackedObjectDelete": { + "desc": "Trinant šiuos {{objectLength}} sekamus objektus taip pat pašalins momentines iškarpas, išsaugotus įterpius, priskirtus objekto gyvavimo ciklo įrašus. Šių sekamų objektų įrašyta filmuota medžiaga Istorijos vaizde ištrinta NEBUS.

    Ar esate įsitikinę, kad norite tęsti?

    Laikykite Shift norint ateityje praleisti šį pranešimą.", + "title": "Patvirtinkite Ištrynimą", + "toast": { + "success": "Sekami objektai sėkmingai ištrinti.", + "error": "Nepavyko ištrinti sekamų objektų: {{errorMessage}}" + } + }, + "classes": { + "label": "Klasės", + "all": { + "title": "Visos Klasės" + }, + "count_one": "{{count}} Klasė", + "count_other": "{{count}} Klasių" + }, + "dates": { + "selectPreset": "Pasirinkti Nustatytą poziciją…", + "all": { + "title": "Visos Datos", + "short": "Datos" + } + }, + "more": "Daugiau Filtrų", + "reset": { + "label": "Atstatyti bazines filtrų reikšmes" + }, + "timeRange": "Laiko Rėžis", + "subLabels": { + "label": "Sub Etiketės", + "all": "Visos Sub Etiktės" + }, + "score": "Balas", + "estimatedSpeed": "Nustatytas Greitis ({{unit}})", + "features": { + "label": "Funkcijos", + "hasSnapshot": "Turi Momentinę Nuotrauką", + "hasVideoClip": "Turi vaizdo klipą", + "submittedToFrigatePlus": { + "label": "Pateikta į Frigate+", + "tips": "Pradžioje turite išfiltruoti sekamus objektus su momentinėmis nuotraukomis.

    Sekami Objektai be momentinių nuotraukų negali būti pateikti į Frigate+." + } + }, + "sort": { + "label": "Rikiuoti", + "dateAsc": "Datos (Didėjančiai)", + "dateDesc": "Datos (Mažėjančiai)", + "scoreAsc": "Objekto Balai (Didėjančiai)", + "scoreDesc": "Objekto Balai (Mažėjančiai)", + "speedAsc": "Įvertintas Greitis (Didėjančiai)", + "speedDesc": "Įvertintas Greitis (Mažėjančiai)", + "relevance": "Aktualumą" + }, + "cameras": { + "label": "Kamerų Filtrai", + "all": { + "title": "Visos Kameros", + "short": "Kameros" + } + }, + "motion": { + "showMotionOnly": "Rodyti Tik Judesius" + }, + "explore": { + "settings": { + "title": "Nustatymai", + "defaultView": { + "title": "Bazinis Vaizdas", + "summary": "Santrauka", + "unfilteredGrid": "Nefiltruotas Tinklelis", + "desc": "Kai jokie filtrai nėra parinkti, rodom santrauka naujienų sekamiems objektas pagal etiketę arba nefiltruotas tinklelis." + }, + "gridColumns": { + "title": "Tiklelio Stulpeliai", + "desc": "Pasirinkti kiekį stulpelių atvaizduojant tinkleliu." + }, + "searchSource": { + "label": "Paiškos Šaltinis", + "desc": "Pasirinkite kaip jūsų sekamiems objektams bus vykdoma paieška, naudojant miniatiūras ar tekstinius aprašymus.", + "options": { + "thumbnailImage": "Miniatiūros Paveikslėlis", + "description": "Aprašymas" + } + } + }, + "date": { + "selectDateBy": { + "label": "Pasirinkite datą filtravimui" + } + } + }, + "logSettings": { + "label": "Filtruoti sekimo įrašų lygį", + "filterBySeverity": "Filtruoti įrašus pagal svarbą", + "loading": { + "title": "Kraunama", + "desc": "Kai įrašų puslapyje pasiekiama įrašų pabaiga, nauji įrašai atsiras automatiškai." + }, + "disableLogStreaming": "Išjungti įrašų transliavimą", + "allLogs": "Visi įrašai" + }, + "zoneMask": { + "filterBy": "Filtruoti naudojant zonų maskavimus" + }, + "recognizedLicensePlates": { + "title": "Atpažinti Registracijos Numeriai", + "loadFailed": "Nepavyko pateikti atpažintų registracijos numerių.", + "loading": "Ištraukiami atpažinti registracijos numeriai…", + "placeholder": "Įveskite norėdami ieškoti registracijos numerių…", + "noLicensePlatesFound": "Registracjos numerių nerasta.", + "selectPlatesFromList": "Pasirinkti vieną ar daugiau numerių iš sąrašo.", + "selectAll": "Pasirinkti viską", + "clearAll": "Išvalyti viską" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/icons.json new file mode 100644 index 0000000..82dadb3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Pasirinkti ikoną", + "search": { + "placeholder": "Surasti ikoną…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/input.json new file mode 100644 index 0000000..6d3ff72 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Parsisiųsti Video", + "toast": { + "success": "Jūsų peržiūros elemento parsisiuntimas pradėtas." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/lt/components/player.json new file mode 100644 index 0000000..4924c6c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Šiam laiko tarpui įrašų nerasta", + "noPreviewFound": "Peržiūrų nerasta", + "noPreviewFoundFor": "Peržiūrų nerasta {{cameraName}}", + "submitFrigatePlus": { + "title": "Pateikti šį kadrą į Frigate+?", + "submit": "Pateikti" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ar naujesni yra privalomi šiam tiesioginės transliacijos tipui.", + "streamOffline": { + "title": "Transliacija nepasiekiama", + "desc": "Jokių transliacijos kadrų negauta iš {{cameraName}}detect, patikrinkite klaidų sąrašus" + }, + "cameraDisabled": "Kamera yra išjungta", + "stats": { + "streamType": { + "title": "Transliacijos Tipas:", + "short": "Tipas" + }, + "bandwidth": { + "title": "Pralaidumas:", + "short": "Pralaidumas" + }, + "latency": { + "title": "Vėlavimas:", + "value": "{{seconds}} sekundžių", + "short": { + "title": "Vėlavimas", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Iš viso Kadrų:", + "droppedFrames": { + "title": "Pamestų Kadrų:", + "short": { + "title": "Pamesti", + "value": "{{droppedFrames}} kadrai" + } + }, + "decodedFrames": "Dekoduoti Kadrai:", + "droppedFrameRate": "Pamestų Kadrų Dažnis:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Kadras sėkmingai pateiktas į Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepavyko pateikti kadro į Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/objects.json b/sam2-cpu/frigate-dev/web/public/locales/lt/objects.json new file mode 100644 index 0000000..9aa9b5d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Žmogus", + "bicycle": "Dviratis", + "car": "Automobilis", + "motorcycle": "Motociklas", + "airplane": "Lėktuvas", + "bus": "Autobusas", + "train": "Traukinys", + "boat": "Valtis", + "traffic_light": "Šviesoforas", + "fire_hydrant": "Hidrantas", + "street_sign": "Kelio ženklas", + "stop_sign": "Stop ženklas", + "parking_meter": "Stovėjimo automatas", + "bench": "Suoliukas", + "bird": "Paukštis", + "cat": "Katė", + "dog": "Šuo", + "horse": "Arklys", + "sheep": "Avis", + "cow": "Karvė", + "elephant": "Dramblys", + "bear": "Lokys", + "zebra": "Zebras", + "giraffe": "Žirafa", + "hat": "Kepurė", + "backpack": "Kuprinė", + "umbrella": "Skėtis", + "shoe": "Batas", + "eye_glasses": "Akiniai", + "handbag": "Rankinė", + "tie": "Kaklaraštis", + "suitcase": "Lagaminas", + "frisbee": "Skraidanti lėkštė", + "snowboard": "Snieglentė", + "skis": "Slidės", + "sports_ball": "Sporto Kamuolys", + "kite": "Aitvaras", + "baseball_bat": "Beisbolo lazda", + "baseball_glove": "Beisbolo Pirštinė", + "skateboard": "Riedlentė", + "surfboard": "Banglentė", + "tennis_racket": "Teniso Raketė", + "bottle": "Butelis", + "plate": "Lėkštė", + "wine_glass": "Vyno Taurė", + "cup": "Puodelis", + "fork": "Šakutė", + "knife": "Peilis", + "spoon": "Šaukštas", + "bowl": "Dubuo", + "banana": "Bananas", + "apple": "Obuolys", + "sandwich": "Sumuštinis", + "orange": "Apelsinas", + "broccoli": "Brokolis", + "carrot": "Morka", + "hot_dog": "Hot Dog", + "pizza": "Pica", + "donut": "Spurga", + "cake": "Tortas", + "chair": "Kėdė", + "couch": "Sofa", + "potted_plant": "Pasodintas Augalas", + "bed": "Lova", + "mirror": "Veidrodis", + "dining_table": "Valgomasis Stalas", + "window": "Langas", + "desk": "Stalas", + "toilet": "Tualetas", + "door": "Durys", + "tv": "TV", + "laptop": "Nešiojamasis Kompiuteris", + "mouse": "Pelė", + "remote": "Nuotolinis valdymo pultas", + "keyboard": "Klaviatūra", + "cell_phone": "Mobilus Telefonas", + "microwave": "Mikrobangų krosnelė", + "oven": "Orkaitė", + "toaster": "Skrudintuvas", + "sink": "Kriauklė", + "refrigerator": "Šaldiklis", + "blender": "Plakiklis", + "book": "Knyga", + "clock": "Laikrodis", + "vase": "Vaza", + "scissors": "Žirklės", + "teddy_bear": "Pliušinis Meškiukas", + "hair_dryer": "Plaukų Džiovintuvas", + "toothbrush": "Dantų šepetėlis", + "hair_brush": "Plaukų šepetys", + "vehicle": "Mašina", + "squirrel": "Voverė", + "deer": "Elnias", + "animal": "Gyvūnas", + "bark": "Lojimas", + "fox": "Lapė", + "goat": "Ožka", + "rabbit": "Triušis", + "raccoon": "Meškėnas", + "robot_lawnmower": "Robotas Vejapjovė", + "waste_bin": "Šiukšliadėžė", + "on_demand": "Pagal Poreikį", + "face": "Veidas", + "license_plate": "Registracijos Numeris", + "package": "Pakuotė", + "bbq_grill": "BBQ kepsninė", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/classificationModel.json new file mode 100644 index 0000000..9deea36 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/classificationModel.json @@ -0,0 +1,68 @@ +{ + "documentTitle": "Klasifikavimo Modeliai", + "button": { + "deleteClassificationAttempts": "Trinti Klasisifikavimo Nuotraukas", + "renameCategory": "Pervadinti Klasę", + "deleteCategory": "Trinti Klasę", + "deleteImages": "Trinti Nuotraukas", + "trainModel": "Treniruoti Modelį" + }, + "toast": { + "success": { + "deletedCategory": "Ištrinta Klasę", + "deletedImage": "Ištrinti Nuotraukas", + "categorizedImage": "Sekmingai Klasifikuotas Nuotrauka", + "trainedModel": "Modelis sėkmingai apmokytas.", + "trainingModel": "Sėkmingai pradėtas modelio apmokymas." + }, + "error": { + "deleteImageFailed": "Nepavyko ištrinti:{{errorMessage}}", + "deleteCategoryFailed": "Nepavyko ištrinti klasės:{{errorMessage}}", + "categorizeFailed": "Nepavyko kategorizuoti nuotraukos:{{errorMessage}}", + "trainingFailed": "Nepavyko pradėti modelio apmokymo:{{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Trinti Klasę", + "desc": "Esate įsitikinę, norite ištrinti klasę {{name}}? Tai negrįžtamai ištrins visas susijusias nuotraukas ir reikės iš naujo apmokinti modelį." + }, + "deleteDatasetImages": { + "title": "Ištrinti Imties Nuotraukas", + "desc_one": "Esate įsitikinę norite ištrinti {{count}} nautraukas iš {{dataset}}? Šis veiksmas negrįžtamas ir reikės iš naujo apmokinti modelį.", + "desc_few": "", + "desc_other": "" + }, + "deleteTrainImages": { + "title": "Ištrinti Apmokymo Nuotraukas", + "desc_one": "Ar esate įsitikinę, kad norite ištrinti {{count}} nuotraukas? Šis veiksmas negrįžtamas.", + "desc_few": "", + "desc_other": "" + }, + "renameCategory": { + "title": "Pervadinti Klasę", + "desc": "Įveskite naują vardą vietoje {{name}}. Jums reikės iš naujo apmokinti modelį, kad vardas įsigaliotų." + }, + "description": { + "invalidName": "Netinkamas vardas. Vardas gali būti sudarytas tik iš raidžiū, skaičių, tarpų, apostrofų, pabraukimų ar brūkšnelių." + }, + "train": { + "title": "Pastarosios Klasifikacijos", + "aria": "Pasirinkti Pastarasias Klasifikacijas" + }, + "categories": "Klasės", + "createCategory": { + "new": "Sukurti Naują Klasę" + }, + "categorizeImageAs": "Klasifikuoti Nuotrauką Kaip:", + "categorizeImage": "Klasifikuoti Nuotrauką", + "noModels": { + "object": { + "title": "Nėra Objektų Klasifikavimo Modelių", + "description": "Sukurti individualų modelį ištrintų objektų klasifikavimui.", + "buttonText": "Sukurti Objekto Modelį" + }, + "state": { + "title": "Nėra Būklės Klasifikavimo Modelių" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/configEditor.json new file mode 100644 index 0000000..e9c67dc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfiguracijos redaktorius - Frigate", + "configEditor": "Konfiguracijos Redaktorius", + "copyConfig": "Kopijuoti Konfiguraciją", + "saveAndRestart": "Išsaugoti ir Perkrauti", + "saveOnly": "Tik Išsaugoti", + "confirm": "Išeiti neišsaugant?", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija nukopijuota į atmintį." + }, + "error": { + "savingError": "Klaida išsaugant konfiguraciją" + } + }, + "safeConfigEditor": "Konfiguracijos Redaktorius (Saugus Rėžimas)", + "safeModeDescription": "Frigate yra saugiame rėžime dėl konfiguracijos tinkamumo klaidos." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/events.json new file mode 100644 index 0000000..bd4ab28 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/events.json @@ -0,0 +1,52 @@ +{ + "motion": { + "label": "Judesys", + "only": "Tik judesys" + }, + "allCameras": "Visos kameros", + "timeline": "Laiko juosta", + "timeline.aria": "Pasirink laiko juostą", + "events": { + "label": "Įvykiai", + "aria": "Pasirinkti įvykius", + "noFoundForTimePeriod": "Šiam laiko periodui įvykių nėrasta." + }, + "calendarFilter": { + "last24Hours": "Paskutinė para" + }, + "selected_one": "{{count}} pasirinktas", + "selected_other": "{{count}} pasirinkta", + "camera": "Kamera", + "alerts": "Įspėjimai", + "detections": "Aptikimai", + "empty": { + "alert": "Nėra pranešimų peržiūrai", + "detection": "Nėra aptikimų peržiūrai", + "motion": "Duomenų apie judesius nėra" + }, + "documentTitle": "Peržiūros - Frigate", + "recordings": { + "documentTitle": "Įrašai - Frigate" + }, + "markAsReviewed": "Pažymėti kaip peržiūrėtą", + "markTheseItemsAsReviewed": "Pažymėti šiuos įrašus kaip peržiūrėtus", + "newReviewItems": { + "label": "Pamatyti naujus peržiūros įrašus", + "button": "Nauji Įrašai Peržiūrėjimui" + }, + "detected": "aptikta", + "suspiciousActivity": "Įtartina Veikla", + "threateningActivity": "Grėsminga Veikla", + "detail": { + "noDataFound": "Peržiūrai informacijos nėra", + "aria": "Perjungti į detalų vaizdą", + "trackedObject_one": "objektas", + "trackedObject_other": "objektai", + "noObjectDetailData": "Nėra objekto detalių duomenų.", + "label": "Detalės" + }, + "objectTrack": { + "trackedPoint": "Susektas taškas", + "clickToSeek": "Spustelkite perkelti į šį laiką" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/explore.json new file mode 100644 index 0000000..0186e73 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/explore.json @@ -0,0 +1,226 @@ +{ + "documentTitle": "Tyrinėti - Frigate", + "generativeAI": "Generatyvinis DI", + "exploreMore": "Apžvelgti daugiau {{label}} objektų", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Paleidžiama…", + "estimatedTime": "Apytikris likęs laikas:", + "context": "Tyrinėjimai gali būti naudojami po to kai sekamų objektų įterpiai bus užbaigti indeksuoti.", + "finishingShortly": "Paigiama netrukus", + "step": { + "thumbnailsEmbedded": "Įterptos Miniatiūros: ", + "descriptionsEmbedded": "Įterpti aprašymai: ", + "trackedObjectsProcessed": "Apdorota sekamų objektų: " + } + }, + "title": "Tyrinėjimai Negalimi", + "downloadingModels": { + "context": "Frigate siunčiasi reikalingus įterpimo modelius, kad būtų palaikoma Semantic Paieškos funkcija. Tai gali užtrukti priklausomai nuo duomenų srauto greičio.", + "setup": { + "visionModel": "Vaizdo modelis", + "visionModelFeatureExtractor": "Vaizdo modelio funkcijų išgavimas", + "textModel": "Teksto modelis", + "textTokenizer": "Teksto tekenizatorius" + }, + "tips": { + "context": "Galimai norėsite iš naujo indeksuoti savo sekamų objektų įterpius po to kai modeliai parsisiųs." + }, + "error": "Įvyko klaida. Patikrinkite Frigate įrašus." + } + }, + "details": { + "timestamp": "Laiko žyma", + "item": { + "tips": { + "mismatch_one": "{{count}} neesamas objektas aptiktas ir pridėtas į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "mismatch_few": "{{count}} neesami objektai aptikti ir pridėti į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "mismatch_other": "{{count}} neesamų objektų aptiktų ir pridėtų į šį peržiuros įrašą. Tie objektai arba neatitinką įspėjimų ar aptikimų sąlygų arba jau buvo išvalyti/ištrinti.", + "hasMissingObjects": "Koreguokite savo nustatymus jeigu norite, kad Frigate saugoti objektus su šiomis etiketėmis: {{objects}}" + }, + "title": "Peržiūrėti Įrašo Detales", + "desc": "Peržiūrėti Įrašo detales", + "button": { + "share": "Dalintis šiuo peržiūros įrašu", + "viewInExplore": "Žiūrėti Tyrinėjime" + }, + "toast": { + "success": { + "regenerate": "Gauta nauja užklausa iš {{provider}} naujam aprašymui. Priklausomai nuo jūsų tiekėjo greičio, naują aprašymą sukurti gali užtrukti.", + "updatedSublabel": "Sėkmingai atnaujinta sub etiketė.", + "updatedLPR": "Sėkmingai atnaujinti registracijos numeriai.", + "audioTranscription": "Sėkmingai užklausta garso aprašymo." + }, + "error": { + "regenerate": "Nepavyko pakviesti {{provider}} naujam aprašymui: {{errorMessage}}", + "updatedSublabelFailed": "Nepavyko atnaujinti sub etikečių: {{errorMessage}}", + "updatedLPRFailed": "Nepavyko atnaujinti registracijos numerių: {{errorMessage}}", + "audioTranscription": "Nepavyko užklausti garso aprašymo: {{errorMessage}}" + } + } + }, + "label": "Etiketė", + "editSubLabel": { + "title": "Koreguoti sub etiketę", + "desc": "Įveskite naują sub etiketę šiai etiketei {{label}}", + "descNoLabel": "Įveskite naują sub etiketę šiam sekamam objektui" + }, + "editLPR": { + "title": "Redaguoti registracijos numerį", + "desc": "Įvesti naują registracijos numerio reikšmę šiai etiketei {{label}}", + "descNoLabel": "Įvesti naują registracijos numerio reikšmę šiam objektui" + }, + "snapshotScore": { + "label": "Momentinės nuotraukos balas" + }, + "topScore": { + "label": "Top Balas", + "info": "Aukščiausias balas yra didžiausia medianos reikšmė sekamam objektui, taigi tai gali skirtis nuo balų pateiktų miniatiūrų paieškos rezultatuose." + }, + "score": { + "label": "Balas" + }, + "recognizedLicensePlate": "Atpažinti Registracijos Numeriai", + "estimatedSpeed": "Nustatytas Greitis", + "objects": "Objektai", + "camera": "Kamera", + "zones": "Zonos", + "button": { + "findSimilar": "Rasti Panašų", + "regenerate": { + "title": "Regeneruoti", + "label": "Regeneruoti sekamų objektų aprašymus" + } + }, + "description": { + "label": "Aprašymas", + "placeholder": "Sekamo objekto aprašymas", + "aiTips": "Iki kol sekamo objekto gyvavimo ciklas užsibaigs Frigate neklaus aprašymo iš jūsų Generatyvinio DI tiekėjo." + }, + "expandRegenerationMenu": "Išskleisti regeneravimo meniu", + "regenerateFromSnapshot": "Regeneruoti iš Momentinės Nuotraukos", + "regenerateFromThumbnails": "Regenruoti iš Miniatiūros", + "tips": { + "descriptionSaved": "Aprašymas sėkmingai išsaugotas", + "saveDescriptionFailed": "Nepavyko atnaujinti aprašymo: {{errorMessage}}" + } + }, + "trackedObjectsCount_one": "{{count}} sekamas objektas ", + "trackedObjectsCount_few": "{{count}} sekami objektai ", + "trackedObjectsCount_other": "{{count}} sekamų objektų ", + "objectLifecycle": { + "lifecycleItemDesc": { + "visible": "{{label}} aptikta", + "attribute": { + "faceOrLicense_plate": "{{attribute}} aptiktas etiketei {{label}}", + "other": "{{label}} atpažintas kaip {{attribute}}" + }, + "external": "{{label}} aptiktas", + "entered_zone": "{{label}} pateko į {{zones}}", + "active": "{{label}} tapo aktyvus", + "stationary": "{{label}} nebejuda", + "gone": "{{label}} paliko", + "heard": "{{label}} girdėta", + "header": { + "zones": "Zonos", + "ratio": "Santykis", + "area": "Plotas" + } + }, + "annotationSettings": { + "offset": { + "desc": "Šie duomenys gaunami iš jūsų kameros aptikimo srauto bet yra užkeliami ant vaizdo gaunamo iš įrašymo srauto. Mažai tikėtina kad abeji srautais bus tobulai sinchronizuoti. Rezultate, apibrėžimo dėžutė ir įrašas nesilygiuos tobulai. Tačiau, annotation_offset reikšmė gali būti naudojama tai koreguoti.", + "millisecondsToOffset": "Praslinkti aptikimų anotacijas per mili-sekundes. Bazinis: 0", + "label": "Anotacijos Perstūmimas", + "tips": "Patarimas: Įsivaizduokite kad yra įvykio klipas kur žmogus eina iš kairės į dešinę. Jei apibrėžimo dėžutė nuolatos yra žmogui iš kairės tuomet reikšmę sumažinkite. Analogiškai, jei dėžutė piešiama priekyje žmogaus tuomet reikšmę padidinkite.", + "toast": { + "success": "Anotacijos perslinkimas kamerai {{camera}} buvo išsaugota konfiguracijoje. Perkraukite Frigate, kad pritaikytumėte pokyčius." + } + }, + "title": "Anotacijų Nustatymai", + "showAllZones": { + "title": "Rodyti Visas Zonas", + "desc": "Visada rodyti zonas tuose kadruose, kuriuose objektas pateko į zoną." + } + }, + "title": "Objekto Gyvavimo Ciklas", + "noImageFound": "Šiam laikotarpiui vaizdų nerasta.", + "createObjectMask": "Sukurta Objekto Maskuotė", + "adjustAnnotationSettings": "Koreguoti anotacijų nustatymus", + "scrollViewTips": "Peržiūrėti šio objekto gyvavimo cikle esančius reikšmingus momentus.", + "autoTrackingTips": "Automatiškai sekančių kamerų apibrėžiančios dėžutės pozicija bus netiksli.", + "count": "{{first}} iš {{second}}", + "trackedPoint": "Sekamas Taškas", + "carousel": { + "previous": "Ankstesnė skaidrė", + "next": "Sekanti skaidrė" + } + }, + "dialog": { + "confirmDelete": { + "desc": "Trinant šį sekamą objektą taip pat bus pašalintos momentinės iškarpos, išsaugoti įterpiai, priskirti objekto gyvavimo ciklo įrašai. Šių sekamų objektų įrašyta filmuota medžiaga Istorijos vaizde ištrinta NEBUS.

    Ar esate įsitikinę, kad norite tęsti?", + "title": "Patvirtinti Ištrynimą" + } + }, + "trackedObjectDetails": "Sekamų Objektų Detalės", + "type": { + "details": "detalės", + "snapshot": "momentinės nuotraukos", + "video": "vaizdas", + "object_lifecycle": "objekto gyvavimo ciklas" + }, + "itemMenu": { + "downloadVideo": { + "label": "Atsisiųsti video", + "aria": "Atsisiųsti video" + }, + "downloadSnapshot": { + "label": "Atsisiųsti momentinę nuotrauką", + "aria": "Atsisiųsti momentinę nuotrauką" + }, + "viewObjectLifecycle": { + "label": "Peržiūrėti objekto gyvavimo ciklą", + "aria": "Rodyti objekto gyvavimo ciklą" + }, + "findSimilar": { + "label": "Rasti panašų", + "aria": "Rasti panašius sekamus objektus" + }, + "addTrigger": { + "label": "Pridėti trigerį", + "aria": "Šiam sekamam objektui pridėti trigerį" + }, + "audioTranscription": { + "label": "Aprašyti", + "aria": "Užklausti garso aprašymo" + }, + "submitToPlus": { + "label": "Pateikti į Frigate+", + "aria": "Pateikti į Frigate Plius" + }, + "viewInHistory": { + "label": "Žiūrėti Istorijoje", + "aria": "Žiūrėti Istorijoje" + }, + "deleteTrackedObject": { + "label": "Ištrinti šį sekamą objektą" + } + }, + "noTrackedObjects": "Sekamų Objektų Nerasta", + "fetchingTrackedObjectsFailed": "Sekamų objektų ištraukti nepavyko: {{errorMessage}}", + "searchResult": { + "tooltip": "Sutapo {{type}} su {{confidence}}% patikimumu", + "deleteTrackedObject": { + "toast": { + "success": "Sekami objektai sėkmingai ištrinti.", + "error": "Sekamų objektų nepavyko ištrinti: {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "DI Analizė" + }, + "concerns": { + "label": "Rūpesčiai" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/exports.json new file mode 100644 index 0000000..c8b257a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/exports.json @@ -0,0 +1,17 @@ +{ + "search": "Paieška", + "documentTitle": "Eksportuoti - Frigate", + "noExports": "Eksportuotų įrašų nerasta", + "deleteExport": "Ištrinti Eksportuotą Įrašą", + "deleteExport.desc": "Esate įsitikinę, kad norite ištrinti {{exportName}}?", + "editExport": { + "title": "Pervadinti Eksportuojamą įrašą", + "desc": "Įveskite nauja pavadinimą šiam eksportuojamam įrašui.", + "saveExport": "Išsaugoti Eksportuojamą Įrašą" + }, + "toast": { + "error": { + "renameExportFailed": "Nepavyko pervadinti eksportuojamo įrašo: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/faceLibrary.json new file mode 100644 index 0000000..721e119 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "description": { + "addFace": "Pridėkite naują kolekciją į Veidų Kolekciją įkeldami savo pirmą nuotrauką.", + "placeholder": "Įveskite pavadinimą šiai kolekcijai", + "invalidName": "Netinkamas vardas. Vardas gali būti sudarytas tik iš raidžiū, skaičių, tarpų, apostrofų, pabraukimų ar brūkšnelių." + }, + "details": { + "person": "Žmogus", + "face": "Veido detelės", + "timestamp": "Laiko žyma", + "unknown": "Nežinoma", + "subLabelScore": "Sub Etiketės Balas", + "scoreInfo": "Sub etiketės balas yra pasvertas balas pagal visų atpažintų veidų užtikrintumą, taigi gali skirtis nuo balo rodomo momentinėje nuotraukoje.", + "faceDesc": "Papildoma informacija sekamo objekto, kuris sugeneravo šį veidą" + }, + "selectItem": "Pasirinkti {{item}}", + "deleteFaceAttempts": { + "desc_one": "Esate įsitikine, kad norite ištrinti {{count}} veidą? Šio veiksmo sugrąžinimas negalimas.", + "desc_few": "Esate įsitikine, kad norite ištrinti {{count}} veidus? Šio veiksmo sugrąžinimas negalimas.", + "desc_other": "Esate įsitikine, kad norite ištrinti {{count}} veidų? Šio veiksmo sugrąžinimas negalimas.", + "title": "Ištrinti Veidus" + }, + "toast": { + "success": { + "deletedFace_one": "Sėkmingai ištrintas{{count}} veidas.", + "deletedFace_few": "Sėkmingai ištrinti {{count}} veidai.", + "deletedFace_other": "Sėkmingai ištrinta {{count}} veidų.", + "deletedName_one": "{{count}} veidas buvo sėkmingai ištrintas.", + "deletedName_few": "{{count}} veidai buvo sėkmingai ištrinti.", + "deletedName_other": "{{count}} veidų buvo sėkmingai ištrinta.", + "uploadedImage": "Nuotrauka sėkmingai įkelta.", + "addFaceLibrary": "{{name}} vardas buvo sėkmingai pridėtas į Veidų Katalogą!", + "renamedFace": "Sėkmingai veidas pervadintas į {{name}}", + "trainedFace": "Veidas apmokytas sėkmingai.", + "updatedFaceScore": "Veido balas atnaujintas sėkmingai." + }, + "error": { + "uploadingImageFailed": "Nepavyko įkelti nuotraukos: {{errorMessage}}", + "addFaceLibraryFailed": "Nepavyko priskirti vardo veidui: {{errorMessage}}", + "deleteFaceFailed": "Nepavyko ištrinti: {{errorMessage}}", + "deleteNameFailed": "Vardo ištrinti nepavyko: {{errorMessage}}", + "renameFaceFailed": "Nepavyko pervardinti veido: {{errorMessage}}", + "trainFailed": "Nepavyko apmokinti: {{errorMessage}}", + "updateFaceScoreFailed": "Veido balų atnaujinti nepavyko: {{errorMessage}}" + } + }, + "createFaceLibrary": { + "nextSteps": "Kad sukurtumėte stiprų pagrindą:
  • Naudokite Pastarieji Atpažinimai skirtuką pasirinkti ir paveikslėliais apmokyti kiekvieną aptiktą asmenį.
  • Norint pasiekti geriausią režultatą, susitelkite prie nuotraukų iš priekio; Venkite naudoti veidų nuotraukas kampu pasuktu veidu.
  • ", + "title": "Sukurti Kolekciją", + "desc": "Sukurti naują kolekciją", + "new": "Sukurti Naują Veidą" + }, + "deleteFaceLibrary": { + "desc": "Esate įsitikinę, kad norite ištrinti kolenkciją vardu {{name}}? Visi susiję veidai bus ištrinti negražinamai.", + "title": "Ištrinti Vardą" + }, + "documentTitle": "Veidų Katalogas - Frigate", + "uploadFaceImage": { + "title": "Įkelti Veido Nuotrauką", + "desc": "Įkelti nuotrauką veidų skanavimui ir įtraukti {{pageToggle}}" + }, + "collections": "Kolekcijos", + "steps": { + "faceName": "Įveskite Vardą Veidui", + "uploadFace": "Įkelti Veido Nuotrauką", + "nextSteps": "Sekantis Žingsnis", + "description": { + "uploadFace": "Įkelti nuotrauką {{name}} kuri atvaizduoja veidą iš priekio. Nuotraukos kadruoti nereikia." + } + }, + "train": { + "title": "Pastarieji Atpažinimai", + "aria": "Pasirinkti pastaruosius atpažinimus", + "empty": "Pastaruoju metu nebuvo atliktas veidų atpažinimas" + }, + "selectFace": "Pasirinkti Veidą", + "renameFace": { + "title": "Pervadinti Veidą", + "desc": "Įveskite {{name}} naują vardą" + }, + "button": { + "deleteFaceAttempts": "Ištrinti Veidus", + "addFace": "Pridėti Veidą", + "renameFace": "Pervadinti Veidą", + "deleteFace": "Ištrinti Veidą", + "uploadImage": "Įkelti Nuotrauką", + "reprocessFace": "Patikrinti Veidą" + }, + "imageEntry": { + "validation": { + "selectImage": "Prašome pasirinkti nuotraukos bylą." + }, + "dropActive": "Įkelkite nuotrauką čia…", + "dropInstructions": "Užvilkite nuotrauką čia, arba spragtelkite pasirinkti", + "maxSize": "Max dydis: {{size}}MB" + }, + "nofaces": "Nėra veidų", + "pixels": "{{area}}px", + "trainFaceAs": "Apmokyti Veidą kaip:", + "trainFace": "Apmokyti Veidą" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/live.json new file mode 100644 index 0000000..06f1577 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/live.json @@ -0,0 +1,183 @@ +{ + "documentTitle": "Gyvai - Frigate", + "documentTitle.withCamera": "{{camera}} - Tiesiogiai - Frigate", + "lowBandwidthMode": "Mažo-pralaidumo Rėžimas", + "cameraAudio": { + "enable": "Įgalinti Kamerų Garsą", + "disable": "Išjungti Kamerų Garsą" + }, + "twoWayTalk": { + "enable": "Įgalinti Dvipusį Pokalbį", + "disable": "Išjungti Dvipusį Pokalbį" + }, + "detect": { + "enable": "Įjungti Aptikimą", + "disable": "Išjungti Aptikimą" + }, + "audioDetect": { + "enable": "Įjungti Garso Aptikimą", + "disable": "Išjungti Garso Aptikimą" + }, + "cameraSettings": { + "objectDetection": "Objektų Aptikimai", + "audioDetection": "Garso Aptikimas", + "title": "{{camera}} Nustatymai", + "cameraEnabled": "Kamera įjungta", + "recording": "Įrašinėjimas", + "snapshots": "Momentinės Nuotraukos", + "transcription": "Garso Transkripcija", + "autotracking": "Automatinis sekimas" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Norint išcentruoti kamerą, spragtelti kadre", + "enable": "Įjungti paspaudimą, kad judinti", + "disable": "Išjungti paspaudimą kad judinti" + }, + "left": { + "label": "Pasukti PTZ kamerą į kairę" + }, + "up": { + "label": "Pasukti PTZ kamerą į viršų" + }, + "down": { + "label": "Pasukti PTZ kamera žemyn" + }, + "right": { + "label": "Pasukti PTZ kamerą į dešinę" + } + }, + "zoom": { + "in": { + "label": "Priartinti PTZ kamerą" + }, + "out": { + "label": "Atitolinti PTZ kamerą" + } + }, + "focus": { + "in": { + "label": "Sutelkti PTZ kameros fokusą" + }, + "out": { + "label": "Išleisti PTZ kameros fokusą" + } + }, + "frame": { + "center": { + "label": "Spragtelkite kadre norint centruoti PTZ kamerą" + } + }, + "presets": "PTZ kameros nustatytos pozicijos" + }, + "camera": { + "enable": "Įjungti Kamerą", + "disable": "Išjungti Kamerą" + }, + "muteCameras": { + "enable": "Užtildyti Visas Kameras", + "disable": "Aktyvuoti Garsą Visoms Kameroms" + }, + "recording": { + "enable": "Įjungti Įrašymus", + "disable": "Įšjungti Įrašymus" + }, + "snapshots": { + "enable": "Įjungti Momentines Nuotraukas", + "disable": "Išjungti Momentines Nuotraukas" + }, + "transcription": { + "enable": "Įjungti Gyvą Garso Aprašymą", + "disable": "Išjungti Gyvą Garso Aprašymą" + }, + "autotracking": { + "enable": "Įjungti Autosekimą", + "disable": "Išjungti Autosekimą" + }, + "streamStats": { + "enable": "Rodyti Transliacijos Stats", + "disable": "Paslėpti Transliacijos Stats" + }, + "manualRecording": { + "title": "Pagal-Poreikį", + "tips": "Atsisiųsti momentinę nuotrauką arba kurti manualaus saugojimo nustatymus pagal įvykius iš šios kameros.", + "playInBackground": { + "label": "Paleisti fone", + "desc": "Įjungti šią funkciją kad transliaciją išliktų net paslėpus grotuvą." + }, + "showStats": { + "label": "Rodytis Stats", + "desc": "Įjungti šią funkciją, kad matytumėte transliacijos statistiką kameros vaizde." + }, + "debugView": "Debug Vaizdas", + "start": "Pradėti įrašymą pagal pageidavimą", + "started": "Pradėtas įrašymas pagal pageidavimą.", + "failedToStart": "Nepavyko pradėti įrašymo pagal poreikį.", + "recordDisabledTips": "Įrašymas šiai kamerai yra išjungtas todėl bus saugomos tik momentinės nuotraukos.", + "end": "Baigti įrašymą pagal pageidavimą", + "ended": "Baigtas įrašymas pagal pageidavimą.", + "failedToEnd": "Nepavyko sustabdyti įrašymo pagal pageidavimą." + }, + "streamingSettings": "Transliacijos Nustatymai", + "notifications": "Pranešimai", + "audio": "Garsas", + "suspend": { + "forTime": "Sustabdyti laikui: " + }, + "stream": { + "title": "Transliacija", + "audio": { + "tips": { + "title": "Šiai transliacijai garso išvestis turi būti sukonfiguruota naudojant go2rtc." + }, + "available": "Ši transliacija palaiko garsą", + "unavailable": "Ši transliacija nepalaiko garso" + }, + "twoWayTalk": { + "tips": "Jūsų įranga turi palaikyti šią funkciją, taip pat dvipusiam pokalbiui reikia sukonfiguruoti WebRTC.", + "available": "Šioje transliacijoje galimas dvipusis pokalbis", + "unavailable": "Šioje transliacijoje dvipusio pokalbio galimybių nėra" + }, + "lowBandwidth": { + "tips": "Dėl buffering ar transliacijos klaidų tiesioginė transliacija yra mažos reiškos rėžime.", + "resetStream": "Atstatyti transliaciją" + }, + "playInBackground": { + "label": "Paleisti fone", + "tips": "Norėdami kad transliacija tęstūsi kai grotuvas paslėpiamas įjunkite šią funkciją." + }, + "debug": { + "picker": "Debug rėžime srauto pasirinkimas negalimas. Debug lange naudojamas tas srautas, kuris priskirtas aptikimo rolei." + } + }, + "history": { + "label": "Rodyti istorinius įrašus" + }, + "effectiveRetainMode": { + "modes": { + "all": "Visi", + "motion": "Judesys", + "active_objects": "Aktyvūs Objektai" + }, + "notAllTips": "Jūsų {{source}} įrašų saugojimo pasirinkimas nustatytas rėžime: {{effectiveRetainMode}}, taigi įrašai pagal poreikį irgi bus saugomi pritaikant {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Redaguoti Išdėstymą", + "group": { + "label": "Redaguoti Kamerų Grupę" + }, + "exitEdit": "Išeiti Iš Redagavimo" + }, + "noCameras": { + "title": "Nėra Sukonfiguruotų Kamerų", + "description": "Pradėti nuo kameros prijungimo pire Frigate.", + "buttonText": "Pridėti Kamerą" + }, + "snapshot": { + "takeSnapshot": "Atsisiųsti momentinį kadrą", + "noVideoSource": "Momentinei nuotraukai nėra prieinamo video šaltinio.", + "captureFailed": "Nepavyko užfiksuoti kadro.", + "downloadStarted": "Momentinės nuotraukos atsisiuntimas pradėtas." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/recording.json new file mode 100644 index 0000000..5acee13 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtras", + "export": "Eksportuoti", + "calendar": "Kalendorius", + "filters": "Filtrai", + "toast": { + "error": { + "noValidTimeSelected": "Pasriniktas laiko periodas netinkamas", + "endTimeMustAfterStartTime": "Pabaigos laikas privalo būti vėlesnis nei pradžios laikas" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/search.json new file mode 100644 index 0000000..054efd0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Paieška", + "savedSearches": "Išsaugotos Paieškos", + "searchFor": "Ieškoti {{inputValue}}", + "button": { + "clear": "Išvalyti paiešką", + "save": "Išsaugoti paiešką", + "delete": "Ištrinti išsaugotą paiešką", + "filterInformation": "Filtruoti informaciją", + "filterActive": "Aktyvūs filtrai" + }, + "trackedObjectId": "Sekamo Objekto ID", + "filter": { + "label": { + "cameras": "Kameros", + "labels": "Etiketės", + "zones": "Zonos", + "search_type": "Paieškos Tipas", + "time_range": "Laiko rėžis", + "before": "Prieš", + "after": "Po", + "min_score": "Min Balas", + "max_score": "Max Balas", + "min_speed": "Min Greitis", + "max_speed": "Max Greitis", + "recognized_license_plate": "Atpažinti Registracijos Numeriai", + "has_clip": "Turi Klipą", + "has_snapshot": "Turi Nuotrauką", + "sub_labels": "Sub Etiketės" + }, + "searchType": { + "thumbnail": "Miniatiūra", + "description": "Aprašymas" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data 'prieš' turi būti vėliau nei data 'po'.", + "afterDatebeEarlierBefore": "Data 'po' turi būti anksčiau nei data 'prieš'.", + "minScoreMustBeLessOrEqualMaxScore": "'min balas' turi būti mažesnis arba lygus 'max balui'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'max balas' turi būti didesnis arba lygus 'min balui'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'min greitis' privalo būti mažesnis arba lygus 'max greičiui'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'max greitis' privalo būti didesnis arba lygus 'min greičiui'." + } + }, + "tips": { + "title": "Kaip naudoti tekstinius filtrus", + "desc": { + "text": "Filtrai leidžia susiaurinti paieškos rezultatus. Štai kaip juos naudoti įvesties laukelyje:", + "step1": "Įveskite filtravimo raktą po kurio seks dvitaškis (pvz., \"cameras:\").", + "step2": "Pasirinkite reikšmę iš siūlomų arba įveskite savo sugalvotą.", + "step3": "Naudokite kelis filtrus įvesdami juos vieną paskui kitą su tarpu tarp jų.", + "step5": "Laiko rėžio filtro naudojamas {{exampleTime}} formatas.", + "step6": "Pašalinti filtrus spaudžiant 'x' šalia jų.", + "exampleLabel": "Pavyzdys:", + "step4": "Datų filtrai (before: and after:) naudoti {{DateFormat}} formatą." + } + }, + "header": { + "currentFilterType": "Filtruoti Reikšmes", + "noFilters": "Filtrai", + "activeFilters": "Aktyvūs Filtrai" + } + }, + "similaritySearch": { + "title": "Panašumų Paieška", + "active": "Panašumų paieška aktyvi", + "clear": "Išvalyti panašumų paiešką" + }, + "placeholder": { + "search": "Ieškoma…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/settings.json new file mode 100644 index 0000000..4fcd9cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/settings.json @@ -0,0 +1,877 @@ +{ + "documentTitle": { + "default": "Nustatymai - Frigate", + "authentication": "Autentifikavimo Nustatymai - Frigate", + "camera": "Kameros Nustatymai - Frigate", + "object": "Debug - Frigate", + "general": "Bendrieji Nustatymai - Frigate", + "frigatePlus": "Frigate+ Nustatymai - Frigate", + "notifications": "Pranešimų Nustatymai - Frigate", + "motionTuner": "Judesio Derinimas - Frigate", + "enrichments": "Patobulinimų Nustatymai - Frigate", + "masksAndZones": "Maskavimo ir Zonų redaktorius - Frigate", + "cameraManagement": "Valdyti Kameras - Frigate", + "cameraReview": "Kameros Peržiūros Nustatymai - Frigate" + }, + "menu": { + "ui": "UI", + "enrichments": "Patobulinimai", + "cameras": "Kameros Nustatymai", + "masksAndZones": "Maskavimai / Zonos", + "motionTuner": "Judesio Derintojas", + "debug": "Debug", + "users": "Vartotojai", + "notifications": "Pranešimai", + "frigateplus": "Frigate+", + "triggers": "Trigeriai", + "roles": "Rolės", + "cameraManagement": "Valdymas", + "cameraReview": "Peržiūra" + }, + "dialog": { + "unsavedChanges": { + "title": "Yra neišsaugotų pakeitimų.", + "desc": "Ar norite išsaugoti savo pakeitimus prieš tęsdami?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Nėra Kameros" + }, + "general": { + "title": "Bendri Nustatymai", + "liveDashboard": { + "title": "Tiesioginės Transliacijos Skydelis", + "automaticLiveView": { + "label": "Automatinis Tiesioginis Vaizdas", + "desc": "Automatiškai perjungti į kameros tiesioginį vaizdą kai aptinkama veikla. Išjungus šią funkciją tiesioginės transliacijos skydelyje kamerų vaizdai atsinaujis tik kartą per minutę." + }, + "playAlertVideos": { + "label": "Leist Įspejimų Vaizdus", + "desc": "Pagal nutylėjimą, paskutinieji įspėjimai rodomį kaip maži cikliški vaizdo įrašai. Šią funkciją išjunkite jei norite matyti statinius įspėjimų paveiksliukus šiame įrenginyje/naršyklėje." + } + }, + "storedLayouts": { + "title": "Išsaugoti Išdėstymai", + "desc": "Kamerų išdėstymai kamerų grupėje gali būti perkeliami/keičiami dydžiai. Pozicijos išsaugomos jūsų naršyklės vietinėje atmintyje.", + "clearAll": "Išvalyti Visus Išdėstymus" + }, + "cameraGroupStreaming": { + "title": "Kamerų Grupės Transliacijos Nustatymai", + "desc": "Transliacijos nustatymai kiekvienai kamerų grupei yra saugomi jūsų naršyklės vietinėje atmintyje.", + "clearAll": "Išvalyti Visus Transliavimo Nustatymus" + }, + "recordingsViewer": { + "title": "Įrašų Naršyklė", + "defaultPlaybackRate": { + "label": "Numatytasis Atkūrimo Dažnis", + "desc": "Numatytas atkūrimo dažnis įrašų atkūrimui." + } + }, + "calendar": { + "title": "Kalendorius", + "firstWeekday": { + "label": "Pirma Savaitės Diena", + "desc": "Diena kuria prasideda savaitės peržiūrų kalendoriuje.", + "sunday": "Sekmadienis", + "monday": "Pirmadienis" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Saugoti išdėstimai kamerai{{cameraName}} išvalyti", + "clearStreamingSettings": "Visų kamerų grupių transliavimo nustatymai išvalyti." + }, + "error": { + "clearStoredLayoutFailed": "Nepavyko išvalyti išsaugotų pozicijų išdėstymų: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nepavyko išvalyti transliavimo nustatymų: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Pagerinimų Nustatymai", + "unsavedChanges": "Neišsaugoti Pagerinimų nustatymų pakeitimai", + "birdClassification": { + "title": "Paukščių Klasifikatorius", + "desc": "Paukščių klasifikatorius identifikuoja žinomus paukščius naudojant kvantinizuotą Tensorflow modelį. Kai žinomas paukštis atpažįstamas, jo bendrinis pavadinimas bus pridėtas prie sub_etikečių. Ši informacija yra pridedama vartotojo sąsajoje, filtruose, taip pat ir pranešimuose." + }, + "semanticSearch": { + "title": "Semantic Paieška", + "desc": "Frigate Semantic Paieška leidžia jums atrasti sekamus objektus tarp peržiūrų, naudojant arba pačius paveiksliuks, vartotojo pateiktus tekstinius aprašymus arba automatiškai sugeneruotas reikšmes.", + "reindexNow": { + "label": "Perindeksuoti Dabar", + "desc": "Perindeksavimas sugeneruos įterpinius visiems sekamiems objektams. Šis procesas veiks fone ir priklausomai nuo jūsų turimo sekamų objektų kiekio gali maksimaliai apkrauti jūsų CPU bei užtrukti nemažai laiko.", + "confirmTitle": "Patvirtinti Reindeksavimą", + "confirmDesc": "Ar esate įsitikinę, kad norite reindeksuoti visų sekamų objektų įterpius? Šis processas veiks fone ir gali maksimaliai apkrauti jūsų CPU bei užtrukti nemažai laiko. Progresą jūs galėsite stebėti Tyrinėjimo puslapyje.", + "confirmButton": "Reindeksuoti", + "success": "Reindeksavimas pradėtas sėkmingai.", + "alreadyInProgress": "Redindeksavimas jau vykdomas.", + "error": "Nepavyko pradėti reindeksavimo: {{errorMessage}}" + }, + "modelSize": { + "label": "Modelio Dydis", + "desc": "Modelio dydis naudojamas semantic paieškos įterpiuose.", + "small": { + "title": "mažas", + "desc": "Naudojant mažą pasitelkiama kvantizuota modelio versija kuri reikalauja mažiau RAM, naudojant CPU veikia greičiau su nežįmiu skirtumu įterpių kokybei." + }, + "large": { + "title": "didelis", + "desc": "Naudojant didelį pasitelkiamas pilnas Jina modelis ir automatiškai naudos GPU jei yra galimas." + } + } + }, + "faceRecognition": { + "title": "Veidų Atpažinimas", + "desc": "Veidų atpažinimas leidžia priskirti vardus žmonėms ir kai jų veidai atpažįstami Frigate priskirs žmogaus vardą kaip sub etiketę. Ši informacija prieinama vartotojo sąsajoje, filtruose, taip ir pranešimuose.", + "modelSize": { + "label": "Modelio Dydis", + "desc": "Modelio dydis naudojamas veidų atpažinimui.", + "small": { + "title": "mažas", + "desc": "Naudojant mažą pasitelkiamas FaceNet veidų įterpių modelis kuris efektyviai veikia su daugeliu CPU." + }, + "large": { + "title": "didelis", + "desc": "Naudojant didelį pasitelkiamas ArcFace face embedding modelis ir jei yra galimybė automatiškai naudos GPU." + } + } + }, + "licensePlateRecognition": { + "title": "Registracijos Numerių Atpažinimas", + "desc": "Frigate gali atpažinti automobilių registracijos numerius ir automatiškai pridėti aptikitus simbolius į \"recognized_license_plate\" laukelį arba žinoma pavadinima kaip sub_etiketę \"mašina\" tipo objektams. Dažnas panaudojimas būtų nuskaityti numerius mašinų įvažiuojančių į įvažiavimą arba mašinų pravažiuojančių gatve." + }, + "restart_required": "Privaloma perkrauti (Patobulinimų nustatymai pakeisti)", + "toast": { + "success": "Patobulinimų nustaty buvo pakeisti. Kad pkyčiai būtų pritaikyti perkraukite Frigate.", + "error": "Nepavyko išsaugoti konfiguracijos pakeitimų: {{errorMessage}}" + } + }, + "camera": { + "title": "Kamerų Nustatymai", + "streams": { + "title": "Transliacijos", + "desc": "Laikinai išjunkite kamerą kol Frigate bus perkrautas. Išjungiant kamerą visiškai sustabdo Frigate veiklą šiai kamerai. Nebus aptikimų, įrašų ar debug informacijos.
    Pastaba: Tai neišjungs go2rtc sratų." + }, + "review": { + "desc": "Trumpam įjungti/išjungti įspėjimus ir aptikimus šiai kamerai iki kol Frigate bus perkrautas. Kai išjungta, naujos peržiūros nebus kuriamos. ", + "detections": "Aptikimai ", + "title": "Peržiūra", + "alerts": "Įspėjimai " + }, + "reviewClassification": { + "desc": "Frigate kategorizuoja peržiūras į Įspėjimus ir Aptikimus. Pagal nutylėjimą, visi žmonių ir mašinų objektai yra vertinami kaip įspėjimai. Jūs galite detalizuoti peržiūrų kategorizavimą priskiriant objektas privalomas zonas.", + "zoneObjectAlertsTips": "Visi {{alertsLabels}} objektai aptikti {{zone}} ir {{cameraName}} bus rodomi kaip Įspėjimai.", + "objectDetectionsTips": "Visi {{detectionsLabels}} objektai nekategorizuoti {{cameraName}} bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra.", + "zoneObjectDetectionsTips": { + "text": "Visi {{detectionsLabels}} objektai nekategorizuoti {{zone}} ir {{cameraName}} bus rodomi kaip Aptikimai.", + "notSelectDetections": "Visi {{detectionsLabels}} objektai aptikti {{zone}} ir {{cameraName}} nekategorizuojami kaip Įspėjimai bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra.", + "regardlessOfZoneObjectDetectionsTips": "Visi {{detectionsLabels}} objektai nekategorizuoti {{cameraName}} bus rodomi kaip Aptikimai nepriklausomai kurioje zonoje ji yra." + }, + "selectDetectionsZones": "Pasirinkti zonas Aptikimams", + "limitDetections": "Apriboti aptikimus specifinėms zonoms", + "title": "Apžvalgų Kalsifikavimas", + "noDefinedZones": "Šiai kamerai sukurtų zonų nėra.", + "objectAlertsTips": "Visi {{alertsLabels}} objektai kemeroje {{cameraName}} bus rodomi kaip Įspėjimai.", + "unsavedChanges": "Neišsaugoti Apžvalgos Klasifikavimo nustatymai kamerai {{camera}}", + "selectAlertsZones": "Pasirinkti zonas Įspėjimams", + "toast": { + "success": "Apžvalgų Klasifikavimo konfiguracija buvo išsaugota. Restartuoti Frigate kad pokyčiai būtų pritaikyti." + } + }, + "cameraConfig": { + "ffmpeg": { + "rolesUnique": "Kiekviena role (garso, aptikimo, įrašymo) gali buti priskirta tik vienam srautui", + "inputs": "Įvesties Srautas", + "path": "Srauto Kelias", + "pathRequired": "Srauto Kelias yra privalomas", + "pathPlaceholder": "rtsp://...", + "roles": "Rolės", + "rolesRequired": "Privaloma bent viena rolė", + "addInput": "Pridėti Įvesties Srautą", + "removeInput": "Pašalinti Įvesties Srautą", + "inputsRequired": "Privalomas bent vienas įvesties srautas" + }, + "add": "Pridėti Kamerą", + "edit": "Koreguoti Kamerą", + "description": "Konfiguruoti kameros nustatymus įskaitant įvesties srautus ir roles.", + "name": "Kamera Pavadinimas", + "nameRequired": "Kamera pavadinimas yra privalomas", + "nameLength": "Kamera pavadininas privalo būti trumpesnis nei 24 simboliai.", + "namePlaceholder": "pvz., priekinės_durys", + "enabled": "Šjungti", + "toast": { + "success": "Kamera {{cameraName}} sėkmingai išsaugota" + } + }, + "object_descriptions": { + "title": "Generatyvinio DI Objektų Aprašymai", + "desc": "Laikinai įjungti/ išjungti Ganaratyvinio DI objektų aprašymus šiai kamerai. Kai išjungta, šios kameros sekamiems objektams nebus generuojami aprašymai." + }, + "review_descriptions": { + "title": "Generatyvinio DI Apžvalgų Aprašymai", + "desc": "Laikinai įjungti/ išjungti Ganaratyvinio DI apžvalgų aprašymus šiai kamerai. Kai išjungta, šios kameros apžvalgoms nebus generuojami aprašymai." + }, + "addCamera": "Pridėti Naują Kamerą", + "editCamera": "Koreguoti Kamerą:", + "selectCamera": "Pasirinkti Kamera", + "backToSettings": "Atgal į Kameros Nustatymus" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "label": "Zonos", + "documentTitle": "Redaguoti Zonas - Frigate", + "desc": { + "title": "Zonos leidžia apibrėžti specifinį kadro plotą tam, kad galėtumėte įvardinti ar objektas yra tam tikrame plote.", + "documentation": "Dokumentacija" + }, + "add": "Pridėti Zoną", + "edit": "Redaguoti Zoną", + "clickDrawPolygon": "Spragtelkite ant paveiksliuko kad pradėtumėte piešti poligoną.", + "name": { + "title": "Pavadinimas", + "inputPlaceHolder": "Įveskite pavadinimą …", + "tips": "Pavadinimas privalo būti bent 2 simboliai, privalo turėti bent vieną raidę ir negali būti toks pat kaip kita kamera ar zona." + }, + "inertia": { + "title": "Inercija", + "desc": "Nurodo kiek kadrų objektas turi būti zonoje, kad užskaitytu kaip esantį zonoje. Bazinis: 3" + }, + "loiteringTime": { + "title": "Delsos Laikas", + "desc": "Nurodo minimalų laiką sekundėmis, kurį objektas turi būti zonoje, kad aktyvuotūsi. Bazinis: 0" + }, + "objects": { + "title": "Objektai", + "desc": "Objektų sąrašas kurie taikomi šiai zonai." + }, + "allObjects": "Visi Objektai", + "speedEstimation": { + "title": "Greičio Vertinimas", + "desc": "Įjungti greičio vertinimą objektams šioje zonoje. Zona privalo turėti būtent 4 taškus.", + "lineADistance": "Linijos A atstumas ({{unit}})", + "lineBDistance": "Linijos B atstumas ({{unit}})", + "lineCDistance": "Linijos C atstumas ({{unit}})", + "lineDDistance": "Linijos D atstumas ({{unit}})" + }, + "speedThreshold": { + "title": "Greičio Riba ({{unit}})", + "desc": "Nurodo mnimalų objekto greitį, kad užskaityti esantį zonoje.", + "toast": { + "error": { + "pointLengthError": "Greičio vertinimas buvo išjungtas šiai zonai. Zonos su greičio vertinimu privalo turėti tiksliai 4 taškus.", + "loiteringTimeError": "Zonos su delsos laiku didesniu nei 0 turėtų būti nenaudojamos su greičio vertinimu." + } + } + }, + "toast": { + "success": "Zona ({{zoneName}}) buvo išsaugota. Perkrauti Frigate kad įgalinti pokyčius." + } + }, + "motionMasks": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "desc": { + "title": "Judesių maskavimai yra naudojami sumažinti aptikimų užklausoms dėl nepageidajamų judesių. Objektų sekimas taps kėblesnis jei maskuosite per daug.", + "documentation": "Dokumentacija" + }, + "context": { + "title": "Judesių maskavimai yra naudojami sumažinti aptikimų užklausoms dėl nepageidajamų judesių (pvz: medžių šakos, kameros laiko užrašas). Judesių maskavimas turi būti naudojamas labai saikingai, bjektų sekimas taps kėblesnis jei maskuosite per daug." + }, + "polygonAreaTooLarge": { + "tips": "Judesių maskavimas netrukdo objektų aptikimui. Vietoj to turėtumėte naudoti privalomas zonas.", + "title": "Judesio maskuoti dengia {{polygonArea}}% kameros ploto. Didelės judesio maskuotės nerekomenduojamos." + }, + "label": "Judesio Maskuotė", + "documentTitle": "Redaguoti Judesio Maskuotę - Frigate", + "add": "Nauja Judesio Maskuotė", + "edit": "Redaguoti Judesio Maskuotę", + "clickDrawPolygon": "Spragtelti kad piešti poligoną ant atvaizdo.", + "toast": { + "success": { + "title": "{{polygonName}} išsaugotas. Perkrauti Frigate, kad pritaikyti pokyčius.", + "noName": "Judesio Maskuotė buvo išsaugota. Perkrauti Frigate, kad pritaikyti pokyčius." + } + } + }, + "objectMasks": { + "point_one": "{{count}} taškas", + "point_few": "{{count}} taškai", + "point_other": "{{count}} taškų", + "label": "Objekto Maskuotė", + "documentTitle": "Redaguoti Objekto Maskuotę - Frigate", + "desc": { + "title": "Objektų filtravimo maskuotės naudojamos išfiltruoti klaidingus teigiamus rezultatus pagal vietą parinktam objekto tipui.", + "documentation": "Dokumentacija" + }, + "add": "Pridėti Objekto Maskuotę", + "edit": "Redaguoti Objekto Maskuotę", + "context": "Objektų filtravimo maskuotės naudojamos išfiltruoti klaidingus teigiamus rezultatus pagal vietą parinktam objekto tipui.", + "clickDrawPolygon": "Spragtelti kad piešti poligoną ant atvaizdo.", + "objects": { + "title": "Objektai", + "desc": "Objekto tipai, kurie taikomi šiai objekto maskuotei.", + "allObjectTypes": "Visi objektų tipai" + }, + "toast": { + "success": { + "title": "{{polygonName}} buvo išsaugotas. Perkrauti Frigate, kad pritaikyti pokyčius.", + "noName": "Objektų Maskuotė buvo išsaugota. Perkrauti Frigate, kad pritaikyti pokyčius." + } + } + }, + "filter": { + "all": "Visos Maskuotės ir Zonos" + }, + "restart_required": "Reikalingas perkrovimas (maskavimai/ zonos pakeisti)", + "toast": { + "success": { + "copyCoordinates": "Poligono {{polyName}} koordinatės nukopijuotos į iškarpinę." + }, + "error": { + "copyCoordinatesFailed": "Nepavyko koordinačių nukopijuoti į iškarpinę." + } + }, + "motionMaskLabel": "Judesio Maskuotė {{number}}", + "objectMaskLabel": "Obejkto Maskuotė {{number}} {{label}}", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zonos pavadinime turi būti bent 2 simboliai.", + "mustNotBeSameWithCamera": "Zonos pavadinimas privalo skirtis nuo kameros pavadinimo.", + "alreadyExists": "Šiai kamerai zona šiuo pavadinimu jau egzistuoja.", + "mustNotContainPeriod": "Zonos pavadinimas negali turėti taško.", + "hasIllegalCharacter": "Zonos pavadinime yra neleistinų simbolių." + } + }, + "distance": { + "error": { + "text": "Atstumas privalo būti didesnis arba lygu 0.1.", + "mustBeFilled": "Norint naudoti greičio nustatymą visi atstumų laukai privalo būti užpildyti." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Incerciją privalo būti virš 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Delsos laikas privalo būti didesnis arba lygus 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Greičio riba privalo būti didesnė arba lygi 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Pašalinti paskutinį tašką", + "reset": { + "label": "Išvalyti visus taškus" + }, + "snapPoints": { + "true": "Prikabinti taškus", + "false": "Neprikabinti taškų" + }, + "delete": { + "title": "Patvirtinti Trynimą", + "desc": "Ar esate įsitikinę, kad norite ištrinti {{type}} {{name}}?", + "success": "{{name}} buvo ištrintas." + }, + "error": { + "mustBeFinished": "Poligono brėžinys privalo būti užbaigtas prieš išsaugant." + } + } + } + }, + "motionDetectionTuner": { + "title": "Judesių Aptikimų Derinimas", + "unsavedChanges": "Neišsaugoti Judesių Derinimo pokyčiai ({{camera}})", + "desc": { + "title": "Frigate naudoja judesių aptikimą kaip pirmos eilės patikrinimą įvertinti ar yra kadre kažkas, kam verta būtų atlikti objektų atpažinimą.", + "documentation": "Skaityti Judesių Derinimo Gidą" + }, + "Threshold": { + "title": "Riba", + "desc": "Ribos reikšmė diktuoja kiek pokyčio pikselio apšvietime turi būti, kad būtų traktuojama kaip judesys. Bazinis: 30" + }, + "contourArea": { + "title": "Kontūro Plotas", + "desc": "Kontūro ploto reikšmė yra naudojama įvertinti kurios grupės pasikeitusių pikselių bus vertinami kaip judesys. Bazinis: 10" + }, + "improveContrast": { + "title": "Pagerinti Kontrastą", + "desc": "Pagerinti kontrastą tamsiose scenose. Bazinis: Įjungta" + }, + "toast": { + "success": "Judesių nustatymai buvo išsaugoti." + } + }, + "debug": { + "detectorDesc": "Frigate naudoja jūsų detektorius ({{detectors}}) objektų aptikimui jūsų kameros transliacijoje.", + "audio": { + "noAudioDetections": "Nėra Garso aptikimų", + "title": "Garsas", + "score": "balai", + "currentRMS": "Dabartinis RMS", + "currentdbFS": "Dabartinis dbFS" + }, + "title": "Debug", + "desc": "Debug vaizde rodomas tiesioginis vaizdas sekamų objektų ir statistikos. Objektų sąrašas rodo užvėlintą santrauką aptiktų objektų.", + "openCameraWebUI": "Atverti {{camera}} kameros Web prieigą", + "debugging": "Debugging", + "objectList": "Objektų sąrašas", + "noObjects": "Objektų nėra", + "boundingBoxes": { + "title": "Apibrėžiančios dėžutės", + "desc": "Rodyti apibrėžiančias dėžutes aplink sekamus objektus", + "colors": { + "label": "Objektus Apibrėžiančių Dėžučių Spalvos", + "info": "
  • Pradžioje, skirtingos spalvos bus priskirtos kiekvienai objekto etiketei
  • Tamsiai mėlyna plona linija simbolizuoja, kad objektas esamu momentu dar nėra aptiktas
  • Pilka linija nurodo kad objektas yra aptiktas kaip nejudantis
  • Stora linija nurodo kad objektas yra automatiškai sekamas (kai įjungta)
  • " + } + }, + "timestamp": { + "title": "Laiko Žyma", + "desc": "Atvaizduoti laiko žymą vaizde" + }, + "zones": { + "title": "Zonos", + "desc": "Rodyti bet kurios zonos ribas" + }, + "mask": { + "title": "Judesio maskuotės", + "desc": "Rodyti judesio maskavimo poligonus" + }, + "motion": { + "title": "Judesio dėžutės", + "desc": "Rodyti dėžutes aplink vietas kur yra aptiktas judesys", + "tips": "

    Judesio Dėžutės


    Raudonos dėžutės bus kadro vietose kur judesys yra aptiktas

    " + }, + "regions": { + "title": "Regionai", + "desc": "Rodyti dėžutes regionų kurie yra parduoti į detektorių", + "tips": "

    Regionų Dėžutės


    Ryškiai žalios dėžutės atvaizduojamos vietose kurios yra perduotos objektų detektoriui.

    " + }, + "paths": { + "title": "Keliai", + "desc": "Rodyti sekamo objekto kelio išskirtinius taškus", + "tips": "

    Keliai


    Linijos ir apskritimai nurodo sekamo objekto judėjimo išskirtinius taškus

    " + }, + "objectShapeFilterDrawing": { + "title": "Filtrų Brėžiniai Objektų Formoms", + "desc": "Norėdami sužinoti atvaizdo plotą ir santykio detales nubrėžkite keturkampį ant atvaizdo", + "score": "Balai", + "ratio": "Santykis", + "area": "Plotas", + "tips": "Įjunkite šią funkciją nupiešti keturkampį ant kameros vaizdo, kad parodyti plotą ir santykį. Šios reikšmės gali būti naudojamos objekto formos filtro parametrams jūsų konfiguracijoje." + } + }, + "users": { + "dialog": { + "deleteUser": { + "warn": "Ar esate įsitikinę, kad norite ištrinti {{username}}?", + "title": "Ištrinti Vartotoją", + "desc": "Šis veiksmas negalės būti atkurtas. Tai visam laikui ištrins vartotojo paskyrą ir ištrins visą susijusią informaciją." + }, + "form": { + "user": { + "title": "Vartotojo vardas", + "desc": "Leidžiamos tik raidės, skaičiai, taškai ir pabraukimai.", + "placeholder": "Įvesti vartotojo vardą" + }, + "password": { + "title": "Slaptažodis", + "placeholder": "Įvesti slaptažodį", + "confirm": { + "title": "Patvirtinti Slaptažodį", + "placeholder": "Patvirtinti Slaptažodį" + }, + "strength": { + "title": "Slaptažodžio sudėtingumas: ", + "weak": "Silpnas", + "medium": "Vidutinis", + "strong": "Stiprus", + "veryStrong": "Labai Stiprus" + }, + "match": "Slaptažodžiai sutampa", + "notMatch": "Slaptažodžiai nesutampa" + }, + "newPassword": { + "title": "Naujas Slaptažodis", + "placeholder": "Įveskite naują slaptažodį", + "confirm": { + "placeholder": "Pakartokite naują slaptažodį" + } + }, + "usernameIsRequired": "Vartotojo vardas yra privalomas", + "passwordIsRequired": "Slaptažodis yra privalomas" + }, + "createUser": { + "title": "Sukurti Naują Vartotoją", + "desc": "Pridėti naują vartotojo paskyrą ir nurodyti prieigos roles prie Frigate funkcijų.", + "usernameOnlyInclude": "Vartotojo vardas gali būti sudarytas iš raidžių, skaičių, . arba _", + "confirmPassword": "Prašome patvirtinti slaptažodį" + }, + "passwordSetting": { + "cannotBeEmpty": "Slaptažodis negali būti tuščias", + "doNotMatch": "Slaptažodžiai nesutampa", + "updatePassword": "Atnaujinkite Spaltažodį vartotojui {{username}}", + "setPassword": "Sukurti Slaptažodį", + "desc": "Sukurti stiprų slaptažodį kad apsaugoti paskyrą." + }, + "changeRole": { + "title": "Pakeisti vartotojo Rolę", + "select": "Pasirinkti rolę", + "desc": "Atnaujinti leidimus vartotojui {{username}}", + "roleInfo": { + "intro": "Pasirinkti tinkama rolę šiam vartotojui:", + "admin": "Admin", + "adminDesc": "Pilna prieiga prie visų funkcijų.", + "viewer": "Žiūrovas", + "viewerDesc": "Leidžiama prie Tiesioginio vaizdo tinklelio, Peržiūrų, Paieškų ir Eksportavimo funkcijų.", + "customDesc": "Specializuota rolė su prieiga prie konkrečios kameros." + } + } + }, + "title": "Vartotojai", + "management": { + "title": "Vartotojų Valdymas", + "desc": "Valdyti šios Frigate aplinkos vartotojų paskyras." + }, + "addUser": "Pridėti Vartotoją", + "updatePassword": "Atnaujinti Slaptažodį", + "toast": { + "success": { + "createUser": "Vartotojas {{user}} sėkmingai sukurtas", + "deleteUser": "Vartotojas {{user}} sėkmingai ištrintas", + "updatePassword": "Slaptažodis atnaujintas sėkmingai.", + "roleUpdated": "Vartotojui {{user}} rolė sėkmingai atnaujinta" + }, + "error": { + "setPasswordFailed": "nepavyko išsaugoti slaptažodžio: {{errorMessage}}", + "createUserFailed": "Nepavyko sukurti vartotojo: {{errorMessage}}", + "deleteUserFailed": "Nepavyko ištrinti vartotojo: {{errorMessage}}", + "roleUpdateFailed": "Nepavyko atnaujinti rolės: {{errorMessage}}" + } + }, + "table": { + "username": "Vartotojo vardas", + "actions": "Veiksmai", + "role": "Rolė", + "noUsers": "Vartotojų nerasta.", + "changeRole": "Pakeisti vartotojo rolę", + "password": "Slaptažodis", + "deleteUser": "Ištrinti vartotoją" + } + }, + "triggers": { + "dialog": { + "deleteTrigger": { + "desc": "Ar esate įsitikinę, kad norite ištrinti trigerį {{triggerName}}? Šis veiksmas negalės būti atstatytas.", + "title": "Ištrinti Trigerį" + }, + "createTrigger": { + "title": "Sukurti Trigerį", + "desc": "Sukurti trigerį kamerai {{camera}}" + }, + "editTrigger": { + "title": "Koreguoti Trigerį", + "desc": "Koreguoti trigerio nustatymus kamerai {{camera}}" + }, + "form": { + "name": { + "title": "Pavadinimas", + "placeholder": "Įvesti trigerio pavadinimą", + "error": { + "minLength": "Pavadinimas turi būti bent dviejų simbolių ilgio.", + "invalidCharacters": "Pavadinime gali būti tik raidės, skaičiai, pabraukimai ir brūkšnelis.", + "alreadyExists": "Trigeris su tokiu vardu jau yra šiai kamerai." + } + }, + "enabled": { + "description": "Įjungti ar išjungti šį trigerį" + }, + "type": { + "title": "Tipas", + "placeholder": "Pasirinkti trigerio tipą" + }, + "content": { + "title": "Turinys", + "imagePlaceholder": "Pasirinkti paveikslėlį", + "textPlaceholder": "Įvesti teksto turinį", + "imageDesc": "Pasirinkite paveikslėli kad inicijuotumėte veiksmą kai panašus vaizdas bus aptiktas.", + "textDesc": "Įveskite tekstą kad inicijuotumėte veiksmą kai panašus sekamo objekto aprašymas bus aptiktas.", + "error": { + "required": "Turinys privalomas." + } + }, + "threshold": { + "title": "Riba", + "error": { + "min": "Riba privalo būti bent jau 0", + "max": "Riba privalo būti daugiausiai 1" + } + }, + "actions": { + "title": "Veiksmai", + "desc": "Pagal nutylėjimą, Frigate sukuria MQTT žinutę visiem trigeriams. Pasirinkite kokius papildomus veiksmus atlikti kai trigeris suveiks.", + "error": { + "min": "Bent vienas veiksmas privalo būti parinktas." + } + }, + "friendly_name": { + "title": "Draugiškas Pavadinimas", + "placeholder": "Pavadinikite ar apibūdinkite trigerį", + "description": "Draugiškas pavadinimas ar apibūdinimas šiam trigeriui nėra būtinas." + } + } + }, + "documentTitle": "Trigeriai", + "management": { + "title": "Trigerių Valdymas", + "desc": "Valdykite trigerius kamerai {{camera}}. Naudokite miniatiūros tipą, kad panašios miniatiūros būtų jūsų pasirinkto objekto trigeris, o aprašymo trigerį kad panašūs aprašymai būtų trigeris pagal jūsų parašytą tekstą." + }, + "addTrigger": "Pridėti Trigerį", + "table": { + "name": "Pavadinimas", + "type": "Tipas", + "content": "Turinys", + "threshold": "Riba", + "actions": "Veiksmai", + "noTriggers": "Šiai kamerai nėra sukonfiguruotų trigerių.", + "edit": "Koreguoti", + "deleteTrigger": "Trinti Trigerį", + "lastTriggered": "Paskutinį kartą suveikė" + }, + "type": { + "thumbnail": "Miniatiūra", + "description": "Aprašymas" + }, + "actions": { + "alert": "Pažymėti kaip įspėjimą", + "notification": "Siųsti Pranešimą" + }, + "toast": { + "success": { + "createTrigger": "Trigeris {{name}} sėkmingai sukurtas.", + "updateTrigger": "Trigeris {{name}} sėkmingai atnaujintas.", + "deleteTrigger": "Trigeris {{name}} sėkmingai ištrintas." + }, + "error": { + "createTriggerFailed": "Nepavyko sukurti trigerio: {{errorMessage}}", + "updateTriggerFailed": "Nepavyko atnaujinti trigerio: {{errorMessage}}", + "deleteTriggerFailed": "Nepavyko ištrinti trigerio: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantic Paieška išjungta", + "desc": "Norint naudoti Trigerius Semantic Paieška privalo būti įjungta." + } + }, + "notification": { + "title": "Pranešimai", + "notificationSettings": { + "title": "Pranešimų Nustatymai", + "desc": "Frigate praneįimai sukurti veikti su push pranešimais į įrenginį kai naršoma per naršyklę arba įdiegta kaip PWA." + }, + "notificationUnavailable": { + "title": "Pranešimai Negalimi", + "desc": "Web push pranešimai reikalauja saugios aplinkos (https://...). Tai yra naršyklės apribojimai. Atsidarykit Frigate saugiu kanalu kad galėtumėte naudotis pranešimais." + }, + "globalSettings": { + "title": "Visuotiniai Nustatymai", + "desc": "Laikinai sustabdyti pranešimus iš konkrečios kameros į registruotus įrenginius." + }, + "email": { + "title": "El.paštas", + "placeholder": "pvz.: laiskas@laiskas.com", + "desc": "El paštas turi būti veikiantis ir jums prieinamas, kad jus pasiektų informacija jei bus problemų su push pranešimų paslauga." + }, + "cameras": { + "title": "Kameros", + "noCameras": "Nėra kamerų", + "desc": "Pasirinkite kurioms kameroms norite įjungti pranešimus." + }, + "deviceSpecific": "Įrenginio Specifiniai Nustatymai", + "registerDevice": "Registruoti Šį Įrenginį", + "unregisterDevice": "Išregistruoti Šį Įrenginį", + "sendTestNotification": "Siųsti bandomąjį pranešimą", + "unsavedRegistrations": "Neišsaugotos Pranešimo registracijos", + "unsavedChanges": "Neišsaugoti Pranešimų pakeitimai", + "active": "Aktyvūs Pranešimai", + "suspended": "Pranešimai sustabdyti {{time}}", + "suspendTime": { + "suspend": "Sustabdyti", + "5minutes": "Sustabdyti 5 minutėms", + "10minutes": "Sustabdyti 10 minučių", + "30minutes": "Sustabdyti 30 minučių", + "1hour": "Sustabdyti 1 valandai", + "12hours": "Sustabdyti 12 valandų", + "24hours": "Sustabdyti 24 valandoms", + "untilRestart": "Sustabdyti iki perkrovimo" + }, + "cancelSuspension": "Atšaukti Sustabdymą", + "toast": { + "success": { + "registered": "Sėkmingai užregistruota pranešimams. Frigate perkrovimas yra būtinas, kad nors kokie pranešimai būtų išsiųsti (net ir bandomieji).", + "settingSaved": "Pranešimų nustatymai buvo išsaugoti." + }, + "error": { + "registerFailed": "Nepavyko išsaugoti pranešimų registravimo." + } + } + }, + "frigatePlus": { + "title": "Frigate+ Nustatymai", + "apiKey": { + "title": "Frigate+ API raktas", + "validated": "Frigate+ API raktas aptiktas ir patvirtintas", + "notValidated": "Frigate+ API raktas neaptiktas ir nepatvirtintas", + "desc": "Frigate+ API raktas įgalina integraciją su Frigate+ paslauga.", + "plusLink": "Skaityti daugiau apie Frigate+" + }, + "snapshotConfig": { + "title": "Momentinių kadrų Konfiguravimas", + "desc": "Pateikti į Frigate+ reikalauja abiejų, momentinių kadrų ir švarios_kopijosmomentinių kadrų įjungimo jūsų konfiguracijoje.", + "cleanCopyWarning": "Kai kurios kameros turi momentinius kadrus įjungtus tačiau švari kopija išjungta. Turite įjungti švarią_kopiją savo momentinės kopijos nustatymuose, kad galėtumėte teikti paveikslėlius į Frigate+.", + "table": { + "camera": "Kamera", + "snapshots": "Momentiniai Kadrai", + "cleanCopySnapshots": "Momentiniai kadrai švari_kopija" + } + }, + "modelInfo": { + "title": "Modelio Informacija", + "modelType": "Modelio Tipas", + "trainDate": "Apmokymo Data", + "baseModel": "Bazinis Modelis", + "plusModelType": { + "baseModel": "Bazinis Modelis", + "userModel": "Priderinta" + }, + "supportedDetectors": "Palaikomi Detektoriai", + "cameras": "Kameros", + "loading": "Užkraunama modelio informacija…", + "error": "Nepavyko užkrauti modelio informacijos", + "availableModels": "Pireinami Modeliai", + "loadingAvailableModels": "Kraunami prieinami modeliai…", + "modelSelect": "Jūsų prieinami modeliai iš Frigate+ gali būti pasirinkti čia. Pastaba, pasirinkiti galite modelius tik tuos kurie suderinami su esamu detektoriumi." + }, + "unsavedChanges": "Neišsaugoti Frigate+ nustatymų pokyčiai", + "restart_required": "Perkrovimas privalomas (Frigate+ modeliai pakeisti)", + "toast": { + "success": "Frigate+ nustatymai buvo išsaugoti. Perkraukite Frigate kad pritaikytumėte pokyčius.", + "error": "Nepavyko išsaugoti konfiguraijos pokyčių: {{errorMessage}}" + } + }, + "roles": { + "addRole": "Pridėti rolę", + "table": { + "role": "Rolė", + "cameras": "Kameros", + "actions": "Veiksmai", + "deleteRole": "Pašalinti rolę", + "noRoles": "Specializuotų rolių nerasta.", + "editCameras": "Koreguoti Kameras" + }, + "toast": { + "success": { + "deleteRole": "Rolė {{role}} sėkmingai pašalinta", + "userRolesUpdated_one": "{{count}} šios rolės vartotojai buvo priskirti rolei 'žiūrovas', kuri turi prieigą prie visų kamerų.", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "", + "createRole": "Rolė {{role}} sėkmingai sukurta", + "updateCameras": "Atnaujintos kameros rolei {{role}}" + }, + "error": { + "createRoleFailed": "Nepavyko sukurti rolės: {{errorMessage}}", + "updateCamerasFailed": "Nepavyko atnaujinti kamerų: {{errorMessage}}", + "deleteRoleFailed": "Nepavyko ištrinti rolės: {{errorMessage}}", + "userUpdateFailed": "Nepavyko atnaujinti vartotojo rolių: {{errorMessage}}" + } + }, + "dialog": { + "deleteRole": { + "title": "Pašalinti rolę", + "deleting": "Šalinama...", + "desc": "Šis veiksmas neatkuriamas. Rolė bus ištrinta, likusiems jos turėtojams bus priskirta 'žiūrovo' rolė, kuri leis vartotojui matyti visas kameras.", + "warn": "Ar esate įsitikinę, kad norite ištrinti {{role}}?" + }, + "form": { + "cameras": { + "title": "Kameros", + "required": "Mažiausiai viena kamera turi būti pažymėta.", + "desc": "Pasirinkinte kamerą prie kurios ši rolė suteiks prieigą. Privaloma nurodyti bent vieną." + }, + "role": { + "title": "Rolės pavadinimas", + "placeholder": "Įveskite rolės pavadinimą", + "roleIsRequired": "Rolės pavadinimas yra privalomas", + "roleExists": "Toks rolės pavadinimas jau egzistuoja.", + "desc": "Ledžiama naudoti tik raides, skaičius, taškus ir pabraukimus.", + "roleOnlyInclude": "Rolės pavadinime gali būti tik raides, skaičius, . ar _" + } + }, + "createRole": { + "title": "Sukurti Naują Rolę", + "desc": "Pridėti naują rolę ir priskirti prieigas prie kamerų." + }, + "editCameras": { + "title": "Koreguoti Rolės Kameras", + "desc": "Atnaujinti prieigą prie kameros rolei {{role}}." + } + }, + "management": { + "title": "Žiūrovo Rolės Valdymas", + "desc": "Valdyti šios Frigate aplinkos specializuotas žiūrovo roles ir kamerų prieigos leidimus." + } + }, + "cameraWizard": { + "title": "Pridėti Kamerą", + "description": "Sekite žemiau nurodytus žingsnius norėdami pridėti naują kamerą prie savo Frigate.", + "steps": { + "nameAndConnection": "Pavadinimas ir Jungtis", + "streamConfiguration": "Transliacijos Nustatymai", + "validationAndTesting": "Patikra ir Testavimas" + }, + "save": { + "success": "Nauja kamera sėkmingai išsaugota {{cameraName}}.", + "failure": "Klaida išsaugant {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezoliucija", + "video": "Vaizdas", + "audio": "Garsas", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Prašau pateikti galiojantį transliacijos URL", + "testFailed": "Transliacijos testas nepavyko: {{error}}" + }, + "step1": { + "description": "Įveskite savo kameros informaciją ir testuokite prisijungimą.", + "cameraName": "Kameros Pavadinimas", + "cameraNamePlaceholder": "pvz., priekines_durys arba Galinio Kiemo Vaizdas", + "host": "Host/IP Adresas", + "port": "Port", + "username": "Vartotojo vardas", + "usernamePlaceholder": "Pasirinktinai", + "password": "Slaptažodis", + "passwordPlaceholder": "Pasirinktinai", + "selectTransport": "Pasirinkite perdavimo protokolą", + "cameraBrand": "Kameros Gamintojas", + "selectBrand": "Pasirinkite kameros gamintoją URL šablonui", + "customUrl": "Kameros Transliacijos URL", + "brandInformation": "Gamintojo informacija", + "brandUrlFormat": "Kamerai su RTSP URL formatas kaip: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://vartotojas:slaptažodis@host:port/path", + "testConnection": "Testuoti Susijungimą", + "testSuccess": "Susijungimo testas sėkmingas!" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lt/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/lt/views/system.json new file mode 100644 index 0000000..8918ad3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lt/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "Kamerų Statistika - Frigate", + "storage": "Saugyklos Statistika - Frigate", + "logs": { + "frigate": "Frigate Žurnalas - Frigate", + "go2rtc": "Go2RTC Žurnalas - Frigate", + "nginx": "Nginx Žurnalas - Frigate" + }, + "general": "Bendroji Statistika - Frigate", + "enrichments": "Pagerinimų Statistika - Frigate" + }, + "title": "Sistema", + "metrics": "Sistemos metrikos", + "logs": { + "download": { + "label": "Parsisiųsti Žurnalą" + }, + "copy": { + "label": "Kopijuoti į iškarpinę", + "success": "Nukopijuoti įrašai į iškarpinę", + "error": "Nepavyko nukopijuoti įrašų į iškarpinę" + }, + "type": { + "label": "Tipas", + "timestamp": "Laiko žymė", + "tag": "Žyma", + "message": "Žinutė" + }, + "tips": "Įrašai yra transliuojami iš serverio", + "toast": { + "error": { + "fetchingLogsFailed": "Klaida nuskaitant įrašus: {{errorMessage}}", + "whileStreamingLogs": "Klaidai transliuojant įrašus: {{errorMessage}}" + } + } + }, + "general": { + "title": "Bendrinis", + "detector": { + "title": "Detektoriai", + "inferenceSpeed": "Detektorių darbo greitis", + "temperature": "Detektorių Temperatūra", + "cpuUsage": "Detektorių CPU Naudojimas", + "memoryUsage": "Detektorių Atminties Naudojimas", + "cpuUsageInformation": "CPU vartojimas ruošiant duomenis detektorių modeliams. Ši reikšmė nevertina inference vartojimo, net jei yra naudojamas GPU akseleratorius." + }, + "hardwareInfo": { + "title": "Techninės įrangos Info", + "gpuUsage": "GPU Naudojimas", + "gpuMemory": "GPU Atmintis", + "gpuEncoder": "GPU Kodavimas", + "gpuDecoder": "GPU Dekodavimas", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Išvestis", + "returnCode": "Grįžtamas Kodas: {{code}}", + "processOutput": "Proceso Išvestis:", + "processError": "Proceso Klaida:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI Išvestis", + "name": "Pavadinimas: {{name}}", + "driver": "Tvarkyklė: {{driver}}", + "cudaComputerCapability": "CUDA Compute Galimybės: {{cuda_compute}}", + "vbios": "VBios Info: {{vbios}}" + }, + "closeInfo": { + "label": "Užverti GPU info" + }, + "copyInfo": { + "label": "Kopijuoti GPU Info" + }, + "toast": { + "success": "Nukopijuota GPU info į iškarpinę" + } + }, + "npuUsage": "NPU Naudojimas", + "npuMemory": "NPU Atmintis" + }, + "otherProcesses": { + "title": "Kiti Procesai", + "processCpuUsage": "Procesų CPU Naudojimas", + "processMemoryUsage": "Procesu Atminties Naudojimas" + } + }, + "storage": { + "title": "Saugykla", + "overview": "Apžvalga", + "recordings": { + "title": "Įrašai", + "tips": "Ši reikšmė nurodo kiek iš viso Frigate duombazėje esantys įrašai užima vietos saugykloje. Frigate neseka kiek vietos užima visi kiti failai esantys laikmenoje.", + "earliestRecording": "Anksčiausias esantis įrašas:" + }, + "cameraStorage": { + "title": "Kameros Saugykla", + "camera": "Kamera", + "unusedStorageInformation": "Neišnaudotos Saugyklos Informacija", + "storageUsed": "Saugykla", + "percentageOfTotalUsed": "Procentas nuo Viso", + "bandwidth": "Pralaidumas", + "unused": { + "title": "Nepanaudota", + "tips": "Jei saugykloje turite daugiau failų apart Frigate įrašų, ši reikšmė neatspindės tikslios likusios laisvos vietos Frigate panaudojimui. Frigate neseka saugyklos panaudojimo už savo įrašų ribų." + } + }, + "shm": { + "title": "SHM (bendrinama atmintis) priskyrimas", + "warning": "Esamas SHM dydis {{total}}MB yra per mažas. Pridėkite bent jau {{min_shm}}MB." + } + }, + "cameras": { + "title": "Kameros", + "overview": "Apžvalga", + "info": { + "aspectRatio": "formato santykis", + "cameraProbeInfo": "{{camera}} Kameros srauto informacija", + "streamDataFromFFPROBE": "Transliacijos duomenys yra surenkami su ffprobe.", + "fetching": "Gaunamai Kameros Duomenys", + "stream": "Transliacija {{idx}}", + "video": "Vaizdas:", + "codec": "Kodekas:", + "resolution": "Raiška:", + "fps": "FPS:", + "unknown": "Nežinoma", + "audio": "Garsas:", + "error": "Klaida:{{error}}", + "tips": { + "title": "Kameros Srauto Informacija" + } + }, + "framesAndDetections": "Kadrai / Aptikimai", + "label": { + "camera": "kamera", + "detect": "aptikti", + "skipped": "praleista", + "ffmpeg": "FFmpeg", + "capture": "užfiksuota", + "overallFramesPerSecond": "viso kadrų per sekundę", + "overallDetectionsPerSecond": "viso aptikimų per sekundę", + "overallSkippedDetectionsPerSecond": "viso praleista aptikimų per sekundę", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} ufiksuota", + "cameraDetect": "{{camName}} susekta", + "cameraFramesPerSecond": "{{camName}} kadrai per sekundę", + "cameraDetectionsPerSecond": "{{camName}} aptikimai per sekundę", + "cameraSkippedDetectionsPerSecond": "{{camName}} praleista aptikimų per sekundę" + }, + "toast": { + "success": { + "copyToClipboard": "Srauto informacija nukopijuotą į iškarpinę." + }, + "error": { + "unableToProbeCamera": "Negalima gauti kameros mėginio: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Paskutinį kartą atnaujinta: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} turi aukštą CPU suvartojimą FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} turi auktšą CPU vartojimą aptikimams ({{detectAvg}}%)", + "healthy": "Sistemos būklė sveika", + "reindexingEmbeddings": "Įterpinių reideksavimas ({{processed}}% baigtas)", + "cameraIsOffline": "{{camera}} yra nepasiekiama", + "detectIsSlow": "{{detect}} yra lėtas ({{speed}}ms)", + "detectIsVerySlow": "{{detect}} yra labai lėtas ({{speed}}ms)", + "shmTooLow": "/dev/shm priskirta ({{total}} MB) turi būti padidinta bent jau iki {{min}} MB." + }, + "enrichments": { + "title": "Patobulinimai", + "embeddings": { + "yolov9_plate_detection": "YOLOv9 Numerių Aptikimai", + "yolov9_plate_detection_speed": "YOLOv9 Numerių Aptikimų Greitis", + "text_embedding_speed": "Teksto Įterpimų Greitis", + "plate_recognition_speed": "Numerių Atpažinimo Greitis", + "face_recognition_speed": "Veidų Atpažinimo Greitis", + "face_embedding_speed": "Veidų Įterpimų Greitis", + "image_embedding_speed": "Vaizdo Įterpimo Greitis", + "plate_recognition": "Numerių Atpažinimas", + "face_recognition": "Veido Atpažinimas", + "text_embedding": "Teksto Įterpimas", + "image_embedding": "Vaizdo Įterpimas" + }, + "infPerSecond": "Išvadų Per Sekundę" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/audio.json b/sam2-cpu/frigate-dev/web/public/locales/lv/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/audio.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/common.json b/sam2-cpu/frigate-dev/web/public/locales/lv/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/common.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/input.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/lv/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/components/player.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/objects.json b/sam2-cpu/frigate-dev/web/public/locales/lv/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/objects.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/events.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/live.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/search.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/lv/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/lv/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/lv/views/system.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/audio.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/audio.json new file mode 100644 index 0000000..cdc92c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/audio.json @@ -0,0 +1,503 @@ +{ + "bird": "Fugl", + "door": "Dør", + "sink": "Vask", + "blender": "Blender", + "bicycle": "Sykkel", + "motorcycle": "Motorsykkel", + "car": "Bil", + "bus": "Buss", + "train": "Tog", + "boat": "Båt", + "cat": "Katt", + "dog": "Hund", + "horse": "Hest", + "sheep": "Sau", + "skateboard": "Skateboard", + "mouse": "Mus", + "keyboard": "Tastatur", + "clock": "Klokke", + "scissors": "Saks", + "hair_dryer": "Hårføner", + "toothbrush": "Tannbørste", + "vehicle": "Kjøretøy", + "animal": "Dyr", + "bark": "Bjeff", + "goat": "Geit", + "groan": "Stønn", + "throat_clearing": "Kremting", + "sneeze": "Nysing", + "applause": "Applaus", + "chatter": "Skravling", + "crowd": "Folkemengde", + "bow_wow": "Voff-voff", + "livestock": "Husdyr", + "clip_clop": "Klipp-klopp", + "cluck": "Kakling", + "cock_a_doodle_doo": "Kykeliky", + "wild_animals": "Ville dyr", + "roaring_cats": "Brølende kattedyr", + "dogs": "Hunder", + "whale_vocalization": "Hval-vokalisering", + "strum": "Strumming", + "gong": "Gong", + "tuning_fork": "Stemmegaffel", + "opera": "Opera", + "waterfall": "Foss", + "motor_vehicle": "Motorvogn", + "emergency_vehicle": "Utrykningskjøretøy", + "police_car": "Politibil", + "rail_transport": "Jernbanetransport", + "fixed-wing_aircraft": "Fly med faste vinger", + "engine": "Motor", + "bathtub": "Badekar", + "toilet_flush": "Toalettskylling", + "coin": "Mynt", + "mechanisms": "Mekanismer", + "field_recording": "Feltinnspilling", + "speech": "Tale", + "babbling": "Babling", + "yell": "Rop", + "bellow": "Brøl", + "whoop": "Jubelrop", + "whispering": "Hvisking", + "laughter": "Latter", + "snicker": "Fnising", + "crying": "Gråt", + "sigh": "Sukk", + "singing": "Sang", + "choir": "Kor", + "yodeling": "Jodling", + "chant": "Sangrop", + "mantra": "Mantra", + "child_singing": "Barnesang", + "synthetic_singing": "Syntetisk sang", + "rapping": "Rapping", + "humming": "Nynning", + "grunt": "Grynt", + "whistling": "Plystring", + "breathing": "Pusting", + "wheeze": "Hvesing", + "snoring": "Snorking", + "gasp": "Gisp", + "pant": "Pesing", + "snort": "Snøfting", + "cough": "Hoste", + "sniff": "Snufs", + "run": "Løping", + "shuffle": "Sleping (av føtter)", + "footsteps": "Fottrinn", + "chewing": "Tygging", + "biting": "Biting", + "gargling": "Gurgling", + "stomach_rumble": "Mageknurr", + "burping": "Raping", + "hiccup": "Hikke", + "fart": "Promp", + "hands": "Hender", + "finger_snapping": "Fingerknipsing", + "clapping": "Applaus", + "heartbeat": "Hjerteslag", + "heart_murmur": "Hjertelyd (unormal)", + "cheering": "Jubel", + "children_playing": "Barn som leker", + "pets": "Kjæledyr", + "yip": "Klynk", + "howl": "Uling", + "growling": "Knurring", + "whimper_dog": "Klynking (hund)", + "purr": "Malelyd", + "meow": "Mjauing", + "hiss": "Fres", + "caterwaul": "Kattemjau", + "neigh": "Vrinsk", + "cattle": "Storfe", + "moo": "Rauting", + "cowbell": "Kubjelle", + "pig": "Gris", + "oink": "Nøff", + "bleat": "Breking", + "fowl": "Fjærfe", + "chicken": "Kylling", + "turkey": "Kalkun", + "gobble": "Kalkunlyd", + "duck": "And", + "quack": "Kvakking", + "goose": "Gås", + "honk": "Gåselyd", + "roar": "Brøl", + "chirp": "Kvitre", + "squawk": "Skvatring", + "pigeon": "Due", + "coo": "Kurre", + "crow": "Kråke", + "caw": "Krah", + "owl": "Ugle", + "hoot": "Uglelyd", + "flapping_wings": "Vingeslag", + "rats": "Rotter", + "patter": "Tripping", + "insect": "Insekt", + "cricket": "Gresshoppe", + "mosquito": "Mygg", + "fly": "Flue", + "buzz": "Summing", + "frog": "Frosk", + "croak": "Kvekking", + "snake": "Slange", + "rattle": "Ranglelyd", + "music": "Musikk", + "musical_instrument": "Musikkinstrument", + "plucked_string_instrument": "Klimpreinstrument", + "guitar": "Gitar", + "electric_guitar": "Elektrisk gitar", + "bass_guitar": "Bassgitar", + "acoustic_guitar": "Akustisk gitar", + "steel_guitar": "Steelgitar", + "tapping": "Tapping", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Mandolin", + "zither": "Siter", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Elektrisk piano", + "organ": "Orgel", + "electronic_organ": "Elektronisk orgel", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkusjon", + "drum_kit": "Trommesett", + "drum_machine": "Trommemaskin", + "drum": "Tromme", + "snare_drum": "Skarptromme", + "rimshot": "Slag på trommekanten", + "drum_roll": "Trommevirvel", + "bass_drum": "Basstromme", + "timpani": "Pauker", + "tabla": "Tabla", + "cymbal": "Symbal", + "hi_hat": "Hi-hat", + "wood_block": "Treblokk", + "tambourine": "Tamburin", + "maraca": "Maracas", + "tubular_bells": "Rørklokker", + "mallet_percussion": "Slagverk med køller", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan", + "orchestra": "Orkester", + "brass_instrument": "Messingblåseinstrument", + "french_horn": "Valthorn", + "trumpet": "Trompet", + "trombone": "Trombone", + "bowed_string_instrument": "Strykeinstrument", + "string_section": "Strykere", + "violin": "Fiolin", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabass", + "wind_instrument": "Treblåseinstrument", + "flute": "Fløyte", + "saxophone": "Saksofon", + "clarinet": "Klarinett", + "harp": "Harpe", + "bell": "Klokke", + "church_bell": "Kirkeklokke", + "jingle_bell": "Bjelle", + "bicycle_bell": "Sykkelbjelle", + "chime": "Klokkespill", + "wind_chime": "Vindklokke", + "harmonica": "Munnspill", + "accordion": "Akkordeon", + "bagpipes": "Sekkepipe", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Syngeskål", + "scratching": "Skraping", + "pop_music": "Popmusikk", + "hip_hop_music": "Hip-hop musikk", + "beatboxing": "Beatboxing", + "rock_music": "Rockemusikk", + "heavy_metal": "Heavy metal", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiv rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psykedelisk rock", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Soulmusikk", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Swingmusikk", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkemusikk", + "middle_eastern_music": "Midtøsten-musikk", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Klassisk musikk", + "electronic_music": "Elektronisk musikk", + "house_music": "House-musikk", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and bass", + "electronica": "Electronica", + "electronic_dance_music": "Elektronisk dansemusikk", + "trance_music": "Trancemusikk", + "ambient_music": "Ambient musikk", + "music_of_latin_america": "Latinamerikansk musikk", + "salsa_music": "Salsamusikk", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Musikk for barn", + "new-age_music": "New age-musikk", + "vocal_music": "Vokalmusikk", + "a_capella": "A cappella", + "music_of_africa": "Afrikansk musikk", + "afrobeat": "Afrobeat", + "christian_music": "Kristelig musikk", + "gospel_music": "Gospelmusikk", + "music_of_asia": "Asiatisk musikk", + "carnatic_music": "Karnatisk musikk", + "music_of_bollywood": "Bollywood-musikk", + "ska": "Ska", + "traditional_music": "Tradisjonell musikk", + "independent_music": "Indie-musikk", + "song": "Sang", + "background_music": "Bakgrunnsmusikk", + "theme_music": "Temamusikk", + "jingle": "Jingle", + "soundtrack_music": "Filmmusikk", + "lullaby": "Vuggevise", + "video_game_music": "Videospillmusikk", + "christmas_music": "Julemusikk", + "dance_music": "Dansemusikk", + "wedding_music": "Bryllupsmusikk", + "happy_music": "Glad musikk", + "sad_music": "Trist musikk", + "tender_music": "Vakker musikk", + "exciting_music": "Spennende musikk", + "angry_music": "Sint musikk", + "scary_music": "Skummel musikk", + "wind": "Vind", + "rustling_leaves": "Raslende blader", + "wind_noise": "Vindstøy", + "thunderstorm": "Tordenvær", + "thunder": "Torden", + "water": "Vann", + "rain": "Regn", + "raindrop": "Regndråpe", + "rain_on_surface": "Regn på overflate", + "stream": "Bekkeleie", + "ocean": "Hav", + "waves": "Bølger", + "steam": "Damp", + "gurgling": "Gurgling", + "fire": "Brann", + "crackle": "Knakking", + "sailboat": "Seilbåt", + "rowboat": "Robåt", + "motorboat": "Motorbåt", + "ship": "Skip", + "toot": "Tuting", + "car_alarm": "Bilalarm", + "power_windows": "Elektriske vinduer", + "skidding": "Skrens", + "tire_squeal": "Hvinende dekk", + "car_passing_by": "Bil som kjører forbi", + "race_car": "Racerbil", + "truck": "Lastebil", + "air_brake": "Luftbrems", + "air_horn": "Lufthorn", + "reversing_beeps": "Ryggesignal", + "ice_cream_truck": "Iskrembil", + "ambulance": "Ambulanse", + "fire_engine": "Brannbil", + "aircraft_engine": "Flymotor", + "traffic_noise": "Trafikkstøy", + "train_whistle": "Togfløyte", + "train_horn": "Toghorn", + "railroad_car": "Jernbanevogn", + "train_wheels_squealing": "Hvinende togskinner", + "subway": "T-bane", + "aircraft": "Fly", + "jet_engine": "Jetmotor", + "propeller": "Propell", + "helicopter": "Helikopter", + "light_engine": "Lett motor", + "dental_drill's_drill": "Tannlegebor", + "lawn_mower": "Gressklipper", + "chainsaw": "Motorsag", + "medium_engine": "Middels tung motor", + "heavy_engine": "Tung motor", + "engine_knocking": "Motorbanking", + "engine_starting": "Motorstart", + "idling": "Tomgang", + "accelerating": "Akselerasjon", + "doorbell": "Dørklokke", + "ding-dong": "Ding-dong", + "sliding_door": "Skyvedør", + "slam": "Smell", + "knock": "Bank", + "tap": "Tapp", + "squeak": "Knirk", + "cupboard_open_or_close": "Skapdør som åpnes eller lukkes", + "drawer_open_or_close": "Skuff som åpnes eller lukkes", + "dishes": "Oppvask", + "cutlery": "Bestikk", + "chopping": "Hugging", + "frying": "Steking", + "microwave_oven": "Mikrobølgeovn", + "water_tap": "Vannkran", + "electric_toothbrush": "Elektrisk tannbørste", + "vacuum_cleaner": "Støvsuger", + "zipper": "Glidelås", + "keys_jangling": "Klingende nøkler", + "electric_shaver": "Elektrisk barbermaskin", + "shuffling_cards": "Kortstokk som stokkes", + "typing": "Skriving (på tastatur)", + "typewriter": "Skrivemaskin", + "computer_keyboard": "Datatastatur", + "writing": "Skriving", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Telefon som ringer", + "ringtone": "Ringetone", + "telephone_dialing": "Telefon som slås", + "dial_tone": "Summetone", + "busy_signal": "Opptattsignal", + "alarm_clock": "Vekkerklokke", + "siren": "Sirene", + "civil_defense_siren": "Luftsirene", + "buzzer": "Summer", + "smoke_detector": "Røykvarsler", + "fire_alarm": "Brannalarm", + "foghorn": "Tåkelur", + "whistle": "Fløyte", + "steam_whistle": "Dampfløyte", + "ratchet": "Skralle", + "tick": "Tikk", + "tick-tock": "Tikk-takk", + "gears": "Tannhjul", + "pulleys": "Trinser", + "sewing_machine": "Symaskin", + "mechanical_fan": "Mekanisk vifte", + "air_conditioning": "Klimaanlegg", + "cash_register": "Kasseapparat", + "printer": "Skriver", + "single-lens_reflex_camera": "Speilreflekskamera", + "camera": "Kamera", + "tools": "Verktøy", + "hammer": "Hammer", + "jackhammer": "Trykkluftbor", + "sawing": "Saging", + "filing": "Filing", + "sanding": "Pussing", + "power_tool": "Elektroverktøy", + "drill": "Boremaskin", + "explosion": "Eksplosjon", + "gunshot": "Skudd", + "machine_gun": "Maskingevær", + "fusillade": "Salver", + "artillery_fire": "Artilleriild", + "cap_gun": "Leketøyspistol", + "fireworks": "Fyrverkeri", + "firecracker": "Kinaputt", + "burst": "Spreng", + "eruption": "Utslipp", + "boom": "Drønn", + "wood": "Tre", + "chop": "Hakk", + "splinter": "Splint", + "crack": "Sprekk", + "glass": "Glass", + "chink": "Klirr", + "shatter": "Knuse", + "silence": "Stillhet", + "sound_effect": "Lydeffekt", + "environmental_noise": "Miljøstøy", + "static": "Statisk støy", + "white_noise": "Hvit støy", + "pink_noise": "Rosa støy", + "television": "Fjernsyn", + "radio": "Radio", + "scream": "Skrik", + "sodeling": "sodeling", + "chird": "chird", + "change_ringing": "klokkeringing", + "shofar": "shofar", + "liquid": "væske", + "splash": "plask", + "slosh": "skvulp", + "squish": "klemmelyd", + "drip": "drypp", + "pour": "helle", + "trickle": "sildre", + "gush": "strøm", + "fill": "fylle", + "spray": "spray", + "pump": "pumpe", + "stir": "røre", + "boiling": "koking", + "sonar": "sonar", + "arrow": "pil", + "whoosh": "sus", + "thump": "dump", + "thunk": "dunk", + "electronic_tuner": "elektronisk stemmeapparat", + "effects_unit": "effektenhet", + "chorus_effect": "kor-effekt", + "basketball_bounce": "basketsprettp", + "bang": "smell", + "slap": "klask", + "whack": "slag", + "smash": "knuselyd", + "breaking": "bryting", + "bouncing": "spretting", + "whip": "pisk", + "flap": "flaks", + "scratch": "skrap", + "scrape": "skrape", + "rub": "gnidning", + "roll": "rulling", + "crushing": "knusing", + "crumpling": "krølling", + "tearing": "riving", + "beep": "pip", + "ping": "ping", + "ding": "ding", + "clang": "klang", + "squeal": "hvin", + "creak": "knirk", + "rustle": "rasling", + "whir": "surr", + "clatter": "klirrelyd", + "sizzle": "susing", + "clicking": "klikkelyd", + "clickety_clack": "klikk-klakk", + "rumble": "rumling", + "plop": "plopp", + "hum": "brumming", + "zing": "svisj", + "boing": "boing", + "crunch": "knekk", + "sine_wave": "sinusbølge", + "harmonic": "harmonisk", + "chirp_tone": "pipetone", + "pulse": "puls", + "inside": "innendørs", + "outside": "utendørs", + "reverberation": "etterklang", + "echo": "ekko", + "noise": "støy", + "mains_hum": "nettbrumming", + "distortion": "forvrengning", + "sidetone": "sidetone", + "cacophony": "kakofoni", + "throbbing": "pulsering", + "vibration": "vibrasjon" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/common.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/common.json new file mode 100644 index 0000000..a2fbcdf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "yr": "{{time}} år", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "minute_one": "{{time}} minutt", + "minute_other": "{{time}} minutter", + "s": "{{time}}s", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestampExcludeSeconds": { + "24hour": "%-d. %b, %H:%M", + "12hour": "%-d. %b, %I:%M %p" + }, + "untilForTime": "Inntil {{time}}", + "untilForRestart": "Inntil Frigate starter på nytt.", + "untilRestart": "Inntil omstart", + "ago": "{{timeAgo}} siden", + "justNow": "Akkurat nå", + "today": "I dag", + "yesterday": "I går", + "last7": "Siste 7 dager", + "last14": "Siste 14 dager", + "last30": "Siste 30 dager", + "thisWeek": "Denne uken", + "lastWeek": "Forrige uke", + "thisMonth": "Denne måneden", + "lastMonth": "Forrige måned", + "5minutes": "5 minutter", + "10minutes": "10 minutter", + "30minutes": "30 minutter", + "1hour": "1 time", + "12hours": "12 timer", + "24hours": "24 timer", + "pm": "pm", + "am": "am", + "mo": "{{time}} mnd", + "month_one": "{{time}} måned", + "month_other": "{{time}} måneder", + "d": "{{time}}d", + "day_one": "{{time}} dag", + "day_other": "{{time}} dager", + "h": "{{time}}t", + "hour_one": "{{time}} time", + "hour_other": "{{time}} timer", + "m": "{{time}}m", + "formattedTimestamp": { + "12hour": "d. MMM, h:mm:ss aaa", + "24hour": "d. MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d. MMM HH:mm:ss" + }, + "formattedTimestampWithYear": { + "12hour": "%-d. %b %Y, %I:%M %p", + "24hour": "%-d. %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d. %b", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d. MMM, h:mm aaa", + "24hour": "d. MMM, HH:mm" + }, + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d. MMM yyyy, h:mm aaa", + "24hour": "d. MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d. MMM", + "formattedTimestampMonthDayYear": { + "24hour": "d. MMM, yyyy", + "12hour": "d. MMM, yyyy" + }, + "inProgress": "Pågår", + "invalidStartTime": "Ugyldig starttid", + "invalidEndTime": "Ugyldig sluttid" + }, + "button": { + "copy": "Kopier", + "delete": "Slett", + "apply": "Bruk", + "reset": "Tilbakestill", + "done": "Ferdig", + "enabled": "Aktivert", + "enable": "Aktiver", + "disabled": "Deaktivert", + "disable": "Deaktiver", + "save": "Lagre", + "saving": "Lagrer…", + "cancel": "Avbryt", + "close": "Lukk", + "back": "Tilbake", + "history": "Historikk", + "fullscreen": "Fullskjerm", + "exitFullscreen": "Avslutt fullskjerm", + "pictureInPicture": "Bilde-i-bilde", + "twoWayTalk": "Toveis tale", + "cameraAudio": "Kameralyd", + "on": "PÅ", + "off": "AV", + "edit": "Rediger", + "copyCoordinates": "Kopier koordinater", + "yes": "Ja", + "no": "Nei", + "download": "Last ned", + "info": "Info", + "suspended": "Suspendert", + "unsuspended": "Opphev suspensjon", + "play": "Spill av", + "unselect": "Fjern valg", + "export": "Eksporter", + "deleteNow": "Slett nå", + "next": "Neste", + "continue": "Fortsett" + }, + "menu": { + "help": "Hjelp", + "documentation": { + "title": "Dokumentasjon", + "label": "Frigate-dokumentasjon" + }, + "restart": "Start Frigate på nytt", + "live": { + "title": "Direkte", + "allCameras": "Alle kameraer", + "cameras": { + "title": "Kameraer", + "count_one": "{{count}} kamera", + "count_other": "{{count}} kameraer" + } + }, + "review": "Inspiser", + "explore": "Utforsk", + "export": "Eksporter", + "uiPlayground": "UI-Sandkasse", + "faceLibrary": "Ansiktsbibliotek", + "user": { + "title": "Bruker", + "account": "Konto", + "current": "Nåværende bruker: {{user}}", + "anonymous": "anonym", + "logout": "Logg ut", + "setPassword": "Angi passord" + }, + "system": "System", + "systemMetrics": "Systemmålinger", + "configuration": "Konfigurasjon", + "systemLogs": "Systemlogger", + "settings": "Innstillinger", + "configurationEditor": "Rediger konfigurasjonen", + "languages": "Språk", + "language": { + "en": "English (Engelsk)", + "zhCN": "简体中文 (Forenklet kinesisk)", + "withSystem": { + "label": "Bruk systemets språkinnstillinger" + }, + "fr": "Français (Fransk)", + "es": "Español (Spansk)", + "hi": "हिन्दी (Hindi)", + "ar": "العربية (Arabisk)", + "pt": "Português (Portugisisk)", + "ru": "Русский (Russisk)", + "de": "Deutsch (Tysk)", + "ja": "日本語 (Japansk)", + "tr": "Türkçe (Tyrkisk)", + "it": "Italiano (Italiensk)", + "nl": "Nederlands (Nederlandsk)", + "sv": "Svenska (Svensk)", + "cs": "Čeština (Tsjekkisk)", + "nb": "Norsk Bokmål", + "ko": "한국어 (Koreansk)", + "vi": "Tiếng Việt (Vietnamesisk)", + "fa": "فارسی (Persisk)", + "he": "עברית (Hebraisk)", + "el": "Ελληνικά (Gresk)", + "ro": "Română (Rumensk)", + "hu": "Magyar (Ungarsk)", + "fi": "Suomi (Finsk)", + "da": "Dansk (Dansk)", + "sk": "Slovenčina (Slovensk)", + "pl": "Polski (Polsk)", + "uk": "Українська (Ukrainsk)", + "yue": "粵語 (Kantonesisk)", + "th": "ไทย (Thai)", + "ca": "Català (Katalansk)", + "ptBR": "Português brasileiro (Brasiliansk portugisisk)", + "sr": "Српски (Serbisk)", + "sl": "Slovenščina (Slovensk)", + "lt": "Lietuvių (Litauisk)", + "bg": "Български (Bulgarsk)", + "gl": "Galego (Galisisk)", + "id": "Bahasa Indonesia (Indonesisk)", + "ur": "اردو (Urdu)" + }, + "appearance": "Utseende", + "darkMode": { + "label": "Mørk modus", + "light": "Lys", + "dark": "Mørk", + "withSystem": { + "label": "Bruk systemets innstillinger for lys eller mørk modus" + } + }, + "withSystem": "System", + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grønn", + "nord": "Nord", + "red": "Rød", + "contrast": "Høy kontrast", + "default": "Standard", + "highcontrast": "Høy kontrast" + }, + "classification": "Klassifisering" + }, + "pagination": { + "next": { + "title": "Neste", + "label": "Gå til neste side" + }, + "label": "paginering", + "previous": { + "title": "Forrige", + "label": "Gå til forrige side" + }, + "more": "Flere sider" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/t" + }, + "length": { + "meters": "meter", + "feet": "fot" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/time", + "mbph": "MB/time", + "gbph": "GB/time" + } + }, + "label": { + "back": "Gå tilbake", + "hide": "Skjul {{item}}", + "show": "Vis {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alle" + }, + "toast": { + "copyUrlToClipboard": "Nettadresse kopiert til utklippstavlen.", + "save": { + "title": "Lagre", + "error": { + "title": "Kunne ikke lagre endringer i konfigurasjonen: {{errorMessage}}", + "noMessage": "Kunne ikke lagre endringer i konfigurasjonen" + } + } + }, + "role": { + "title": "Rolle", + "admin": "Administrator", + "viewer": "Visningsbruker", + "desc": "Administratorer har full tilgang til alle funksjoner i Frigate brukergrensesnittet. Visningsbrukere er begrenset til å se kameraer, inspisere elementer og se historiske opptak." + }, + "accessDenied": { + "documentTitle": "Ingen tilgang – Frigate", + "title": "Ingen tilgang", + "desc": "Du har ikke tillatelse til å vise denne siden." + }, + "notFound": { + "documentTitle": "Ikke funnet – Frigate", + "title": "404", + "desc": "Siden ble ikke funnet" + }, + "selectItem": "Velg {{item}}", + "readTheDocumentation": "Se dokumentasjonen", + "information": { + "pixels": "{{area}}piklser" + }, + "field": { + "internalID": "Den interne ID-en som Frigate bruker i konfigurasjonen og databasen", + "optional": "Valgfritt" + }, + "list": { + "two": "{{0}} og {{1}}", + "many": "{{items}}, og {{last}}", + "separatorWithSpace": ", " + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/auth.json new file mode 100644 index 0000000..c59cd4e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Brukernavn", + "password": "Passord", + "login": "Logg inn", + "errors": { + "usernameRequired": "Brukernavn er påkrevd", + "passwordRequired": "Passord er påkrevd", + "rateLimit": "Grense for antall forsøk overskredet. Prøv igjen senere.", + "loginFailed": "Innlogging mislyktes", + "unknownError": "Ukjent feil. Sjekk loggene.", + "webUnknownError": "Ukjent feil. Sjekk konsoll-loggene." + }, + "firstTimeLogin": "Prøver du å logge inn for første gang? Påloggingsinformasjonen er skrevet ut i Frigate-loggene." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/camera.json new file mode 100644 index 0000000..750e09e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kameragrupper", + "name": { + "placeholder": "Skriv inn et navn…", + "errorMessage": { + "mustLeastCharacters": "Navnet på kameragruppen må være minst 2 tegn.", + "exists": "Navnet på kameragruppen finnes allerede.", + "nameMustNotPeriod": "Navnet på kameragruppen kan ikke inneholde punktum.", + "invalid": "Ugyldig navn på kameragruppe." + }, + "label": "Navn" + }, + "camera": { + "setting": { + "streamMethod": { + "method": { + "continuousStreaming": { + "label": "Kontinuerlig strømming", + "desc": { + "title": "Kamerabildet vil alltid være en direktestrøm når det vises på dashbordet, selv om ingen aktivitet oppdages.", + "warning": "Kontinuerlig strømming kan føre til høy båndbreddebruk og ytelsesproblemer. Bruk med forsiktighet." + } + }, + "noStreaming": { + "label": "Ingen strømming", + "desc": "Kamerabilder vil bare oppdateres én gang i minuttet, og ingen direktestrømming vil finne sted." + }, + "smartStreaming": { + "label": "Smart strømming (anbefalt)", + "desc": "Smart strømming oppdaterer kamerabilder én gang i minuttet når ingen aktivitet oppdages, for å spare båndbredde og ressurser. Når aktivitet oppdages, byttes bildet sømløst til direktestrøm." + } + }, + "label": "Strømmemetode", + "placeholder": "Velg en strømmemetode" + }, + "compatibilityMode": { + "label": "Kompatibilitetsmodus", + "desc": "Aktiver dette alternativet kun hvis kameraets direktestrøm viser fargeforstyrrelser og har en diagonal linje på høyre side av bildet." + }, + "label": "Innstillinger for kamerastrømming", + "title": "{{cameraName}} strømmeinnstillinger", + "desc": "Endre direktestrømmingsalternativene for denne kameragruppens dashbord. Disse innstillingene er spesifikke for enhet/nettleser.", + "audioIsAvailable": "Lyd er tilgjengelig for denne strømmen", + "audioIsUnavailable": "Lyd er ikke tilgjengelig for denne strømmen", + "audio": { + "tips": { + "title": "Lyd må komme fra kameraet ditt og konfigureres i go2rtc for denne strømmen.", + "document": "Se dokumentasjonen " + } + }, + "stream": "Strøm", + "placeholder": "Velg en strøm" + }, + "birdseye": "Fugleperspektiv" + }, + "add": "Legg til kameragruppe", + "edit": "Rediger kameragruppe", + "delete": { + "label": "Slett kameragruppe", + "confirm": { + "title": "Bekreft sletting", + "desc": "Er du sikker på at du vil slette kameragruppen {{name}}?" + } + }, + "cameras": { + "label": "Kameraer", + "desc": "Velg kameraer for denne gruppen." + }, + "icon": "Ikon", + "success": "Kameragruppen ({{name}}) er lagret." + }, + "debug": { + "options": { + "label": "Innstillinger", + "title": "Alternativer", + "showOptions": "Vis alternativer", + "hideOptions": "Skjul alternativer" + }, + "boundingBox": "Avgrensningsboks", + "timestamp": "Tidsstempel", + "zones": "Soner", + "mask": "Maske", + "motion": "Bevegelse", + "regions": "Regioner" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/dialog.json new file mode 100644 index 0000000..fb9bb31 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/dialog.json @@ -0,0 +1,133 @@ +{ + "restart": { + "title": "Er du sikker på at du vil starte Frigate på nytt?", + "button": "Start på nytt", + "restarting": { + "title": "Frigate starter på nytt", + "button": "Tving omlasting nå", + "content": "Denne siden vil lastes inn på nytt om {{countdown}} sekunder." + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Send til Frigate+", + "desc": "Objekter på steder du vil unngå er ikke falske positiver. Å sende dem som falske positiver vil forvirre modellen." + }, + "review": { + "true": { + "label": "Bekreft denne merkelappen for Frigate Plus", + "true_one": "Dette er en {{label}}", + "true_other": "Dette er en {{label}}" + }, + "state": { + "submitted": "Sendt inn" + }, + "false": { + "label": "Ikke bekreft denne merkelappen for Frigate Plus", + "false_one": "Dette er ikke en {{label}}", + "false_other": "Dette er ikke en {{label}}" + }, + "question": { + "label": "Bekreft denne etiketten for Frigate Plus", + "ask_an": "Er dette objekt en {{label}}?", + "ask_a": "Er dette objektet en {{label}}?", + "ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Vis i historikk" + } + }, + "export": { + "time": { + "lastHour_one": "Siste time", + "lastHour_other": "Siste {{count}} timer", + "custom": "Tilpasset", + "start": { + "title": "Starttid", + "label": "Velg starttid" + }, + "fromTimeline": "Velg fra tidslinje", + "end": { + "title": "Sluttid", + "label": "Velg sluttid" + } + }, + "toast": { + "success": "Eksport startet. Se filen på eksportsiden.", + "error": { + "failed": "Klarte ikke å starte eksport: {{error}}", + "noVaildTimeSelected": "Ingen gyldig tidsperiode valgt", + "endTimeMustAfterStartTime": "Sluttid må være etter starttid" + }, + "view": "Vis" + }, + "fromTimeline": { + "previewExport": "Forhåndsvis eksport", + "saveExport": "Lagre eksport" + }, + "name": { + "placeholder": "Gi eksporten et navn" + }, + "select": "Velg", + "export": "Eksporter", + "selectOrExport": "Velg eller eksporter" + }, + "streaming": { + "label": "Strøm", + "restreaming": { + "disabled": "Restrømming er ikke aktivert for dette kameraet.", + "desc": { + "readTheDocumentation": "Se dokumentasjonen", + "title": "Konfigurer go2rtc for flere direktestrømmingsalternativer og lyd for dette kameraet." + } + }, + "showStats": { + "label": "Vis strømmestatistikk", + "desc": "Aktiver dette alternativet for å vise strømmestatistikk som et overlegg på kamerabildet." + }, + "debugView": "Feilsøkingsvisning" + }, + "search": { + "saveSearch": { + "button": { + "save": { + "label": "Lagre dette søket" + } + }, + "label": "Lagre søk", + "desc": "Skriv inn et navn for dette lagrede søket.", + "placeholder": "Skriv inn et navn for søket", + "overwrite": "{{searchName}} finnes allerede. Lagring vil overskrive eksisterende verdi.", + "success": "Søk ({{searchName}}) er lagret." + } + }, + "recording": { + "confirmDelete": { + "title": "Bekreft sletting", + "desc": { + "selected": "Er du sikker på at du vil slette alle opptak knyttet til dette inspeksjonselementet?

    Hold inne Shift-tasten for å hoppe over denne dialogen i fremtiden." + }, + "toast": { + "success": "Videomaterialet knyttet til de valgte inspeksjonselementene har blitt slettet.", + "error": "Kunne ikke slette: {{error}}" + } + }, + "button": { + "export": "Eksportér", + "markAsReviewed": "Merk som inspisert", + "deleteNow": "Slett nå", + "markAsUnreviewed": "Merk som ikke inspisert" + } + }, + "imagePicker": { + "selectImage": "Velg et sporet objekts miniatyrbilde", + "search": { + "placeholder": "Søk etter (under-)etikett..." + }, + "noImages": "Ingen miniatyrbilder funnet for dette kameraet", + "unknownLabel": "Lagret utløserbilde" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/filter.json new file mode 100644 index 0000000..55fff1b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/filter.json @@ -0,0 +1,137 @@ +{ + "filter": "Filter", + "labels": { + "label": "Etiketter", + "all": { + "title": "Alle etiketter", + "short": "Etiketter" + }, + "count": "{{count}} merkelapper", + "count_other": "{{count}} Etiketter", + "count_one": "{{count}} Etikett" + }, + "features": { + "hasVideoClip": "Har et videoklipp", + "submittedToFrigatePlus": { + "label": "Sendt til Frigate+", + "tips": "Du må først filtrere på sporede objekter som har et stillbilde.

    Sporede objekter uten et stillbilde kan ikke sendes til Frigate+." + }, + "label": "Funksjoner", + "hasSnapshot": "Har et stillbilde" + }, + "sort": { + "label": "Sorter", + "dateAsc": "Dato (Stigende)", + "dateDesc": "Dato (Synkende)", + "scoreAsc": "Objektscore (Stigende)", + "scoreDesc": "Objektscore (Synkende)", + "speedAsc": "Estimert hastighet (Stigende)", + "speedDesc": "Estimert hastighet (Synkende)", + "relevance": "Relevans" + }, + "explore": { + "date": { + "selectDateBy": { + "label": "Velg en dato å filtrere etter" + } + }, + "settings": { + "title": "Innstillinger", + "defaultView": { + "title": "Standard visning", + "desc": "Når ingen filtre er valgt, vis et sammendrag av de nyeste sporede objektene per etikett, eller vis et ufiltrert rutenett.", + "summary": "Sammendrag", + "unfilteredGrid": "Ufiltrert rutenett" + }, + "gridColumns": { + "title": "Rutenett kolonner", + "desc": "Velg antall kolonner i rutenettvisningen." + }, + "searchSource": { + "label": "Søkekilde", + "desc": "Velg om du vil søke i bildene eller beskrivelsene av de sporede objektene dine.", + "options": { + "thumbnailImage": "Miniatyrbilde", + "description": "Beskrivelse" + } + } + } + }, + "logSettings": { + "label": "Filtrer loggnivå", + "filterBySeverity": "Filtrer logger etter alvorlighetsgrad", + "loading": { + "title": "Laster inn", + "desc": "Når loggvinduet rulles til bunnen, strømmes nye logger automatisk etter hvert som de legges til." + }, + "disableLogStreaming": "Deaktiver loggstrømming", + "allLogs": "Alle logger" + }, + "trackedObjectDelete": { + "title": "Bekreft sletting", + "desc": "Sletting av disse {{objectLength}} sporede objektene fjerner stillbildet, eventuelle lagrede vektorrepresentasjoner og tilhørende objekt livssyklusoppføringer. Opptak av disse sporede objektene i Historikkvisning vil IKKE bli slettet.

    Er du sikker på at du vil fortsette?

    Hold Shift-tasten for å unngå denne dialogboksen i fremtiden.", + "toast": { + "success": "Sporede objekter ble slettet.", + "error": "Kunne ikke slette sporede objekter: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrer etter sonemaske" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Ingen kjennemerker funnet.", + "selectPlatesFromList": "Velg ett eller flere kjennemerker fra listen.", + "title": "Gjenkjente kjennemerker", + "loadFailed": "Kunne ikke laste inn gjenkjente kjennemerker.", + "loading": "Laster inn gjenkjente kjennemerker…", + "placeholder": "Skriv for å søke etter kjennemerker…", + "selectAll": "Velg alle", + "clearAll": "Fjern alle" + }, + "dates": { + "all": { + "title": "Alle datoer", + "short": "Datoer" + }, + "selectPreset": "Velg en forhåndsinnstilling.…" + }, + "more": "Flere filtre", + "reset": { + "label": "Nullstill filtre til standardverdier" + }, + "timeRange": "Tidsrom", + "subLabels": { + "label": "Underetiketter", + "all": "Alle underetiketter" + }, + "score": "Score", + "estimatedSpeed": "Estimert hastighet ({{unit}})", + "cameras": { + "all": { + "title": "Alle kameraer", + "short": "Kameraer" + }, + "label": "Kamerafilter" + }, + "review": { + "showReviewed": "Vis inspiserte" + }, + "motion": { + "showMotionOnly": "Vis kun bevegelse" + }, + "zones": { + "label": "Soner", + "all": { + "title": "Alle soner", + "short": "Soner" + } + }, + "classes": { + "label": "Klasser", + "all": { + "title": "Alle klasser" + }, + "count_one": "{{count}} Klasse", + "count_other": "{{count}} Klasser" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/icons.json new file mode 100644 index 0000000..937a8d0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Velg et ikon", + "search": { + "placeholder": "Søk etter et ikon…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/input.json new file mode 100644 index 0000000..eb03da4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Last ned video", + "toast": { + "success": "Videoen for inspeksjonselementet ditt har startet nedlasting." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/player.json new file mode 100644 index 0000000..b08459c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Ingen opptak funnet for dette tidspunktet", + "stats": { + "streamType": { + "short": "Type", + "title": "Strømmetype:" + }, + "droppedFrames": { + "short": { + "title": "Tapt", + "value": "{{droppedFrames}} bilder" + }, + "title": "Tapte bilder:" + }, + "bandwidth": { + "title": "Båndbredde:", + "short": "Båndbredde" + }, + "latency": { + "title": "Forsinkelse:", + "value": "{{seconds}} sekunder", + "short": { + "title": "Forsinkelse", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Totalt antall bilder:", + "decodedFrames": "Dekodede bilder:", + "droppedFrameRate": "Tapte bilder per sekund:" + }, + "noPreviewFound": "Ingen forhåndsvisning funnet", + "noPreviewFoundFor": "Ingen forhåndsvisning funnet for {{cameraName}}", + "submitFrigatePlus": { + "title": "Send dette bildet til Frigate+?", + "submit": "Send" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller høyere kreves for denne typen direkte-strømming.", + "streamOffline": { + "title": "Strømmen er frakoblet", + "desc": "Ingen bilder er mottatt på {{cameraName}} detekt-strømmen, sjekk feilloggene" + }, + "cameraDisabled": "Kameraet er deaktivert", + "toast": { + "success": { + "submittedFrigatePlus": "Bildet ble sendt til Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Kunne ikke sende bildet til Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/objects.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/objects.json new file mode 100644 index 0000000..5c7c5ed --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/objects.json @@ -0,0 +1,120 @@ +{ + "motorcycle": "Motorsykkel", + "airplane": "Fly", + "bus": "Buss", + "train": "Tog", + "boat": "Båt", + "traffic_light": "Trafikklys", + "wine_glass": "Vinglass", + "cup": "Kopp", + "chair": "Stol", + "couch": "Sofa", + "potted_plant": "Potteplante", + "bed": "Seng", + "gls": "GLS", + "person": "Person", + "bicycle": "Sykkel", + "car": "Bil", + "fire_hydrant": "Brannhydrant", + "street_sign": "Gateskilt", + "stop_sign": "Stoppskilt", + "parking_meter": "Parkeringsautomat", + "bench": "Benk", + "bird": "Fugl", + "cat": "Katt", + "dog": "Hund", + "horse": "Hest", + "sheep": "Sau", + "cow": "Ku", + "elephant": "Elefant", + "bear": "Bjørn", + "zebra": "Sebra", + "giraffe": "Giraff", + "hat": "Hatt", + "backpack": "Ryggsekk", + "umbrella": "Paraply", + "shoe": "Sko", + "eye_glasses": "Briller", + "handbag": "Håndveske", + "tie": "Slips", + "suitcase": "Koffert", + "frisbee": "Frisbee", + "skis": "Ski", + "snowboard": "Snøbrett", + "sports_ball": "Ball", + "kite": "Drage", + "baseball_bat": "Baseballkølle", + "baseball_glove": "Baseballhanske", + "skateboard": "Skateboard", + "surfboard": "Surfebrett", + "tennis_racket": "Tennisracket", + "bottle": "Flaske", + "plate": "Tallerken", + "fork": "Gaffel", + "knife": "Kniv", + "spoon": "Skje", + "bowl": "Bolle", + "banana": "Banan", + "apple": "Eple", + "broccoli": "Brokkoli", + "sandwich": "Sandwich", + "orange": "Appelsin", + "carrot": "Gulrot", + "hot_dog": "Pølse i brød", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Kake", + "mirror": "Speil", + "dining_table": "Spisebord", + "window": "Vindu", + "desk": "Skrivebord", + "toilet": "Toalett", + "door": "Dør", + "tv": "TV", + "laptop": "Bærbar datamaskin", + "mouse": "Mus", + "remote": "Fjernkontroll", + "keyboard": "Tastatur", + "cell_phone": "Mobiltelefon", + "sink": "Vask", + "microwave": "Mikrobølgeovn", + "oven": "Ovn", + "toaster": "Brødrister", + "refrigerator": "Kjøleskap", + "blender": "Blender", + "book": "Bok", + "clock": "Klokke", + "vase": "Vase", + "scissors": "Saks", + "teddy_bear": "Teddybjørn", + "hair_dryer": "Hårføner", + "toothbrush": "Tannbørste", + "hair_brush": "Hårbørste", + "vehicle": "Kjøretøy", + "squirrel": "Ekorn", + "deer": "Hjort", + "animal": "Dyr", + "bark": "Bjeff", + "fox": "Rev", + "goat": "Geit", + "rabbit": "Kanin", + "raccoon": "Vaskebjørn", + "robot_lawnmower": "Robotgressklipper", + "waste_bin": "Avfallsbeholder", + "on_demand": "Manuelt opptak", + "face": "Ansikt", + "license_plate": "Kjennemerke", + "package": "Pakke", + "bbq_grill": "Grill", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Filter", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/classificationModel.json new file mode 100644 index 0000000..1863122 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/classificationModel.json @@ -0,0 +1,182 @@ +{ + "documentTitle": "Klassifiseringsmodeller - Frigate", + "button": { + "deleteClassificationAttempts": "Slett klassifiseringsbilder", + "renameCategory": "Omdøp klasse", + "deleteCategory": "Slett klasse", + "deleteImages": "Slett bilder", + "trainModel": "Tren modell", + "addClassification": "Legg til klassifisering", + "deleteModels": "Slett modeller", + "editModel": "Rediger modell" + }, + "toast": { + "success": { + "deletedCategory": "Klasse slettet", + "deletedImage": "Bilder slettet", + "categorizedImage": "Klassifiserte bildet", + "trainedModel": "Modellen ble trent.", + "trainingModel": "Modelltrening startet.", + "deletedModel_one": "{{count}} modell ble slettet", + "deletedModel_other": "{{count}} modeller ble slettet", + "updatedModel": "Modellkonfigurasjonen ble oppdatert", + "renamedCategory": "Klassen ble omdøpt til {{name}}" + }, + "error": { + "deleteImageFailed": "Kunne ikke slette: {{errorMessage}}", + "deleteCategoryFailed": "Kunne ikke slette klasse: {{errorMessage}}", + "categorizeFailed": "Kunne ikke klassifisere bilde: {{errorMessage}}", + "trainingFailed": "Modelltrening mislyktes. Sjekk Frigate-loggene for detaljer.", + "deleteModelFailed": "Kunne ikke slette modell: {{errorMessage}}", + "trainingFailedToStart": "Kunne ikke starte modelltrening: {{errorMessage}}", + "updateModelFailed": "Kunne ikke oppdatere modell: {{errorMessage}}", + "renameCategoryFailed": "Kunne ikke omdøpe klasse: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Slett klasse", + "desc": "Er du sikker på at du vil slette klassen {{name}}? Dette vil permanent slette alle tilknyttede bilder og kreve at modellen trenes på nytt.", + "minClassesTitle": "Kan ikke slette klasse", + "minClassesDesc": "En klassifiseringsmodell må ha minst 2 klasser. Legg til en ny klasse før du sletter denne." + }, + "deleteDatasetImages": { + "title": "Slett datasettbilder", + "desc": "Er du sikker på at du vil slette {{count}} bilder fra {{dataset}}? Denne handlingen kan ikke angres og krever at modellen trenes på nytt." + }, + "deleteTrainImages": { + "title": "Slett treningsbilder", + "desc": "Er du sikker på at du vil slette {{count}} bilder? Denne handlingen kan ikke angres." + }, + "renameCategory": { + "title": "Omdøp klasse", + "desc": "Skriv inn et nytt navn for {{name}}. Du må trene modellen på nytt for at navneendringen skal tre i kraft." + }, + "description": { + "invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek." + }, + "train": { + "title": "Nylige klassifiseringer", + "aria": "Velg nylige klassifiseringer", + "titleShort": "Nylige" + }, + "categories": "Klasser", + "createCategory": { + "new": "Opprett ny klasse" + }, + "categorizeImageAs": "Klassifiser bilde som:", + "categorizeImage": "Klassifiser bilde", + "noModels": { + "object": { + "title": "Ingen objektklassifiseringsmodeller", + "description": "Opprett en tilpasset modell for å klassifisere oppdagede objekter.", + "buttonText": "Opprett objektmodell" + }, + "state": { + "title": "Ingen tilstandsklassifiseringsmodeller", + "description": "Opprett en tilpasset modell for å overvåke og klassifisere tilstandsendringer i spesifikke kamerasoner.", + "buttonText": "Opprett tilstandsmodell" + } + }, + "wizard": { + "title": "Opprett ny klassifisering", + "steps": { + "nameAndDefine": "Navn og definér", + "stateArea": "Tilstandsområde", + "chooseExamples": "Velg eksempler" + }, + "step1": { + "description": "Tilstandsmodeller overvåker faste kamerasoner for endringer (f.eks. dør åpen/lukket). Objektmodeller legger til klassifiseringer for oppdagede objekter (f.eks. kjente dyr, bud, osv.).", + "name": "Navn", + "namePlaceholder": "Skriv inn modellnavn...", + "type": "Type", + "typeState": "Tilstand", + "typeObject": "Objekt", + "objectLabel": "Objektetikett", + "objectLabelPlaceholder": "Velg objekttype...", + "classificationType": "Klassifiseringstype", + "classificationTypeTip": "Lær om klassifiseringstyper", + "classificationTypeDesc": "Underetiketter legger til ekstra tekst på objektetiketten (f.eks. 'Person: Posten'). Attributter er søkbare metadata som lagres separat i objektets metadata.", + "classificationSubLabel": "Underetikett", + "classificationAttribute": "Attributt", + "classes": "Klasser", + "classesTip": "Lær om klasser", + "classesStateDesc": "Definer de ulike tilstandene kamerasonen kan være i. For eksempel: 'åpen' og 'lukket' for en garasjeport.", + "classesObjectDesc": "Definer klassene du vil klassifisere oppdagede objekter i. For eksempel: 'bud', 'beboer', 'fremmed' for personklassifisering.", + "classPlaceholder": "Skriv inn klassenavn...", + "errors": { + "nameRequired": "Modellnavn er påkrevd", + "nameLength": "Modellnavn må være på 64 tegn eller mindre", + "nameOnlyNumbers": "Modellnavn kan ikke bare inneholde tall", + "classRequired": "Minst én klasse er påkrevd", + "classesUnique": "Klassenavn må være unike", + "stateRequiresTwoClasses": "Tilstandsmodeller krever minst 2 klasser", + "objectLabelRequired": "Velg en objektetikett", + "objectTypeRequired": "Velg en klassifiseringstype" + }, + "states": "Tilstander" + }, + "step2": { + "description": "Velg kameraer og definer området som skal overvåkes for hvert kamera. Modellen vil klassifisere tilstanden til disse områdene.", + "cameras": "Kameraer", + "selectCamera": "Velg kamera", + "noCameras": "Klikk + for å legge til kameraer", + "selectCameraPrompt": "Velg et kamera fra listen for å definere overvåkingsområdet" + }, + "step3": { + "selectImagesPrompt": "Velg alle bilder med: {{className}}", + "selectImagesDescription": "Klikk på bilder for å velge dem. Klikk Fortsett når du er ferdig med denne klassen.", + "generating": { + "title": "Genererer eksempelbilder", + "description": "Frigate henter representative bilder fra opptakene dine. Dette kan ta litt tid..." + }, + "training": { + "title": "Trener modell", + "description": "Modellen din trenes i bakgrunnen. Lukk dette vinduet, så starter modellen når treningen er ferdig." + }, + "retryGenerate": "Prøv å generere på nytt", + "noImages": "Ingen eksempelbilder generert", + "classifying": "Klassifiserer og trener...", + "trainingStarted": "Trening startet", + "errors": { + "noCameras": "Ingen kameraer konfigurert", + "noObjectLabel": "Ingen objektetikett valgt", + "generateFailed": "Kunne ikke generere eksempler: {{error}}", + "generationFailed": "Generering mislyktes. Prøv igjen.", + "classifyFailed": "Kunne ikke klassifisere bilder: {{error}}" + }, + "generateSuccess": "Eksempelbilder ble generert", + "allImagesRequired_one": "Vennligst klassifiser alle bildene. {{count}} bilde gjenstår.", + "allImagesRequired_other": "Vennligst klassifiser alle bildene. {{count}} bilder gjenstår.", + "modelCreated": "Modellen ble opprettet. Bruk visningen Nylige klassifiseringer for å legge til bilder for manglende tilstander, og tren deretter modellen.", + "missingStatesWarning": { + "title": "Manglende tilstandseksempler", + "description": "Det anbefales å velge eksempler for alle tilstander for å oppnå best mulig resultat. Du kan fortsette uten å velge alle tilstander, men modellen vil ikke bli trent før alle tilstander har bilder. Etter at du har gått videre, bruk visningen Nylige klassifiseringer for å klassifisere bilder for de manglende tilstandene, og tren deretter modellen." + } + } + }, + "deleteModel": { + "title": "Slett klassifiseringsmodell", + "single": "Er du sikker på at du vil slette {{name}}? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres.", + "desc": "Er du sikker på at du vil slette {{count}} modell(er)? Dette vil permanent slette alle tilknyttede data, inkludert bilder og treningsdata. Denne handlingen kan ikke angres." + }, + "menu": { + "objects": "Objekter", + "states": "Tilstander" + }, + "details": { + "scoreInfo": "Score representerer gjennomsnittlig klassifiseringskonfidens på tvers av alle deteksjoner av dette objektet." + }, + "tooltip": { + "trainingInProgress": "Modellen trenes nå", + "noNewImages": "Ingen nye bilder å trene på. Klassifiser flere bilder i datasettet først.", + "noChanges": "Ingen endringer i datasettet siden forrige trening.", + "modelNotReady": "Modellen er ikke klar til å trenes" + }, + "edit": { + "title": "Rediger klassifiseringsmodell", + "descriptionState": "Rediger klassene for denne tilstandsklassifiseringsmodellen. Endringer vil kreve at modellen trenes på nytt.", + "descriptionObject": "Rediger objekttypen og klassifiseringstypen for denne objektklassifiseringsmodellen.", + "stateClassesInfo": "Merk: Endring av tilstandsklasser krever at modellen trenes på nytt med de oppdaterte klassene." + }, + "none": "Ingen" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/configEditor.json new file mode 100644 index 0000000..c0c9253 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Konfigurasjonsredigering - Frigate", + "toast": { + "error": { + "savingError": "Feil ved lagring av konfigurasjon" + }, + "success": { + "copyToClipboard": "Konfigurasjonen ble kopiert til utklippstavlen." + } + }, + "configEditor": "Konfigurasjonsredigering", + "copyConfig": "Kopier konfigurasjonen", + "saveAndRestart": "Lagre og omstart", + "saveOnly": "Kun lagre", + "confirm": "Avslutt uten å lagre?", + "safeConfigEditor": "Konfigurasjonsredigering (Sikker modus)", + "safeModeDescription": "Frigate er i sikker modus grunnet en feil i validering av konfigurasjonen." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/events.json new file mode 100644 index 0000000..8c54ca7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/events.json @@ -0,0 +1,63 @@ +{ + "camera": "Kamera", + "empty": { + "alert": "Det er ingen varsler å inspisere", + "detection": "Det er ingen deteksjoner å inspisere", + "motion": "Ingen bevegelsesdata funnet" + }, + "timeline": "Tidslinje", + "events": { + "label": "Hendelser", + "aria": "Velg hendelser", + "noFoundForTimePeriod": "Ingen hendelser funnet for denne tidsperioden." + }, + "newReviewItems": { + "label": "Vis nye inspeksjonselementer", + "button": "Nye elementer å inspisere" + }, + "alerts": "Varsler", + "detections": "Deteksjoner", + "motion": { + "label": "Bevegelse", + "only": "Kun bevegelse" + }, + "allCameras": "Alle kameraer", + "timeline.aria": "Velg tidslinje", + "documentTitle": "Inspeksjon - Frigate", + "recordings": { + "documentTitle": "Opptak - Frigate" + }, + "calendarFilter": { + "last24Hours": "Siste 24 timer" + }, + "markAsReviewed": "Merk som inspisert", + "markTheseItemsAsReviewed": "Merk disse elementene som inspiserte", + "selected_one": "{{count}} valgt", + "selected_other": "{{count}} valgt", + "detected": "detektert", + "suspiciousActivity": "Mistenkelig aktivitet", + "threateningActivity": "Truende aktivitet", + "detail": { + "noDataFound": "Ingen detaljer å inspisere", + "aria": "Vis/skjul detaljvisning", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekter", + "noObjectDetailData": "Ingen detaljdata for objektet tilgjengelig.", + "label": "Detalj", + "settings": "Detaljvisning – innstillinger", + "alwaysExpandActive": { + "desc": "Utvid alltid objektdetaljene for det aktive gjennomgangselementet når tilgjengelig.", + "title": "Utvid alltid for aktive" + } + }, + "objectTrack": { + "trackedPoint": "Sporingspunkt", + "clickToSeek": "Klikk for å gå til dette tidspunktet" + }, + "zoomIn": "Zoom inn", + "zoomOut": "Zoom ut", + "normalActivity": "Normal", + "needsReview": "Trenger inspeksjon", + "securityConcern": "Sikkerhetsrisiko", + "select_all": "Alle" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/explore.json new file mode 100644 index 0000000..c6a8e4e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/explore.json @@ -0,0 +1,293 @@ +{ + "documentTitle": "Utforsk - Frigate", + "generativeAI": "Generativ AI", + "exploreIsUnavailable": { + "title": "Utforsk er utilgjengelig", + "embeddingsReindexing": { + "startingUp": "Starter opp…", + "estimatedTime": "Estimert gjenværende tid:", + "context": "Utforsk kan brukes etter at reindekseringen av vektorrepresentasjoner for sporede objekter er fullført.", + "finishingShortly": "Avsluttes snart", + "step": { + "thumbnailsEmbedded": "Miniatyrbilder innebygd: ", + "descriptionsEmbedded": "Beskrivelser innebygd: ", + "trackedObjectsProcessed": "Sporede objekter behandlet: " + } + }, + "downloadingModels": { + "setup": { + "visionModel": "Visjonsmodell", + "visionModelFeatureExtractor": "Funksjonsekstraktor for visjonsmodell", + "textModel": "Tekstmodell", + "textTokenizer": "Tekst-tokeniserer" + }, + "context": "Frigate laster ned de nødvendige vektorrepresentasjonsmodellene for å støtte funksjonen for semantisk søk. Dette kan ta flere minutter, avhengig av hastigheten på nettverksforbindelsen din.", + "tips": { + "context": "Du bør vurdere å reindeksere vektorrepresentasjoner for de sporede objektene dine når modellene er lastet ned.", + "documentation": "Se dokumentasjonen" + }, + "error": "En feil har oppstått. Sjekk Frigate-loggene." + } + }, + "objectLifecycle": { + "createObjectMask": "Lag objektmaske", + "adjustAnnotationSettings": "Juster annoteringsinnstillinger", + "scrollViewTips": "Sveip for å se de viktigste hendelsene i dette objektets livssyklus.", + "autoTrackingTips": "Posisjoner for omsluttende boks vil være unøyaktige for kameraer med automatisk sporing.", + "lifecycleItemDesc": { + "visible": "{{label}} oppdaget", + "attribute": { + "other": "{{label}} gjenkjent som {{attribute}}", + "faceOrLicense_plate": "{{attribute}} oppdaget for {{label}}" + }, + "gone": "{{label}} forlot", + "heard": "{{label}} hørt", + "external": "{{label}} oppdaget", + "entered_zone": "{{label}} gikk inn i {{zones}}", + "active": "{{label}} ble aktiv", + "stationary": "{{label}} ble stasjonær", + "header": { + "zones": "Soner", + "ratio": "Forhold", + "area": "Areal" + } + }, + "annotationSettings": { + "title": "Annoteringsinnstillinger", + "showAllZones": { + "title": "Vis alle soner", + "desc": "Vis alltid soner på bilder der objekter har gått inn i en sone." + }, + "offset": { + "documentation": "Se dokumentasjonen ", + "label": "Annoteringsforskyvning", + "desc": "Disse dataene kommer fra kameraets deteksjonsstrøm, men legges over bilder fra opptaksstrømmen. Det er usannsynlig at de to strømmene er perfekt synkronisert. Som et resultat vil ikke den omsluttende boksen og opptakene stemme perfekt overens. Imidlertid kan feltet annotation_offset brukes til å justere dette.", + "millisecondsToOffset": "Millisekunder å forskyve annoteringsdata. Standard: 0", + "tips": "TIPS: Tenk deg et hendelsesklipp med en person som går fra venstre til høyre. Hvis den omsluttende boksen i hendelsestidslinjen konsekvent er til venstre for personen, bør verdien reduseres. Tilsvarende, hvis en person går fra venstre til høyre og den omsluttende boksen konsekvent er foran personen, bør verdien økes.", + "toast": { + "success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen. Start Frigate på nytt for å aktivere endringene dine." + } + } + }, + "carousel": { + "previous": "Forrige lysbilde", + "next": "Neste lysbilde" + }, + "title": "Objektets livssyklus", + "noImageFound": "Ingen bilder funnet for dette tidsstempelet.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Sporet punkt" + }, + "details": { + "item": { + "title": "Detaljer for inspeksjonelement", + "button": { + "share": "Del dette inspeksjonselementet", + "viewInExplore": "Vis i Utforsk" + }, + "toast": { + "success": { + "updatedSublabel": "Underetikett ble oppdatert.", + "updatedLPR": "Vellykket oppdatering av kjennemerke.", + "regenerate": "En ny beskrivelse har blitt anmodet fra {{provider}}. Avhengig av hastigheten til leverandøren din, kan den nye beskrivelsen ta litt tid å regenerere.", + "audioTranscription": "Lydtranskripsjon ble forespurt. Avhengig av ytelsen på din Frigate server kan transkripsjonen ta noe tid å fullføre." + }, + "error": { + "regenerate": "Feil ved anrop til {{provider}} for en ny beskrivelse: {{errorMessage}}", + "updatedLPRFailed": "Oppdatering av kjennemerke feilet: {{errorMessage}}", + "updatedSublabelFailed": "Feil ved oppdatering av underetikett: {{errorMessage}}", + "audioTranscription": "Forespørsel om lydtranskripsjon feilet: {{errorMessage}}" + } + }, + "desc": "Detaljer for inspeksjonselement", + "tips": { + "mismatch_one": "{{count}} utilgjengelig objekt ble oppdaget og inkludert i dette inspeksjonselementet. Disse objektene kvalifiserte ikke som et varsel eller deteksjon, eller har allerede blitt ryddet opp/slettet.", + "mismatch_other": "{{count}} utilgjengelige objekter ble oppdaget og inkludert i dette inspeksjonselementet. Disse objektene kvalifiserte ikke som et varsel eller deteksjon, eller har allerede blitt ryddet opp/slettet.", + "hasMissingObjects": "Juster konfigurasjonen hvis du vil at Frigate skal lagre sporede objekter for følgende etiketter: {{objects}}" + } + }, + "topScore": { + "info": "Toppscoren er den høyeste medianverdien for det sporede objektet, så denne kan avvike fra scoren som vises på miniatyrbildet i søkeresultatet.", + "label": "Toppscore" + }, + "estimatedSpeed": "Estimert hastighet", + "objects": "Objekter", + "button": { + "findSimilar": "Finn lignende", + "regenerate": { + "title": "Regenerer", + "label": "Regenerer beskrivelse for sporet objekt" + } + }, + "description": { + "placeholder": "Beskrivelse av det sporede objektet", + "aiTips": "Frigate vil ikke anmode om en beskrivelse fra din generative AI-leverandør før livssyklusen til det sporede objektet er avsluttet.", + "label": "Beskrivelse" + }, + "regenerateFromThumbnails": "Regenerer fra miniatyrbilder", + "tips": { + "descriptionSaved": "Beskrivelsen ble lagret", + "saveDescriptionFailed": "Feil ved lagring av beskrivelse: {{errorMessage}}" + }, + "label": "Etikett", + "editLPR": { + "title": "Rediger kjennemerke", + "descNoLabel": "Skriv inn et nytt kjennemerke for dette sporede objekt", + "desc": "Skriv inn et nytt kjennemerke for denne {{label}}" + }, + "recognizedLicensePlate": "Gjenkjent kjennemerke", + "camera": "Kamera", + "zones": "Soner", + "timestamp": "Tidsstempel", + "expandRegenerationMenu": "Utvid regenereringsmenyen", + "regenerateFromSnapshot": "Regenerer fra stillbilde", + "editSubLabel": { + "title": "Rediger underetikett", + "desc": "Angi en ny underetikett for \"{{label}}\"", + "descNoLabel": "Angi en ny underetikett for dette sporede objektet" + }, + "snapshotScore": { + "label": "Stillbilde score" + }, + "score": { + "label": "Score" + } + }, + "itemMenu": { + "viewInHistory": { + "label": "Vis i Historikk", + "aria": "Vis i Historikk" + }, + "downloadVideo": { + "aria": "Last ned video", + "label": "Last ned video" + }, + "downloadSnapshot": { + "label": "Last ned stillbilde", + "aria": "Last ned stillbilde" + }, + "viewObjectLifecycle": { + "label": "Vis objektets livssyklus", + "aria": "Vis objektets livssyklus" + }, + "findSimilar": { + "label": "Finn lignende", + "aria": "Finn lignende sporede objekter" + }, + "deleteTrackedObject": { + "label": "Slett dette sporede objektet" + }, + "submitToPlus": { + "label": "Send til Frigate+", + "aria": "Send til Frigate Plus" + }, + "addTrigger": { + "label": "Legg til utløser", + "aria": "Legg til en utløser for dette sporede objektet" + }, + "audioTranscription": { + "label": "Transkriber", + "aria": "Forespør lydtranskripsjon" + }, + "showObjectDetails": { + "label": "Vis objektets sti" + }, + "hideObjectDetails": { + "label": "Skjul objektets sti" + }, + "viewTrackingDetails": { + "label": "Vis sporingsdetaljer", + "aria": "Vis sporingsdetaljene" + }, + "downloadCleanSnapshot": { + "label": "Last ned rent stillbilde", + "aria": "Last ned stillbilde uten markeringer" + } + }, + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Feil ved sletting av sporet objekt: {{errorMessage}}", + "success": "Sporet objekt ble slettet." + } + }, + "tooltip": "Samsvarer {{type}} til {{confidence}}%", + "previousTrackedObject": "Forrige sporede objekt", + "nextTrackedObject": "Neste sporede objekt" + }, + "trackedObjectDetails": "Detaljer om sporet objekt", + "type": { + "details": "detaljer", + "snapshot": "stillbilde", + "video": "video", + "object_lifecycle": "objektets livssyklus", + "thumbnail": "miniatyrbilde", + "tracking_details": "sporingsdetaljer" + }, + "dialog": { + "confirmDelete": { + "title": "Bekreft sletting", + "desc": "Sletting av dette sporede objektet fjerner stillbildet, alle lagrede vektorrepresentasjoner og tilknyttede oppføringer for sporingsdetaljer. Opptak av dette objektet i Historikk-visningen vil IKKE bli slettet.

    Er du sikker på at du vil fortsette?" + } + }, + "noTrackedObjects": "Fant ingen sporede objekter", + "fetchingTrackedObjectsFailed": "Feil ved henting av sporede objekter: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} sporet objekt ", + "trackedObjectsCount_other": "{{count}} sporede objekter ", + "exploreMore": "Utforsk flere {{label}} objekter", + "aiAnalysis": { + "title": "AI-Analyse" + }, + "concerns": { + "label": "Bekymringer" + }, + "trackingDetails": { + "title": "Sporingsdetaljer", + "noImageFound": "Ingen bilder funnet for dette tidsstempelet.", + "createObjectMask": "Opprett objektmaske", + "adjustAnnotationSettings": "Juster annoteringsinnstillinger", + "scrollViewTips": "Klikk for å se de viktige øyeblikkene i dette objektets livssyklus.", + "autoTrackingTips": "Posisjonene til avgrensningsboksene vil være unøyaktige for kameraer med automatisk sporing.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Sporet punkt", + "lifecycleItemDesc": { + "visible": "{{label}} oppdaget", + "entered_zone": "{{label}} gikk inn i {{zones}}", + "active": "{{label}} ble aktiv", + "stationary": "{{label}} ble stasjonær", + "attribute": { + "faceOrLicense_plate": "{{attribute}} oppdaget for {{label}}", + "other": "{{label}} gjenkjent som {{attribute}}" + }, + "gone": "{{label}} forsvant", + "heard": "{{label}} hørt", + "external": "{{label}} oppdaget", + "header": { + "zones": "Soner", + "ratio": "Sideforhold", + "area": "Område", + "score": "Score" + } + }, + "annotationSettings": { + "title": "Annoteringsinnstillinger", + "showAllZones": { + "title": "Vis alle soner", + "desc": "Alltid vis soner på bilderammer der objekter har gått inn i en sone." + }, + "offset": { + "label": "Annoteringsforskyvning", + "desc": "Disse dataene kommer fra kameraets deteksjonsstrøm, men legges over bilder fra opptaksstrømmen. Det er lite sannsynlig at de to strømmene er perfekt synkronisert. Som et resultat vil avgrensningsboksen og opptaket ikke stemme perfekt overens. Du kan bruke denne innstillingen til å forskyve annoteringene fremover eller bakover i tid for å tilpasse dem bedre til det innspilte opptaket.", + "millisecondsToOffset": "Antall millisekunder deteksjonsannoteringene skal forskyves med. Standard: 0", + "tips": "TIPS: Se for deg et hendelsesklipp med en person som går fra venstre mot høyre. Hvis avgrensningsboksen på tidslinjen for hendelsen konsekvent er til venstre for personen, bør verdien reduseres. På samme måte, hvis en person går fra venstre mot høyre og avgrensningsboksen konsekvent er foran personen, bør verdien økes.", + "toast": { + "success": "Annoteringsforskyvning for {{camera}} er lagret i konfigurasjonsfilen." + } + } + }, + "carousel": { + "previous": "Forrige lysbilde", + "next": "Neste lysbilde" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/exports.json new file mode 100644 index 0000000..4ced2fc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Eksport - Frigate", + "search": "Søk", + "noExports": "Ingen eksporter funnet", + "deleteExport": "Slett eksport", + "deleteExport.desc": "Er du sikker på at du vil slette {{exportName}}?", + "editExport": { + "title": "Gi nytt navn til eksport", + "desc": "Skriv inn et nytt navn for denne eksporten.", + "saveExport": "Lagre eksport" + }, + "toast": { + "error": { + "renameExportFailed": "Kunne ikke gi nytt navn til eksport: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Del eksport", + "downloadVideo": "Last ned video", + "editName": "Rediger navn", + "deleteExport": "Slett eksport" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/faceLibrary.json new file mode 100644 index 0000000..d2e0f6b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "selectItem": "Velg {{item}}", + "description": { + "addFace": "Legg til en ny samling i ansiktsbiblioteket ved å laste opp ditt første bilde.", + "placeholder": "Skriv inn et navn for denne samlingen", + "invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek." + }, + "details": { + "person": "Person", + "confidence": "Konfidens", + "face": "Ansiktsdetaljer", + "faceDesc": "Detaljer for sporet objekt som genererte dette ansiktet", + "timestamp": "Tidsstempel", + "scoreInfo": "Score er et vektet gjennomsnitt av alle ansiktsscorer, vektet etter størrelsen på ansiktet i hvert bilde.", + "subLabelScore": "Poengsum for under-merkelapp", + "unknown": "Ukjent" + }, + "documentTitle": "Ansiktsbibliotek – Frigate", + "createFaceLibrary": { + "new": "Opprett nytt ansikt", + "title": "Opprett samling", + "desc": "Opprett en ny samling", + "nextSteps": "For å bygge et sterkt grunnlag:
  • Bruk Nylige gjenkjennelser-fanen for å velge og trene på bilder for hver oppdaget person.
  • Fokuser på bilder rett forfra for best resultat; unngå å trene bilder som fanger ansikter i vinkel.
  • " + }, + "train": { + "aria": "Velg nylige gjenkjennelser", + "title": "Nylige gjenkjennelser", + "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning", + "titleShort": "Nylige" + }, + "selectFace": "Velg ansikt", + "deleteFaceLibrary": { + "title": "Slett navn", + "desc": "Er du sikker på at du vil slette samlingen {{name}}? Dette vil permanent slette alle tilknyttede ansikter." + }, + "trainFace": "Tren ansikt", + "toast": { + "error": { + "deleteFaceFailed": "Kunne ikke slette: {{errorMessage}}", + "uploadingImageFailed": "Kunne ikke laste opp bilde: {{errorMessage}}", + "trainFailed": "Kunne ikke trene: {{errorMessage}}", + "updateFaceScoreFailed": "Kunne ikke oppdatere ansiktsscore: {{errorMessage}}", + "addFaceLibraryFailed": "Kunne ikke angi ansiktsnavn: {{errorMessage}}", + "deleteNameFailed": "Kunne ikke slette navn: {{errorMessage}}", + "renameFaceFailed": "Kunne ikke gi nytt navn til ansikt: {{errorMessage}}" + }, + "success": { + "deletedFace_one": "Slettet {{count}} ansikt.", + "deletedFace_other": "Slettet {{count}} ansikter.", + "deletedName_one": "{{count}} ansikt ble slettet.", + "deletedName_other": "{{count}} ansikter ble slettet.", + "trainedFace": "Ansiktet ble trent.", + "updatedFaceScore": "Oppdaterte ansiktsscore for {{name}} ({{score}}).", + "uploadedImage": "Bildet ble lastet opp.", + "addFaceLibrary": "{{name}} ble lagt til i ansiktsbiblioteket!", + "renamedFace": "Nytt navn ble gitt til ansikt {{name}}" + } + }, + "imageEntry": { + "dropActive": "Slipp bildet her…", + "dropInstructions": "Dra og slipp, lim inn et bilde her eller klikk for å velge", + "maxSize": "Maks størrelse: {{size}}MB", + "validation": { + "selectImage": "Vennligst velg en bildefil." + } + }, + "readTheDocs": "Se dokumentasjonen", + "button": { + "addFace": "Legg til ansikt", + "uploadImage": "Last opp bilde", + "deleteFaceAttempts": "Slett ansikter", + "reprocessFace": "Prosesser ansiktet på nytt", + "deleteFace": "Slett ansikt", + "renameFace": "Gi nytt navn til ansikt" + }, + "uploadFaceImage": { + "desc": "Last opp et bilde for å skanne etter ansikter og inkludere det for {{pageToggle}}", + "title": "Last opp ansiktsbilde" + }, + "trainFaceAs": "Tren ansikt som:", + "steps": { + "faceName": "Skriv inn ansiktsnavn", + "uploadFace": "Last opp ansiktsbilde", + "nextSteps": "Neste trinn", + "description": { + "uploadFace": "Last opp et bilde av {{name}} som viser ansiktet deres forfra. Bildet trenger ikke å være beskåret til kun ansiktet." + } + }, + "renameFace": { + "desc": "Skriv inn et nytt navn for {{name}}", + "title": "Gi nytt navn til ansikt" + }, + "collections": "Samlinger", + "deleteFaceAttempts": { + "title": "Slett ansikter", + "desc_one": "Er du sikker på at du vil slette {{count}} ansikt? Denne handlingen kan ikke angres.", + "desc_other": "Er du sikker på at du vil slette {{count}} ansikter? Denne handlingen kan ikke angres." + }, + "nofaces": "Ingen ansikter tilgjengelig", + "pixels": "{{area}}piksler" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/live.json new file mode 100644 index 0000000..e23fa9b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Direkte - Frigate", + "lowBandwidthMode": "Lav båndbreddemodus", + "documentTitle.withCamera": "{{camera}} - Direkte - Frigate", + "ptz": { + "move": { + "clickMove": { + "label": "Klikk i rammen for å sentrere kameraet", + "enable": "Aktiver klikk for å flytte", + "disable": "Deaktiver klikk for å flytte" + }, + "left": { + "label": "Flytt PTZ-kameraet til venstre" + }, + "up": { + "label": "Flytt PTZ-kameraet opp" + }, + "down": { + "label": "Flytt PTZ-kameraet ned" + }, + "right": { + "label": "Flytt PTZ-kameraet til høyre" + } + }, + "presets": "PTZ-kamera forhåndsinnstillinger", + "zoom": { + "in": { + "label": "Zoom inn på PTZ-kameraet" + }, + "out": { + "label": "Zoom ut på PTZ-kameraet" + } + }, + "frame": { + "center": { + "label": "Klikk i rammen for å sentrere PTZ-kameraet" + } + }, + "focus": { + "in": { + "label": "Fokuser inn på PTZ-kameraet" + }, + "out": { + "label": "Fokuser ut på PTZ-kameraet" + } + } + }, + "camera": { + "enable": "Aktiver kamera", + "disable": "Deaktiver kamera" + }, + "snapshots": { + "enable": "Aktiver stillbilder", + "disable": "Deaktiver stillbilder" + }, + "audioDetect": { + "enable": "Aktiver lydregistrering", + "disable": "Deaktiver lydregistrering" + }, + "autotracking": { + "enable": "Aktiver automatisk sporing", + "disable": "Deaktiver automatisk sporing" + }, + "manualRecording": { + "tips": "Last ned et stillbilde, eller start en manuell hendelse basert på dette kameraets innstillinger for opptaksbevaring.", + "playInBackground": { + "label": "Spill av i bakgrunnen", + "desc": "Aktiver dette alternativet for å fortsette strømming når spilleren er skjult." + }, + "showStats": { + "label": "Vis statistikk", + "desc": "Aktiver dette alternativet for å vise strømmestatistikk som et overlegg på kamerastrømmen." + }, + "started": "Startet manuelt opptak.", + "end": "Avslutt manuelt opptak", + "title": "Manuelt opptak", + "debugView": "Feilsøkingsvisning", + "start": "Start manuelt opptak", + "failedToStart": "Kunne ikke starte manuelt opptak.", + "recordDisabledTips": "Siden opptak er deaktivert eller begrenset i konfigurasjonen for dette kameraet, vil kun et stillbilde bli lagret.", + "ended": "Avsluttet manuelt opptak.", + "failedToEnd": "Kunne ikke avslutte manuelt opptak." + }, + "audio": "Lyd", + "suspend": { + "forTime": "Pause i: " + }, + "stream": { + "audio": { + "tips": { + "title": "Lyd må være aktivert på kameraet ditt og konfigurert i go2rtc for denne strømmen.", + "documentation": "Se dokumentasjonen " + }, + "available": "Lyd er tilgjengelig for denne strømmen", + "unavailable": "Lyd er ikke tilgjengelig for denne strømmen" + }, + "twoWayTalk": { + "tips": "Enheten din må støtte funksjonen og WebRTC må være konfigurert for toveis tale.", + "tips.documentation": "Se dokumentasjonen ", + "available": "Toveis tale er tilgjengelig for denne strømmen", + "unavailable": "Toveis tale er ikke tilgjengelig for denne strømmen" + }, + "lowBandwidth": { + "tips": "Direktevisning er i lav båndbreddemodus på grunn av buffering eller strømmefeil.", + "resetStream": "Tilbakestill strøm" + }, + "title": "Strøm", + "playInBackground": { + "label": "Spill av i bakgrunnen", + "tips": "Aktiver dette alternativet for å fortsette strømming når spilleren er skjult." + }, + "debug": { + "picker": "Strømmevalg er ikke tilgjengelig i feilsøkingsmodus. Feilsøkingsvisningen bruker alltid strømmen som er tildelt deteksjonsrollen." + } + }, + "history": { + "label": "Vis historiske opptak" + }, + "effectiveRetainMode": { + "modes": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + }, + "notAllTips": "Konfigurasjonen for opptaksbevaring for {{source}} er satt til mode: {{effectiveRetainMode}}, så dette manuelle opptaket vil kun beholde segmenter med {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Rediger oppsett", + "group": { + "label": "Rediger kameragruppe" + }, + "exitEdit": "Avslutt redigering" + }, + "twoWayTalk": { + "enable": "Aktiver toveis tale", + "disable": "Deaktiver toveis tale" + }, + "cameraAudio": { + "enable": "Aktiver kameralyd", + "disable": "Deaktiver kameralyd" + }, + "muteCameras": { + "enable": "Demp alle kameraer", + "disable": "Slå på lyd på alle kameraer" + }, + "detect": { + "enable": "Aktiver deteksjon", + "disable": "Deaktiver deteksjon" + }, + "recording": { + "enable": "Aktiver opptak", + "disable": "Deaktiver opptak" + }, + "streamStats": { + "enable": "Vis Strømmestatistikk", + "disable": "Skjul strømmestatistikk" + }, + "streamingSettings": "Strømmingsinnstillinger", + "notifications": "Meldingsvarsler", + "cameraSettings": { + "title": "{{camera}}-innstillinger", + "cameraEnabled": "Kamera aktivert", + "objectDetection": "Objektdeteksjon", + "recording": "Opptak", + "snapshots": "Stillbilder", + "audioDetection": "Lydregistrering", + "autotracking": "Automatisk sporing", + "transcription": "Lydtranskripsjon" + }, + "transcription": { + "enable": "Aktiver direkte lydtranskripsjon", + "disable": "Deaktiver direkte lydtranskripsjon" + }, + "snapshot": { + "noVideoSource": "Ingen videokilde tilgjengelig for stillbilde.", + "captureFailed": "Kunne ikke ta stillbilde.", + "downloadStarted": "Nedlasting av stillbilde startet.", + "takeSnapshot": "Last ned stillbilde" + }, + "noCameras": { + "title": "Ingen kameraer konfigurert", + "description": "Kom i gang ved å koble et kamera til Frigate.", + "buttonText": "Legg til kamera", + "restricted": { + "title": "Ingen kameraer tilgjengelig", + "description": "Du har ikke tilgang for å se noen kameraer i denne gruppen." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/recording.json new file mode 100644 index 0000000..262eb43 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Eksporter", + "calendar": "Kalender", + "filters": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "Ingen gyldig tidsperiode valgt", + "endTimeMustAfterStartTime": "Sluttid må være etter starttid" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/search.json new file mode 100644 index 0000000..07e57f4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "Søk", + "savedSearches": "Lagrede søk", + "searchFor": "Søk etter {{inputValue}}", + "button": { + "clear": "Fjern søk", + "save": "Lagre søk", + "delete": "Slett lagret søk", + "filterInformation": "Filterinformasjon", + "filterActive": "Filtre aktive" + }, + "filter": { + "label": { + "cameras": "Kameraer", + "labels": "Etiketter", + "search_type": "Søketype", + "after": "Etter", + "min_score": "Min. score", + "max_score": "Maks. score", + "min_speed": "Min. hastighet", + "zones": "Soner", + "sub_labels": "Underetiketter", + "time_range": "Tidsintervall", + "before": "Før", + "max_speed": "Maks. hastighet", + "recognized_license_plate": "Gjenkjent kjennemerke", + "has_clip": "Har videoklipp", + "has_snapshot": "Har stillbilde" + }, + "searchType": { + "thumbnail": "Miniatyrbilde", + "description": "Beskrivelse" + }, + "toast": { + "error": { + "minSpeedMustBeLessOrEqualMaxSpeed": "Minimum hastighet 'min_speed' må være mindre enn eller lik maksimum hastighet 'max_speed'.", + "beforeDateBeLaterAfter": "Før-datoen 'before' må være senere enn etter-datoen 'after'.", + "afterDatebeEarlierBefore": "Etter-datoen 'after' må være tidligere enn før-datoen 'before'.", + "minScoreMustBeLessOrEqualMaxScore": "Minimum score 'min_score' må være mindre enn eller lik maksimum score 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Maksimum score 'max_score' må være større enn eller lik minimum score 'min_score'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimum hastighet 'max_speed' må være større enn eller lik minimum hastighet 'min_speed'." + } + }, + "tips": { + "title": "Hvordan bruke tekstfiltre", + "desc": { + "text": "Filtre hjelper deg med å begrense søkeresultatene dine. Slik bruker du dem i inndatafeltet:", + "example": "Eksempel: cameras:inngangsdør label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Skriv inn et filternavn etterfulgt av et kolon (f.eks. \"cameras:\").
    • Velg en verdi fra forslagene eller skriv inn din egen.
    • Bruk flere filtre ved å legge dem til etter hverandre med mellomrom imellom.
    • Dato-filtre (before: og after:) bruker {{DateFormat}}-formatet.
    • Tidsintervall-filteret bruker {{exampleTime}}-formatet.
    • Fjern filtre ved å klikke på 'x' ved siden av dem.
    ", + "step2": "Velg en verdi fra forslagene eller skriv inn din egen.", + "step4": "Dato-filtre (before: and after:) bruker {{DateFormat}} format.", + "step5": "Tidsintervall-filter bruker {{exampleTime}} format.", + "step6": "Fjern filtre ved å klikke på 'x'en ved siden av dem.", + "exampleLabel": "Eksempel:", + "step1": "Skriv inn et filter-nøkkelnavn etterfulgt av et kolon (f.eks \"cameras:\").", + "step3": "Bruk flere filtre ved å legge dem til en etter en, med mellomrom." + } + }, + "header": { + "currentFilterType": "Filterverdier", + "noFilters": "Filtre", + "activeFilters": "Aktive filtre" + } + }, + "placeholder": { + "search": "Søk…" + }, + "trackedObjectId": "Sporings-ID for objekt", + "similaritySearch": { + "title": "Søk etter likhet", + "active": "Søk etter likhet er aktivt", + "clear": "Fjern søk etter likhet" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/settings.json new file mode 100644 index 0000000..f2563cc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/settings.json @@ -0,0 +1,1310 @@ +{ + "documentTitle": { + "default": "Innstillinger - Frigate", + "authentication": "Autentiseringsinnstillinger - Frigate", + "camera": "Kamerainnstillinger - Frigate", + "masksAndZones": "Maske- og soneeditor - Frigate", + "motionTuner": "Bevegelsesjustering - Frigate", + "object": "Test og feilsøk - Frigate", + "general": "Innstillinger for brukergrensesnitt - Frigate", + "classification": "Klassifiseringsinnstillinger - Frigate", + "frigatePlus": "Frigate+ innstillinger - Frigate", + "notifications": "Innstillinger for meldingsvarsler - Frigate", + "enrichments": "Innstillinger for utvidelser - Frigate", + "cameraManagement": "Administrer kameraer - Frigate", + "cameraReview": "Innstillinger for kamerainspeksjon - Frigate" + }, + "menu": { + "classification": "Klassifisering", + "cameras": "Kamerainnstillinger", + "masksAndZones": "Masker / Soner", + "motionTuner": "Finjustering av bevegelse", + "debug": "Test og feilsøk", + "users": "Brukere", + "frigateplus": "Frigate+", + "ui": "Brukergrensesnitt", + "notifications": "Meldingsvarsler", + "enrichments": "Utvidelser", + "triggers": "Utløsere", + "cameraManagement": "Administrasjon", + "cameraReview": "Inspeksjon", + "roles": "Roller" + }, + "dialog": { + "unsavedChanges": { + "title": "Du har ulagrede endringer.", + "desc": "Vil du lagre endringene dine før du fortsetter?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Ingen kamera" + }, + "general": { + "liveDashboard": { + "playAlertVideos": { + "label": "Spill av varselvideoer", + "desc": "Som standard vises nylige varsler på Direkte-dashbord som små videoer som gjentas. Deaktiver dette alternativet for kun å vise et statisk bilde av nylige varsler på denne enheten/nettleseren." + }, + "title": "Direkte-dashbord", + "automaticLiveView": { + "label": "Automatisk direktevisning", + "desc": "Bytt automatisk til et kameras direktevisning når aktivitet oppdages. Deaktivering av dette valget gjør at statiske kamerabilder i Direkte-dashbord kun oppdateres én gang i minuttet." + }, + "displayCameraNames": { + "label": "Vis alltid kameranavn", + "desc": "Vis alltid kameranavnene i en merkelapp i dashbordet for direktevisning med flere kameraer." + }, + "liveFallbackTimeout": { + "label": "Tidsavbrudd for reserveløsning for direkteavspiller", + "desc": "Når et kameras direktestrøm med høy kvalitet er utilgjengelig, bytt til lav båndbreddemodus etter dette antallet sekunder. Standard: 3." + } + }, + "storedLayouts": { + "title": "Lagrede oppsett", + "desc": "Kameraplasseringer i en gruppe kan dras og endres. Posisjonene lagres lokalt i nettleseren.", + "clearAll": "Fjern alle oppsett" + }, + "recordingsViewer": { + "title": "Opptaksvisning", + "defaultPlaybackRate": { + "label": "Standard avspillingshastighet", + "desc": "Standard hastighet for avspilling av opptak." + } + }, + "calendar": { + "firstWeekday": { + "sunday": "Søndag", + "label": "Første ukedag", + "desc": "Dagen ukene starter på i inspeksjonskalenderen.", + "monday": "Mandag" + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStreamingSettings": "Strømmingsinnstillinger for alle kameragrupper ble fjernet.", + "clearStoredLayout": "Lagret oppsett for {{cameraName}} ble fjernet" + }, + "error": { + "clearStoredLayoutFailed": "Kunne ikke fjerne lagret oppsett: {{errorMessage}}", + "clearStreamingSettingsFailed": "Kunne ikke fjerne strømmingsinnstillinger: {{errorMessage}}" + } + }, + "title": "Innstillinger for brukergrensesnitt", + "cameraGroupStreaming": { + "title": "Strømmingsinnstillinger for kameragrupper", + "desc": "Strømmingsinnstillingene lagres lokalt i nettleseren.", + "clearAll": "Fjern alle strømmingsinnstillinger" + } + }, + "classification": { + "semanticSearch": { + "title": "Semantisk søk", + "desc": "Semantisk søk i Frigate lar deg finne sporede objekter i inspeksjonselementene ved hjelp av enten bildet, en egendefinert tekstbeskrivelse eller en automatisk generert beskrivelse.", + "reindexNow": { + "confirmTitle": "Bekreft reindeksering", + "error": "Kunne ikke starte reindeksering: {{errorMessage}}", + "label": "Reindekser nå", + "confirmButton": "Reindekser", + "success": "Reindeksering startet.", + "alreadyInProgress": "Reindeksering pågår allerede.", + "desc": "Reindeksering vil regenerere vektorrepresentasjoner for alle sporede objekter. Prosessen kjøres i bakgrunnen, kan belaste CPU-en maksimalt og ta mye tid avhengig av antall sporede objekter.", + "confirmDesc": "Er du sikker på at du vil reindeksere vektorrepresentasjoner for alle sporede objekter? Dette vil kjøres i bakgrunnen, men kan bruke all CPU og ta tid. Du kan følge fremdriften på Utforsk-siden." + }, + "readTheDocumentation": "Les dokumentasjonen", + "modelSize": { + "label": "Modellstørrelse", + "small": { + "title": "liten", + "desc": "Ved å bruke liten brukes en kvantisert modell som bruker mindre RAM og er raskere, med ubetydelig tap av kvalitet for vektorrepresentasjoner." + }, + "large": { + "title": "stor", + "desc": "Ved å bruke stor brukes hele Jina-modellen og den vil automatisk bruke GPU hvis tilgjengelig." + }, + "desc": "Størrelsen på modellen som brukes for vektorrepresentasjoner for semantiske søk." + } + }, + "faceRecognition": { + "title": "Ansiktsgjenkjenning", + "modelSize": { + "small": { + "title": "liten", + "desc": "Liten bruker en FaceNet-modell for vektorrepresentasjoner som fungerer effektivt på de fleste CPU-er." + }, + "large": { + "title": "stor", + "desc": "Stor bruker en ArcFace-modell for vektorrepresentasjoner og vil automatisk kjøre på GPU hvis tilgjengelig." + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt for ansiktsgjenkjenning." + }, + "readTheDocumentation": "Les dokumentasjonen", + "desc": "Ansiktsgjenkjenning lar deg tilordne navn til personer. Når et ansikt gjenkjennes, legges navnet til som under-merkelapp. Denne informasjonen vises i brukergrensesnittet, i filtre, samt i meldingsvarsler." + }, + "licensePlateRecognition": { + "title": "Gjenkjenning av kjennemerker", + "readTheDocumentation": "Les dokumentasjonen", + "desc": "Frigate kan gjenkjenne kjennemerker og automatisk legge inn tall/bokstaver i 'recognized_license_plate'-feltet, eller et kjent navn som under-merkelapp til objekter av typen bil. Vanlig bruk er å lese kjennemerker ved innkjørsel eller på vei." + }, + "title": "Klassifiseringsinnstillinger", + "toast": { + "success": "Klassifiseringsinnstillinger lagret. Start Frigate på nytt for å bruke endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + }, + "birdClassification": { + "title": "Artsbestemmelse for fugler", + "desc": "Artsbestemmelse identifiserer kjente fugler ved hjelp av en kvantisert TensorFlow-modell. Når en kjent fugl gjenkjennes, legges det vanlige navnet til som en under-merkelapp. Denne informasjonen vises i brukergrensesnittet, i filtre, samt i meldingsvarsler." + }, + "restart_required": "Omstart påkrevd (Klassifiseringsinnstillinger endret)", + "unsavedChanges": "Ulagrede endringer i klassifiseringsinnstillinger" + }, + "camera": { + "streams": { + "desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke go2rtc-restrømming.", + "title": "Strømmer" + }, + "reviewClassification": { + "title": "Inspeksjonssklassifisering", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} vises som varsler.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert i {{zone}} på {{cameraName}}, vises som deteksjoner.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vises som deteksjoner uavhengig av sone.", + "notSelectDetections": "Alle {{detectionsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} som ikke er kategorisert som Varsler, vises som deteksjoner uavhengig av sone." + }, + "selectAlertsZones": "Velg soner for varsler", + "desc": "Frigate kategoriserer inspeksjonselementer som Varsler og Deteksjoner. Som standard regnes alle person- og bil-objekter som Varsler. Du kan finjustere klassifiseringen ved å konfigurere nødvendige soner.", + "readTheDocumentation": "Se dokumentasjonen", + "noDefinedZones": "Ingen soner er definert for dette kameraet.", + "objectAlertsTips": "Alle {{alertsLabels}}-objekter på {{cameraName}} vises som varsler.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vises som deteksjoner uavhengig av sone.", + "selectDetectionsZones": "Velg soner for deteksjoner", + "limitDetections": "Avgrens deteksjoner til bestemte soner", + "toast": { + "success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å bruke endringer." + }, + "unsavedChanges": "Ulagrede innstillinger for inspeksjonsklassifisering for {{camera}}" + }, + "title": "Kamerainnstillinger", + "review": { + "title": "Inspeksjon", + "desc": "Aktiver/deaktiver varsler og deteksjoner midlertidig for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke genereres nye inspeksjonselementer. ", + "alerts": "Varsler ", + "detections": "Deteksjoner " + }, + "object_descriptions": { + "desc": "Midlertidig aktiver/deaktiver generative KI-objektbeskrivelser for dette kameraet. Når deaktivert, vil KI-genererte beskrivelser ikke bli forespurt for sporede objekter på dette kameraet.", + "title": "Generative KI-objektbeskrivelser" + }, + "cameraConfig": { + "nameInvalid": "Kameranavnet kan bare inneholde bokstaver, tall, understreker eller bindestreker", + "add": "Legg til kamera", + "edit": "Rediger kamera", + "description": "Konfigurer kamerainnstillinger, inkludert strømmeinnganger og roller.", + "name": "Kamera navn", + "nameRequired": "Kamera navn er påkrevd", + "nameLength": "Kamera navn må være mindre enn 24 tegn.", + "namePlaceholder": "f.eks front_dør", + "enabled": "Aktivert", + "ffmpeg": { + "inputs": "Inngangsstrømmer", + "path": "Lenke til strøm", + "pathRequired": "Lenke til strøm er påkrevd", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst én rolle er påkrevd", + "rolesUnique": "Hver rolle (lyd, gjenkjenning, opptak) kan bare tildeles én strøm", + "addInput": "Legg til inngangsstrøm", + "removeInput": "Fjern inngangsstrøm", + "inputsRequired": "Minst én inngangsstrøm er påkrevd" + }, + "toast": { + "success": "Kamera {{cameraName}} ble lagret" + } + }, + "review_descriptions": { + "title": "Generative KI beskrivelser for inspeksjon", + "desc": "Midlertidig aktiver/deaktiver inspeksjonsbeskrivelser med generativ KI for dette kameraet. Når deaktivert, vil det ikke bli bedt om KI-genererte beskrivelser for inspeksjonselementer på dette kameraet." + }, + "addCamera": "Legg til nytt kamera", + "editCamera": "Rediger kamera:", + "selectCamera": "Velg et kamera", + "backToSettings": "Tilbake til kamerainnstillinger" + }, + "masksAndZones": { + "filter": { + "all": "Alle masker og soner" + }, + "toast": { + "success": { + "copyCoordinates": "Koordinater for {{polyName}} kopiert til utklippstavlen." + }, + "error": { + "copyCoordinatesFailed": "Kunne ikke kopiere koordinater til utklippstavlen." + } + }, + "form": { + "zoneName": { + "error": { + "mustNotBeSameWithCamera": "Sonenavnet kan ikke være det samme som kameranavnet.", + "alreadyExists": "En sone med dette navnet finnes allerede for dette kameraet.", + "mustBeAtLeastTwoCharacters": "Sonenavnet må være minst 2 tegn langt.", + "mustNotContainPeriod": "Sonenavnet kan ikke inneholde punktum.", + "hasIllegalCharacter": "Sonenavnet inneholder ugyldige tegn.", + "mustHaveAtLeastOneLetter": "Sonenavnet må inneholde minst én bokstav." + } + }, + "distance": { + "error": { + "mustBeFilled": "Alle avstandsfeltene må fylles ut for å bruke hastighetsestimering.", + "text": "Avstanden må være større enn eller lik 0,1." + } + }, + "polygonDrawing": { + "delete": { + "title": "Bekreft sletting", + "desc": "Er du sikker på at du vil slette {{type}} {{name}}?", + "success": "{{name}} har blitt slettet." + }, + "removeLastPoint": "Fjern siste punkt", + "reset": { + "label": "Fjern alle punkter" + }, + "snapPoints": { + "true": "Fest punkter", + "false": "Ikke fest punkter" + }, + "error": { + "mustBeFinished": "Tegningen av polygonet må fullføres før lagring." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Treghet må være over 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oppholdstid må være større enn eller lik 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Terskelverdi for hastighet må være større enn eller lik 0.1." + } + } + }, + "zones": { + "label": "Soner", + "documentTitle": "Rediger sone - Frigate", + "edit": "Rediger sone", + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "inertia": { + "title": "Treghet", + "desc": "Angir hvor mange bilder et objekt må være i en sone før det regnes som en del av sonen. Standard: 3" + }, + "desc": { + "title": "Soner lar deg definere et spesifikt område i bildet, slik at du kan bestemme om et objekt er innenfor et bestemt område.", + "documentation": "Dokumentasjon" + }, + "add": "Legg til sone", + "name": { + "title": "Navn", + "inputPlaceHolder": "Skriv inn et navn…", + "tips": "Navnet må være minst 2 tegn langt, inneholde minst én bokstav, og må ikke være det samme som et kamera- eller sone-navn for dette kameraet." + }, + "loiteringTime": { + "title": "Oppholdstid", + "desc": "Setter minimumstid i sekunder som objektet må være i sonen for at den skal aktiveres. Standard: 0" + }, + "objects": { + "title": "Objekter", + "desc": "Liste over objekter som gjelder for denne sonen." + }, + "allObjects": "Alle objekter", + "speedEstimation": { + "title": "Hastighetsestimering", + "desc": "Aktiver hastighetsestimering for objekter i denne sonen. Sonen må ha nøyaktig 4 punkter.", + "docs": "Se dokumentasjonen", + "lineADistance": "Linje A avstand ({{unit}})", + "lineBDistance": "Linje B avstand ({{unit}})", + "lineCDistance": "Linje C avstand ({{unit}})", + "lineDDistance": "Linje D avstand ({{unit}})" + }, + "speedThreshold": { + "title": "Hastighetsgrense ({{unit}})", + "desc": "Angir en minimumshastighet for objekter for at de skal anses som en del av denne sonen.", + "toast": { + "error": { + "pointLengthError": "Hastighetsestimering er deaktivert for denne sonen. Soner med hastighetsestimering må ha nøyaktig 4 punkter.", + "loiteringTimeError": "Soner med oppholdstider større enn 0 bør ikke brukes til hastighetsestimering." + } + } + }, + "toast": { + "success": "Sone ({{zoneName}}) er lagret." + } + }, + "motionMasks": { + "label": "Bevegelsesmasker", + "desc": { + "documentation": "Dokumentasjon", + "title": "Bevegelsesmasker brukes til å hindre uønsket type bevegelse fra å utløse deteksjon. For mye maskering kan gjøre det vanskeligere å spore objekter." + }, + "add": "Ny bevegelsesmaske", + "documentTitle": "Rediger bevegelsesmaske - Frigate", + "edit": "Rediger bevegelsesmaske", + "context": { + "title": "Bevegelsesmasker brukes til å hindre uønsket type bevegelse fra å utløse deteksjon (eksempel: tregrener, kameratidsstempler). Bevegelsesmasker bør brukes svært sparsomt. For mye maskering vil gjøre det vanskeligere å spore objekter.", + "documentation": "Se dokumentasjonen" + }, + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "polygonAreaTooLarge": { + "title": "Bevegelsesmasken dekker {{polygonArea}}% av kameraets bilde. Store bevegelsesmasker anbefales ikke.", + "tips": "Bevegelsesmasker hindrer ikke objektene fra å bli detektert. Du bør bruke en påkrevd sone i stedet.", + "documentation": "Se dokumentasjonen" + }, + "toast": { + "success": { + "title": "{{polygonName}} er lagret.", + "noName": "Bevegelsesmasken er lagret." + } + } + }, + "objectMasks": { + "clickDrawPolygon": "Klikk for å tegne et polygon på bildet.", + "point_one": "{{count}} punkt", + "point_other": "{{count}} punkter", + "label": "Objektmasker", + "documentTitle": "Rediger objektmaske - Frigate", + "desc": { + "title": "Objektfiltermasker brukes for å filtrere ut falske positiver for en gitt objekttype basert på plassering.", + "documentation": "Dokumentasjon" + }, + "add": "Legg til objektmaske", + "edit": "Rediger objektmaske", + "context": "Objektfiltermasker brukes for å filtrere ut falske positiver for en gitt objekttype basert på plassering.", + "objects": { + "title": "Objekter", + "desc": "Objekttypen som gjelder for denne objektmasken.", + "allObjectTypes": "Alle objekttyper" + }, + "toast": { + "success": { + "title": "{{polygonName}} er lagret.", + "noName": "Objektmasken er lagret." + } + } + }, + "restart_required": "Omstart påkrevd (masker/soner endret)", + "motionMaskLabel": "Bevegelsesmaske {{number}}", + "objectMaskLabel": "Objektmaske {{number}} ({{label}})" + }, + "motionDetectionTuner": { + "title": "Finjustering av bevegelsesdeteksjon", + "Threshold": { + "title": "Terskel", + "desc": "Terskelverdien bestemmer hvor mye endring i en piksels lysstyrke som kreves for å bli betraktet som bevegelse. Standard: 30" + }, + "contourArea": { + "title": "Konturområde", + "desc": "Konturområdets verdi brukes til å bestemme hvilke grupper av endrede piksler som kvalifiserer som bevegelse. Standard: 10" + }, + "improveContrast": { + "desc": "Forbedre kontrasten for mørkere scener. Standard: PÅ", + "title": "Forbedre kontrast" + }, + "desc": { + "title": "Frigate bruker bevegelsesdeteksjon som en første sjekk for å se om det skjer noe i bildet som er verdt å sjekke med objektdeteksjon.", + "documentation": "Les guiden for bevegelsesjustering" + }, + "toast": { + "success": "Bevegelsesinnstillingene er lagret." + }, + "unsavedChanges": "Ulagrede endringer i bevegelsesjustering ({{camera}})" + }, + "debug": { + "title": "Test og feilsøking", + "objectList": "Objektliste", + "noObjects": "Ingen objekter", + "boundingBoxes": { + "title": "Avgrensningsbokser", + "desc": "Vis omsluttende bokser rundt sporede objekter", + "colors": { + "label": "Farge på omsluttende bokser for objekt", + "info": "
  • Ved oppstart vil forskjellige farger bli tildelt hver objekttype
  • En mørkeblå tynn linje indikerer at objektet ikke er detektert på dette tidspunktet
  • En grå tynn linje indikerer at objektet er detektert som stasjonært
  • En tykk linje indikerer at objektet er under autosporing (når aktivert)
  • " + } + }, + "timestamp": { + "title": "Tidsstempel", + "desc": "Legg et tidsstempel over bildet" + }, + "zones": { + "title": "Soner", + "desc": "Vis en kontur av alle definerte soner" + }, + "motion": { + "desc": "Vis bokser rundt områder der bevegelse er detektert", + "tips": "

    Bevegelsesbokser


    Røde bokser vil vises på områder i bildet hvor bevegelse for øyeblikket blir detektert

    ", + "title": "Bevegelsesbokser" + }, + "regions": { + "tips": "

    Regionbokser


    Lysegrønne bokser vil vises på områder av interesse i bildet som blir sendt til objektdetektoren.

    ", + "title": "Regioner", + "desc": "Vis en boks for interesseområdet sendt til objektdetektoren" + }, + "objectShapeFilterDrawing": { + "document": "Se dokumentasjonen ", + "score": "Score", + "ratio": "Sideforhold", + "area": "Areal", + "title": "Tegning av objektformfilter", + "desc": "Tegn et rektangel på bildet for å vise areal- og størrelsesforhold", + "tips": "Aktiver dette alternativet for å tegne et rektangel på kamerabildet for å vise areal og størrelsesforholdet. Disse verdiene kan deretter brukes til å sette filterparametere for objektform i konfigurasjonen." + }, + "detectorDesc": "Frigate bruker dine detektorer ({{detectors}}) for å oppdage objekter i kameraets videostrøm.", + "desc": "Test og feilsøk viser sporede objekter i sanntid og deres statistikk. Objektlisten viser en tidsforsinket oppsummering av detekterte objekter.", + "debugging": "Test og feilsøk", + "mask": { + "title": "Bevegelsesmasker", + "desc": "Vis polygoner for bevegelsesmasker" + }, + "openCameraWebUI": "Åpne {{camera}} sitt nettgrensesnitt", + "audio": { + "title": "Lyd", + "noAudioDetections": "Ingen lyddeteksjoner", + "score": "score", + "currentRMS": "Nåværende RMS", + "currentdbFS": "Nåværende dbFS" + }, + "paths": { + "title": "Stier", + "desc": "Vis betydningsfulle punkter på det sporede objektets sti", + "tips": "

    Stier


    Linjer og sirkler vil indikere viktige punkter som det sporede objektet har beveget seg gjennom i løpet av sin livssyklus.

    " + } + }, + "users": { + "title": "Brukere", + "management": { + "title": "Brukeradministrasjon", + "desc": "Administrer brukerprofiler for denne Frigate-instansen." + }, + "addUser": "Legg til bruker", + "updatePassword": "Oppdater passord", + "toast": { + "success": { + "deleteUser": "Bruker {{user}} ble slettet", + "updatePassword": "Passordet ble oppdatert.", + "createUser": "Bruker {{user}} ble opprettet", + "roleUpdated": "Rolle oppdatert for {{user}}" + }, + "error": { + "deleteUserFailed": "Kunne ikke slette bruker: {{errorMessage}}", + "setPasswordFailed": "Kunne ikke lagre passord: {{errorMessage}}", + "createUserFailed": "Kunne ikke opprette bruker: {{errorMessage}}", + "roleUpdateFailed": "Kunne ikke oppdatere rolle: {{errorMessage}}" + } + }, + "dialog": { + "form": { + "user": { + "placeholder": "Skriv inn brukernavn", + "title": "Brukernavn", + "desc": "Bare bokstaver, tall, punktum og understreker tillatt." + }, + "password": { + "title": "Passord", + "placeholder": "Skriv inn passord", + "confirm": { + "placeholder": "Bekreft passord", + "title": "Bekreft passord" + }, + "strength": { + "title": "Passordstyrke: ", + "veryStrong": "Veldig sterkt", + "weak": "Svakt", + "medium": "Medium", + "strong": "Sterkt" + }, + "match": "Passordene samsvarer", + "notMatch": "Passordene samsvarer ikke", + "show": "Vis passord", + "hide": "Skjul passord", + "requirements": { + "title": "Passordkrav:", + "length": "Minst 8 tegn", + "uppercase": "Minst en stor bokstav", + "digit": "Minst ett tall", + "special": "Minst ett spesialtegn (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nytt passord", + "placeholder": "Skriv inn nytt passord", + "confirm": { + "placeholder": "Skriv inn nytt passord igjen" + } + }, + "usernameIsRequired": "Brukernavn er påkrevd", + "passwordIsRequired": "Passord er påkrevd", + "currentPassword": { + "title": "Nåværende passord", + "placeholder": "Skriv inn nåværende passord" + } + }, + "changeRole": { + "desc": "Oppdater tillatelser for {{username}}", + "title": "Endre brukerrolle", + "roleInfo": { + "intro": "Velg en passende rolle for denne bruker:", + "admin": "Administrator", + "adminDesc": "Full tilgang til alle funksjoner.", + "viewer": "Visningsbruker", + "viewerDesc": "Begrenset til kun Direkte-dashbord, Inspiser, Utforsk og Eksporter.", + "customDesc": "Tilpasset rolle med spesifikk kameratilgang." + }, + "select": "Velg en rolle" + }, + "createUser": { + "title": "Opprett ny bruker", + "desc": "Legg til en ny brukerkonto og spesifiser en rolle for tilgang til Frigate-brukergrensesnittet.", + "usernameOnlyInclude": "Brukernavn kan bare inneholde bokstaver, tall, punktum eller _", + "confirmPassword": "Vennligst bekreft ditt passord" + }, + "deleteUser": { + "title": "Slett bruker", + "desc": "Denne handlingen kan ikke angres. Dette vil permanent slette brukerkontoen og fjerne alle tilknyttede data.", + "warn": "Er du sikker på at du vil slette {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Oppdater passord for {{username}}", + "setPassword": "Angi passord", + "desc": "Opprett et sterkt passord for å sikre denne kontoen.", + "cannotBeEmpty": "Passordet kan ikke være tomt", + "doNotMatch": "Passordene samsvarer ikke", + "currentPasswordRequired": "Nåværende passord er påkrevd", + "incorrectCurrentPassword": "Nåværende passord er feil", + "passwordVerificationFailed": "Kunne ikke verifisere passord", + "multiDeviceWarning": "Andre enheter du er logget inn på vil kreve ny innlogging innen {{refresh_time}}.", + "multiDeviceAdmin": "Du kan også tvinge alle brukere til å logge inn på nytt umiddelbart ved å rotere JWT-hemmeligheten din." + } + }, + "table": { + "username": "Brukernavn", + "actions": "Handlinger", + "role": "Rolle", + "changeRole": "Endre brukerrolle", + "password": "Passord", + "deleteUser": "Slett bruker", + "noUsers": "Ingen brukere funnet." + } + }, + "notification": { + "notificationSettings": { + "desc": "Frigate kan sende push-varsler til enheten din når den kjører i nettleseren eller er installert som en progressiv webapplikasjon (PWA).", + "documentation": "Se dokumentasjonen", + "title": "Innstillinger for meldingsvarsler" + }, + "notificationUnavailable": { + "documentation": "Se dokumentasjonen", + "title": "Meldingsvarsler utilgjengelig", + "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler." + }, + "email": { + "title": "E-post", + "placeholder": "f.eks. eksempel@email.com", + "desc": "En gyldig e-postadresse kreves og vil bli brukt til å varsle deg om det skulle oppstå problemer med push-tjenesten." + }, + "cameras": { + "title": "Kameraer", + "noCameras": "Ingen kameraer tilgjengelig", + "desc": "Velg hvilke kameraer meldingsvarsler skal aktiveres for." + }, + "deviceSpecific": "Enhetsspesifikke innstillinger", + "registerDevice": "Registrer denne enheten", + "unregisterDevice": "Fjern registrering av enheten", + "suspendTime": { + "5minutes": "Suspender i 5 minutter", + "10minutes": "Suspender i 10 minutter", + "30minutes": "Suspender i 30 minutter", + "1hour": "Suspender i 1 time", + "12hours": "Suspender i 12 timer", + "24hours": "Suspender i 24 timer", + "untilRestart": "Suspender til omstart", + "suspend": "Suspender" + }, + "suspended": "Meldingsvarsler suspendert {{time}}", + "toast": { + "success": { + "registered": "Registrering for meldingsvarsler var vellykket. En omstart av Frigate er nødvendig før noen meldingsvarsler (inkludert et testvarsel) kan sendes.", + "settingSaved": "Innstillinger for meldingsvarsler er lagret." + }, + "error": { + "registerFailed": "Kunne ikke lagre registrering for meldingsvarsler." + } + }, + "globalSettings": { + "title": "Globale innstillinger", + "desc": "Midlertidig suspender meldingsvarsler for spesifikke kameraer på alle registrerte enheter." + }, + "cancelSuspension": "Avbryt suspensjon", + "title": "Meldingsvarsler", + "sendTestNotification": "Send en meldingsvarsel for test", + "active": "Meldingsvarsler aktivert", + "unsavedChanges": "Ulagrede endringer for meldingsvarsler", + "unsavedRegistrations": "Ulagrede registreringer for meldingsvarsler" + }, + "frigatePlus": { + "apiKey": { + "notValidated": "Frigate+ API-nøkkel er ikke detektert eller validert", + "title": "Frigate+ API-nøkkel", + "validated": "Frigate+ API-nøkkel er detektert og validert", + "desc": "Frigate+ API-nøkkelen muliggjør integrasjon med Frigate+ tjenesten.", + "plusLink": "Les mer om Frigate+" + }, + "modelInfo": { + "trainDate": "Treningsdato", + "baseModel": "Basismodell", + "loading": "Laster modellinformasjon…", + "error": "Kunne ikke laste modellinformasjon", + "loadingAvailableModels": "Laster tilgjengelige modeller…", + "title": "Modellinformasjon", + "modelType": "Modelltype", + "supportedDetectors": "Støttede detektorer", + "dimensions": "Dimensjoner", + "cameras": "Kameraer", + "availableModels": "Tilgjengelige modeller", + "modelSelect": "Dine tilgjengelige modeller på Frigate+ kan velges her. Merk at bare modeller som er kompatible med din nåværende detektorkonfigurasjon kan velges.", + "plusModelType": { + "userModel": "Finjustert", + "baseModel": "Basismodell" + } + }, + "title": "Frigate+ Innstillinger", + "snapshotConfig": { + "title": "Konfigurasjon av stillbilde", + "desc": "Innsending til Frigate+ krever at både stillbilder og clean_copy-stillbilder er aktivert i konfigurasjonen din.", + "documentation": "Se dokumentasjonen", + "table": { + "camera": "Kamera", + "snapshots": "Stillbilder", + "cleanCopySnapshots": "clean_copy-stillbilder" + }, + "cleanCopyWarning": "Noen kameraer har stillbilder aktivert, men ren kopi er deaktivert. Du må aktivere clean_copy i stillbilde-konfigurasjonen for å kunne sende bilder fra disse kameraene til Frigate+." + }, + "toast": { + "success": "Frigate+ innstillingene er lagret. Start Frigate på nytt for å aktivere endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + }, + "restart_required": "Omstart påkrevd (Frigate+ modell endret)", + "unsavedChanges": "Ulagrede endringer for Frigate+ innstillinger" + }, + "enrichments": { + "title": "Innstillinger for utvidelser", + "licensePlateRecognition": { + "desc": "Frigate kan gjenkjenne kjennemerker på kjøretøy og automatisk legge til de oppdagede tegnene i feltet \"recognized_license_plate\", eller et kjent navn som en underetikett på objekter av typen bil. Et vanlig brukstilfelle kan være å lese kjennemerker på biler som kjører inn i en innkjørsel eller biler som passerer på en gate.", + "title": "Kjennemerke gjenkjenning", + "readTheDocumentation": "Se dokumentasjonen" + }, + "birdClassification": { + "desc": "Fugleklassifisering identifiserer kjente fugler ved hjelp av en kvantisert TensorFlow-modell. Når en fugl gjenkjennes, vil det vanlige navnet legges til som en underetikett. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "title": "Klassifisering av fugler" + }, + "semanticSearch": { + "reindexNow": { + "desc": "Reindeksering vil regenerere vektorrepresentasjoner for alle sporede objekter. Denne prosessen kjøres i bakgrunnen og kan maksimere CPU-belastningen, samt ta en del tid avhengig av hvor mange sporede objekter du har.", + "confirmButton": "Reindekser", + "confirmTitle": "Bekreft reindeksering", + "confirmDesc": "Er du sikker på at du vil reindeksere alle vektorrepresentasjoner for sporede objekter? Denne prosessen vil kjøre i bakgrunnen, men den kan maksimere CPU-belastningen og ta en del tid. Du kan følge fremdriften på Utforsk-siden.", + "label": "Reindekser nå", + "success": "Reindeksering ble startet.", + "alreadyInProgress": "Reindeksering pågar allerede.", + "error": "Kunne ikke starte reindeksering: {{errorMessage}}" + }, + "modelSize": { + "small": { + "desc": "Ved å bruke liten benyttes en kvantisert versjon av modellen som bruker mindre RAM og kjører raskere på CPU, med en svært ubetydelig forskjell i kvalitet på vektorrepresentasjoner.", + "title": "liten" + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt til vektorrepresentasjoner for semantisk søk.", + "large": { + "title": "stor", + "desc": "Ved å bruke stor benyttes den fullstendige Jina-modellen, og den vil automatisk kjøres på GPU dersom tilgjengelig." + } + }, + "title": "Semantisk søk", + "desc": "Semantisk søk i Frigate lar deg finne sporede objekter i inspeksjonsselementene dine ved å bruke enten selve bildet, en brukerdefinert tekstbeskrivelse eller en automatisk generert beskrivelse.", + "readTheDocumentation": "Se dokumentasjonen" + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "liten", + "desc": "Ved å bruke liten benyttes en FaceNet-modell for vektorrepresentasjoner for ansikt som kjører effektivt på de fleste CPU-er." + }, + "label": "Modellstørrelse", + "desc": "Størrelsen på modellen brukt til ansiktsgjenkjenning.", + "large": { + "title": "stor", + "desc": "Ved å bruke stor benyttes en ArcFace-modell for vektorrepresentasjoner for ansikt, og den vil automatisk kjøres på GPU dersom tilgjengelig." + } + }, + "title": "Ansiktsgjenkjenning", + "desc": "Ansiktsgjenkjenning gjør det mulig å tildele navn til personer, og når ansiktet deres gjenkjennes, vil Frigate tildele personens navn som en underetikett. Denne informasjonen vises i brukergrensesnittet, filtre, samt i meldingsvarsler.", + "readTheDocumentation": "Se dokumentasjonen" + }, + "unsavedChanges": "Ulagrede endringer i innstillinger for utvidelser", + "restart_required": "Omstart påkrevd (Innstillinger for utvidelser er endret)", + "toast": { + "success": "Innstillinger for utvidelser har blitt lagret. Start Frigate på nytt for å aktivere endringene.", + "error": "Kunne ikke lagre konfigurasjonsendringer: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Utløsere", + "management": { + "title": "Utløser", + "desc": "Administrer utløsere for {{camera}}. Bruk miniatyrbilde-type for å utløse på lignende miniatyrbilder som det sporede objektet du har valgt, og beskrivelsestype for å utløse på lignende beskrivelser basert på teksten du spesifiserer." + }, + "addTrigger": "Legg til utløser", + "table": { + "name": "Navn", + "type": "Type", + "content": "Innhold", + "threshold": "Terskel", + "actions": "Handlinger", + "noTriggers": "Ingen utløsere er konfigurert for dette kameraet.", + "edit": "Rediger", + "deleteTrigger": "Slett utløser", + "lastTriggered": "Sist utløst" + }, + "type": { + "thumbnail": "Miniatyrbilde", + "description": "Beskrivelse" + }, + "actions": { + "alert": "Marker som varsel", + "notification": "Send meldingsvarsel", + "sub_label": "Legg til underetikett", + "attribute": "Legg til attributt" + }, + "dialog": { + "createTrigger": { + "title": "Opprett utløser", + "desc": "Opprett en utløser for kamera {{camera}}" + }, + "editTrigger": { + "title": "Rediger utløser", + "desc": "Rediger innstillingene for utløser på kamera {{camera}}" + }, + "deleteTrigger": { + "title": "Slett utløser", + "desc": "Er du sikker på at du vil slette utløseren {{triggerName}}? Denne handlingen kan ikke angres." + }, + "form": { + "name": { + "title": "Navn", + "placeholder": "Navngi denne utløseren", + "error": { + "minLength": "Feltet må være minst 2 tegn langt.", + "invalidCharacters": "Feltet kan bare inneholde bokstaver, tall, understreker og bindestreker.", + "alreadyExists": "En utløser med dette navnet finnes allerede for dette kameraet." + }, + "description": "Skriv inn et unikt navn eller beskrivelse for å identifisere denne utløseren" + }, + "enabled": { + "description": "Aktiver eller deaktiver denne utløseren" + }, + "type": { + "title": "Type", + "placeholder": "Velg utløsertype", + "description": "Utløs når en lignende sporet objektbeskrivelse blir detektert", + "thumbnail": "Utløs når et lignende sporet miniatyrbilde blir detektert" + }, + "content": { + "title": "Innhold", + "imagePlaceholder": "Velg et miniatyrbilde", + "textPlaceholder": "Skriv inn tekstinnhold", + "imageDesc": "Kun de siste 100 miniatyrbildene vises. Hvis du ikke finner ønsket miniatyrbilde, kan du se gjennom tidligere objekter i Utforsk og opprette en utløser fra menyen der.", + "textDesc": "Skriv inn tekst for å utløse denne handlingen når en lignende beskrivelse av et sporet objekt oppdages.", + "error": { + "required": "Innhold er påkrevd." + } + }, + "threshold": { + "title": "Terskel", + "error": { + "min": "Terskelverdien må minst være 0", + "max": "Terskelverdien kan maksimum være 1" + }, + "desc": "Angi likhetsgrensen for denne utløseren. En høyere grense betyr at et høyere samsvar kreves for å utløse hendelsen." + }, + "actions": { + "title": "Handlinger", + "desc": "Som standard sender Frigate en MQTT-melding for alle utløsere. Underetiketter legger til navnet på utløseren i objektetiketten. Attributter er søkbare metadata som lagres separat i objektets sporingsmetadata.", + "error": { + "min": "Minst én handling må velges." + } + }, + "friendly_name": { + "description": "Et valgfritt brukervennlig navn eller beskrivende tekst for denne utløseren.", + "title": "Brukervennlig navn", + "placeholder": "Navngi eller beskriv denne utløseren" + } + } + }, + "toast": { + "success": { + "createTrigger": "Utløseren {{name}} ble opprettet.", + "updateTrigger": "Utløseren {{name}} ble oppdatert.", + "deleteTrigger": "Utløseren {{name}} ble slettet." + }, + "error": { + "createTriggerFailed": "Kunne ikke opprette utløser: {{errorMessage}}", + "updateTriggerFailed": "Kunne ikke oppdatere utløser: {{errorMessage}}", + "deleteTriggerFailed": "Kunne ikke slette utløser: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisk søk er deaktivert", + "desc": "Semantisk søk må aktiveres for å bruke utløsere." + }, + "wizard": { + "title": "Opprett utløser", + "step1": { + "description": "Konfigurer grunnleggende innstillinger for utløseren." + }, + "step2": { + "description": "Sett opp innholdet som skal utløse denne handlingen." + }, + "step3": { + "description": "Konfigurer terskelen og handlingene for denne utløseren." + }, + "steps": { + "nameAndType": "Navn og type", + "configureData": "Konfigurer data", + "thresholdAndActions": "Terskel og handlinger" + } + } + }, + "roles": { + "management": { + "title": "Administrasjon av visningsrolle", + "desc": "Administrer tilpassede visningsroller og deres kameratilgangstillatelser for denne Frigate-instansen." + }, + "addRole": "Legg til rolle", + "table": { + "role": "Rolle", + "cameras": "Kameraer", + "actions": "Handlinger", + "noRoles": "Ingen tilpassede roller funnet.", + "editCameras": "Rediger kameraer", + "deleteRole": "Slett rolle" + }, + "toast": { + "success": { + "createRole": "Rollen {{role}} ble opprettet", + "updateCameras": "Kameraer oppdatert for rollen {{role}}", + "deleteRole": "Rollen {{role}} ble slettet", + "userRolesUpdated_one": "{{count}} bruker tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer.", + "userRolesUpdated_other": "{{count}} brukere tildelt denne rollen er oppdatert til \"visningsbruker\", som har tilgang til alle kameraer." + }, + "error": { + "createRoleFailed": "Kunne ikke opprette rolle: {{errorMessage}}", + "updateCamerasFailed": "Kunne ikke oppdatere kameraer: {{errorMessage}}", + "deleteRoleFailed": "Kunne ikke slette rolle: {{errorMessage}}", + "userUpdateFailed": "Kunne ikke oppdatere brukerroller: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Opprett ny rolle", + "desc": "Legg til en ny rolle og angi tillatelser for kameratilgang." + }, + "editCameras": { + "desc": "Oppdater kameratilgang for rollen {{role}}.", + "title": "Rediger kameraer for rolle" + }, + "deleteRole": { + "title": "Slett rolle", + "desc": "Denne handlingen kan ikke angres. Dette vil permanent slette rollen og tildele alle brukere med denne rollen til «visningsbruker»-rollen, som gir tilgang til alle kameraer.", + "warn": "Er du sikker på at du vil slette {{role}}?", + "deleting": "Sletter..." + }, + "form": { + "role": { + "title": "Rollenavn", + "placeholder": "Skriv inn rollenavn", + "desc": "Kun bokstaver, tall, punktum og understreker er tillatt.", + "roleIsRequired": "Rollenavn er påkrevd", + "roleOnlyInclude": "Rollenavn kan kun inneholde bokstaver, tall, . eller _", + "roleExists": "En rolle med dette navnet finnes allerede." + }, + "cameras": { + "title": "Kameraer", + "desc": "Velg hvilke kameraer denne rollen skal ha tilgang til. Minst ett kamera må velges.", + "required": "Minst ett kamera må velges." + } + } + } + }, + "cameraWizard": { + "title": "Legg til kamera", + "description": "Følg trinnene nedenfor for å legge til et nytt kamera i din Frigate-installasjon.", + "steps": { + "nameAndConnection": "Navn og tilkobling", + "streamConfiguration": "Strømkonfigurasjon", + "validationAndTesting": "Validering og testing", + "probeOrSnapshot": "Sjekk eller stillbilde" + }, + "save": { + "success": "Lagret nytt kamera {{cameraName}}.", + "failure": "Feil ved lagring av {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Oppløsning", + "video": "Video", + "audio": "Lyd", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Vennligst oppgi en gyldig strøm-URL", + "testFailed": "Strømmetest feilet: {{error}}" + }, + "step1": { + "description": "Skriv inn kameradetaljene dine og velg å sjekke kameraet eller manuelt velg produsent.", + "cameraName": "Kameranavn", + "cameraNamePlaceholder": "f.eks. front_dor eller Hageoversikt", + "host": "Vert/IP-adresse", + "port": "Port", + "username": "Brukernavn", + "usernamePlaceholder": "Valgfritt", + "password": "Passord", + "passwordPlaceholder": "Valgfritt", + "selectTransport": "Velg transportprotokoll", + "cameraBrand": "Kameramerke", + "selectBrand": "Velg kameramerke for URL-mal", + "customUrl": "Egendefinert strømme-URL", + "brandInformation": "Merkevare-informasjon", + "brandUrlFormat": "For kameraer med RTSP URL-format som: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "testConnection": "Test tilkobling", + "testSuccess": "Tilkoblingstesten var vellykket!", + "testFailed": "Tilkoblingstesten feilet. Vennligst sjekk inntastingen din og prøv igjen.", + "streamDetails": "Strømdetaljer", + "warnings": { + "noSnapshot": "Kunne ikke hente et øyeblikksbilde fra den konfigurerte strømmen." + }, + "errors": { + "brandOrCustomUrlRequired": "Enten velg et kameramerke med vert/IP eller velg 'Annet' med en egendefinert URL", + "nameRequired": "Kameranavn er påkrevd", + "nameLength": "Kameranavnet må være 64 tegn eller mindre", + "invalidCharacters": "Kameranavnet inneholder ugyldige tegn", + "nameExists": "Kameranavnet finnes allerede", + "brands": { + "reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvare-innstillinger og start kameraveiviseren på nytt." + }, + "customUrlRtspRequired": "Egendefinerte URL-er må begynne med \"rtsp://\". Manuell konfigurering kreves for kamera­strømmer som ikke bruker RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Henter kamerametadata…", + "fetchingSnapshot": "Henter øyeblikksbilde for kamera..." + }, + "connectionSettings": "Tilkoblingsinnstillinger", + "detectionMethod": "Metode for strømdeteksjon", + "onvifPort": "ONVIF-port", + "probeMode": "Sjekk kamera", + "manualMode": "Manuelt valg", + "detectionMethodDescription": "Sjekk kameraet med ONVIF (hvis støttet) for å finne kameraets strømme-URL-er, eller velg kameramerke manuelt for å bruke forhåndsdefinerte URL-er. For å skrive inn en egendefinert RTSP URL, velg manuell metode og velg \"Annet\".", + "onvifPortDescription": "For kameraer som støtter ONVIF, er dette vanligvis 80 eller 8080.", + "useDigestAuth": "Bruk digest-autentisering", + "useDigestAuthDescription": "Bruk HTTP digest-autentisering for ONVIF. Noen kameraer kan kreve et dedikert ONVIF brukernavn/passord i stedet for standard administrator-bruker." + }, + "step2": { + "description": "Sjekk kameraet for tilgjengelige strømmer eller konfigurer manuelle innstillinger basert på valgt deteksjonsmetode.", + "streamsTitle": "Kamerastrømmer", + "addStream": "Legg til strøm", + "addAnotherStream": "Legg til en annen strøm", + "streamTitle": "Strøm {{number}}", + "streamUrl": "Strøm-URL", + "streamUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "url": "URL", + "resolution": "Oppløsning", + "selectResolution": "Velg oppløsning", + "quality": "Kvalitet", + "selectQuality": "Velg kvalitet", + "roles": "Roller", + "roleLabels": { + "detect": "Objektdeteksjon", + "record": "Opptak", + "audio": "Lyd" + }, + "testStream": "Test tilkobling", + "testSuccess": "Tilkoblingstest vellykket!", + "testFailed": "Tilkoblingstest feilet. Vennligst sjekk inndataene dine og prøv igjen.", + "testFailedTitle": "Test feilet", + "connected": "Tilkoblet", + "notConnected": "Ikke tilkoblet", + "featuresTitle": "Funksjoner", + "go2rtc": "Reduser tilkoblinger til kameraet", + "detectRoleWarning": "Minst én strøm må ha rollen «deteksjon» for å fortsette.", + "rolesPopover": { + "title": "Strømroller", + "detect": "Hovedstrøm for objektdeteksjon.", + "record": "Lagrer segmenter av videostrømmen basert på konfigurasjonsinnstillinger.", + "audio": "Strøm for lydbasert deteksjon." + }, + "featuresPopover": { + "title": "Strømfunksjoner", + "description": "Bruk go2rtc-restrømming for å redusere antall tilkoblinger til kameraet ditt." + }, + "streamDetails": "Strømdetaljer", + "probing": "Test kamera...", + "retry": "Prøv på nytt", + "testing": { + "probingMetadata": "Sjekker metadata for kamera...", + "fetchingSnapshot": "Henter stillbilde fra kamera..." + }, + "probeFailed": "Kunne ikke å sjekke kamera: {{error}}", + "probingDevice": "Tester enhet...", + "probeSuccessful": "Sjekk vellykket", + "probeError": "Sjekk feilet", + "probeNoSuccess": "Sjekk mislyktes", + "deviceInfo": "Enhetsinformasjon", + "manufacturer": "Produsent", + "model": "Modell", + "firmware": "Fastvare", + "profiles": "Profiler", + "ptzSupport": "PTZ-støtte", + "autotrackingSupport": "Autosporing-støtte", + "presets": "Forhåndsinnstillinger", + "rtspCandidates": "RTSP-kandidater", + "rtspCandidatesDescription": "Følgende RTSP URL-er ble funnet ved sjekk av kameraet. Test tilkoblingen for å se strømmetadata.", + "noRtspCandidates": "Ingen RTSP URL-er ble funnet fra kameraet. Det kan hende at påloggingsinformasjonen din er feil, eller at kameraet ikke støtter ONVIF eller metoden som brukes for å hente RTSP URL-er. Gå tilbake og skriv inn RTSP URL-en manuelt.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Bruk", + "uriCopy": "Kopier", + "uriCopied": "URI kopiert til utklippstavlen", + "testConnection": "Test tilkobling", + "toggleUriView": "Klikk for å vise/skjule full URI", + "errors": { + "hostRequired": "Vert/IP-adresse er påkrevd" + } + }, + "step3": { + "description": "Konfigurer strømroller og legg til flere strømmer for kameraet ditt.", + "validationTitle": "Strømvalidering", + "connectAllStreams": "Koble til alle strømmer", + "reconnectionSuccess": "Gjenoppkobling vellykket.", + "reconnectionPartial": "Noen strømmer kunne ikke gjenoppkobles.", + "streamUnavailable": "Forhåndsvisning av strøm utilgjengelig", + "reload": "Last inn på nytt", + "connecting": "Kobler til...", + "streamTitle": "Strøm {{number}}", + "valid": "Gyldig", + "failed": "Feilet", + "notTested": "Ikke testet", + "connectStream": "Koble til", + "connectingStream": "Kobler til", + "disconnectStream": "Koble fra", + "estimatedBandwidth": "Estimert båndbredde", + "roles": "Roller", + "none": "Ingen", + "error": "Feil", + "streamValidated": "Strøm {{number}} ble validert", + "streamValidationFailed": "Validering av strøm {{number}} feilet", + "saveAndApply": "Lagre nytt kamera", + "saveError": "Ugyldig konfigurasjon. Vennligst sjekk innstillingene dine.", + "issues": { + "title": "Strømvalidering", + "videoCodecGood": "Video-kodek er {{codec}}.", + "audioCodecGood": "Lyd-kodek er {{codec}}.", + "noAudioWarning": "Ingen lyd oppdaget for denne strømmen, opptak vil ikke ha lyd.", + "audioCodecRecordError": "AAC lyd-kodek er påkrevd for å støtte lyd i opptak.", + "audioCodecRequired": "En lydstrøm er påkrevd for å støtte lyddeteksjon.", + "restreamingWarning": "Å redusere tilkoblinger til kameraet for opptaksstrømmen kan øke CPU-bruken noe.", + "dahua": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Dahua / Amcrest / EmpireTech-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "hikvision": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "resolutionHigh": "En oppløsning på {{resolution}} kan føre til økt ressursbruk.", + "resolutionLow": "En oppløsning på {{resolution}} kan være for lav for pålitelig deteksjon av små objekter." + }, + "ffmpegModuleDescription": "Hvis strømmen ikke lastes inn etter flere forsøk, kan du prøve å aktivere dette. Når det er aktivert, vil Frigate bruke ffmpeg-modulen sammen med go2rtc. Dette kan gi bedre kompatibilitet med enkelte kamerastrømmer.", + "ffmpegModule": "Bruk kompatibilitetsmodus for strøm", + "streamsTitle": "Kamerastrømmer", + "addStream": "Legg til strøm", + "addAnotherStream": "Legg til en annen strøm", + "streamUrl": "Strøm-URL", + "streamUrlPlaceholder": "rtsp://brukernavn:passord@vert:port/sti", + "selectStream": "Velg en strøm", + "searchCandidates": "Søk blant kandidater...", + "noStreamFound": "Ingen strøm funnet", + "url": "URL", + "resolution": "Oppløsning", + "selectResolution": "Velg oppløsning", + "quality": "Kvalitet", + "selectQuality": "Velg kvalitet", + "roleLabels": { + "detect": "Objektdeteksjon", + "record": "Opptak", + "audio": "Lyd" + }, + "testStream": "Test tilkobling", + "testSuccess": "Strømmetest vellykket!", + "testFailed": "Strømmetest feilet", + "testFailedTitle": "Test feilet", + "connected": "Tilkoblet", + "notConnected": "Ikke tilkoblet", + "featuresTitle": "Funksjoner", + "go2rtc": "Reduser antall tilkoblinger til kamera", + "detectRoleWarning": "Minst én strøm må ha \"deteksjon\"-rollen for å fortsette.", + "rolesPopover": { + "title": "Strømroller", + "detect": "Hovedstrøm for objektdeteksjon.", + "record": "Lagrer segmenter av videostrømmen basert på konfigurasjonsinnstillinger.", + "audio": "Strøm for lydbasert deteksjon." + }, + "featuresPopover": { + "title": "Strømfunksjoner", + "description": "Bruk go2rtc-restrømming for å redusere antall tilkoblinger til kameraet." + } + }, + "step4": { + "description": "Endelig validering og analyse før du lagrer det nye kameraet. Koble til hver strøm før du lagrer.", + "validationTitle": "Strømvalidering", + "connectAllStreams": "Koble til alle strømmer", + "reconnectionSuccess": "Tilkobling vellykket.", + "reconnectionPartial": "Noen strømmer kunne ikke koble til på nytt.", + "streamUnavailable": "Forhåndsvisning av strøm utilgjengelig", + "reload": "Last på nytt", + "connecting": "Kobler til...", + "streamTitle": "Strøm {{number}}", + "valid": "Gyldig", + "failed": "Feilet", + "notTested": "Ikke testet", + "connectStream": "Koble til", + "connectingStream": "Kobler til", + "disconnectStream": "Koble fra", + "estimatedBandwidth": "Estimert båndbredde", + "roles": "Roller", + "ffmpegModule": "Bruk kompatibilitetsmodus for strøm", + "ffmpegModuleDescription": "Hvis strømmen ikke lastes etter flere forsøk, prøv å aktivere dette. Når aktivert, vil Frigate bruke ffmpeg-modulen med go2rtc. Dette kan gi bedre kompatibilitet med noen kamerastrømmer.", + "none": "Ingen", + "error": "Feil", + "streamValidated": "Strøm {{number}} validert", + "streamValidationFailed": "Validering av strøm {{number}} feilet", + "saveAndApply": "Lagre nytt kamera", + "saveError": "Ugyldig konfigurasjon. Vennligst sjekk innstillingene dine.", + "issues": { + "title": "Strømvalidering", + "videoCodecGood": "Videokodek er {{codec}}.", + "audioCodecGood": "Lydkodek er {{codec}}.", + "resolutionHigh": "En oppløsning på {{resolution}} kan føre til økt ressursbruk.", + "resolutionLow": "En oppløsning på {{resolution}} kan være for lav for pålitelig deteksjon av små objekter.", + "noAudioWarning": "Ingen lyd oppdaget for denne strømmen, opptak vil ikke ha lyd.", + "audioCodecRecordError": "AAC-lydkodeken kreves for å støtte lyd i opptak.", + "audioCodecRequired": "En lydstrøm kreves for å støtte lyddeteksjon.", + "restreamingWarning": "Å redusere tilkoblinger til kameraet for opptaksstrømmen kan øke CPU-bruken noe.", + "brands": { + "reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvareinnstillinger og start veiviseren på nytt.", + "reolink-http": "Reolink HTTP-strømmer bør bruke FFmpeg for bedre kompatibilitet. Aktiver 'Bruk kompatibilitetsmodus for strøm' for denne strømmen." + }, + "dahua": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Dahua / Amcrest / EmpireTech-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + }, + "hikvision": { + "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." + } + } + } + }, + "cameraManagement": { + "title": "Administrer kameraer", + "addCamera": "Legg til nytt kamera", + "editCamera": "Rediger kamera:", + "selectCamera": "Velg et kamera", + "backToSettings": "Tilbake til kamerainnstillinger", + "streams": { + "title": "Aktiver / Deaktiver kameraer", + "desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke go2rtc-restrømming." + }, + "cameraConfig": { + "add": "Legg til kamera", + "edit": "Rediger kamera", + "description": "Konfigurer kamerainnstillinger, inkludert strømmeinnganger og roller.", + "name": "Kameranavn", + "nameRequired": "Kameranavn er påkrevd", + "nameLength": "Kameranavnet må være mindre enn 64 tegn.", + "namePlaceholder": "f.eks front_dor eller Hage Oversikt", + "enabled": "Aktivert", + "ffmpeg": { + "inputs": "Inngangsstrømmer", + "path": "Lenke til strøm", + "pathRequired": "Lenke til strøm er påkrevd", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst én rolle er påkrevd", + "rolesUnique": "Hver rolle (lyd, deteksjon, opptak) kan bare tildeles én strøm", + "addInput": "Legg til inngangsstrøm", + "removeInput": "Fjern inngangsstrøm", + "inputsRequired": "Minst én inngangsstrøm er påkrevd" + }, + "go2rtcStreams": "go2rtc-strømmer", + "streamUrls": "Strøm-URL'er", + "addUrl": "Legg til URL", + "addGo2rtcStream": "Legg til go2rtc-strøm", + "toast": { + "success": "Kamera {{cameraName}} ble lagret" + } + } + }, + "cameraReview": { + "title": "Innstillinger for kamerainspeksjon", + "object_descriptions": { + "title": "Generative KI-objektbeskrivelser", + "desc": "Midlertidig aktiver/deaktiver generative KI-objektbeskrivelser for dette kameraet. Når deaktivert, vil KI-genererte beskrivelser ikke bli forespurt for sporede objekter på dette kameraet." + }, + "review_descriptions": { + "title": "Generative KI beskrivelser for inspeksjon", + "desc": "Midlertidig aktiver/deaktiver inspeksjonsbeskrivelser med generativ KI for dette kameraet. Når deaktivert, vil det ikke bli bedt om KI-genererte beskrivelser for inspeksjonselementer på dette kameraet." + }, + "review": { + "title": "Inspiser", + "desc": "Aktiver/deaktiver varsler og deteksjoner midlertidig for dette kameraet til Frigate startes på nytt. Når deaktivert, vil det ikke genereres nye inspeksjonselementer. ", + "alerts": "Varsler ", + "detections": "Deteksjoner " + }, + "reviewClassification": { + "title": "Inspeksjonsklassifisering", + "desc": "Frigate kategoriserer inspeksjonselementer som Varsler og Deteksjoner. Som standard regnes alle person- og bil-objekter som Varsler. Du kan finjustere klassifiseringen ved å konfigurere nødvendige soner.", + "noDefinedZones": "Ingen soner er definert for dette kameraet.", + "objectAlertsTips": "Alle {{alertsLabels}}-objekter på {{cameraName}} vil bli vist som varsler.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} vil bli vist som varsler.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vil bli vist som deteksjoner uavhengig av hvilken sone de er i.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert i {{zone}} på {{cameraName}}, vil bli vist som deteksjoner.", + "notSelectDetections": "Alle {{detectionsLabels}}-objekter oppdaget i {{zone}} på {{cameraName}} som ikke er kategorisert som varsler, vil bli vist som deteksjoner uavhengig av hvilken sone de er i.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objekter som ikke er kategorisert på {{cameraName}}, vil bli vist som deteksjoner uavhengig av hvilken sone de er i." + }, + "unsavedChanges": "Ulagrede innstillinger for inspeksjonsklassifisering for {{camera}}", + "selectAlertsZones": "Velg soner for varsler", + "selectDetectionsZones": "Velg soner for deteksjoner", + "limitDetections": "Avgrens deteksjoner til bestemte soner", + "toast": { + "success": "Konfigurasjonen for inspeksjonsklassifisering er lagret. Start Frigate på nytt for å aktivere endringer." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/system.json new file mode 100644 index 0000000..8aaca62 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nb-NO/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "cameras": "Kamerastatistikk - Frigate", + "storage": "Lagringsstatistikk - Frigate", + "logs": { + "frigate": "Frigate-logger - Frigate", + "go2rtc": "Go2RTC-logger - Frigate", + "nginx": "Nginx-logger - Frigate" + }, + "general": "Generell statistikk - Frigate", + "enrichments": "Statistikk for utvidelser - Frigate" + }, + "logs": { + "copy": { + "success": "Logger kopiert til utklippstavlen", + "error": "Kunne ikke kopiere logger til utklippstavlen", + "label": "Kopier til utklippstavle" + }, + "type": { + "label": "Type", + "timestamp": "Tidsstempel", + "tag": "Merke", + "message": "Melding" + }, + "toast": { + "error": { + "fetchingLogsFailed": "Feil ved henting av logger: {{errorMessage}}", + "whileStreamingLogs": "Feil under strømming av logger: {{errorMessage}}" + } + }, + "download": { + "label": "Last ned logger" + }, + "tips": "Logger strømmer fra serveren" + }, + "general": { + "title": "Generelt", + "detector": { + "inferenceSpeed": "Detektor inferenshastighet", + "title": "Detektorer", + "cpuUsage": "Detektor CPU-belastning", + "memoryUsage": "Detektor minnebruk", + "temperature": "Detektor temperatur", + "cpuUsageInformation": "CPU brukt til å forberede inn- og utdata til/fra deteksjonsmodeller. Denne verdien måler ikke bruk under inferens, selv om en GPU eller akselerator benyttes." + }, + "hardwareInfo": { + "gpuMemory": "GPU-minne", + "gpuEncoder": "GPU-enkoder", + "gpuDecoder": "GPU-dekoder", + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "CUDA beregningsevne: {{cuda_compute}}", + "vbios": "VBios-info: {{vbios}}", + "title": "Nvidia SMI-utdata", + "name": "Navn: {{name}}" + }, + "copyInfo": { + "label": "Kopier GPU-informasjon" + }, + "toast": { + "success": "GPU-informasjon kopiert til utklippstavlen" + }, + "vainfoOutput": { + "title": "Vainfo-utdata", + "returnCode": "Returkode: {{code}}", + "processOutput": "Prosessutdata:", + "processError": "Prosessfeil:" + }, + "closeInfo": { + "label": "Lukk GPU-informasjon" + } + }, + "title": "Maskinvareinformasjon", + "gpuUsage": "GPU-belastning", + "npuMemory": "NPU minne", + "npuUsage": "NPU-belastning", + "intelGpuWarning": { + "title": "Til info om Intel GPU-statistikk", + "message": "GPU statistikk ikke tilgjengelig", + "description": "Dette er en kjent feil i Intels verktøy for rapportering av GPU-statistikk (intel_gpu_top), der verktøyet slutter å fungere og gjentatte ganger viser 0 % GPU-bruk, selv om maskinvareakselerasjon og objektdeteksjon kjører korrekt på (i)GPU-en. Dette er ikke en feil i Frigate. Du kan starte verten på nytt for å løse problemet midlertidig, og for å bekrefte at GPU-en fungerer som den skal. Dette påvirker ikke ytelsen." + } + }, + "otherProcesses": { + "title": "Andre prosesser", + "processCpuUsage": "Prosessenes CPU-belastning", + "processMemoryUsage": "Prosessenes minnebruk" + } + }, + "storage": { + "overview": "Oversikt", + "recordings": { + "earliestRecording": "Tidligste opptak tilgjengelig:", + "title": "Opptak", + "tips": "Denne verdien representerer total lagringsplass brukt av opptakene i Frigates database. Frigate sporer ikke lagringsbruk for alle filer på disken din." + }, + "cameraStorage": { + "storageUsed": "Lagringsbruk", + "bandwidth": "Båndbredde", + "title": "Kameralagring", + "camera": "Kamera", + "unusedStorageInformation": "Ubrukt lagringsinformasjon", + "percentageOfTotalUsed": "Prosentandel av tilgjengelig", + "unused": { + "title": "Ubrukt", + "tips": "Denne verdien representerer kanskje ikke nøyaktig den ledige plassen Frigate har tilgang til, dersom det finnes andre filer lagret på disken. Frigate sporer kun lagring brukt av egne opptak." + } + }, + "title": "Lagring", + "shm": { + "title": "SHM (delt minne) allokering", + "warning": "Den nåværende SHM-størrelsen på {{total}} MB er for liten. Øk den til minst {{min_shm}} MB." + } + }, + "cameras": { + "info": { + "codec": "Kodek:", + "resolution": "Oppløsning:", + "audio": "Lyd:", + "error": "Feil: {{error}}", + "cameraProbeInfo": "{{camera}} - kamerainformasjon", + "streamDataFromFFPROBE": "Strømmedata er hentet med ffprobe.", + "fetching": "Henter kameradata", + "stream": "Strøm {{idx}}", + "video": "Video:", + "fps": "FPS:", + "unknown": "Ukjent", + "tips": { + "title": "Kamerainformasjon" + }, + "aspectRatio": "bildeforhold" + }, + "framesAndDetections": "Bilder / Deteksjoner", + "title": "Kameraer", + "overview": "Oversikt", + "label": { + "camera": "kamera", + "detect": "detektering", + "skipped": "forkastet", + "ffmpeg": "FFmpeg", + "capture": "opptak", + "cameraDetectionsPerSecond": "{{camName}} deteksjoner per sekund", + "cameraSkippedDetectionsPerSecond": "{{camName}} forkastede deteksjoner per sekund", + "cameraFramesPerSecond": "{{camName}} bilder per sekund", + "cameraCapture": "{{camName}} opptak", + "cameraDetect": "{{camName}} detekt", + "cameraFfmpeg": "{{camName}} FFmpeg", + "overallDetectionsPerSecond": "totale deteksjoner per sekund", + "overallSkippedDetectionsPerSecond": "totalt forkastede deteksjoner per sekund", + "overallFramesPerSecond": "totalt bilder per sekund" + }, + "toast": { + "success": { + "copyToClipboard": "Kameradata kopiert til utklippstavlen." + }, + "error": { + "unableToProbeCamera": "Kunne ikke hente informasjon fra kamera: {{errorMessage}}" + } + } + }, + "enrichments": { + "embeddings": { + "plate_recognition_speed": "Hastighet for kjennemerkegjenkjenning", + "face_embedding_speed": "Hastighet ansikt-vektorrepresentasjon", + "text_embedding_speed": "Hastighet tekst-vektorrepresentasjoner", + "image_embedding_speed": "Hastighet bilde-vektorrepresentasjoner", + "face_recognition_speed": "Hastighet for ansiktsgjenkjenning", + "image_embedding": "Bilde-vektorrepresentasjoner", + "face_recognition": "Ansiktsgjenkjenning", + "text_embedding": "Tekst-vektorrepresentasjoner", + "plate_recognition": "Kjennemerke gjenkjenning", + "yolov9_plate_detection_speed": "Hastighet for YOLOv9 kjennemerkedeteksjon", + "yolov9_plate_detection": "YOLOv9 kjennemerkedeteksjon", + "object_description": "Objektbeskrivelse", + "object_description_speed": "Objektbeskrivelse hastighet", + "object_description_events_per_second": "Objektbeskrivelse", + "review_description": "Inspeksjonsbeskrivelse", + "review_description_events_per_second": "Inspeksjonsbeskrivelse", + "review_description_speed": "Inspeksjonsbeskrivelse hastighet" + }, + "title": "Utvidelser", + "infPerSecond": "Inferenser per sekund", + "averageInf": "Gjennomsnittlig inferenstid" + }, + "title": "System", + "metrics": "Systemmålinger", + "lastRefreshed": "Sist oppdatert: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} har høy CPU-belastning for FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} har høy CPU-belastning for detektering ({{detectAvg}}%)", + "healthy": "Systemet fungerer som det skal", + "reindexingEmbeddings": "Reindeksering av vektorrepresentasjoner ({{processed}}% fullført)", + "cameraIsOffline": "{{camera}} er frakoblet", + "detectIsSlow": "{{detect}} er treg ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} er veldig treg ({{speed}} ms)", + "shmTooLow": "/dev/shm-allokeringen ({{total}} MB) bør økes til minst {{min}} MB." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/audio.json b/sam2-cpu/frigate-dev/web/public/locales/nl/audio.json new file mode 100644 index 0000000..59268c7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/audio.json @@ -0,0 +1,503 @@ +{ + "babbling": "Brabbelen", + "bellow": "Brullen", + "laughter": "Gelach", + "snicker": "Grinniken", + "crying": "Huilen", + "sigh": "Zucht", + "singing": "Zingen", + "choir": "Koor", + "yodeling": "Jodelen", + "mantra": "Mantra", + "child_singing": "Zingend kind", + "rapping": "Rappen", + "breathing": "Ademhaling", + "wheeze": "Piepen", + "snoring": "Snurken", + "gasp": "Snakken naar adem", + "pant": "Hijgen", + "snort": "Snuiven", + "sneeze": "Niezen", + "shuffle": "Schudden", + "footsteps": "Voetstappen", + "gargling": "Gorgelen", + "stomach_rumble": "Maag rommelt", + "burping": "Boeren", + "fart": "Scheet", + "finger_snapping": "Vingerknippen", + "applause": "Applaus", + "chatter": "Geklets", + "howl": "Huilkreet", + "bow_wow": "Woef woef", + "growling": "Grommen", + "whimper_dog": "Hondengejank", + "meow": "Miauw", + "hiss": "Sissen", + "livestock": "Vee", + "horse": "Paard", + "clip_clop": "Hoefslagen", + "neigh": "Hinniken", + "cattle": "Runderen", + "moo": "loeien", + "cowbell": "Koeienbel", + "pig": "Varkens", + "oink": "Knorren", + "bleat": "Blaten", + "chicken": "Kip", + "cluck": "Tok", + "turkey": "Kalkoen", + "quack": "Kwak", + "goose": "Gans", + "honk": "gakken", + "wild_animals": "Wilde dieren", + "roaring_cats": "Schreeuwende kat", + "roar": "Brul", + "bird": "Vogel", + "chirp": "Tjilpen", + "squawk": "Gekrijs", + "pigeon": "Duif", + "coo": "Koeren", + "crow": "Kraai", + "caw": "Kauw", + "owl": "Uil", + "hoot": "uilengeroep", + "flapping_wings": "Flapperende vleugels", + "rats": "Ratten", + "mouse": "Muis", + "patter": "Getrippel", + "insect": "Insect", + "mosquito": "Mug", + "buzz": "Gezoem", + "frog": "Kikker", + "croak": "Kwaken", + "snake": "Slang", + "rattle": "Rammel", + "music": "Muziek", + "plucked_string_instrument": "Snaarinstrument", + "guitar": "Gitaar", + "electric_guitar": "Elektrische gitaar", + "bass_guitar": "Basgitaar", + "acoustic_guitar": "Akoestische gitaar", + "steel_guitar": "Steel Guitar", + "tapping": "Tikken", + "strum": "Aanslaan", + "sitar": "Sitar", + "mandolin": "Mandoline", + "zither": "Citer", + "sniff": "Snuif", + "yell": "Schreeuwen", + "whoop": "Gejuich", + "chant": "Lied", + "whistling": "Gefluit", + "hands": "Handen", + "sheep": "Schaap", + "synthetic_singing": "Synthetisch zingen", + "run": "Ren", + "humming": "Zoemen", + "chewing": "Kauwen", + "bark": "Blaffen", + "animal": "Dier", + "cough": "Hoest", + "throat_clearing": "Keel schrapen", + "biting": "Bijten", + "heart_murmur": "Hartruis", + "crowd": "Menigte", + "pets": "Huisdieren", + "heartbeat": "Hartslag", + "children_playing": "Kinderen spelen", + "purr": "Spinnen", + "speech": "Spraak", + "whispering": "Fluisteren", + "yip": "Kef", + "groan": "Kreunen", + "fowl": "Gevogelte", + "grunt": "brommend", + "cock_a_doodle_doo": "Kukeleku", + "cheering": "Juichen", + "caterwaul": "Kattengehuil", + "hiccup": "Hik", + "clapping": "Klappen", + "dogs": "Honden", + "cat": "Kat", + "dog": "Hond", + "goat": "Geit", + "cricket": "Krekel", + "musical_instrument": "Muziek Instrument", + "duck": "Eend", + "banjo": "Banjo", + "gobble": "kalkoenroep", + "fly": "Vlieg", + "whale_vocalization": "Vocalisatie van walvissen", + "keyboard": "Klavier", + "piano": "Piano", + "ukulele": "Ukulele", + "electric_piano": "Elektrische piano", + "organ": "Orgel", + "sampler": "Sampler", + "harpsichord": "Klavecimbel", + "percussion": "Slagwerk", + "drum_kit": "Drumstel", + "snare_drum": "Snaartrommel", + "drum_roll": "Tromgeroffel", + "bass_drum": "Basdrum", + "tabla": "Tabla", + "cymbal": "Bekken", + "hi_hat": "Hi-Hat", + "maraca": "Sambabal", + "tubular_bells": "Buisklokken", + "mallet_percussion": "Mallet instrumenten", + "marimba": "Marimba", + "glockenspiel": "Klokkenspel", + "steelpan": "Steeldrum", + "brass_instrument": "Koperblaasinstrumenten", + "french_horn": "Waldhoorn", + "trombone": "Trombone", + "string_section": "Snaar sectie", + "wind_instrument": "Blaasinstrument", + "clarinet": "Klarinet", + "harp": "Harp", + "bell": "Klok", + "church_bell": "Kerkklok", + "jingle_bell": "Klingelbel", + "bicycle_bell": "Fietsbel", + "tuning_fork": "Stemvork", + "chime": "Bel", + "wind_chime": "Windgong", + "accordion": "Accordeon", + "bagpipes": "Doedelzakken", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Klankschaal", + "rock_music": "Rockmuziek", + "rhythm_and_blues": "Rhythm-and-blues", + "soul_music": "Soulmuziek", + "reggae": "Reggae", + "country": "Countrymuziek", + "bluegrass": "Bluegrass", + "funk": "Funk", + "middle_eastern_music": "Midden-Oosterse muziek", + "jazz": "Jazz", + "cello": "Cello", + "swing_music": "Swingmuziek", + "gong": "gong", + "synthesizer": "Synthesizer", + "punk_rock": "Punkrock", + "wood_block": "Houten klankblok", + "double_bass": "Contrabas", + "beatboxing": "Beatbox", + "orchestra": "Orkest", + "progressive_rock": "Progressieve rock", + "pop_music": "Popmuziek", + "folk_music": "Volksmuziek", + "drum_machine": "Drum", + "pizzicato": "Pizzicato", + "grunge": "Grunge", + "heavy_metal": "Heavy Metal", + "timpani": "Pauken", + "electronic_organ": "Elektronisch orgel", + "trumpet": "Trompet", + "hip_hop_music": "Hip-Hop Muziek", + "hammond_organ": "Hammondorgel", + "drum": "Trommel", + "rimshot": "Rimshot", + "harmonica": "Mondharmonica", + "tambourine": "Tamboerijn", + "psychedelic_rock": "Psychedelische rock", + "vibraphone": "Vibrafoon", + "bowed_string_instrument": "Strijkinstrument", + "violin": "Viool", + "flute": "Fluit", + "saxophone": "Saxofoon", + "scratching": "Krabben", + "rock_and_roll": "Rock 'n Roll", + "disco": "Disco", + "classical_music": "Klassieke muziek", + "opera": "Opera", + "electronic_music": "Elektronische muziek", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum en bas", + "electronic_dance_music": "Elektronische dansmuziek", + "electronica": "Electronica", + "ambient_music": "Ambientmuziek", + "house_music": "Housemuziek", + "trance_music": "Trance Music", + "salsa_music": "Salsamuziek", + "flamenco": "Flamenco", + "blues": "Blues", + "music_of_bollywood": "Music of Bollywood", + "ska": "Ska", + "independent_music": "Onafhankelijke muziek", + "soundtrack_music": "Soundtrack", + "bathtub": "Bad", + "keys_jangling": "Sleutels rinkelen", + "coin": "Munt", + "dial_tone": "Kiestoon", + "busy_signal": "In-gesprektoon", + "buzzer": "Zoemer", + "foghorn": "Misthoorn", + "whistle": "Fluiten", + "steam_whistle": "Stoomfluit", + "ratchet": "Ratel", + "gears": "Tandwielen", + "air_conditioning": "Airconditioning", + "single-lens_reflex_camera": "Spiegelreflexcamera", + "machine_gun": "Geweerschot", + "fusillade": "Schotenwisseling", + "artillery_fire": "Artillerievuur", + "firecracker": "Vuurwerkknaller", + "burst": "Knal", + "boom": "Boem", + "chop": "Hout Hakken", + "splinter": "Splinter", + "chink": "Spleet", + "shatter": "Stukslaan", + "static": "Statisch geluid", + "white_noise": "Witte ruis", + "pink_noise": "Roze ruis", + "television": "Televisie", + "radio": "Radio", + "field_recording": "Veldopname", + "music_for_children": "Muziek voor kinderen", + "vocal_music": "Vocal Music", + "a_capella": "A capella", + "christian_music": "Christelijke Muziek", + "crackle": "Gekraak", + "cupboard_open_or_close": "Kast open of dicht", + "motor_vehicle": "Motorvoertuig", + "police_car": "Politieauto", + "knock": "Klop", + "music_of_asia": "Muziek uit Azië", + "dance_music": "Dansmuziek", + "light_engine": "Lichte motor", + "frying": "Frituren", + "gospel_music": "Gospelmuziek", + "ice_cream_truck": "IJscowagen", + "engine_starting": "Motor starten", + "new-age_music": "New age (muziek)", + "wedding_music": "Bruiloftsmuziek", + "music_of_africa": "Muziek van Afrika", + "thunder": "Donder", + "waterfall": "Waterval", + "skidding": "Slippen", + "truck": "Vrachtwagen", + "ambulance": "Ambulance", + "jet_engine": "Straalmotor", + "lullaby": "Slaapliedje", + "sad_music": "Droevige muziek", + "video_game_music": "Videogamemuziek", + "angry_music": "Boze muziek", + "steam": "Stromend water", + "vehicle": "Voertuig", + "boat": "Boot", + "rowboat": "Roeiboot", + "emergency_vehicle": "Hulpverleningsvoertuig", + "slam": "Slag", + "chopping": "Hakken", + "happy_music": "Vrolijke muziek", + "raindrop": "Regendruppel", + "dental_drill's_drill": "Tandartsboor", + "scissors": "Schaar", + "shuffling_cards": "Kaarten schudden", + "printer": "Printer", + "afrobeat": "Afrobeat", + "traditional_music": "Traditionele muziek", + "gurgling": "Gorgelend", + "train_wheels_squealing": "Piepende treinwielen", + "subway": "Metro", + "bicycle": "Fiets", + "medium_engine": "Middelgrote motor", + "squeak": "Piep", + "dishes": "Borden", + "zipper": "Rits", + "tick-tock": "Ticktack", + "race_car": "Raceauto", + "railroad_car": "Spoorwagon", + "music_of_latin_america": "Muziek uit Latijns-Amerika", + "carnatic_music": "Carnatische muziek", + "helicopter": "Helikopter", + "chainsaw": "Kettingzaag", + "ding-dong": "Ding-Dong", + "sink": "Wasbak", + "wind_noise": "Windgeruis", + "wind": "Wind", + "sailboat": "Zeilboot", + "song": "Liedje", + "toot": "Toeteren", + "bus": "Bus", + "traffic_noise": "Verkeerslawaai", + "train_horn": "Treinhoorn", + "thunderstorm": "Onweer", + "typewriter": "Typemachine", + "background_music": "Achtergrondmuziek", + "car": "Auto", + "ringtone": "Beltoon", + "theme_music": "Themamuziek", + "sliding_door": "Schuifdeur", + "jingle": "Jingle", + "waves": "Golven", + "stream": "Stromend water", + "sewing_machine": "Naaimachine", + "mechanical_fan": "Mechanische ventilator", + "camera": "Camera", + "cap_gun": "Speelgoedpistool", + "tender_music": "Tedere muziek", + "ship": "Schip", + "explosion": "Explosie", + "christmas_music": "Kerstmuziek", + "microwave_oven": "Magnetron", + "toilet_flush": "Toilet doorspoelen", + "exciting_music": "Spannende muziek", + "scary_music": "Enge muziek", + "rustling_leaves": "Ritselende bladeren", + "tire_squeal": "Piepende banden", + "fire_engine": "Brandweerwagen", + "water_tap": "Waterkraan", + "water": "Water", + "rain": "Regen", + "motorcycle": "Motorfiets", + "aircraft_engine": "Vliegtuigmotor", + "rain_on_surface": "Regen op een oppervlakte", + "motorboat": "Motorboot", + "car_passing_by": "Passerende auto", + "reversing_beeps": "Achteruitrijsignalen", + "train": "Trein", + "doorbell": "Deurbel", + "drawer_open_or_close": "Lade open of dicht", + "fire": "Vuur", + "power_windows": "Elektrische ramen", + "train_whistle": "Treinfluitje", + "fixed-wing_aircraft": "Vliegtuig met vaste vleugels", + "engine_knocking": "Motorklopgeluid", + "ocean": "Oceaan", + "rail_transport": "Spoorvervoer", + "aircraft": "Vliegtuigen", + "car_alarm": "Autoalarm", + "idling": "Stationair", + "door": "Deur", + "air_brake": "Luchtrem", + "propeller": "Propeller", + "air_horn": "Luchthoorn", + "skateboard": "Skateboard", + "engine": "Motor", + "accelerating": "Versnellen", + "blender": "Blender", + "gunshot": "Schot", + "lawn_mower": "Grasmaaier", + "heavy_engine": "Zware motor", + "tap": "Tik op", + "hair_dryer": "Föhn", + "cash_register": "Kassa", + "cutlery": "Bestek", + "power_tool": "Elektrisch gereedschap", + "computer_keyboard": "Computertoetsenbord", + "vacuum_cleaner": "Stofzuiger", + "tick": "Teek", + "alarm": "Alarm", + "toothbrush": "Tandenborstel", + "electric_shaver": "Scheerapparaat", + "writing": "Schrijven", + "telephone": "Telefoon", + "jackhammer": "Drilboor", + "alarm_clock": "Wekker", + "civil_defense_siren": "Luchtalarm", + "typing": "Typen", + "pulleys": "Katrollen", + "drill": "Boor", + "telephone_dialing": "Telefoonnummer draaien", + "telephone_bell_ringing": "Rinkelen van de telefoon", + "electric_toothbrush": "Elektrische tandenborstel", + "hammer": "Hamer", + "sanding": "Schuren", + "siren": "Sirene", + "smoke_detector": "Rookmelder", + "fire_alarm": "Brandalarm", + "mechanisms": "Mechanismen", + "filing": "Vijlen", + "clock": "Klok", + "glass": "Glas", + "sawing": "Zagen", + "tools": "Hulpmiddelen", + "wood": "Hout", + "fireworks": "Vuurwerk", + "eruption": "Uitbarsting", + "crack": "Scheur", + "environmental_noise": "Omgevingsgeluid", + "silence": "Stilte", + "sound_effect": "Geluidseffect", + "scream": "Schreeuw", + "sodeling": "Sodeling", + "chird": "Chird", + "change_ringing": "Beltoon wijzigen", + "shofar": "Sjofar", + "liquid": "Vloeistof", + "splash": "Plons", + "slosh": "Klotsen", + "squish": "Pletten", + "drip": "Druppelen", + "pour": "Gieten", + "trickle": "Gerinkel", + "gush": "Stroom", + "fill": "Vullen", + "spray": "Spuiten", + "pump": "Pomp", + "stir": "Roeren", + "boiling": "Koken", + "sonar": "Sonar", + "arrow": "Pijl", + "whoosh": "Woesj", + "thump": "Dreun", + "thunk": "doffe dreun", + "electronic_tuner": "Elektronische tuner", + "effects_unit": "Effecteneenheid", + "chorus_effect": "Kooreffect", + "basketball_bounce": "Basketbal stuiteren", + "bang": "Knal", + "slap": "Klap", + "whack": "Mep", + "smash": "Verpletteren", + "breaking": "Breken", + "bouncing": "Stuiteren", + "whip": "Zweep", + "flap": "Klep", + "scratch": "Kras", + "scrape": "Schrapen", + "rub": "Wrijven", + "roll": "Rollen", + "crushing": "Verpletteren", + "crumpling": "Verpletteren", + "tearing": "Scheuren", + "beep": "Piep", + "ping": "Ping", + "ding": "Ding", + "clang": "Klang", + "squeal": "Piepen", + "creak": "Kraken", + "rustle": "Geritsel", + "whir": "Snorren", + "clatter": "Gekletter", + "sizzle": "Sissen", + "clicking": "Klikken", + "clickety_clack": "Klik-klak", + "rumble": "Gerommel", + "plop": "Plop", + "hum": "Hum", + "zing": "Zing", + "boing": "Boing", + "crunch": "Kraak", + "sine_wave": "Sinusgolf", + "harmonic": "Harmonisch", + "chirp_tone": "Pieptoon", + "pulse": "Puls", + "inside": "Binnen", + "outside": "Buiten", + "reverberation": "Nagalm", + "echo": "Echo", + "noise": "Lawaai", + "mains_hum": "Netstroomgezoe", + "distortion": "Vervorming", + "sidetone": "Zijtoon", + "cacophony": "Kakofonie", + "throbbing": "Bonzend", + "vibration": "Trilling" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/common.json b/sam2-cpu/frigate-dev/web/public/locales/nl/common.json new file mode 100644 index 0000000..17d9127 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "untilForTime": "Totdat {{time}}", + "untilForRestart": "Totdat Frigate herstart.", + "untilRestart": "Tot herstart", + "12hours": "12 uur", + "lastWeek": "Vorige week", + "last7": "Afgelopen 7 dagen", + "last30": "Afgelopen 30 dagen", + "yr": "{{time}} jaar", + "5minutes": "5 minuten", + "10minutes": "10 minuten", + "24hours": "24 uur", + "30minutes": "30 minuten", + "ago": "{{timeAgo}} geleden", + "justNow": "Zojuist", + "today": "Vandaag", + "yesterday": "Gisteren", + "last14": "Afgelopen 14 dagen", + "thisWeek": "Deze week", + "thisMonth": "Deze maand", + "lastMonth": "Vorige maand", + "1hour": "1 uur", + "pm": "pm", + "am": "am", + "year_one": "{{time}} Jaar", + "year_other": "{{time}} Jaren", + "mo": "{{time}} maand", + "month_one": "{{time}} maand", + "month_other": "{{time}} maanden", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ss", + "24hour": "d MMM HH:mm:ss" + }, + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, HH:mm:ss", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "d": "{{time}}dag", + "day_one": "{{time}} dag", + "day_other": "{{time}} dagen", + "h": "{{time}}u", + "hour_one": "{{time}} uur", + "hour_other": "{{time}} uren", + "m": "{{time}}min", + "formattedTimestampWithYear": { + "12hour": "%-d %b %Y, %H:%M", + "24hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampExcludeSeconds": { + "24hour": "%-d %b, %H:%M", + "12hour": "%-d %b, %H:%M" + }, + "minute_one": "{{time}} minuut", + "minute_other": "{{time}} minuten", + "second_one": "{{time}} seconde", + "second_other": "{{time}} seconden", + "formattedTimestampHourMinute": { + "24hour": "HH:mm", + "12hour": "HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, HH:mm", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-HH-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "HH:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, HH:mm", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM yyyy", + "24hour": "d MMM yyyy" + }, + "inProgress": "Wordt uitgevoerd", + "invalidStartTime": "Ongeldige starttijd", + "invalidEndTime": "Ongeldige eindtijd" + }, + "button": { + "enabled": "Ingeschakeld", + "back": "Terug", + "apply": "Toepassen", + "reset": "Opnieuw instellen", + "enable": "Inschakelen", + "disabled": "Uitgeschakeld", + "cancel": "Annuleren", + "close": "Sluiten", + "copy": "Kopieer", + "done": "Klaar", + "saving": "Opslaan…", + "disable": "Uitschakelen", + "save": "Opslaan", + "history": "Geschiedenis", + "fullscreen": "Volledig scherm", + "pictureInPicture": "Pop-up venster", + "twoWayTalk": "Tweerichtingsgesprek", + "cameraAudio": "Camera geluid", + "on": "aan", + "copyCoordinates": "Coördinaten kopiëren", + "delete": "Verwijder", + "yes": "Ja", + "no": "Nee", + "suspended": "Opgeschort", + "unsuspended": "Heractiveren", + "export": "Exporteren", + "exitFullscreen": "Verlaat volledig scherm", + "play": "Speel", + "off": "uit", + "info": "Info", + "edit": "Bewerken", + "download": "Download", + "unselect": "Deselecteren", + "next": "Volgende", + "deleteNow": "Nu verwijderen", + "continue": "Doorgaan" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/u" + }, + "length": { + "feet": "voet", + "meters": "meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/uur", + "mbph": "MB/uur", + "gbph": "GB/uur" + } + }, + "label": { + "back": "Ga terug", + "hide": "Verberg {{item}}", + "show": "Toon {{item}}", + "ID": "ID", + "none": "Geen", + "all": "Alle" + }, + "menu": { + "system": "Systeem", + "systemMetrics": "Systeemstatistieken", + "settings": "Instellingen", + "configuration": "Configuratie", + "systemLogs": "Systeem logboeken", + "configurationEditor": "Configuratie bewerker", + "languages": "Talen", + "language": { + "en": "English (Engels)", + "zhCN": "简体中文 (Vereenvoudigd Chinees)", + "withSystem": { + "label": "Gebruik de systeeminstellingen voor de taal" + }, + "ar": "العربية (Arabisch)", + "pt": "Português (Portugees)", + "ru": "Русский (Russisch)", + "de": "Deutsch (Duits)", + "tr": "Türkçe (Turks)", + "it": "Italiano (Italiaans)", + "nl": "Nederlands (Nederlands)", + "sv": "Svenska (Zweeds)", + "cs": "Čeština (Tsjechisch)", + "fa": "فارسی (Perzisch)", + "pl": "Polski (Pools)", + "he": "עברית (Hebreeuws)", + "el": "Ελληνικά (Grieks)", + "ro": "Română (Roemeense)", + "hu": "Magyar (Hongaars)", + "fi": "Suomi (Fins)", + "da": "Dansk (Deens)", + "sk": "Slovenčina (Slowaaks)", + "ko": "한국어 (Koreaans)", + "nb": "Norsk Bokmål (Noors Bokmål)", + "fr": "Français (Frans)", + "uk": "Українська (Oekraïens)", + "es": "Español (Spaans)", + "vi": "Tiếng Việt (Vietnamees)", + "hi": "हिन्दी (Hindi)", + "ja": "日本語 (Japans)", + "yue": "粵語 (Kantonees)", + "th": "ไทย (Thais)", + "ca": "Català (Catalaans)", + "ptBR": "Português brasileiro (Braziliaans Portugees)", + "sr": "Српски (Servisch)", + "sl": "Slovenščina (Sloveens)", + "lt": "Lietuvių (Litouws)", + "bg": "Български (Bulgaars)", + "gl": "Galego (Galicisch)", + "id": "Bahasa Indonesia (Indonesisch)", + "ur": "اردو (Urdu)" + }, + "darkMode": { + "label": "Donkere modus", + "light": "Licht", + "dark": "Donker", + "withSystem": { + "label": "Gebruik de systeeminstellingen voor de lichte of donkere modus" + } + }, + "appearance": "Opmaak", + "theme": { + "blue": "Blauw", + "contrast": "Hoog contrast", + "label": "Thema", + "green": "Groen", + "nord": "Nord", + "red": "Rood", + "default": "Standaard", + "highcontrast": "Hoog contrast" + }, + "withSystem": "Systeem", + "help": "Help", + "live": { + "title": "Live", + "allCameras": "Alle Camera's", + "cameras": { + "title": "Camera's", + "count_one": "{{count}} Camera", + "count_other": "{{count}} Camera's" + } + }, + "restart": "Herstart Frigate", + "documentation": { + "title": "Documentatie", + "label": "Frigate documentatie" + }, + "review": "Beoordelen", + "explore": "Verkennen", + "export": "Exporteren", + "uiPlayground": "Testgebied voor gebruikersinterface", + "faceLibrary": "Gezichtenbibliotheek", + "user": { + "title": "Gebruik", + "current": "Huidige gebruiker: {{user}}", + "logout": "Uitloggen", + "setPassword": "Wachtwoord instellen", + "account": "Account", + "anonymous": "anoniem" + }, + "classification": "Classificatie" + }, + "toast": { + "copyUrlToClipboard": "URL naar klembord gekopieerd.", + "save": { + "title": "Opslaan", + "error": { + "title": "Opslaan van configuratiewijzigingen mislukt: {{errorMessage}}", + "noMessage": "Het opslaan van configuratiewijzigingen is mislukt" + } + } + }, + "role": { + "title": "Rol", + "admin": "Beheerder", + "viewer": "Kijker", + "desc": "Beheerders hebben volledige toegang tot alle functies in de Frigate-interface. Kijkers kunnen alleen camera’s bekijken, items beoordelen en historische beelden terugkijken." + }, + "pagination": { + "previous": { + "title": "Vorig", + "label": "Ga naar de vorige pagina" + }, + "more": "Meer pagina's", + "label": "Paginering", + "next": { + "title": "Volgende", + "label": "Ga naar volgende pagina" + } + }, + "accessDenied": { + "documentTitle": "Toegang geweigerd - Frigate", + "desc": "Je hebt geen toestemming om deze pagina te bekijken.", + "title": "Toegang geweigerd" + }, + "notFound": { + "desc": "Pagina niet gevonden", + "title": "404", + "documentTitle": "Niet gevonden - Frigate" + }, + "selectItem": "Selecteer {{item}}", + "readTheDocumentation": "Lees de documentatie", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} en {{1}}", + "many": "{{items}}, en {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Optioneel", + "internalID": "De interne ID die Frigate gebruikt in de configuratie en database" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/auth.json new file mode 100644 index 0000000..14fb57a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Wachtwoord", + "login": "Inloggen", + "errors": { + "rateLimit": "Limiet overschreden. Probeer het later opnieuw.", + "loginFailed": "Inloggen mislukt", + "usernameRequired": "Gebruikersnaam is vereist", + "passwordRequired": "Wachtwoord is vereist", + "unknownError": "Onbekende fout. Bekijk de logs.", + "webUnknownError": "Onbekende fout. Controleer consolelogboeken." + }, + "user": "Gebruikersnaam", + "firstTimeLogin": "Probeer je voor het eerst in te loggen? De inloggegevens staan vermeld in de Frigate-logs." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/camera.json new file mode 100644 index 0000000..1b84047 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Cameragroepen", + "add": "Cameragroep toevoegen", + "edit": "Cameragroep bewerken", + "delete": { + "label": "Cameragroep verwijderen", + "confirm": { + "title": "Bevestig Verwijderen", + "desc": "Weet u zeker dat u de cameragroep {{name}} wilt verwijderen?" + } + }, + "name": { + "label": "Naam", + "placeholder": "Voer een naam in…", + "errorMessage": { + "exists": "De cameragroepnaam bestaat al.", + "invalid": "Ongeldige cameragroepnaam.", + "nameMustNotPeriod": "De naam van de cameragroep mag geen punt bevatten.", + "mustLeastCharacters": "De naam van de cameragroep moet minimaal 2 tekens lang zijn." + } + }, + "cameras": { + "desc": "Selecteer camera's voor deze groep.", + "label": "Camera's" + }, + "success": "Cameragroep ({{name}}) is opgeslagen.", + "camera": { + "setting": { + "audioIsAvailable": "Audio is beschikbaar voor deze stream", + "audioIsUnavailable": "Audio is niet beschikbaar voor deze stream", + "audio": { + "tips": { + "document": "Lees de documentatie ", + "title": "Audio moet worden uitgevoerd vanaf je camera en geconfigureerd in go2rtc voor deze stream." + } + }, + "streamMethod": { + "method": { + "smartStreaming": { + "label": "Slim streamen (aanbevolen)", + "desc": "Slim streamen werkt het camerabeeld één keer per minuut bij wanneer er geen detecteerbare activiteit is, om bandbreedte en systeembronnen te besparen. Zodra er activiteit wordt gedetecteerd, schakelt het beeld automatisch over naar een livestream." + }, + "continuousStreaming": { + "label": "Continue streaming", + "desc": { + "title": "Het camerabeeld is altijd een live stream wanneer het zichtbaar is op het dashboard, zelfs als er geen activiteit wordt gedetecteerd.", + "warning": "Let op: continu streamen kan leiden tot hoog bandbreedtegebruik en prestatieproblemen." + } + }, + "noStreaming": { + "label": "Geen streaming", + "desc": "Camerabeelden worden slechts één keer per minuut bijgewerkt en er vindt geen livestreaming plaats." + } + }, + "label": "Streamingmethode", + "placeholder": "Kies een streamingmethode" + }, + "compatibilityMode": { + "desc": "Schakel deze optie alleen in als de live stream van je camera kleurvervormingen toont en een diagonale lijn aan de rechterkant van het beeld heeft.", + "label": "Compatibiliteitsmodus" + }, + "desc": "Wijzig de live streaming-opties voor het dashboard van deze cameragroep. Deze instellingen zijn specifiek voor het apparaat en de browser.", + "label": "Camera streaming-instellingen", + "title": "{{cameraName}} Streaming-instellingen", + "stream": "Stream", + "placeholder": "Kies een stream" + }, + "birdseye": "Birdseye" + }, + "icon": "Icon" + }, + "debug": { + "options": { + "label": "Instellingen", + "title": "Opties", + "showOptions": "Opties weergeven", + "hideOptions": "Opties verbergen" + }, + "mask": "Masker", + "motion": "Beweging", + "zones": "Zones", + "boundingBox": "Objectkader", + "timestamp": "Tijdstempel", + "regions": "Regio's" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/dialog.json new file mode 100644 index 0000000..b666c2b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/dialog.json @@ -0,0 +1,133 @@ +{ + "restart": { + "title": "Weet je zeker dat je Frigate opnieuw wilt opstarten?", + "button": "Herstart", + "restarting": { + "title": "Frigate wordt opnieuw gestart", + "button": "Forceer herladen nu", + "content": "Deze pagina zal herladen in {{countdown}} seconden." + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Verzenden naar Frigate+", + "desc": "Objecten op locaties die je wilt vermijden, zijn geen vals-positieven. Als je ze als vals-positieven indient, brengt dit het model in verwarring." + }, + "review": { + "true": { + "true_one": "Dit is een {{label}}", + "true_other": "Dit zijn {{label}}", + "label": "Bevestig dit label voor Frigate Plus" + }, + "false": { + "false_one": "Dit is geen {{label}}", + "false_other": "Dit zijn geen {{label}}", + "label": "Bevestig dit label niet voor Frigate Plus" + }, + "state": { + "submitted": "Ingediend" + }, + "question": { + "ask_an": "Is dit object een {{label}}?", + "label": "Bevestig dit label voor Frigate Plus", + "ask_a": "Is dit object een {{label}}?", + "ask_full": "Is dit object een {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Bekijk in Geschiedenis" + } + }, + "export": { + "time": { + "fromTimeline": "Selecteer uit Tijdlijn", + "end": { + "label": "Selecteer eindtijd", + "title": "Eindtijd" + }, + "lastHour_one": "Afgelopen uur", + "lastHour_other": "Afgelopen {{count}} uren", + "custom": "Aangepast", + "start": { + "title": "Starttijd", + "label": "Selecteer starttijd" + } + }, + "name": { + "placeholder": "Geef de export een naam" + }, + "select": "Selecteer", + "toast": { + "error": { + "failed": "Exporteren is mislukt: {{error}}", + "noVaildTimeSelected": "Geen geldig tijdsbereik geselecteerd", + "endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn" + }, + "success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.", + "view": "Weergeven" + }, + "fromTimeline": { + "saveExport": "Export opslaan", + "previewExport": "Export vooraf bekijken" + }, + "export": "Exporteren", + "selectOrExport": "Selecteren of exporteren" + }, + "streaming": { + "label": "Stream", + "restreaming": { + "desc": { + "title": "Stel go2rtc in voor extra liveweergaveopties en audio voor deze camera.", + "readTheDocumentation": "Lees de documentatie" + }, + "disabled": "Herstreamen is niet ingeschakeld voor deze camera." + }, + "showStats": { + "label": "Streamstatistieken tonen", + "desc": "Schakel deze optie in om streamstatistieken als overlay op de camerafeed weer te geven." + }, + "debugView": "Debugweergave" + }, + "search": { + "saveSearch": { + "label": "Zoekopdracht opslaan", + "desc": "Geef een naam op voor deze opgeslagen zoekopdracht.", + "success": "Zoekopdracht ({{searchName}}) is opgeslagen.", + "button": { + "save": { + "label": "Bewaar deze zoekopdracht" + } + }, + "overwrite": "{{searchName}} bestaat al. Opslaan overschrijft de bestaande waarde.", + "placeholder": "Voer een naam in voor uw zoekopdracht" + } + }, + "recording": { + "button": { + "deleteNow": "Verwijder nu", + "export": "Exporteren", + "markAsReviewed": "Markeren als beoordeeld", + "markAsUnreviewed": "Markeren als niet beoordeeld" + }, + "confirmDelete": { + "desc": { + "selected": "Weet u zeker dat u alle opgenomen videobeelden wilt verwijderen die aan dit beoordelingsitem zijn gekoppeld?

    Houd de Shift-toets ingedrukt om dit dialoogvenster in de toekomst over te slaan." + }, + "title": "Bevestig Verwijderen", + "toast": { + "error": "Verwijderen mislukt: {{error}}", + "success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd." + } + } + }, + "imagePicker": { + "selectImage": "Kies miniatuur van gevolgd object", + "noImages": "Geen miniaturen gevonden voor deze camera", + "search": { + "placeholder": "Zoeken op label of sub label..." + }, + "unknownLabel": "Opgeslagen triggerafbeelding" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/filter.json new file mode 100644 index 0000000..95a9814 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "count": "{{count}} Labels", + "all": { + "short": "Labels", + "title": "Alle labels" + }, + "label": "Labels", + "count_one": "{{count}} Label", + "count_other": "{{count}} Labels" + }, + "zones": { + "label": "Zones", + "all": { + "title": "Alle zones", + "short": "Zones" + } + }, + "dates": { + "all": { + "title": "Alle datums", + "short": "Datums" + }, + "selectPreset": "Selecteer een voorinstelling…" + }, + "features": { + "hasVideoClip": "Heeft een videoclip", + "label": "Functies", + "hasSnapshot": "Heeft een snapshot", + "submittedToFrigatePlus": { + "label": "Ingediend bij Frigate+", + "tips": "Je moet eerst filteren op gevolgde objecten met een snapshot.

    Gevolgde objecten zonder snapshot kunnen niet worden verzonden naar Frigate+." + } + }, + "review": { + "showReviewed": "Toon beoordeelde items" + }, + "motion": { + "showMotionOnly": "Alleen bewegingen weergeven" + }, + "explore": { + "settings": { + "title": "Instellingen", + "defaultView": { + "title": "Standaardweergave", + "unfilteredGrid": "Ongefilterd overzicht", + "summary": "Samenvatting", + "desc": "Wanneer er geen filters zijn geselecteerd, wordt er een samenvatting van de meest recent gevolgde objecten per label weergegeven, of wordt er een ongefilterd overzicht weergegeven." + }, + "gridColumns": { + "title": "Overzichtskolommen", + "desc": "Selecteer het aantal kolommen in het overzicht." + }, + "searchSource": { + "options": { + "description": "Beschrijving", + "thumbnailImage": "Thumbnail afbeelding" + }, + "desc": "Kies of u wilt zoeken in de thumbnails of beschrijvingen van de objecten die u volgt.", + "label": "Zoekbron" + } + }, + "date": { + "selectDateBy": { + "label": "Selecteer een datum om op te filteren" + } + } + }, + "zoneMask": { + "filterBy": "Filteren op zonemasker" + }, + "recognizedLicensePlates": { + "loadFailed": "Het laden van herkende kentekenplaten is mislukt.", + "placeholder": "Type om kentekens te zoeken…", + "title": "Herkende kentekenplaten", + "noLicensePlatesFound": "Geen kentekenplaten gevonden.", + "selectPlatesFromList": "Selecteer een of meer kentekens uit de lijst.", + "loading": "Herkende kentekenplaten laden…", + "selectAll": "Selecteer alles", + "clearAll": "Alles wissen" + }, + "score": "Score", + "sort": { + "scoreAsc": "Objectscore (oplopend)", + "dateAsc": "Datum (oplopend)", + "speedAsc": "Geschatte snelheid (oplopend)", + "label": "Sorteer", + "relevance": "Relevantie", + "dateDesc": "Datum (aflopend)", + "scoreDesc": "Objectscore (aflopend)", + "speedDesc": "Geschatte snelheid (aflopend)" + }, + "cameras": { + "all": { + "title": "Alle camera's", + "short": "Camera's" + }, + "label": "Camerafilter" + }, + "subLabels": { + "label": "Sublabels", + "all": "Alle sublabels" + }, + "logSettings": { + "loading": { + "title": "Bezig met laden", + "desc": "Wanneer u het logvenster naar beneden scrolt, worden nieuwe logs automatisch weergegeven terwijl ze worden toegevoegd." + }, + "disableLogStreaming": "Logstreaming uitschakelen", + "allLogs": "Alle logs", + "label": "Filterlogniveau", + "filterBySeverity": "Filter logs op ernst" + }, + "filter": "Filter", + "timeRange": "Tijdsbereik", + "trackedObjectDelete": { + "toast": { + "success": "Gevolgde objecten succesvol verwijderd.", + "error": "Het verwijderen van gevolgde objecten is mislukt: {{errorMessage}}" + }, + "title": "Bevestig Verwijderen", + "desc": "Het verwijderen van deze {{objectLength}} gevolgde objecten verwijdert de snapshot, eventuele opgeslagen embeddings en bijbehorende levenscyclusgegevens van het object. Opgenomen videobeelden van deze objecten in de geschiedenisweergave worden NIET verwijderd.

    Weet je zeker dat je wilt doorgaan?

    Houd de Shift-toets ingedrukt om deze melding in de toekomst over te slaan." + }, + "reset": { + "label": "Filters resetten naar standaardwaarden" + }, + "more": "Meer filters", + "estimatedSpeed": "Geschatte snelheid ({{unit}})", + "classes": { + "label": "Klassen", + "all": { + "title": "Alle klassen" + }, + "count_one": "{{count}} klasse", + "count_other": "{{count}} Klassen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/icons.json new file mode 100644 index 0000000..af65664 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecteer een pictogram", + "search": { + "placeholder": "Zoek naar een pictogram…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/input.json new file mode 100644 index 0000000..fa5707a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Video downloaden", + "toast": { + "success": "Het downloaden van uw beoordelingsvideo is gestart." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/nl/components/player.json new file mode 100644 index 0000000..ff0dd10 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Geen opnames gevonden voor deze tijd", + "stats": { + "latency": { + "value": "{{seconds}} seconden", + "short": { + "title": "Latentie", + "value": "{{seconds}} s" + }, + "title": "Latentie:" + }, + "totalFrames": "Totaal aantal frames:", + "droppedFrames": { + "title": "Overgeslagen frames:", + "short": { + "value": "{{droppedFrames}} frames", + "title": "Overgeslagen" + } + }, + "droppedFrameRate": "Frequentie van overgeslagen frames:", + "bandwidth": { + "short": "Bandbreedte", + "title": "Bandbreedte:" + }, + "streamType": { + "short": "Type", + "title": "Stream Type:" + }, + "decodedFrames": "Gedecodeerde frames:" + }, + "submitFrigatePlus": { + "title": "Dit frame indienen bij Frigate+?", + "submit": "Indienen" + }, + "streamOffline": { + "title": "Stream is Offline", + "desc": "Er zijn geen frames ontvangen in de detect-stream van {{cameraName}}, controleer de error logs" + }, + "noPreviewFound": "Geen voorbeeld gevonden", + "noPreviewFoundFor": "Geen voorbeeld gevonden voor {{cameraName}}", + "livePlayerRequiredIOSVersion": "Voor dit type livestream is iOS 17.1 of hoger vereist.", + "cameraDisabled": "Camera is uitgeschakeld", + "toast": { + "success": { + "submittedFrigatePlus": "Frame succesvol ingediend bij Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/objects.json b/sam2-cpu/frigate-dev/web/public/locales/nl/objects.json new file mode 100644 index 0000000..1fc914a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/objects.json @@ -0,0 +1,120 @@ +{ + "cat": "Kat", + "horse": "Paard", + "bird": "Vogel", + "bark": "Blaffen", + "goat": "Geit", + "sheep": "Schaap", + "animal": "Dier", + "mouse": "Muis", + "dog": "Hond", + "keyboard": "Klavier", + "person": "Persoon", + "airplane": "Vliegtuig", + "traffic_light": "Verkeerslicht", + "street_sign": "Verkeersbord", + "stop_sign": "Stopbord", + "parking_meter": "Parkeermeter", + "bench": "Bankje", + "cow": "Koe", + "giraffe": "Giraffe", + "hat": "Hoed", + "backpack": "Rugzak", + "shoe": "Schoen", + "baseball_bat": "Honkbalknuppel", + "baseball_glove": "Honkbalhandschoen", + "tennis_racket": "Tennis Racket", + "bottle": "Fles", + "plate": "Bord", + "fork": "Vork", + "spoon": "Lepel", + "bowl": "Schaal", + "sandwich": "Sandwich", + "broccoli": "Broccoli", + "hot_dog": "Hot Dog", + "cake": "Taart", + "chair": "Stoel", + "potted_plant": "Potplant", + "bed": "Bed", + "mirror": "Spiegel", + "laptop": "Laptop", + "cell_phone": "Mobiele telefoon", + "oven": "Oven", + "toaster": "Broodrooster", + "refrigerator": "Koelkast", + "book": "Boek", + "clock": "Klok", + "vase": "Vaas", + "toothbrush": "Tandenborstel", + "teddy_bear": "Teddybeer", + "vehicle": "Voertuig", + "squirrel": "Eekhoorn", + "deer": "Hert", + "fox": "Vos", + "rabbit": "Konijn", + "raccoon": "Wasbeer", + "waste_bin": "Afvalbak", + "on_demand": "Handmatige opnames", + "ups": "UPS", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "skateboard": "Skateboard", + "boat": "Boot", + "scissors": "Schaar", + "bicycle": "Fiets", + "sink": "Wasbak", + "bus": "Bus", + "car": "Auto", + "motorcycle": "Motorfiets", + "train": "Trein", + "door": "Deur", + "blender": "Blender", + "hair_dryer": "Föhn", + "banana": "Banaan", + "umbrella": "Paraplu", + "suitcase": "Koffer", + "license_plate": "Kentekenplaat", + "orange": "Oranje", + "postnl": "PostNL", + "snowboard": "Snowboard", + "sports_ball": "Bal", + "donut": "Donut", + "couch": "Bank", + "package": "Pakket", + "dining_table": "Etenstafel", + "microwave": "Magnetron", + "toilet": "Toilet", + "cup": "Beker", + "carrot": "Wortel", + "eye_glasses": "Brillen", + "bear": "Beer", + "zebra": "Zebra", + "handbag": "Handtas", + "surfboard": "Surfplank", + "wine_glass": "Wijnglas", + "hair_brush": "Haarborstel", + "fire_hydrant": "Brandkraan", + "elephant": "Olifant", + "remote": "Op afstand", + "tie": "Stropdas", + "kite": "Vlieger", + "frisbee": "Frisbee", + "skis": "Ski's", + "desk": "Bureau", + "knife": "Mes", + "apple": "Appel", + "pizza": "Pizza", + "window": "Raam", + "fedex": "FedEx", + "tv": "TV", + "robot_lawnmower": "Robot grasmaaier", + "usps": "USPS", + "dhl": "DHL", + "bbq_grill": "BBQ-grill", + "amazon": "Amazon", + "face": "Gezicht", + "an_post": "An Post", + "purolator": "Purolator" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/classificationModel.json new file mode 100644 index 0000000..5674ca3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Classificatiemodellen - Frigate", + "button": { + "deleteClassificationAttempts": "Classificatieafbeeldingen verwijderen", + "renameCategory": "Klasse hernoemen", + "deleteCategory": "Klasse verwijderen", + "deleteImages": "Afbeeldingen verwijderen", + "trainModel": "Model trainen", + "addClassification": "Classificatie toevoegen", + "deleteModels": "Modellen verwijderen", + "editModel": "Model bewerken" + }, + "toast": { + "success": { + "deletedCategory": "Verwijderde klasse", + "deletedImage": "Verwijderde afbeeldingen", + "categorizedImage": "Succesvol geclassificeerde afbeelding", + "trainedModel": "Succesvol getraind model.", + "trainingModel": "Modeltraining succesvol gestart.", + "deletedModel_one": "{{count}} model succesvol verwijderd", + "deletedModel_other": "{{count}} modellen succesvol verwijderd", + "updatedModel": "Modelconfiguratie succesvol bijgewerkt", + "renamedCategory": "Klasse succesvol hernoemd naar {{name}}" + }, + "error": { + "deleteImageFailed": "Verwijderen mislukt: {{errorMessage}}", + "deleteCategoryFailed": "Het verwijderen van de klasse is mislukt: {{errorMessage}}", + "categorizeFailed": "Afbeelding categoriseren mislukt: {{errorMessage}}", + "trainingFailed": "Modeltraining mislukt. Raadpleeg de Frigate-logs voor details.", + "deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}", + "updateModelFailed": "Bijwerken van model mislukt: {{errorMessage}}", + "renameCategoryFailed": "Hernoemen van klasse mislukt: {{errorMessage}}", + "trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Klasse verwijderen", + "desc": "Weet je zeker dat je de klasse {{name}} wilt verwijderen? Hiermee worden alle bijbehorende afbeeldingen permanent verwijderd en moet het model opnieuw worden getraind.", + "minClassesTitle": "Kan klasse niet verwijderen", + "minClassesDesc": "Een classificatiemodel moet minimaal twee klassen hebben. Voeg een andere klasse toe voordat u deze verwijdert." + }, + "deleteDatasetImages": { + "title": "Datasetafbeeldingen verwijderen", + "desc_one": "Weet u zeker dat u {{count}} afbeelding uit {{dataset}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt en vereist een hertraining van het model.", + "desc_other": "Weet u zeker dat u {{count}} afbeeldingen uit {{dataset}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt en vereist een hertraining van het model." + }, + "deleteTrainImages": { + "title": "Trainingsafbeeldingen verwijderen", + "desc_one": "Weet je zeker dat je {{count}} afbeelding wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet je zeker dat je {{count}} afbeeldingen wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "renameCategory": { + "title": "Klasse hernoemen", + "desc": "Voer een nieuwe naam in voor {{name}}. U moet het model opnieuw trainen om de naamswijziging door te voeren." + }, + "description": { + "invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten." + }, + "train": { + "title": "Recente classificaties", + "aria": "Selecteer recente classificaties", + "titleShort": "Recent" + }, + "categories": "Klassen", + "createCategory": { + "new": "Nieuwe klasse maken" + }, + "categorizeImageAs": "Afbeelding classificeren als:", + "categorizeImage": "Afbeelding classificeren", + "noModels": { + "object": { + "title": "Geen objectclassificatiemodellen", + "description": "Maak een aangepast model om gedetecteerde objecten te classificeren.", + "buttonText": "Objectmodel maken" + }, + "state": { + "title": "Geen status-classificatiemodellen", + "description": "Maak een aangepast model om statuswijzigingen in specifieke cameragebieden te monitoren en te classificeren.", + "buttonText": "Maak een statusmodel" + } + }, + "wizard": { + "title": "Nieuwe classificatie maken", + "steps": { + "nameAndDefine": "Naam & definiëren", + "stateArea": "Staatsgebied", + "chooseExamples": "Voorbeelden kiezen" + }, + "step1": { + "description": "Statusmodellen houden vaste cameragebieden in de gaten op veranderingen (bijv. deur open/dicht). Objectmodellen voegen classificaties toe aan gedetecteerde objecten (bijv. bekende dieren, bezorgers, enz.).", + "name": "Naam", + "namePlaceholder": "Voer modelnaam in...", + "type": "Type", + "typeState": "Staat", + "typeObject": "Object", + "objectLabel": "Objectlabel", + "objectLabelPlaceholder": "Selecteer objecttype...", + "classificationType": "Classificatietype", + "classificationTypeTip": "Leer meer over classificatietypen", + "classificationTypeDesc": "Sublabels voegen extra tekst toe aan het objectlabel (bijv. ‘Persoon: UPS’). Attributen zijn doorzoekbare metadata die apart in de objectmetadata worden opgeslagen.", + "classificationSubLabel": "Sublabel", + "classificationAttribute": "Attribuut", + "classes": "Klassen", + "classesTip": "Meer over klassen leren", + "classesStateDesc": "Definieer de verschillende staten waarin uw cameragebied zich kan bevinden. Bijvoorbeeld: ‘open’ en ‘gesloten’ voor een garagedeur.", + "classesObjectDesc": "Definieer de verschillende categorieën om gedetecteerde objecten in te classificeren. Bijvoorbeeld: ‘bezorger’, ‘bewoner’, ‘vreemdeling’ voor persoonsclassificatie.", + "classPlaceholder": "Voer klassenaam in...", + "errors": { + "nameRequired": "Modelnaam is vereist", + "nameLength": "De modelnaam mag maximaal 64 tekens lang zijn", + "nameOnlyNumbers": "Modelnaam mag niet alleen uit cijfers bestaan", + "classRequired": "Minimaal 1 klasse is vereist", + "classesUnique": "Klassennamen moeten uniek zijn", + "stateRequiresTwoClasses": "Statusmodellen vereisen minimaal 2 klassen", + "objectLabelRequired": "Selecteer een objectlabel", + "objectTypeRequired": "Selecteer een classificatietype" + }, + "states": "Staten" + }, + "step2": { + "description": "Selecteer camera’s en definieer voor elke camera het te monitoren gebied. Het model zal de status van deze gebieden classificeren.", + "cameras": "Camera's", + "selectCamera": "Selecteer camera", + "noCameras": "Klik op + om camera’s toe te voegen", + "selectCameraPrompt": "Selecteer een camera uit de lijst om het te monitoren gebied te definiëren" + }, + "step3": { + "selectImagesPrompt": "Selecteer alle afbeeldingen met: {{className}}", + "selectImagesDescription": "Klik op afbeeldingen om ze te selecteren. Klik op doorgaan wanneer je klaar bent met deze klasse.", + "generating": { + "title": "Voorbeeldafbeeldingen genereren", + "description": "Frigate haalt representatieve afbeeldingen uit je opnames. Dit kan even duren..." + }, + "training": { + "title": "Model trainen", + "description": "Je model wordt op de achtergrond getraind. Sluit dit venster, en je model zal starten zodra de training is voltooid." + }, + "retryGenerate": "Generatie opnieuw proberen", + "noImages": "Geen voorbeeldafbeeldingen gegenereerd", + "classifying": "Classificeren en trainen...", + "trainingStarted": "Training succesvol gestart", + "errors": { + "noCameras": "Geen camera’s geconfigureerd", + "noObjectLabel": "Geen objectlabel geselecteerd", + "generateFailed": "Genereren van voorbeelden mislukt: {{error}}", + "generationFailed": "Generatie mislukt. Probeer het opnieuw.", + "classifyFailed": "Afbeeldingen classificeren mislukt: {{error}}" + }, + "generateSuccess": "Met succes gegenereerde voorbeeldafbeeldingen", + "allImagesRequired_one": "Classificeer alle afbeeldingen. {{count}} afbeelding resterend.", + "allImagesRequired_other": "Classificeer alle afbeeldingen. {{count}} afbeeldingen resterend.", + "modelCreated": "Model succesvol aangemaakt. Gebruik de weergave Recente classificaties om afbeeldingen voor ontbrekende statussen toe te voegen en train vervolgens het model.", + "missingStatesWarning": { + "title": "Voorbeelden van ontbrekende staten", + "description": "Het wordt aanbevolen om voor alle staten voorbeelden te selecteren voor het beste resultaat. Je kunt doorgaan zonder alle staten te selecteren, maar het model wordt pas getraind zodra alle staten afbeeldingen hebben. Na het doorgaan kun je in de weergave ‘Recente Classificaties’ de ontbrekende staten van afbeeldingen voorzien, en daarna het model trainen." + } + } + }, + "deleteModel": { + "title": "Classificatiemodel verwijderen", + "single": "Weet u zeker dat u {{name}} wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "desc_one": "Weet u zeker dat u {{count}} model wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet u zeker dat u {{count}} modellen wilt verwijderen? Hiermee worden alle bijbehorende gegevens, inclusief afbeeldingen en trainingsgegevens, permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt." + }, + "menu": { + "objects": "Objecten", + "states": "Staten" + }, + "details": { + "scoreInfo": "Score geeft het gemiddelde classificatievertrouwen weer over alle detecties van dit object." + }, + "edit": { + "title": "Classificatiemodel bewerken", + "descriptionState": "Bewerk de klassen voor dit statusclassificatiemodel. Wijzigingen vereisen dat het model opnieuw wordt getraind.", + "descriptionObject": "Bewerk het objecttype en het classificatietype voor dit objectclassificatiemodel.", + "stateClassesInfo": "Let op: het wijzigen van statusklassen vereist dat het model opnieuw wordt getraind met de bijgewerkte klassen." + }, + "tooltip": { + "trainingInProgress": "Model is momenteel aan het trainen", + "noNewImages": "Geen nieuwe afbeeldingen om te trainen. Classificeer eerst meer afbeeldingen in de dataset.", + "modelNotReady": "Model is niet klaar voor training", + "noChanges": "Geen wijzigingen in de dataset sinds de laatste training." + }, + "none": "Geen herkenning" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/configEditor.json new file mode 100644 index 0000000..50a146c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Configuratie-bewerken - Frigate", + "copyConfig": "Configuratie kopiëren", + "saveAndRestart": "Opslaan en opnieuw opstarten", + "toast": { + "error": { + "savingError": "Fout bij het opslaan van de configuratie" + }, + "success": { + "copyToClipboard": "Configuratie gekopieerd naar klembord." + } + }, + "configEditor": "Configuratie Bewerken", + "saveOnly": "Alleen opslaan", + "confirm": "Afsluiten zonder op te slaan?", + "safeConfigEditor": "Configuratie-editor (veilige modus)", + "safeModeDescription": "Frigate is in veilige modus vanwege een configuratievalidatiefout." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/events.json new file mode 100644 index 0000000..3086373 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/events.json @@ -0,0 +1,63 @@ +{ + "documentTitle": "Beoordelen - Frigate", + "camera": "Camera", + "newReviewItems": { + "button": "Nieuwe items om te beoordelen", + "label": "Bekijk nieuwe beoordelingsitems" + }, + "timeline.aria": "Selecteer tijdlijn", + "recordings": { + "documentTitle": "Opnamen - Frigate" + }, + "timeline": "Tijdlijn", + "empty": { + "alert": "Er zijn geen meldingen om te beoordelen", + "detection": "Er zijn geen detecties om te beoordelen", + "motion": "Geen bewegingsgegevens gevonden" + }, + "events": { + "aria": "Selecteer activiteiten", + "noFoundForTimePeriod": "Er zijn geen activiteiten gevonden voor deze periode.", + "label": "Activiteiten" + }, + "calendarFilter": { + "last24Hours": "Laatste 24 uur" + }, + "alerts": "Meldingen", + "motion": { + "label": "Bewegingen", + "only": "Alleen bewegingen" + }, + "allCameras": "Alle camera's", + "markAsReviewed": "Markeren als beoordeeld", + "detections": "Detecties", + "markTheseItemsAsReviewed": "Markeer deze items als beoordeeld", + "selected_other": "{{count}} geselecteerd", + "selected_one": "{{count}} geselecteerd", + "detected": "gedetecteerd", + "suspiciousActivity": "Verdachte activiteit", + "threateningActivity": "Bedreigende activiteit", + "detail": { + "noDataFound": "Geen gedetailleerde gegevens om te beoordelen", + "aria": "Detailweergave in- of uitschakelen", + "trackedObject_one": "{{count}} object", + "trackedObject_other": "{{count}} objecten", + "noObjectDetailData": "Geen objectdetails beschikbaar.", + "label": "Detail", + "settings": "Instellingen voor detailweergave", + "alwaysExpandActive": { + "desc": "Altijd de objectdetails van het actieve beoordelingsitem uitklappen wanneer deze beschikbaar zijn.", + "title": "Het huidige item altijd uitvouwen" + } + }, + "objectTrack": { + "trackedPoint": "Gevolgd punt", + "clickToSeek": "Klik om naar deze tijd te zoeken" + }, + "zoomIn": "Zoom in", + "zoomOut": "Zoom uit", + "normalActivity": "Normaal", + "needsReview": "Heeft een beoordeling nodig", + "securityConcern": "Beveiligingsprobleem", + "select_all": "Alle" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/explore.json new file mode 100644 index 0000000..b67bea2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/explore.json @@ -0,0 +1,293 @@ +{ + "generativeAI": "Generatieve AI", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "finishingShortly": "Bijna klaar", + "step": { + "trackedObjectsProcessed": "Gevolgde objecten verwerkt: ", + "descriptionsEmbedded": "Beschrijving ingesloten: ", + "thumbnailsEmbedded": "Thumbnails ingesloten: " + }, + "context": "Verkennen kan worden gebruikt nadat de embeddings van gevolgde objecten opnieuw zijn geïndexeerd.", + "estimatedTime": "Geschatte resterende tijd:", + "startingUp": "Opstarten…" + }, + "downloadingModels": { + "setup": { + "textTokenizer": "Teksttokenizer", + "visionModel": "Visiemodel", + "visionModelFeatureExtractor": "kenmerkextractie van een visiemodel", + "textModel": "Tekstmodel" + }, + "error": "Er is iets misgegaan. Raadpleeg de Frigate-logs.", + "context": "Frigate downloadt de vereiste inbedmodellen om de functie Semantisch zoeken te ondersteunen. Dit kan enkele minuten duren, afhankelijk van de snelheid van je netwerkverbinding.", + "tips": { + "context": "Je wilt misschien de embeddings van je gevolgde objecten opnieuw indexeren zodra de modellen zijn gedownload.", + "documentation": "Lees de documentatie" + } + }, + "title": "Verkennen is niet beschikbaar" + }, + "trackedObjectDetails": "Details getraceerd object", + "type": { + "details": "Details", + "video": "video", + "snapshot": "snapshot", + "object_lifecycle": "objectlevenscyclus", + "thumbnail": "thumbnail", + "tracking_details": "trackinggegevens" + }, + "objectLifecycle": { + "createObjectMask": "Objectmasker maken", + "lifecycleItemDesc": { + "visible": "{{label}} Gedetecteerd", + "entered_zone": "{{label}} in zone {{zones}}", + "attribute": { + "other": "{{label}} Herkend als {{attribute}}", + "faceOrLicense_plate": "{{attribute}} Gedetecteerd voor {{label}}" + }, + "heard": "{{label}} gehoord", + "gone": "{{label}} is vertrokken", + "active": "{{label}} Werd actief", + "stationary": "{{label}} werd stationair", + "external": "{{label}} gedetecteerd", + "header": { + "zones": "Zones", + "ratio": "Verhouding", + "area": "Gebied" + } + }, + "annotationSettings": { + "title": "Annotatie-instellingen", + "offset": { + "millisecondsToOffset": "Aantal milliseconden om objectkader mee te verschuiven. Standaard: 0", + "desc": "Deze gegevens zijn afkomstig van de detectiestroom van je camera, maar worden weergegeven op beelden uit de opnamestroom. Het is onwaarschijnlijk dat deze twee streams perfect gesynchroniseerd zijn. Hierdoor zullen het objectkader en het beeld niet exact op elkaar aansluiten. Het veld annotation_offset kan echter worden gebruikt om deze annotatie-afwijking te corrigeren.", + "documentation": "Lees de documentatie ", + "label": "Annotatie-afwijking", + "tips": "TIP: Stel je voor dat er een clip is waarin een persoon van links naar rechts loopt. Als het objectkader in de tijdlijn van het object steeds links van de persoon ligt, dan moet de waarde verlaagd worden. Op dezelfde manier als het objectkader consequent vóór de persoon ligt dus vooruitloopt, moet de waarde verhoogd worden.", + "toast": { + "success": "Annotatieverschuiving voor {{camera}} is opgeslagen in het configuratiebestand. Herstart Frigate om je wijzigingen toe te passen." + } + }, + "showAllZones": { + "title": "Toon alle zones", + "desc": "Toon altijd zones op frames waar objecten een zone zijn binnengegaan." + } + }, + "noImageFound": "Er is geen afbeelding beschikbaar voor dit tijdstip.", + "title": "Objectlevenscyclus", + "adjustAnnotationSettings": "Annotatie-instellingen aanpassen", + "scrollViewTips": "Scroll om de belangrijke momenten uit de levenscyclus van dit object te bekijken.", + "autoTrackingTips": "Als u een automatische objectvolgende camera gebruikt, zal het objectkader onnauwkeurig zijn.", + "carousel": { + "previous": "Vorige dia", + "next": "Volgende dia" + }, + "count": "{{first}} van {{second}}", + "trackedPoint": "Volgpunt" + }, + "documentTitle": "Verken - Frigate", + "details": { + "item": { + "button": { + "viewInExplore": "Bekijk in Verkennen", + "share": "Deel dit beoordelingsitem" + }, + "title": "Details van item bekijken", + "desc": "Details van item bekijken", + "tips": { + "hasMissingObjects": "Pas je configuratie aan als je wilt dat Frigate gevolgde objecten opslaat voor de volgende labels: {{objects}}", + "mismatch_one": "{{count}} Niet-beschikbaar object werd gedetecteerd en opgenomen in dit beoordelingsitem. Het voldeed mogelijk niet aan de criteria voor een waarschuwing of detectie, of is inmiddels opgeschoond of verwijderd.", + "mismatch_other": "{{count}} Niet-beschikbare objecten zijn gedetecteerd en opgenomen in dit beoordelingsitem. Deze objecten voldeden mogelijk niet aan de voorwaarden voor een waarschuwing of detectie, of zijn inmiddels verwijderd of opgeruimd." + }, + "toast": { + "success": { + "regenerate": "Er is een nieuwe beschrijving aangevraagd bij {{provider}}. Afhankelijk van de snelheid van je provider kan het regenereren van de nieuwe beschrijving enige tijd duren.", + "updatedSublabel": "Sublabel succesvol bijgewerkt.", + "updatedLPR": "Kenteken succesvol bijgewerkt.", + "audioTranscription": "Audio-transcriptie succesvol aangevraagd. Afhankelijk van de snelheid van uw Frigate-server kan het even duren voordat de transcriptie voltooid is." + }, + "error": { + "updatedSublabelFailed": "Het is niet gelukt om het sublabel bij te werken: {{errorMessage}}", + "regenerate": "Het is niet gelukt om {{provider}} aan te roepen voor een nieuwe beschrijving: {{errorMessage}}", + "updatedLPRFailed": "Kentekenplaat bijwerken mislukt: {{errorMessage}}", + "audioTranscription": "Audiotranscriptie aanvragen mislukt: {{errorMessage}}" + } + } + }, + "label": "Label", + "editSubLabel": { + "title": "Sublabel bewerken", + "descNoLabel": "Voer een nieuw sublabel in voor dit gevolgde object", + "desc": "Voer een nieuw sublabel in voor deze {{label}}" + }, + "topScore": { + "label": "Hoogste score", + "info": "De topscore is de hoogste mediaanscore voor het gevolgde object, waardoor deze kan afwijken van de score die wordt weergegeven op de thumbnail van het zoekresultaat." + }, + "objects": "Objecten", + "zones": "Zones", + "button": { + "findSimilar": "Vind vergelijkbare", + "regenerate": { + "label": "Regenereer de beschrijving van het gevolgde object", + "title": "Regenereren" + } + }, + "description": { + "placeholder": "Beschrijving van het gevolgde object", + "label": "Beschrijving", + "aiTips": "Frigate vraagt pas om een beschrijving van uw Generative AI-provider als de levenscyclus van het gevolgde object is afgelopen." + }, + "expandRegenerationMenu": "Regeneratiemenu uitbreiden", + "regenerateFromSnapshot": "Regenereren vanuit Snapshot", + "tips": { + "descriptionSaved": "Beschrijving succesvol opgeslagen", + "saveDescriptionFailed": "Het is niet gelukt om de beschrijving bij te werken: {{errorMessage}}" + }, + "timestamp": "Tijdstempel", + "regenerateFromThumbnails": "Regeneratie van Thumbnails", + "camera": "Camera", + "estimatedSpeed": "Geschatte snelheid", + "editLPR": { + "title": "Kenteken bewerken", + "desc": "Voer een nieuwe kentekenwaarde in voor deze {{label}}", + "descNoLabel": "Voer een nieuwe kentekenwaarde in voor dit gevolgde object" + }, + "recognizedLicensePlate": "Erkende kentekenplaat", + "snapshotScore": { + "label": "Snapshot score" + }, + "score": { + "label": "Score" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Download video", + "aria": "Download video" + }, + "viewObjectLifecycle": { + "aria": "Toon de levenscyclus van het object", + "label": "Levenscyclus van object bekijken" + }, + "submitToPlus": { + "label": "Verzenden naar Frigate+", + "aria": "Verzenden naar Frigate Plus" + }, + "viewInHistory": { + "aria": "Bekijk in Geschiedenis", + "label": "Bekijk in Geschiedenis" + }, + "deleteTrackedObject": { + "label": "Verwijder dit gevolgde object" + }, + "findSimilar": { + "label": "Vind vergelijkbare", + "aria": "Vind vergelijkbare gevolgde objecten" + }, + "downloadSnapshot": { + "label": "Download snapshot", + "aria": "Download snapshot" + }, + "addTrigger": { + "label": "Trigger toevoegen", + "aria": "Voeg een trigger toe voor dit gevolgde object" + }, + "audioTranscription": { + "label": "Transcriberen", + "aria": "Audiotranscriptie aanvragen" + }, + "showObjectDetails": { + "label": "Objectpad weergeven" + }, + "hideObjectDetails": { + "label": "Verberg objectpad" + }, + "viewTrackingDetails": { + "label": "Bekijk trackinggegevens", + "aria": "Toon de trackinggegevens" + }, + "downloadCleanSnapshot": { + "label": "Download schone snapshot", + "aria": "Download schone snapshot" + } + }, + "noTrackedObjects": "Geen gevolgde objecten gevonden", + "trackedObjectsCount_one": "{{count}} gevolgd object ", + "trackedObjectsCount_other": "{{count}} gevolgde objecten ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Het gevolgde object is succesvol verwijderd.", + "error": "Verwijderen van gevolgd object mislukt: {{errorMessage}}" + } + }, + "tooltip": "{{type}} komt voor {{confidence}}% overeen met de zoekopdracht", + "previousTrackedObject": "Vorig gevolgd object", + "nextTrackedObject": "Volgende gevolgde object" + }, + "dialog": { + "confirmDelete": { + "title": "Bevestig Verwijderen", + "desc": "Het verwijderen van dit gevolgde object verwijdert de snapshot, alle opgeslagen embeddings en eventuele bijbehorende trackinggegevens van het object. Opgenomen videobeelden van dit object in de Geschiedenisweergave worden NIET verwijderd.

    Weet je zeker dat je wilt doorgaan?" + } + }, + "fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}", + "exploreMore": "Verken meer {{label}} objecten", + "aiAnalysis": { + "title": "AI-analyse" + }, + "concerns": { + "label": "Zorgen" + }, + "trackingDetails": { + "title": "Trackinggegevens", + "noImageFound": "Er is geen afbeelding beschikbaar voor dit tijdstip.", + "createObjectMask": "Objectmasker maken", + "adjustAnnotationSettings": "Annotatie-instellingen aanpassen", + "scrollViewTips": "Klik om de belangrijke momenten uit de levenscyclus van dit object te bekijken.", + "autoTrackingTips": "Als u een automatische objectvolgende camera gebruikt, zal het objectkader onnauwkeurig zijn.", + "count": "{{first}} van {{second}}", + "trackedPoint": "Volgpunt", + "lifecycleItemDesc": { + "visible": "{{label}} gedetecteerd", + "entered_zone": "{{label}} in zone {{zones}}", + "active": "{{label}} Werd actief", + "stationary": "{{label}} werd stationair", + "attribute": { + "faceOrLicense_plate": "{{attribute}} Gedetecteerd voor {{label}}", + "other": "{{label}} Herkend als {{attribute}}" + }, + "gone": "{{label}} vertrok", + "heard": "{{label}} gehoord", + "external": "{{label}} gedetecteerd", + "header": { + "zones": "Zones", + "ratio": "Verhouding", + "area": "Gebied", + "score": "Score" + } + }, + "annotationSettings": { + "title": "Annotatie-instellingen", + "showAllZones": { + "title": "Toon alle zones", + "desc": "Toon altijd zones op frames waar objecten een zone zijn binnengegaan." + }, + "offset": { + "label": "Annotatie-afwijking", + "desc": "Deze gegevens zijn afkomstig van de detectiestream van je camera, maar worden weergegeven op beelden uit de opnamestream. Het is onwaarschijnlijk dat deze twee streams perfect gesynchroniseerd zijn. Hierdoor zullen het objectkader en het beeld niet exact op elkaar aansluiten. Met deze instelling kun je de annotaties vooruit of achteruit in de tijd verschuiven om ze beter uit te lijnen met het opgenomen beeldmateriaal.", + "millisecondsToOffset": "Aantal milliseconden om objectkader mee te verschuiven. Standaard: 0", + "tips": "Verlaag de waarde als de videoweergave sneller is dan de objectkaders en hun trajectpunten, en verhoog de waarde als de videoweergave achterloopt. Deze waarde kan negatief zijn.", + "toast": { + "success": "Annotatieverschuiving voor {{camera}} is opgeslagen in het configuratiebestand." + } + } + }, + "carousel": { + "previous": "Vorige dia", + "next": "Volgende dia" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/exports.json new file mode 100644 index 0000000..b4223a6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exporteren - Frigate", + "search": "Zoek", + "toast": { + "error": { + "renameExportFailed": "Het is niet gelukt om de export te hernoemen: {{errorMessage}}" + } + }, + "editExport": { + "title": "Hernoemen Export", + "saveExport": "Export opslaan", + "desc": "Voer een nieuwe naam in voor deze export." + }, + "noExports": "Geen export gevonden", + "deleteExport": "Verwijder Export", + "deleteExport.desc": "Weet je zeker dat je dit wilt wissen: {{exportName}}?", + "tooltip": { + "shareExport": "Deel export", + "downloadVideo": "Download video", + "editName": "Naam bewerken", + "deleteExport": "Verwijder export" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/faceLibrary.json new file mode 100644 index 0000000..88ce52e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "selectItem": "Selecteer {{item}}", + "details": { + "timestamp": "Tijdstempel", + "person": "Persoon", + "confidence": "Vertrouwen", + "face": "Gezicht Details", + "faceDesc": "Details van het gevolgde object dat dit gezicht heeft gegenereerd", + "scoreInfo": "De sublabelscore is het gewogen gemiddelde van hoe zeker de gezichtsherkenningen zijn. Deze score kan anders zijn dan die op de snapshot.", + "subLabelScore": "Score voor sublabel", + "unknown": "Onbekend" + }, + "documentTitle": "Gezichtsbibliotheek - Frigate", + "description": { + "placeholder": "Voer een naam in voor deze verzameling", + "addFace": "Voeg een nieuwe collectie toe aan de gezichtenbibliotheek door je eerste afbeelding te uploaden.", + "invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten." + }, + "train": { + "title": "Recente herkenningen", + "aria": "Selecteer recente herkenningen", + "empty": "Er zijn geen recente pogingen tot gezichtsherkenning", + "titleShort": "Recent" + }, + "selectFace": "Selecteer gezicht", + "toast": { + "error": { + "addFaceLibraryFailed": "Het is niet gelukt om de gezichtsnaam in te stellen: {{errorMessage}}", + "deleteFaceFailed": "Verwijderen mislukt: {{errorMessage}}", + "deleteNameFailed": "Naam verwijderen is niet gelukt: {{errorMessage}}", + "updateFaceScoreFailed": "Niet gelukt om gezichtsscore bij te werken: {{errorMessage}}", + "uploadingImageFailed": "Afbeelding uploaden mislukt: {{errorMessage}}", + "trainFailed": "Trainen mislukt: {{errorMessage}}", + "renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}" + }, + "success": { + "deletedFace_one": "{{count}} gezicht is succesvol verwijderd.", + "deletedFace_other": "{{count}} gezichten zijn succesvol verwijderd.", + "trainedFace": "Met succes getraind gezicht.", + "updatedFaceScore": "De gezichtsscore is succesvol bijgewerkt naar {{name}} ({{score}}).", + "deletedName_one": "{{count}} gezicht is succesvol verwijderd.", + "deletedName_other": "{{count}} gezichten zijn succesvol verwijderd.", + "uploadedImage": "Afbeelding succesvol geüpload.", + "addFaceLibrary": "{{name}} is succesvol toegevoegd aan de Gezichtenbibliotheek!", + "renamedFace": "Gezicht succesvol hernoemd naar {{name}}" + } + }, + "imageEntry": { + "dropActive": "Zet de afbeelding hier neer…", + "dropInstructions": "Sleep een afbeelding hierheen, of klik om te selecteren", + "maxSize": "Maximale grootte: {{size}}MB", + "validation": { + "selectImage": "Selecteer een afbeeldingbestand." + } + }, + "createFaceLibrary": { + "title": "Collectie maken", + "desc": "Een nieuwe collectie maken", + "new": "Creëer een nieuw gezicht", + "nextSteps": "Om een sterke basis op te bouwen:
  • Gebruik het tabblad ‘Recente herkenningen’ om afbeeldingen te selecteren en te trainen voor elke gedetecteerde persoon.
  • Richt je op afbeeldingen die recht van voren genomen zijn voor de beste resultaten, vermijd trainingsafbeeldingen waarop gezichten onder een hoek te zien zijn.
  • " + }, + "button": { + "addFace": "Gezicht toevoegen", + "uploadImage": "Afbeelding uploaden", + "deleteFaceAttempts": "Verwijder gezicht", + "reprocessFace": "Herverwerk gezicht", + "renameFace": "Gezicht hernoemen", + "deleteFace": "Gezicht verwijderen" + }, + "uploadFaceImage": { + "desc": "Upload een afbeelding om te scannen op gezichten en op te nemen voor {{pageToggle}}", + "title": "Upload een afbeelding van het gezicht" + }, + "deleteFaceLibrary": { + "title": "Verwijder Naam", + "desc": "Weet je zeker dat je de collectie {{name}} wilt verwijderen? Dit zal permanent alle geassocieerde gezichten verwijderen." + }, + "trainFaceAs": "Gezicht trainen als:", + "trainFace": "Gezicht trainen", + "readTheDocs": "Lees de documentatie", + "steps": { + "nextSteps": "Volgende stappen", + "faceName": "Voer een naam in", + "uploadFace": "Upload een afbeelding van het gezicht", + "description": { + "uploadFace": "Upload een afbeelding van {{name}} waarop het gezicht van voren te zien is. De afbeelding hoeft niet bijgesneden te zijn tot alleen het gezicht." + } + }, + "renameFace": { + "title": "Gezicht hernoemen", + "desc": "Voer een nieuwe naam in voor {{name}}" + }, + "deleteFaceAttempts": { + "title": "Verwijder gezicht", + "desc_one": "Weet je zeker dat je {{count}} gezicht wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "desc_other": "Weet je zeker dat je {{count}} gezichten wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "collections": "Collecties", + "nofaces": "Geen gezichten beschikbaar", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/live.json new file mode 100644 index 0000000..e6dd73b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/live.json @@ -0,0 +1,189 @@ +{ + "lowBandwidthMode": "Modus voor lage bandbreedte", + "twoWayTalk": { + "enable": "Tweerichtingsgesprek inschakelen", + "disable": "Tweerichtingsgesprek uitschakelen" + }, + "cameraAudio": { + "enable": "Camera geluid inschakelen", + "disable": "Camera geluid uitschakelen" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Klik in het frame om de camera te centreren", + "enable": "Klikken om te bewegen inschakelen", + "disable": "Klikken om te bewegen uitschakelen" + }, + "right": { + "label": "Beweeg de PTZ-camera naar rechts" + }, + "down": { + "label": "Beweeg de PTZ-camera naar beneden" + }, + "up": { + "label": "Beweeg de PTZ-camera naar boven" + }, + "left": { + "label": "Beweeg de PTZ-camera naar links" + } + }, + "zoom": { + "in": { + "label": "Zoom de PTZ-camera in" + }, + "out": { + "label": "Zoom de PTZ-camera uit" + } + }, + "frame": { + "center": { + "label": "Klik in het frame om de PTZ-camera te centreren" + } + }, + "presets": "PTZ-camerapresets", + "focus": { + "in": { + "label": "Focus PTZ-camera in" + }, + "out": { + "label": "Focus PTZ-camera uit" + } + } + }, + "camera": { + "enable": "Camera inschakelen", + "disable": "Camera uitschakelen" + }, + "muteCameras": { + "enable": "Alle camera's dempen", + "disable": "Dempen van alle camera's opheffen" + }, + "detect": { + "disable": "Detectie uitschakelen", + "enable": "Detectie inschakelen" + }, + "snapshots": { + "enable": "Momentopnamen inschakelen", + "disable": "Schakel snapshots uit" + }, + "audioDetect": { + "disable": "Audiodetectie uitschakelen", + "enable": "Audiodetectie inschakelen" + }, + "streamStats": { + "disable": "Verberg streamstatistieken", + "enable": "Streamstatistieken weergeven" + }, + "manualRecording": { + "showStats": { + "label": "Statistieken weergeven", + "desc": "Schakel deze optie in om streamstatistieken als overlay op de camerafeed weer te geven." + }, + "started": "Handmatige opname gestart.", + "start": "Handmatige opname starten", + "debugView": "Debug weergave", + "end": "Handmatige opname stoppen", + "ended": "Handmatige opname gestopt.", + "failedToEnd": "Het beëindigen van de handmatige opname is mislukt.", + "playInBackground": { + "label": "Speel op de achtergrond", + "desc": "Schakel deze optie in om te blijven streamen wanneer de speler verborgen is." + }, + "recordDisabledTips": "Aangezien opnemen is uitgeschakeld of beperkt in de configuratie van deze camera, zal alleen een momentopname worden opgeslagen.", + "title": "Op aanvraag", + "tips": "Download direct een snapshot of start handmatig een gebeurtenis op basis van de opnamebewaarinstellingen van deze camera.", + "failedToStart": "Handmatige opname starten mislukt." + }, + "notifications": "Meldingen", + "audio": "Geluid", + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "autotracking": { + "enable": "Automatisch volgen inschakelen", + "disable": "Automatisch volgen uitschakelen" + }, + "recording": { + "disable": "Opname uitschakelen", + "enable": "Opname inschakelen" + }, + "suspend": { + "forTime": "Onderbreken voor: " + }, + "streamingSettings": "Streaming-instellingen", + "stream": { + "twoWayTalk": { + "tips.documentation": "Lees de documenten ", + "tips": "Uw apparaat moet deze functie ondersteunen en WebRTC moet geconfigureerd zijn voor tweerichtingsgesprekken.", + "unavailable": "Tweerichtingsgesprek is niet beschikbaar voor deze stream", + "available": "Voor deze stream is tweerichtingsgesprek beschikbaar" + }, + "lowBandwidth": { + "resetStream": "Stream resetten", + "tips": "Liveweergave staat in de lagebandbreedtemodus vanwege buffering of streamfouten." + }, + "title": "Stream", + "audio": { + "tips": { + "documentation": "Lees de documentatie ", + "title": "Audio moet via je camera komen en in go2rtc geconfigureerd zijn voor deze stream." + }, + "unavailable": "Audio is niet beschikbaar voor deze stream", + "available": "Audio is beschikbaar voor deze stream" + }, + "playInBackground": { + "label": "Afspelen op de achtergrond", + "tips": "Schakel deze optie in om te blijven streamen wanneer de speler verborgen is." + }, + "debug": { + "picker": "Streamselectie is niet beschikbaar in de debugmodus. De debugweergave gebruikt altijd de stream waaraan de detectierol is toegewezen." + } + }, + "cameraSettings": { + "title": "{{camera}} Instellingen", + "objectDetection": "Objectdetectie", + "recording": "Opname", + "audioDetection": "Audiodetectie", + "autotracking": "Automatisch volgen", + "snapshots": "Momentopnames", + "cameraEnabled": "Camera ingeschakeld", + "transcription": "Audiotranscriptie" + }, + "history": { + "label": "Historische beelden weergeven" + }, + "effectiveRetainMode": { + "modes": { + "all": "Alle", + "active_objects": "Actieve objecten", + "motion": "Beweging" + }, + "notAllTips": "De bewaarbeleid-configuratie voor {{source}} is ingesteld op modus: {{effectiveRetainMode}}, dus deze opname op aanvraag bewaart alleen segmenten met {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Lay-out bewerken", + "exitEdit": "Bewerken verlaten", + "group": { + "label": "Cameragroep bewerken" + } + }, + "transcription": { + "enable": "Live audiotranscriptie inschakelen", + "disable": "Live audiotranscriptie uitschakelen" + }, + "snapshot": { + "takeSnapshot": "Direct een snapshot downloaden", + "noVideoSource": "Geen videobron beschikbaar voor snapshot.", + "captureFailed": "Het is niet gelukt om een snapshot te maken.", + "downloadStarted": "Snapshot downloaden gestart." + }, + "noCameras": { + "title": "Geen camera’s ingesteld", + "description": "Begin door een camera te verbinden met Frigate.", + "buttonText": "Camera toevoegen", + "restricted": { + "title": "Geen camera's beschikbaar", + "description": "Je hebt geen toestemming om camera's in deze groep te bekijken." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/recording.json new file mode 100644 index 0000000..5a40650 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exporteren", + "toast": { + "error": { + "endTimeMustAfterStartTime": "De eindtijd moet na de starttijd zijn", + "noValidTimeSelected": "Er is geen geldig tijdsbereik geselecteerd" + } + }, + "filter": "Filter", + "calendar": "Kalender", + "filters": "Filters" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/search.json new file mode 100644 index 0000000..47487be --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/search.json @@ -0,0 +1,74 @@ +{ + "button": { + "delete": "Opgeslagen zoekopdracht verwijderen", + "save": "Zoekopdracht opslaan", + "clear": "Zoekopdracht wissen", + "filterInformation": "Filter informatie", + "filterActive": "Filters actief" + }, + "search": "Zoek", + "savedSearches": "Opgeslagen zoekopdrachten", + "searchFor": "Zoeken naar {{inputValue}}", + "trackedObjectId": "Getraceerd object-ID", + "filter": { + "label": { + "cameras": "Camera's", + "labels": "Labels", + "sub_labels": "Sublabels", + "time_range": "Tijdsbereik", + "before": "Voor", + "min_score": "Min Score", + "max_score": "Max Score", + "min_speed": "Min snelheid", + "recognized_license_plate": "Herkend kenteken", + "has_snapshot": "Heeft Snapshot", + "has_clip": "Heeft Clip", + "search_type": "Zoektype", + "zones": "Zones", + "max_speed": "Max snelheid", + "after": "Na" + }, + "toast": { + "error": { + "maxSpeedMustBeGreaterOrEqualMinSpeed": "De ‘max_speed’ moet groter zijn dan of gelijk aan de ‘min_speed’.", + "beforeDateBeLaterAfter": "De 'voor' datum moet later zijn dan de 'na' datum.", + "afterDatebeEarlierBefore": "De ‘na’ datum moet eerder zijn dan de ‘voor’ datum.", + "minScoreMustBeLessOrEqualMaxScore": "De ‘min_score’ moet kleiner zijn dan of gelijk aan de ‘max_score’.", + "maxScoreMustBeGreaterOrEqualMinScore": "De ‘max_score’ moet groter zijn dan of gelijk aan de ‘min_score’.", + "minSpeedMustBeLessOrEqualMaxSpeed": "De ‘min_snelheid’ moet kleiner zijn dan of gelijk aan de ‘max_snelheid’." + } + }, + "tips": { + "title": "Hoe tekstfilters te gebruiken", + "desc": { + "example": "Voorbeeld: camera's:voordeur label:persoon vóór:01012024 tijdsbereik:15:00-16:00", + "text": "Filters helpen je om je zoekresultaten te beperken. Zo gebruik je ze in het invoerveld:", + "step": "
    • Typ een filternaam gevolgd door een dubbele punt (bijv. \"cameras:\").
    • Selecteer een waarde uit de suggesties of typ je eigen waarde.
    • Gebruik meerdere filters door ze achter elkaar toe te voegen, gescheiden door een spatie.
    • Datumfilters (before: en after:) gebruiken {{DateFormat}} formaat
    • Het tijdsfilter gebruikt {{exampleTime}} formaat
    • Verwijder filters door op de 'x' ernaast te klikken.
    ", + "step3": "Gebruik meerdere filters door ze achter elkaar toe te voegen met een spatie ertussen.", + "step4": "Datumfilters (voor: en na:) gebruiken het {{DateFormat}} formaat.", + "step5": "Het tijdsfilter gebruikt het {{exampleTime}} als formaat.", + "step2": "Selecteer een waarde uit de suggesties of voer je eigen waarde in.", + "step1": "Typ een filtersleutelnaam gevolgd door een dubbele punt (bijv. \"camera's:\").", + "step6": "Verwijder filters door op de 'x' naast de filteroptie te klikken.", + "exampleLabel": "Voorbeeld:" + } + }, + "searchType": { + "thumbnail": "Thumbnail", + "description": "Beschrijving" + }, + "header": { + "currentFilterType": "Filterwaarden", + "noFilters": "Filters", + "activeFilters": "Actieve filters" + } + }, + "similaritySearch": { + "active": "Gelijkenis zoeken actief", + "title": "Gelijkenis zoeken", + "clear": "Gelijksoortige zoekopdracht wissen" + }, + "placeholder": { + "search": "Zoek…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/settings.json new file mode 100644 index 0000000..fdc439b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/settings.json @@ -0,0 +1,1308 @@ +{ + "documentTitle": { + "default": "Instellingen - Frigate", + "camera": "Camera-instellingen - Frigate", + "authentication": "Authenticatie-instellingen - Frigate", + "motionTuner": "Motion Tuner - Frigate", + "classification": "Classificatie-instellingen - Frigate", + "masksAndZones": "Masker- en zone-editor - Frigate", + "object": "Foutopsporing Frigate", + "general": "Gebruikersinterface-instellingen - Frigate", + "frigatePlus": "Frigate+ Instellingen - Frigate", + "notifications": "Meldingsinstellingen - Frigate", + "enrichments": "Verrijkingsinstellingen - Frigate", + "cameraManagement": "Camera's beheren - Frigate", + "cameraReview": "Camera Review Instellingen - Frigate" + }, + "menu": { + "ui": "Gebruikersinterface", + "classification": "Classificatie", + "masksAndZones": "Maskers / Zones", + "motionTuner": "Bewegingsdetectie-afstellen", + "debug": "foutopsporing", + "users": "Gebruikers", + "notifications": "Meldingen", + "cameras": "Camera-instellingen", + "frigateplus": "Frigate+", + "enrichments": "Verrijkingen", + "triggers": "Triggers", + "roles": "Rollen", + "cameraManagement": "Beheer", + "cameraReview": "Beoordeel" + }, + "dialog": { + "unsavedChanges": { + "title": "Je hebt niet-opgeslagen wijzigingen.", + "desc": "Wilt je jouw wijzigingen opslaan voordat u verdergaat?" + } + }, + "cameraSetting": { + "camera": "Camera", + "noCamera": "Geen camera" + }, + "general": { + "liveDashboard": { + "title": "Live-dashboard", + "automaticLiveView": { + "label": "Automatische liveweergave", + "desc": "Schakel automatisch over naar de liveweergave van een camera wanneer er activiteit wordt gedetecteerd. Als u deze optie uitschakelt, worden de statische camerabeelden op het live dashboard slechts eenmaal per minuut bijgewerkt." + }, + "playAlertVideos": { + "label": "Meldingen afspelen", + "desc": "Standaard worden recente meldingen op het Live dashboard afgespeeld als kleine lusvideo's. Schakel deze optie uit om alleen een statische afbeelding van recente meldingen weer te geven op dit apparaat/browser." + }, + "displayCameraNames": { + "label": "Altijd cameranamen weergeven", + "desc": "Toon altijd de cameranamen in een label op het live-cameradashboard." + }, + "liveFallbackTimeout": { + "label": "Live speler fallback time-out", + "desc": "Wanneer de hoogwaardige livestream van een camera niet beschikbaar is, schakel dan na dit aantal seconden terug naar de modus voor lage bandbreedte. Standaard: 3." + } + }, + "title": "Gebruikersinterface instellingen", + "storedLayouts": { + "title": "Opgeslagen indelingen", + "clearAll": "Alle indelingen wissen", + "desc": "De indeling van camera's in een cameragroep kan worden versleept en in formaat worden aangepast. De posities en afmetingen worden opgeslagen in de lokale opslag van je browser." + }, + "cameraGroupStreaming": { + "title": "Streaminginstellingen voor cameragroep", + "desc": "De streaminginstellingen voor elke cameragroep worden opgeslagen in de lokale opslag van uw browser.", + "clearAll": "Alle streaminginstellingen wissen" + }, + "recordingsViewer": { + "title": "Opnameweergave", + "defaultPlaybackRate": { + "label": "Standaard afspeelsnelheid", + "desc": "Standaard afspeelsnelheid voor het afspelen van opnames." + } + }, + "calendar": { + "firstWeekday": { + "label": "Eerste weekdag", + "sunday": "Zondag", + "monday": "Maandag", + "desc": "De eerste dag van de week die in de kalender in de interface wordt weergegeven." + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStoredLayout": "Verwijderde opgeslagen indeling voor {{cameraName}}", + "clearStreamingSettings": "Verwijderde streaming-instellingen voor alle cameragroepen." + }, + "error": { + "clearStoredLayoutFailed": "Het wissen van de opgeslagen indelingen is mislukt: {{errorMessage}}", + "clearStreamingSettingsFailed": "Het wissen van de streaminginstellingen is mislukt: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Semantisch zoeken", + "reindexNow": { + "label": "Nu opnieuw indexeren", + "confirmTitle": "Bevestig herindexering", + "confirmButton": "Opnieuw indexeren", + "alreadyInProgress": "Het herindexeren is al bezig.", + "success": "Het herindexeren is succesvol gestart.", + "error": "Het opnieuw indexeren is mislukt: {{errorMessage}}", + "desc": "Opnieuw indexeren zal embeddings regenereren voor alle gevolgde objecten. Dit proces wordt op de achtergrond uitgevoerd en kan je CPU zwaar belasten en een behoorlijke hoeveelheid tijd in beslag nemen, afhankelijk van het aantal gevolgde objecten dat je hebt.", + "confirmDesc": "Weet u zeker dat u alle gevolgde object-embeddings opnieuw wilt indexeren? Dit proces wordt op de achtergrond uitgevoerd, maar kan uw CPU zwaar belasten en enige tijd in beslag nemen. U kunt de voortgang bekijken op de pagina Verkennen." + }, + "modelSize": { + "label": "Modelgrootte", + "desc": "De grootte van het model dat wordt gebruikt voor semantische zoekopdrachten.", + "small": { + "title": "klein", + "desc": "Het gebruik van small maakt gebruik van een gequantiseerde versie van het model die minder RAM verbruikt en sneller draait op de CPU, met een verwaarloosbaar verschil in embeddingkwaliteit." + }, + "large": { + "title": "groot", + "desc": "Het gebruik van large maakt gebruik van het volledige Jina-model en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + } + }, + "readTheDocumentation": "Lees de documentatie", + "desc": "Met semantisch zoeken in Frigate kun je getraceerde objecten in je overzichtsitems vinden aan de hand van de afbeelding zelf, een door de gebruiker gedefinieerde tekstbeschrijving of een automatisch gegenereerde beschrijving." + }, + "faceRecognition": { + "title": "Gezichtsherkenning", + "modelSize": { + "label": "Modelgrootte", + "desc": "De grootte van het model dat gebruikt wordt voor gezichtsherkenning.", + "small": { + "title": "klein", + "desc": "Met small wordt een FaceNet-model voor gezichtsinbedding gebruikt dat efficiënt werkt op de meeste CPU's." + }, + "large": { + "desc": "Het gebruik van groot maakt gebruik van een ArcFace-gezichtsembeddingmodel en wordt automatisch op de GPU uitgevoerd als die beschikbaar is.", + "title": "groot" + } + }, + "desc": "Gezichtsherkenning maakt het mogelijk om namen aan mensen toe te wijzen. Wanneer hun gezicht wordt herkend, wijst Frigate de naam van de persoon toe als sublabel. Deze informatie is opgenomen in de gebruikersinterface, filters en meldingen.", + "readTheDocumentation": "Lees de documentatie" + }, + "title": "Classificatie-instellingen", + "licensePlateRecognition": { + "title": "Kentekenherkenning", + "readTheDocumentation": "Lees de documentatie", + "desc": "Frigate kan kentekenplaten op voertuigen herkennen en automatisch de gedetecteerde tekens toevoegen aan het veld recognized_license_plate of een bekende naam als sublabel toekennen aan objecten van het type auto. Een veelvoorkomende toepassing is het uitlezen van kentekens van auto's die een oprit oprijden of voorbijrijden op straat." + }, + "toast": { + "success": "Classificatie-instellingen zijn opgeslagen. Start Frigate opnieuw op om de wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + }, + "birdClassification": { + "title": "Vogelclassificatie", + "desc": "Vogelclassificatie herkent bekende vogels met behulp van een gequantiseerd TensorFlow-model. Wanneer een bekende vogel wordt herkend, wordt de algemene naam toegevoegd als sublabel. Deze informatie wordt weergegeven in de interface, is beschikbaar in filters en wordt ook opgenomen in meldingen." + }, + "restart_required": "Opnieuw opstarten vereist (Classificatie-instellingen gewijzigd)", + "unsavedChanges": "Niet-opgeslagen wijzigingen in de classificatie-instellingen" + }, + "camera": { + "review": { + "title": "Beoordeel", + "alerts": "Meldingen ", + "detections": "Detecties ", + "desc": "Schakel waarschuwingen en detecties voor deze camera tijdelijk in of uit totdat Frigate opnieuw wordt gestart. Wanneer uitgeschakeld, worden er geen nieuwe beoordelingsitems gegenereerd. " + }, + "reviewClassification": { + "objectAlertsTips": "Alle {{alertsLabels}}-objecten op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objecten die zijn gedetecteerd in {{zone}} op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties.", + "notSelectDetections": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} worden gedetecteerd en niet als waarschuwing zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden." + }, + "selectAlertsZones": "Zones selecteren voor meldingen", + "selectDetectionsZones": "Selecteer zones voor detecties", + "limitDetections": "Beperk detecties tot specifieke zones", + "toast": { + "success": "Configuratie voor beoordelingsclassificatie is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + }, + "readTheDocumentation": "Lees de documentatie", + "noDefinedZones": "Voor deze camera zijn nog geen zones ingesteld.", + "desc": "Frigate categoriseert beoordelingsitems als waarschuwingen en detecties.Standaard worden alle person- en car-objecten als waarschuwingen beschouwd. Je kunt de categorisatie verfijnen door zones te configureren waarin uitsluitend deze objecten gedetecteerd moeten worden.", + "title": "Beoordelingsclassificatie", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties, ongeacht in welke zone ze zich bevinden.", + "unsavedChanges": "Niet-opgeslagen classificatie-instellingen voor {{camera}}" + }, + "streams": { + "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "title": "Streams" + }, + "title": "Camera-instellingen", + "object_descriptions": { + "title": "AI-gegenereerde objectomschrijvingen", + "desc": "AI-gegenereerde objectomschrijvingen tijdelijk uitschakelen voor deze camera. Wanneer uitgeschakeld, zullen omschrijvingen van gevolgde objecten op deze camera niet aangevraagd worden." + }, + "review_descriptions": { + "title": "Generatieve-AI Beoordelingsbeschrijvingen", + "desc": "Tijdelijk generatieve-AI-beoordelingsbeschrijvingen voor deze camera in- of uitschakelen. Wanneer dit is uitgeschakeld, worden er geen door AI gegenereerde beschrijvingen opgevraagd voor beoordelingsitems van deze camera." + }, + "addCamera": "Nieuwe camera toevoegen", + "editCamera": "Camera bewerken:", + "selectCamera": "Selecteer een camera", + "backToSettings": "Terug naar camera-instellingen", + "cameraConfig": { + "add": "Camera toevoegen", + "edit": "Camera bewerken", + "description": "Configureer de camera-instellingen, inclusief streaming-inputs en functies.", + "name": "Cameranaam", + "nameRequired": "Cameranaam is vereist", + "nameInvalid": "De cameranaam mag alleen letters, cijfers, onderstrepingstekens of koppeltekens bevatten", + "namePlaceholder": "bijv. voor_deur", + "enabled": "Ingeschakeld", + "ffmpeg": { + "inputs": "Streams-Input", + "path": "Stroompad", + "pathRequired": "Streampad is vereist", + "pathPlaceholder": "rtsp://...", + "roles": "Functie", + "rolesRequired": "Er is ten minste één functie vereist", + "rolesUnique": "Elke functie (audio, detecteren, opnemen) kan slechts aan één stream worden toegewezen", + "addInput": "Inputstream toevoegen", + "removeInput": "Inputstream verwijderen", + "inputsRequired": "Er is ten minste één stream-input vereist" + }, + "toast": { + "success": "Camera {{cameraName}} is succesvol opgeslagen" + }, + "nameLength": "Cameranaam mag niet langer zijn dan 24 tekens." + } + }, + "masksAndZones": { + "filter": { + "all": "Alle maskers en zones" + }, + "toast": { + "success": { + "copyCoordinates": "Coördinaten voor {{polyName}} gekopieerd naar klembord." + }, + "error": { + "copyCoordinatesFailed": "De coördinaten konden niet naar het klembord worden gekopieerd." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "De zonenaam moet minimaal 2 tekens lang zijn.", + "mustNotContainPeriod": "De zonenaam mag geen punten bevatten.", + "hasIllegalCharacter": "De zonenaam bevat ongeldige tekens.", + "mustNotBeSameWithCamera": "De zonenaam mag niet gelijk zijn aan de cameranaam.", + "alreadyExists": "Er bestaat al een zone met deze naam voor deze camera.", + "mustHaveAtLeastOneLetter": "De zonenaam moet minimaal één letter bevatten." + } + }, + "distance": { + "error": { + "text": "Afstand moet groter dan of gelijk zijn aan 0,1.", + "mustBeFilled": "Alle afstandsvelden moeten worden ingevuld om de snelheid te kunnen schatten." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "De minimale snelheid moet meer zijn dan 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "De verblijftijd moet groter dan of gelijk aan 0 zijn." + } + }, + "polygonDrawing": { + "removeLastPoint": "Laatste punt verwijderen", + "snapPoints": { + "true": "Verbind punten", + "false": "Punten niet verbinden" + }, + "delete": { + "title": "Bevestig Verwijderen", + "desc": "Weet je zeker dat je de {{type}} {{name}} wilt verwijderen?", + "success": "{{name}} is verwijderd." + }, + "error": { + "mustBeFinished": "De polygoontekening moet voltooid zijn voordat u deze kunt opslaan." + }, + "reset": { + "label": "Alle punten wissen" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "De snelheidsdrempel moet groter dan of gelijk zijn aan 0,1." + } + } + }, + "zones": { + "documentTitle": "Bewerkingszone - Frigate", + "desc": { + "title": "Zones stellen je in staat om een specifiek gedeelte van het beeld te definiëren, zodat je kunt bepalen of een object zich binnen dat gebied bevindt of niet.", + "documentation": "Documentatie" + }, + "edit": "Bewerk zone", + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "name": { + "title": "Naam", + "inputPlaceHolder": "Voer een naam in…", + "tips": "De naam moet minimaal 2 tekens lang zijn, minimaal één letter bevatten en mag niet gelijk zijn aan de naam van een camera of andere zone op deze camera." + }, + "inertia": { + "title": "Traagheid", + "desc": "Geeft aan hoeveel frames een object in een zone moet zijn voordat het als 'in de zone' wordt beschouwd. Standaard: 3" + }, + "loiteringTime": { + "title": "Stationaire tijd", + "desc": "Stelt de minimale tijd in (in seconden) die een object in de zone moet blijven voordat deze wordt geactiveerd. Standaard: 0" + }, + "objects": { + "title": "Objecten", + "desc": "Lijst met objecten die van toepassing zijn op deze zone." + }, + "speedEstimation": { + "desc": "Snelheidsschatting inschakelen voor objecten in deze zone. De zone moet precies 4 punten hebben.", + "title": "Snelheidsschatting", + "docs": "Lees de documentatie", + "lineADistance": "Afstand van lijn A ({{unit}})", + "lineBDistance": "Afstand van lijn B ({{unit}})", + "lineCDistance": "Afstand van lijn C ({{unit}})", + "lineDDistance": "Afstand van lijn D ({{unit}})" + }, + "speedThreshold": { + "desc": "Geeft een minimumsnelheid op voor objecten die in deze zone moeten worden beschouwd.", + "toast": { + "error": { + "pointLengthError": "De snelheidsschatting is uitgeschakeld voor deze zone. Zones met snelheidsschatting moeten precies 4 punten hebben.", + "loiteringTimeError": "Zones met een stationaire tijd groter dan 0 mogen niet worden gebruikt in combinatie met snelheidsschatting." + } + }, + "title": "Snelheidsdrempel ({{unit}})" + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "label": "Zones", + "add": "Zone toevoegen", + "allObjects": "Alle objecten", + "toast": { + "success": "Zone ({{zoneName}}) is opgeslagen." + } + }, + "motionMasks": { + "label": "Bewegingsmasker", + "documentTitle": "Bewerken Bewegingsmasker - Frigate", + "desc": { + "documentation": "Documentatie", + "title": "Bewegingsmaskers worden gebruikt om te voorkomen dat ongewenste vormen van beweging een detectie activeren. Te veel maskeren maakt het moeilijker om objecten te volgen." + }, + "edit": "Bewerk bewegingsmasker", + "context": { + "documentation": "Lees de documentatie", + "title": "Bewegingsmaskers worden gebruikt om te voorkomen dat ongewenste soorten beweging een detectie activeren (bijvoorbeeld: bewegende boomtakken of tijdstempels in het camerabeeld). Bewegingsmaskers moeten zeer spaarzaam worden gebruikt – te veel maskeren maakt het moeilijker om objecten te volgen." + }, + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "polygonAreaTooLarge": { + "documentation": "Lees de documentatie", + "title": "Het bewegingsmasker bedekt {{polygonArea}}% van het camerabeeld. Grote bewegingsmaskers worden niet aanbevolen.", + "tips": "Bewegingsmaskers voorkomen niet dat objecten worden gedetecteerd. Gebruik in plaats daarvan een objectmasker." + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "toast": { + "success": { + "title": "{{polygonName}} is opgeslagen.", + "noName": "Bewegingsmasker is opgeslagen." + } + }, + "add": "Nieuw bewegingsmasker" + }, + "objectMasks": { + "label": "Objectmaskers", + "documentTitle": "Objectmasker bewerken - Frigate", + "desc": { + "title": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.", + "documentation": "Documentatie" + }, + "add": "Objectmasker toevoegen", + "objects": { + "desc": "Het objecttype dat van toepassing is op dit objectmasker.", + "allObjectTypes": "Alle objecttypen", + "title": "Objecten" + }, + "toast": { + "success": { + "title": "{{polygonName}} is opgeslagen.", + "noName": "Objectmasker is opgeslagen." + } + }, + "point_one": "{{count}} punt", + "point_other": "{{count}} punten", + "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", + "context": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.", + "edit": "Objectmasker bewerken" + }, + "restart_required": "Herstart vereist (maskers/zones gewijzigd)", + "motionMaskLabel": "Bewegingsmasker {{number}}", + "objectMaskLabel": "Objectmasker {{number}} ({{label}})" + }, + "motionDetectionTuner": { + "title": "Bewegingsdetectie-afsteller", + "desc": { + "title": "Frigate gebruikt bewegingsdetectie als eerste controle om te zien of er iets gebeurt in het frame dat de moeite waard is om te controleren met objectdetectie.", + "documentation": "Lees de handleiding voor bewegingsafstelling" + }, + "Threshold": { + "title": "Drempelwaarde", + "desc": "De drempelwaarde bepaalt hoeveel verandering in de luminantie van een pixel nodig is om als beweging te worden beschouwd. Standaard: 30" + }, + "contourArea": { + "title": "Contourgebied", + "desc": "De waarde voor het contourgebied wordt gebruikt om te bepalen welke groepen gewijzigde pixels in aanmerking komen als beweging. Standaard: 10" + }, + "improveContrast": { + "title": "Contrast verbeteren", + "desc": "Verbeter het contrast bij weinig licht. Standaard: AAN" + }, + "toast": { + "success": "De bewegingsinstellingen zijn opgeslagen." + }, + "unsavedChanges": "Niet-opgeslagen wijzigingen in Bewegingsdetectie-afsteller ({{camera}})" + }, + "debug": { + "title": "Foutopsporing", + "desc": "De debugweergave toont een realtime overzicht van gevolgde objecten en hun statistieken. De objectlijst toont een samenvatting van gedetecteerde objecten met een tijdsvertraging.", + "debugging": "Foutopsporing", + "objectList": "Objectenlijst", + "noObjects": "Geen objecten", + "boundingBoxes": { + "title": "Objectkaders", + "desc": "Toon objectkaders rond gevolgde objecten", + "colors": { + "label": "Kleuren van objectkaders", + "info": "
  • Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel.
  • Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd.
  • Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend.
  • Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld).
  • " + } + }, + "timestamp": { + "title": "Tijdstempel", + "desc": "Toon een tijdstempel als overlay op het beeld" + }, + "zones": { + "desc": "Toon een overzicht van alle gedefinieerde zones", + "title": "Zones" + }, + "mask": { + "title": "Bewegingsmaskers", + "desc": "Toon bewegingsmasker-polygonen" + }, + "motion": { + "title": "Bewegingskaders", + "desc": "Toon kaders rondom gebieden waar beweging wordt gedetecteerd", + "tips": "

    Bewegingskaders


    Rode kaders worden over het beeld geplaatst op de plekken waar momenteel beweging wordt gedetecteerd.

    " + }, + "regions": { + "title": "Regio's", + "desc": "Toon een kader rond het interessegebied dat naar de objectdetector wordt gestuurd", + "tips": "

    Interessekaders


    Heldergroene kaders worden over het beeld geplaatst op de interessegebieden die naar de objectdetector worden gestuurd.

    " + }, + "objectShapeFilterDrawing": { + "title": "Objectvormfilter tekenen", + "desc": "Teken een rechthoek op het beeld om details over oppervlakte en verhouding te bekijken", + "document": "Lees de documentatie ", + "area": "Gebied", + "tips": "Schakel deze optie in om een rechthoek op het camerabeeld te tekenen die de oppervlakte en verhouding weergeeft. Deze waarden kunnen vervolgens worden gebruikt om parameters voor het objectvormfilter in je configuratie in te stellen.", + "score": "Score", + "ratio": "Verhouding" + }, + "detectorDesc": "Frigate gebruikt je detectoren ({{detectors}}) om objecten in de videostream van je camera te detecteren.", + "paths": { + "title": "Paden", + "desc": "Toon belangrijke punten van het pad van het gevolgde object", + "tips": "

    Paden


    Lijnen en cirkels geven belangrijke punten aan waar het gevolgde object zich tijdens zijn levensduur heeft verplaatst.

    " + }, + "openCameraWebUI": "Open de webinterface van {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "Geen audiodetecties", + "score": "score", + "currentRMS": "Huidige RMS", + "currentdbFS": "Huidige dbFS" + } + }, + "users": { + "title": "Gebruikers", + "management": { + "desc": "Beheer de gebruikersaccounts van deze Frigate-installatie.", + "title": "Gebruikersbeheer" + }, + "addUser": "Gebruiker toevoegen", + "updatePassword": "Wachtwoord bijwerken", + "toast": { + "success": { + "createUser": "Gebruiker {{user}} succesvol aangemaakt", + "deleteUser": "Gebruiker {{user}} succesvol verwijderd", + "updatePassword": "Wachtwoord succesvol bijgewerkt.", + "roleUpdated": "De rol bijgewerkt voor {{user}}" + }, + "error": { + "setPasswordFailed": "Het wachtwoord kon niet worden opgeslagen: {{errorMessage}}", + "createUserFailed": "Gebruiker aanmaken mislukt: {{errorMessage}}", + "deleteUserFailed": "Gebruiker verwijderen mislukt: {{errorMessage}}", + "roleUpdateFailed": "Rol bijwerken mislukt: {{errorMessage}}" + } + }, + "table": { + "actions": "Acties", + "role": "Rol", + "noUsers": "Geen gebruikers gevonden.", + "changeRole": "Gebruikersrol wijzigen", + "password": "Wachtwoord", + "deleteUser": "Verwijder gebruiker", + "username": "Gebruikersnaam" + }, + "dialog": { + "form": { + "user": { + "desc": "Alleen letters, cijfers, punten en onderstrepingstekens zijn toegestaan.", + "title": "Gebruikersnaam", + "placeholder": "Gebruikersnaam invoeren" + }, + "password": { + "title": "Wachtwoord", + "strength": { + "medium": "Matig", + "strong": "Sterk", + "veryStrong": "Zeer sterk", + "title": "Wachtwoordsterkte: ", + "weak": "Zwak" + }, + "match": "Wachtwoorden komen overeen", + "confirm": { + "title": "Wachtwoord bevestigen", + "placeholder": "Wachtwoord bevestigen" + }, + "placeholder": "Wachtwoord invoeren", + "notMatch": "Wachtwoorden komen niet overeen", + "show": "Wachtwoord weergeven", + "hide": "Wachtwoord verbergen", + "requirements": { + "title": "Wachtwoordvereisten:", + "length": "Minimaal 8 tekens", + "uppercase": "Minimaal één hoofdletter", + "digit": "Minimaal één cijfer", + "special": "Minimaal één speciaal teken (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nieuw wachtwoord", + "placeholder": "Voer een nieuw wachtwoord in", + "confirm": { + "placeholder": "Voer het nieuwe wachtwoord opnieuw in" + } + }, + "usernameIsRequired": "Gebruikersnaam is vereist", + "passwordIsRequired": "Wachtwoord is vereist", + "currentPassword": { + "title": "Huidig wachtwoord", + "placeholder": "Voer uw huidige wachtwoord in" + } + }, + "createUser": { + "title": "Nieuwe gebruiker aanmaken", + "desc": "Voeg een nieuw gebruikersaccount toe en geef een rol op voor toegang tot onderdelen van de Frigate-interface.", + "usernameOnlyInclude": "Gebruikersnaam mag alleen letters, cijfers, . of _ bevatten", + "confirmPassword": "Bevestig uw wachtwoord" + }, + "deleteUser": { + "title": "Verwijder gebruiker", + "warn": "Weet je zeker dat je {{username}} wilt verwijderen?", + "desc": "Deze actie kan niet ongedaan worden gemaakt. Het gebruikersaccount wordt permanent verwijderd, samen met alle bijbehorende gegevens." + }, + "changeRole": { + "desc": "Machtigingen bijwerken voor {{username}}", + "title": "Gebruikersrol wijzigen", + "roleInfo": { + "intro": "Selecteer een gepaste rol voor deze gebruiker:", + "admin": "Beheerder", + "adminDesc": "Volledige toegang tot alle functies.", + "viewer": "Kijker", + "viewerDesc": "Alleen toegang tot Live-dashboards, Beoordelen, Verkennen en Exports.", + "customDesc": "Aangepaste rol met specifieke cameratoegang." + }, + "select": "Selecteer een rol" + }, + "passwordSetting": { + "setPassword": "Wachtwoord instellen", + "updatePassword": "Wachtwoord bijwerken voor {{username}}", + "desc": "Maak een sterk wachtwoord aan om dit account te beveiligen.", + "cannotBeEmpty": "Het wachtwoord kan niet leeg zijn", + "doNotMatch": "Wachtwoorden komen niet overeen", + "currentPasswordRequired": "Huidig wachtwoord is vereist", + "incorrectCurrentPassword": "Het huidige wachtwoord is onjuist", + "passwordVerificationFailed": "Wachtwoord kan niet worden geverifieerd", + "multiDeviceWarning": "Op alle andere apparaten waarop u bent ingelogd, wordt u binnen {{refresh_time}} gevraagd opnieuw in te loggen. U kunt ook alle gebruikers direct opnieuw laten inloggen door uw JWT-secret te vernieuwen." + } + } + }, + "notification": { + "notificationSettings": { + "title": "Meldingen instellen", + "desc": "Frigate kan rechtstreeks pushmeldingen naar uw apparaat verzenden als het in de browser actief is of als een PWA geïnstalleerd is.", + "documentation": "Lees de documentatie" + }, + "notificationUnavailable": { + "title": "Meldingen niet beschikbaar", + "documentation": "Lees de documentatie", + "desc": "Webpushmeldingen vereisen een veilige omgeving (https://…). Dit is een beperking van de browser. Open Frigate via een beveiligde verbinding om meldingen te kunnen ontvangen." + }, + "globalSettings": { + "title": "Globale instellingen", + "desc": "Meldingen voor specifieke camera's op alle geregistreerde apparaten tijdelijk uitschakelen." + }, + "email": { + "title": "E-mail", + "placeholder": "bijv. voorbeeld@email.com", + "desc": "Een geldig e-mailadres is verplicht en wordt gebruikt om je te waarschuwen als er problemen zijn met de pushmeldingsdienst." + }, + "cameras": { + "noCameras": "Geen camera's beschikbaar", + "desc": "Selecteer voor welke camera's je meldingen wilt inschakelen.", + "title": "Camera's" + }, + "deviceSpecific": "Apparaatspecifieke instellingen", + "active": "Meldingen actief", + "suspendTime": { + "5minutes": "Onderbreek voor 5 minuten", + "30minutes": "Onderbreek voor 30 minuten", + "1hour": "Onderbreek voor 1 uur", + "12hours": "Onderbreek voor 12 uur", + "24hours": "Onderbreek voor 24 uur", + "untilRestart": "Opschorten tot herstart", + "10minutes": "Onderbreek voor 10 minuten", + "suspend": "Pauzeren" + }, + "cancelSuspension": "Onderbreking annuleren", + "toast": { + "success": { + "settingSaved": "De instellingen voor meldingen zijn opgeslagen.", + "registered": "Succesvol geregistreerd voor meldingen. Het opnieuw starten van Frigate is vereist voordat meldingen kunnen worden verzonden (inclusief een testmelding)." + }, + "error": { + "registerFailed": "Het opslaan van de meldingsregistratie is mislukt." + } + }, + "title": "Meldingen", + "sendTestNotification": "Stuur een testmelding", + "registerDevice": "Registreer dit apparaat", + "unregisterDevice": "Dit apparaat afmelden", + "suspended": "Meldingen onderbroken {{time}}", + "unsavedChanges": "Niet-opgeslagen wijzigingen in meldingen", + "unsavedRegistrations": "Niet-opgeslagen notificatieregistraties" + }, + "frigatePlus": { + "title": "Frigate+ Instellingen", + "apiKey": { + "title": "Frigate+ API-sleutel", + "plusLink": "Lees meer over Frigate+", + "validated": "Frigate+ API-sleutel is gedetecteerd en gevalideerd", + "desc": "Met de Frigate+ API-sleutel is integratie met de Frigate+ service mogelijk.", + "notValidated": "Frigate+ API-sleutel wordt niet gedetecteerd of niet gevalideerd" + }, + "snapshotConfig": { + "title": "Snapshot-configuratie", + "desc": "Om te verzenden naar Frigate+ moeten zowel snapshots als clean_copy-snapshots ingeschakeld zijn in je configuratie.", + "documentation": "Lees de documentatie", + "table": { + "camera": "Camera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "cleanCopyWarning": "Bij sommige camera's zijn snapshots ingeschakeld, maar ontbreekt de 'clean_copy'. Om afbeeldingen van deze camera's naar Frigate+ te kunnen verzenden, moet clean_copy zijn ingeschakeld in de snapshotconfiguratie." + }, + "modelInfo": { + "title": "Modelinformatie", + "modelType": "Type model", + "trainDate": "Trainingsdatum", + "baseModel": "Basismodel", + "cameras": "Camera's", + "error": "Het laden van modelinformatie is mislukt", + "loadingAvailableModels": "Beschikbare modellen laden…", + "modelSelect": "Je beschikbare modellen op Frigate+ kunnen hier worden geselecteerd. Houd er rekening mee dat alleen modellen die compatibel zijn met je huidige detectorconfiguratie geselecteerd kunnen worden.", + "dimensions": "Afmetingen", + "supportedDetectors": "Ondersteunde detectoren", + "availableModels": "Beschikbare modellen", + "loading": "Modelinformatie laden…", + "plusModelType": { + "baseModel": "Basismodel", + "userModel": "Verfijnd" + } + }, + "toast": { + "success": "Frigate+ instellingen zijn opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + }, + "restart_required": "Herstart vereist (Frigate+ model gewijzigd)", + "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen" + }, + "enrichments": { + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Weet u zeker dat u alle gevolgde object-embeddings opnieuw wilt indexeren? Dit proces wordt op de achtergrond uitgevoerd, maar kan uw CPU zwaar belasten en enige tijd in beslag nemen. U kunt de voortgang bekijken op de pagina Verkenner.", + "label": "Nu opnieuw indexeren", + "desc": "Opnieuw indexeren zal embeddings regenereren voor alle gevolgde objecten. Dit proces wordt op de achtergrond uitgevoerd en kan je CPU zwaar belasten en een behoorlijke hoeveelheid tijd in beslag nemen, afhankelijk van het aantal gevolgde objecten dat je hebt.", + "confirmButton": "Opnieuw indexeren", + "success": "Het herindexeren is succesvol gestart.", + "alreadyInProgress": "Het herindexeren is al bezig.", + "error": "Het opnieuw indexeren is mislukt: {{errorMessage}}", + "confirmTitle": "Bevestig herindexering" + }, + "modelSize": { + "large": { + "title": "groot", + "desc": "Het gebruik van large maakt gebruik van het volledige Jina-model en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + }, + "label": "Modelgrootte", + "desc": "De grootte van het model dat wordt gebruikt voor semantische zoekopdrachten.", + "small": { + "title": "klein", + "desc": "Het gebruik van small maakt gebruik van een gequantiseerde versie van het model die minder RAM verbruikt en sneller draait op de CPU, met een verwaarloosbaar verschil in embeddingkwaliteit." + } + }, + "title": "Semantisch zoeken", + "desc": "Semantisch zoeken in Frigate stelt je in staat om getraceerde objecten binnen je review-items te vinden, met behulp van de afbeelding zelf, een door de gebruiker gedefinieerde tekstbeschrijving of een automatisch gegenereerde beschrijving.", + "readTheDocumentation": "Lees de documentatie" + }, + "faceRecognition": { + "modelSize": { + "small": { + "desc": "Met small wordt een FaceNet-model voor gezichtsinbedding gebruikt dat efficiënt werkt op de meeste CPU's.", + "title": "klein" + }, + "desc": "De grootte van het model dat gebruikt wordt voor gezichtsherkenning.", + "label": "Modelgrootte", + "large": { + "title": "groot", + "desc": "Het gebruik van groot maakt gebruik van een ArcFace-gezichtsembeddingmodel en wordt automatisch op de GPU uitgevoerd als die beschikbaar is." + } + }, + "desc": "Gezichtsherkenning maakt het mogelijk om namen aan mensen toe te wijzen. Wanneer hun gezicht wordt herkend, wijst Frigate de naam van de persoon toe als sublabel. Deze informatie is opgenomen in de gebruikersinterface, filters en meldingen.", + "title": "Gezichtsherkenning", + "readTheDocumentation": "Lees de documentatie" + }, + "licensePlateRecognition": { + "desc": "Frigate kan kentekenplaten op voertuigen herkennen en automatisch de gedetecteerde tekens toevoegen aan het veld recognized_license_plate of een bekende naam als sublabel toekennen aan objecten van het type auto. Een veelvoorkomende toepassing is het uitlezen van kentekens van auto's die een oprit oprijden of voorbijrijden op straat.", + "title": "Kentekenherkenning", + "readTheDocumentation": "Lees de documentatie" + }, + "birdClassification": { + "desc": "Vogelclassificatie herkent bekende vogels met behulp van een gequantiseerd TensorFlow-model. Wanneer een bekende vogel wordt herkend, wordt de algemene naam toegevoegd als sublabel. Deze informatie wordt weergegeven in de interface, is beschikbaar in filters en wordt ook opgenomen in meldingen.", + "title": "Vogelclassificatie" + }, + "title": "Verrijkingsinstellingen", + "unsavedChanges": "Niet-opgeslagen wijzigingen in verrijkingsinstellingen", + "restart_required": "Opnieuw opstarten vereist (verrijkingsinstellingen gewijzigd)", + "toast": { + "success": "Verrijkingsinstellingen zijn opgeslagen. Start Frigate opnieuw op om je wijzigingen toe te passen.", + "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Triggers", + "management": { + "title": "Triggers", + "desc": "Beheer triggers voor {{camera}}. Gebruik een thumbnail om te triggeren op vergelijkbare thumbnails van het door jou gevolgde object, of gebruik een objectbeschrijving om te triggeren op vergelijkbare beschrijvingen van de door jou opgegeven tekst." + }, + "addTrigger": "Trigger toevoegen", + "table": { + "name": "Naam", + "type": "Type", + "content": "Inhoud", + "threshold": "Drempel", + "actions": "Acties", + "noTriggers": "Er zijn geen triggers geconfigureerd voor deze camera.", + "edit": "Bewerken", + "deleteTrigger": "Trigger verwijderen", + "lastTriggered": "Laatst geactiveerd" + }, + "type": { + "thumbnail": "Thumbnail", + "description": "Beschrijving" + }, + "actions": { + "alert": "Markeren als waarschuwing", + "notification": "Melding verzenden", + "sub_label": "Sublabel toevoegen", + "attribute": "Attribuut toevoegen" + }, + "dialog": { + "createTrigger": { + "title": "Trigger aanmaken", + "desc": "Maak een trigger voor camera {{camera}}" + }, + "editTrigger": { + "title": "Trigger bewerken", + "desc": "Wijzig de instellingen voor de trigger op camera {{camera}}" + }, + "deleteTrigger": { + "title": "Trigger verwijderen", + "desc": "Weet u zeker dat u de trigger {{triggerName}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." + }, + "form": { + "name": { + "title": "Naam", + "placeholder": "Geef deze trigger een naam", + "error": { + "minLength": "Het veld moet minimaal 2 tekens lang zijn.", + "invalidCharacters": "Dit veld mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.", + "alreadyExists": "Er bestaat al een trigger met deze naam voor deze camera." + }, + "description": "Voer een unieke naam of beschrijving in om deze trigger te identificeren" + }, + "enabled": { + "description": "Deze trigger in- of uitschakelen" + }, + "type": { + "title": "Type", + "placeholder": "Selecteer het type trigger", + "description": "Activeer wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd", + "thumbnail": "Activeer wanneer een vergelijkbare thumbnail van een gevolgd object wordt gedetecteerd" + }, + "content": { + "title": "Inhoud", + "imagePlaceholder": "Selecteer een thumbnail", + "textPlaceholder": "Tekst invoeren", + "imageDesc": "Alleen de meest recente 100 thumbnails worden weergegeven. Als je de gewenste thumbnail niet kunt vinden, bekijk dan eerdere objecten in Verkennen en stel daar een trigger in via het menu.", + "textDesc": "Voer tekst in om deze actie te activeren wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd.", + "error": { + "required": "Inhoud is vereist." + } + }, + "threshold": { + "title": "Drempel", + "error": { + "min": "De drempelwaarde moet minimaal 0 zijn", + "max": "De drempelwaarde mag maximaal 1 zijn" + }, + "desc": "Stel de vergelijkingsdrempel in voor deze trigger. Een hogere drempel betekent dat er een nauwere overeenkomst vereist is om de trigger te activeren." + }, + "actions": { + "title": "Acties", + "desc": "Standaard stuurt Frigate een MQTT-bericht voor alle triggers. Sublabels voegen de triggernaam toe aan het objectlabel. Attributen zijn doorzoekbare metadata die afzonderlijk worden opgeslagen in de metadata van het gevolgde object.", + "error": { + "min": "Er moet ten minste één actie worden geselecteerd." + } + }, + "friendly_name": { + "title": "Gebruiksvriendelijke naam", + "placeholder": "Geef een naam of beschrijf deze trigger", + "description": "Een optionele gebruiksvriendelijke naam of beschrijvende tekst voor deze trigger." + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} is succesvol aangemaakt.", + "updateTrigger": "Trigger {{name}} is succesvol bijgewerkt.", + "deleteTrigger": "Trigger {{name}} succesvol verwijderd." + }, + "error": { + "createTriggerFailed": "Trigger kan niet worden gemaakt: {{errorMessage}}", + "updateTriggerFailed": "Trigger kan niet worden bijgewerkt: {{errorMessage}}", + "deleteTriggerFailed": "Trigger kan niet worden verwijderd: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisch zoeken is uitgeschakeld", + "desc": "Semantisch zoeken moet ingeschakeld zijn om triggers te kunnen gebruiken." + }, + "wizard": { + "title": "Trigger maken", + "step1": { + "description": "Configureer de basisinstellingen voor uw trigger." + }, + "step2": { + "description": "Stel de inhoud in die deze trigger activeert." + }, + "step3": { + "description": "Configureer de drempelwaarde en acties voor deze trigger." + }, + "steps": { + "nameAndType": "Naam en type", + "configureData": "Gegevens configureren", + "thresholdAndActions": "Drempel en acties" + } + } + }, + "roles": { + "management": { + "title": "Beheer van kijkersrollen", + "desc": "Beheer aangepaste kijkersrollen en hun camera-toegangsrechten voor deze Frigate-instantie." + }, + "addRole": "Rol toevoegen", + "table": { + "role": "Rol", + "cameras": "Camera's", + "actions": "Acties", + "noRoles": "Er zijn geen aangepaste rollen gevonden.", + "editCameras": "Camera's bewerken", + "deleteRole": "Rol verwijderen" + }, + "toast": { + "success": { + "createRole": "Rol {{role}} succesvol aangemaakt", + "updateCameras": "Camera's bijgewerkt voor rol {{role}}", + "deleteRole": "Rol {{role}} succesvol verwijderd", + "userRolesUpdated_one": "{{count}} gebruiker die aan deze rol was toegewezen, is bijgewerkt naar de rol ‘kijker’, die toegang heeft tot alle camera’s.", + "userRolesUpdated_other": "{{count}} gebruikers die aan deze rol waren toegewezen, zijn bijgewerkt naar de rol ‘kijker’, die toegang heeft tot alle camera’s." + }, + "error": { + "createRoleFailed": "Kan rol niet aanmaken: {{errorMessage}}", + "updateCamerasFailed": "Het is niet gelukt om de camera's bij te werken: {{errorMessage}}", + "deleteRoleFailed": "Kan rol niet verwijderen: {{errorMessage}}", + "userUpdateFailed": "Het bijwerken van gebruikersrollen is mislukt: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Nieuwe rol maken", + "desc": "Voeg een nieuwe rol toe en specificeer de camera-toegangsrechten." + }, + "editCameras": { + "title": "Camera’s voor rol bewerken", + "desc": "Werk de camera-toegang bij voor de rol {{role}}." + }, + "deleteRole": { + "title": "Rol verwijderen", + "desc": "Deze actie kan niet ongedaan worden gemaakt. De rol wordt permanent verwijderd en alle gebruikers met deze rol worden toegewezen aan de rol ‘kijker’, die toegang geeft tot alle camera’s.", + "warn": "Weet u zeker dat u {{role}} wilt verwijderen?", + "deleting": "Verwijderen..." + }, + "form": { + "role": { + "title": "Rolnaam", + "placeholder": "Voer rolnaam in", + "desc": "Alleen letters, cijfers, punten en underscores zijn toegestaan.", + "roleIsRequired": "Rolnaam is vereist", + "roleOnlyInclude": "De rolnaam mag alleen letters, cijfers, . of _ bevatten", + "roleExists": "Er bestaat al een rol met deze naam." + }, + "cameras": { + "title": "Camera's", + "desc": "Selecteer de camera's waartoe deze rol toegang heeft. Er is minimaal één camera vereist.", + "required": "Er moet minimaal één camera worden geselecteerd." + } + } + } + }, + "cameraWizard": { + "title": "Camera toevoegen", + "description": "Volg de onderstaande stappen om een nieuwe camera toe te voegen aan uw Frigate-installatie.", + "steps": { + "nameAndConnection": "Naam & Verbinding", + "streamConfiguration": "Streamconfiguratie", + "validationAndTesting": "Validatie & testen", + "probeOrSnapshot": "Test of Snapshot" + }, + "save": { + "success": "Nieuwe camera {{cameraName}} succesvol opgeslagen.", + "failure": "Fout bij het opslaan van {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolutie", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Geef een geldige stream-URL op", + "testFailed": "Streamtest mislukt: {{error}}" + }, + "step1": { + "description": "Voer de gegevens van uw camera in en kies ervoor om de camera te scannen of selecteer handmatig het merk.", + "cameraName": "Cameranaam", + "cameraNamePlaceholder": "bijv. voordeur of achtertuin camera", + "host": "Host/IP-adres", + "port": "Port", + "username": "Gebruikersnaam", + "usernamePlaceholder": "Optioneel", + "password": "Wachtwoord", + "passwordPlaceholder": "Optioneel", + "selectTransport": "Selecteer transportprotocol", + "cameraBrand": "Cameramerk", + "selectBrand": "Selecteer cameramerk voor URL-sjabloon", + "customUrl": "Aangepaste stream-URL", + "brandInformation": "Merkinformatie", + "brandUrlFormat": "Voor camera's met het RTSP URL-formaat als: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "testConnection": "Testverbinding", + "testSuccess": "Verbindingstest succesvol!", + "testFailed": "Verbindingstest mislukt. Controleer uw invoer en probeer het opnieuw.", + "streamDetails": "Streamdetails", + "warnings": { + "noSnapshot": "Er kan geen snapshot worden opgehaald uit de geconfigureerde stream." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecteer een cameramerk met host/IP of kies 'Overig' voor een aangepaste URL", + "nameRequired": "Cameranaam is vereist", + "nameLength": "De cameranaam mag maximaal 64 tekens lang zijn", + "invalidCharacters": "Cameranaam bevat ongeldige tekens", + "nameExists": "Cameranaam bestaat al", + "brands": { + "reolink-rtsp": "Reolink RTSP wordt niet aanbevolen. Schakel HTTP in via de firmware-instellingen van de camera en start de wizard opnieuw." + }, + "customUrlRtspRequired": "Aangepaste URL’s moeten beginnen met “rtsp://”. Handmatige configuratie is vereist voor camera­streams die geen RTSP gebruiken." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Camerametadata wordt onderzocht...", + "fetchingSnapshot": "Camerasnapshot ophalen..." + }, + "connectionSettings": "Verbindingsinstellingen", + "detectionMethod": "Stream-detectiemethode", + "onvifPort": "ONVIF-poort", + "probeMode": "Camera testen", + "manualMode": "Handmatige selectie", + "detectionMethodDescription": "Test de camera met ONVIF (indien ondersteund) om de stream-URL’s van de camera te vinden, of selecteer handmatig het cameramerk om vooraf gedefinieerde URL’s te gebruiken. Om een aangepaste RTSP-URL in te voeren, kies de handmatige methode en selecteer “Anders”.", + "onvifPortDescription": "Voor camera's die ONVIF ondersteunen, is dit meestal 80 of 8080.", + "useDigestAuth": "Gebruik digest-authenticatie", + "useDigestAuthDescription": "Gebruik HTTP-digestauthenticatie voor ONVIF. Sommige camera’s vereisen mogelijk een aparte ONVIF-gebruikersnaam en -wachtwoord in plaats van de standaard ‘admin’ gebruiker." + }, + "step2": { + "description": "Controleer de camera op beschikbare streams of configureer handmatige instellingen op basis van de door u geselecteerde detectiemethode.", + "streamsTitle": "Camerastreams", + "addStream": "Stream toevoegen", + "addAnotherStream": "Voeg een extra stream toe", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream-URL", + "streamUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "url": "URL", + "resolution": "Resolutie", + "selectResolution": "Selecteer resolutie", + "quality": "Kwaliteit", + "selectQuality": "Selecteer kwaliteit", + "roles": "Functie", + "roleLabels": { + "detect": "Objectdetectie", + "record": "Opname", + "audio": "Audio" + }, + "testStream": "Testverbinding", + "testSuccess": "Verbindingstest succesvol!", + "testFailed": "Verbindingstest mislukt. Controleer uw invoer en probeer het opnieuw.", + "testFailedTitle": "Test mislukt", + "connected": "Aangesloten", + "notConnected": "Niet verbonden", + "featuresTitle": "Functies", + "go2rtc": "Verminder verbindingen met de camera", + "detectRoleWarning": "Er moet minimaal één stream de rol 'detecteren' hebben om door te kunnen gaan.", + "rolesPopover": { + "title": "Streamrollen", + "detect": "Hoofdfeed voor objectdetectie.", + "record": "Slaat segmenten van de videofeed op op basis van de configuratie-instellingen.", + "audio": "Feed voor op audio gebaseerde detectie." + }, + "featuresPopover": { + "title": "Streamfuncties", + "description": "Gebruik go2rtc-herstreaming om het aantal verbindingen met je camera te verminderen." + }, + "streamDetails": "Streamdetails", + "probing": "Camera wordt getest...", + "retry": "Opnieuw proberen", + "testing": { + "probingMetadata": "Camera-metadata onderzoeken...", + "fetchingSnapshot": "Camerasnapshot ophalen..." + }, + "probeFailed": "Het testen van de camera is mislukt: {{error}}", + "probingDevice": "Onderzoekapparaat...", + "probeSuccessful": "Test succesvol", + "probeError": "Testfout", + "probeNoSuccess": "Test mislukt", + "deviceInfo": "Apparaatinformatie", + "manufacturer": "Fabrikant", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profielen", + "ptzSupport": "PTZ-ondersteuning", + "autotrackingSupport": "Ondersteuning voor automatische tracking", + "presets": "Standaardinstellingen", + "rtspCandidates": "RTSP-kandidaten", + "rtspCandidatesDescription": "De volgende RTSP-URL's zijn gevonden door de camera te scannen. Test de verbinding om de metagegevens van de stream te bekijken.", + "noRtspCandidates": "Er zijn geen RTSP-URL’s gevonden van de camera. Je inloggegevens zijn mogelijk onjuist, of de camera ondersteunt ONVIF of de gebruikte methode voor het ophalen van RTSP-URL’s niet. Ga terug en voer de RTSP-URL handmatig in.", + "candidateStreamTitle": "Kandidaat {{number}}", + "useCandidate": "Gebruik", + "uriCopy": "Kopiëren", + "uriCopied": "URI gekopieerd naar klembord", + "testConnection": "Testverbinding", + "toggleUriView": "Klik om te schakelen tussen volledige URI-weergave", + "errors": { + "hostRequired": "Host/IP-adres is vereist" + } + }, + "step3": { + "description": "Configureer streamrollen en voeg extra streams toe voor uw camera.", + "validationTitle": "Streamvalidatie", + "connectAllStreams": "Verbind alle streams", + "reconnectionSuccess": "Opnieuw verbinden gelukt.", + "reconnectionPartial": "Bij sommige streams kon de verbinding niet worden hersteld.", + "streamUnavailable": "Streamvoorbeeld niet beschikbaar", + "reload": "Herladen", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "Geldig", + "failed": "Mislukt", + "notTested": "Niet getest", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "Verbreek verbinding", + "estimatedBandwidth": "Geschatte bandbreedte", + "roles": "Functie", + "none": "Niets", + "error": "Fout", + "streamValidated": "Stream {{number}} is succesvol gevalideerd", + "streamValidationFailed": "Stream {{number}} validatie mislukt", + "saveAndApply": "Nieuwe camera opslaan", + "saveError": "Ongeldige configuratie, Controleer uw instellingen.", + "issues": { + "title": "Streamvalidatie", + "videoCodecGood": "Videocodec is {{codec}}.", + "audioCodecGood": "Audiocodec is {{codec}}.", + "noAudioWarning": "Geen audio gedetecteerd voor deze stream, opnames bevatten geen audio.", + "audioCodecRecordError": "De AAC-audiocodec is vereist om audio in opnames te ondersteunen.", + "audioCodecRequired": "Ter ondersteuning van audiodetectie is een audiostream vereist.", + "restreamingWarning": "Als u het aantal verbindingen met de camera voor de opnamestream vermindert, kan het CPU-gebruik iets toenemen.", + "dahua": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Dahua / Amcrest / EmpireTech camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "hikvision": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "resolutionHigh": "Een resolutie van {{resolution}} kan leiden tot een verhoogd gebruik van systeembronnen.", + "resolutionLow": "Een resolutie van {{resolution}} kan te laag zijn voor betrouwbare detectie van kleine objecten." + }, + "ffmpegModule": "Gebruik stream-compatibiliteitsmodus", + "ffmpegModuleDescription": "Als de stream na meerdere pogingen niet wordt geladen, probeer dit dan in te schakelen. Wanneer deze optie is ingeschakeld, gebruikt Frigate de ffmpeg-module samen met go2rtc. Dit kan zorgen voor een betere compatibiliteit met sommige camerastreams.", + "streamsTitle": "Camerastreams", + "addStream": "Stream toevoegen", + "addAnotherStream": "Voeg een extra stream toe", + "streamUrl": "Stream-URL", + "streamUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", + "selectStream": "Selecteer een stream", + "searchCandidates": "Zoek kandidaten...", + "noStreamFound": "Geen stream gevonden", + "url": "URL", + "resolution": "Resolutie", + "selectResolution": "Selecteer resolutie", + "quality": "Kwaliteit", + "selectQuality": "Selecteer kwaliteit", + "roleLabels": { + "detect": "Objectdetectie", + "record": "Opname", + "audio": "Audio" + }, + "testStream": "Testverbinding", + "testSuccess": "Streamtest succesvol!", + "testFailed": "Streamtest mislukt", + "testFailedTitle": "Test mislukt", + "connected": "Aangesloten", + "notConnected": "Niet verbonden", + "featuresTitle": "Functies", + "go2rtc": "Verminder verbindingen met de camera", + "detectRoleWarning": "Er moet minimaal één stream de rol 'detecteren' hebben om door te kunnen gaan.", + "rolesPopover": { + "title": "Streamrollen", + "detect": "Hoofdstream voor objectdetectie.", + "record": "Slaat segmenten van de videostream op op basis van de configuratie-instellingen.", + "audio": "Stream voor op audio gebaseerde detectie." + }, + "featuresPopover": { + "title": "Streamfuncties", + "description": "Gebruik go2rtc-herstreaming om het aantal verbindingen met je camera te verminderen." + } + }, + "step4": { + "description": "Laatste controle en analyse voordat je je nieuwe camera opslaat. Verbind elke stream voordat je opslaat.", + "validationTitle": "Streamvalidatie", + "connectAllStreams": "Verbind alle streams", + "reconnectionSuccess": "Opnieuw verbinden gelukt.", + "reconnectionPartial": "Bij sommige streams kon de verbinding niet worden hersteld.", + "streamUnavailable": "Streamvoorbeeld niet beschikbaar", + "reload": "Herladen", + "connecting": "Verbinden...", + "streamTitle": "Stream {{number}}", + "valid": "Geldig", + "failed": "Mislukt", + "notTested": "Niet getest", + "connectStream": "Verbinden", + "connectingStream": "Verbinden", + "disconnectStream": "Verbreek verbinding", + "estimatedBandwidth": "Geschatte bandbreedte", + "roles": "Rollen", + "ffmpegModule": "Gebruik stream-compatibiliteitsmodus", + "ffmpegModuleDescription": "Als de stream na meerdere pogingen niet wordt geladen, probeer dit dan in te schakelen. Wanneer deze optie is ingeschakeld, gebruikt Frigate de ffmpeg-module samen met go2rtc. Dit kan zorgen voor een betere compatibiliteit met sommige camerastreams.", + "none": "Geen", + "error": "Fout", + "streamValidated": "Stream {{number}} is succesvol gevalideerd", + "streamValidationFailed": "Stream {{number}} validatie mislukt", + "saveAndApply": "Nieuwe camera opslaan", + "saveError": "Ongeldige configuratie, Controleer uw instellingen.", + "issues": { + "title": "Streamvalidatie", + "videoCodecGood": "Videocodec is {{codec}}.", + "audioCodecGood": "Audiocodec is {{codec}}.", + "resolutionHigh": "Een resolutie van {{resolution}} kan leiden tot een verhoogd gebruik van systeembronnen.", + "resolutionLow": "Een resolutie van {{resolution}} kan te laag zijn voor betrouwbare detectie van kleine objecten.", + "noAudioWarning": "Geen audio gedetecteerd voor deze stream, opnames bevatten geen audio.", + "audioCodecRecordError": "De AAC-audiocodec is vereist om audio in opnames te ondersteunen.", + "audioCodecRequired": "Ter ondersteuning van audiodetectie is een audiostream vereist.", + "restreamingWarning": "Als u het aantal verbindingen met de camera voor de opnamestream vermindert, kan het CPU-gebruik iets toenemen.", + "brands": { + "reolink-rtsp": "Reolink RTSP wordt niet aanbevolen. Schakel HTTP in via de firmware-instellingen van de camera en start de wizard opnieuw." + }, + "dahua": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Dahua / Amcrest / EmpireTech camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + }, + "hikvision": { + "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." + } + } + } + }, + "cameraManagement": { + "title": "Camera’s beheren", + "addCamera": "Nieuwe camera toevoegen", + "editCamera": "Camera bewerken:", + "selectCamera": "Selecteer een camera", + "backToSettings": "Terug naar camera-instellingen", + "streams": { + "title": "Camera's in-/uitschakelen", + "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit." + }, + "cameraConfig": { + "add": "Camera toevoegen", + "edit": "Camera bewerken", + "description": "Configureer de camera-instellingen, inclusief streaming-inputs en functies.", + "name": "Cameranaam", + "nameRequired": "Cameranaam is vereist", + "nameLength": "Cameranaam mag niet langer zijn dan 64 tekens.", + "namePlaceholder": "bijv. voordeur of achtertuin camera", + "enabled": "Ingeschakeld", + "ffmpeg": { + "inputs": "Streams-Input", + "path": "Streampad", + "pathRequired": "Streampad is vereist", + "pathPlaceholder": "rtsp://...", + "roles": "Functie", + "rolesRequired": "Er is ten minste één functie vereist", + "rolesUnique": "Elke functie (audio, detecteren, opnemen) kan slechts aan één stream worden toegewezen", + "addInput": "Inputstream toevoegen", + "removeInput": "Inputstream verwijderen", + "inputsRequired": "Er is ten minste één stream-input vereist" + }, + "go2rtcStreams": "go2C Streams", + "streamUrls": "Stream URLs", + "addUrl": "URL toevoegen", + "addGo2rtcStream": "Voeg go2rtc Stream toe", + "toast": { + "success": "Camera {{cameraName}} is succesvol opgeslagen" + } + } + }, + "cameraReview": { + "title": "Camerabeoordelings-instellingen", + "object_descriptions": { + "title": "AI-gegenereerde objectomschrijvingen", + "desc": "AI-gegenereerde objectomschrijvingen tijdelijk uitschakelen voor deze camera. Wanneer uitgeschakeld, zullen omschrijvingen van gevolgde objecten op deze camera niet aangevraagd worden." + }, + "review_descriptions": { + "title": "Generatieve-AI Beoordelingsbeschrijvingen", + "desc": "Tijdelijk generatieve-AI-beoordelingsbeschrijvingen voor deze camera in- of uitschakelen. Wanneer dit is uitgeschakeld, worden er geen door AI gegenereerde beschrijvingen opgevraagd voor beoordelingsitems van deze camera." + }, + "review": { + "title": "Beoordeel", + "desc": "Schakel waarschuwingen en detecties voor deze camera tijdelijk in of uit totdat Frigate opnieuw wordt gestart. Wanneer uitgeschakeld, worden er geen nieuwe beoordelingsitems gegenereerd. ", + "alerts": "Meldingen ", + "detections": "Detecties " + }, + "reviewClassification": { + "title": "Beoordelingsclassificatie", + "desc": "Frigate categoriseert beoordelingsitems als meldingen en detecties.Standaard worden alle person- en car-objecten als meldingen beschouwd. Je kunt de categorisatie verfijnen door zones te configureren waarin uitsluitend deze objecten gedetecteerd moeten worden.", + "noDefinedZones": "Voor deze camera zijn nog geen zones ingesteld.", + "objectAlertsTips": "Alle {{alertsLabels}}-objecten op {{cameraName}} worden weergegeven als meldingen.", + "zoneObjectAlertsTips": "Alle {{alertsLabels}}-objecten die zijn gedetecteerd in {{zone}} op {{cameraName}} worden weergegeven als meldingen.", + "objectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties ongeacht in welke zone ze zich bevinden.", + "zoneObjectDetectionsTips": { + "text": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties.", + "notSelectDetections": "Alle {{detectionsLabels}}-objecten die in {{zone}} op {{cameraName}} worden gedetecteerd en niet als melding zijn gecategoriseerd, worden weergegeven als detecties – ongeacht in welke zone ze zich bevinden.", + "regardlessOfZoneObjectDetectionsTips": "Alle {{detectionsLabels}}-objecten die op {{cameraName}} niet zijn gecategoriseerd, worden weergegeven als detecties ongeacht in welke zone ze zich bevinden." + }, + "unsavedChanges": "Niet-opgeslagen classificatie-instellingen voor {{camera}}", + "selectAlertsZones": "Zones selecteren voor meldingen", + "selectDetectionsZones": "Selecteer zones voor detecties", + "limitDetections": "Beperk detecties tot specifieke zones", + "toast": { + "success": "Configuratie voor beoordelingsclassificatie is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/nl/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/nl/views/system.json new file mode 100644 index 0000000..9479795 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/nl/views/system.json @@ -0,0 +1,199 @@ +{ + "documentTitle": { + "general": "Algemene Statistieken - Frigate", + "logs": { + "nginx": "Nginx Logboeken - Frigate", + "go2rtc": "Go2RTC Logboeken - Frigate", + "frigate": "Frigate Logboek - Frigate" + }, + "storage": "Opslag Statistieken - Frigate", + "cameras": "Camera Statistieken - Frigate", + "enrichments": "Verrijkings Statistieken - Frigate" + }, + "title": "Systeem", + "metrics": "Systeemstatistieken", + "logs": { + "download": { + "label": "Logs Downloaden" + }, + "copy": { + "label": "Kopieeren naar klembord", + "success": "Logs zijn gekopieerd naar klembord", + "error": "Logs kopieeren naar klembord mislukt" + }, + "type": { + "timestamp": "Tijdsvermelding", + "message": "Bericht", + "tag": "Tag", + "label": "Type" + }, + "toast": { + "error": { + "whileStreamingLogs": "Fout bij streamen van logs: {{errorMessage}}", + "fetchingLogsFailed": "Fout bij ophalen van logs: {{errorMessage}}" + } + }, + "tips": "Logs worden gestreamd vanaf de server" + }, + "general": { + "detector": { + "title": "Detectoren", + "cpuUsage": "Detector CPU-verbruik", + "memoryUsage": "Detector Geheugen Gebruik", + "inferenceSpeed": "Detector Interferentie Snelheid", + "temperature": "Detectortemperatuur", + "cpuUsageInformation": "CPU-gebruik bij het voorbereiden van in en uitvoer van gegevens voor detectiemodellen. Deze waarde geeft geen inferentie gebruik weer, ook niet wanneer een GPU of accelerator wordt gebruikt." + }, + "hardwareInfo": { + "title": "Systeemgegevens", + "gpuUsage": "GPU-verbruik", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo Resultaat", + "returnCode": "Terugvoer Code: {{code}}", + "processError": "Process Fout:", + "processOutput": "Process Resultaat:" + }, + "nvidiaSMIOutput": { + "name": "Naam: {{name}}", + "vbios": "VBios Informatie: {{vbios}}", + "cudaComputerCapability": "CUDA Verwerking Capaciteit: {{cuda_compute}}", + "driver": "Stuurprogramma: {{driver}}", + "title": "Nvidia SMI Uitvoer" + }, + "closeInfo": { + "label": "Sluit GPU info" + }, + "copyInfo": { + "label": "Kopieer GPU Info" + }, + "toast": { + "success": "GPU info gekopieerd naar klembord" + } + }, + "gpuDecoder": "GPU Decodeerder", + "gpuEncoder": "GPU Encodeerder", + "gpuMemory": "GPU-geheugen", + "npuUsage": "NPU-gebruik", + "npuMemory": "NPU-geheugen", + "intelGpuWarning": { + "title": "Waarschuwing Intel GPU-statistieken", + "message": "GPU-statistieken niet beschikbaar", + "description": "Dit is een bekend probleem in de GPU-statistiekentools van Intel (intel_gpu_top). Deze raken defect en geven herhaaldelijk een GPU-gebruik van 0% weer, zelfs wanneer hardware-acceleratie en objectdetectie correct draaien op de (i)GPU. Dit is geen bug in Frigate. Je kunt de host opnieuw opstarten om het tijdelijk op te lossen en te controleren dat de GPU goed werkt. Dit heeft geen invloed op de prestaties." + } + }, + "otherProcesses": { + "processMemoryUsage": "Process Geheugen Gebruik", + "processCpuUsage": "Process CPU-verbruik", + "title": "Verdere Processen" + }, + "title": "Algemeen" + }, + "storage": { + "overview": "Overzicht", + "recordings": { + "title": "Opnames", + "earliestRecording": "Oudste beschikbare opname:", + "tips": "Deze waarde laat het totale opslag ruimte gebruik voor opnames zien in de database van Frigate. Frigate houdt geen opslag gebruiks gegevens bij van alle bestanden op uw schijf." + }, + "cameraStorage": { + "title": "Camera Opslag", + "unusedStorageInformation": "Ongebruikte opslagruimte informatie", + "storageUsed": "Opslag", + "percentageOfTotalUsed": "Percentage van Totaal", + "unused": { + "title": "Ongebruikt", + "tips": "Deze waarde kan de beschikbare opslag ruimte voor Frigate niet goed weergeven indien er ook andere bestanden op de schijf staan. Frigate houdt geen opslag gegevens bij van bestanden buiten haar eigen opnames." + }, + "camera": "Camera", + "bandwidth": "Bandbreedte" + }, + "title": "Opslag", + "shm": { + "title": "SHM (gedeeld geheugen) toewijzing", + "warning": "De huidige SHM-grootte van {{total}} MB is te klein. Vergroot deze tot minimaal {{min_shm}} MB.", + "readTheDocumentation": "Lees de documentatie" + } + }, + "cameras": { + "title": "Cameras", + "overview": "Overzicht", + "info": { + "cameraProbeInfo": "{{camera}} Informatie opgehaald uit de camerastream", + "streamDataFromFFPROBE": "Streamgegevens zijn ontvangen via ffprobe.", + "stream": "Stream {{idx}}", + "resolution": "Resolutie:", + "unknown": "Onbekend", + "error": "Fout: {{error}}", + "tips": { + "title": "Informatie ophalen uit de camerastream" + }, + "fps": "FPS:", + "codec": "Codec:", + "video": "Video:", + "fetching": "Camera Gegevens Opvragen", + "audio": "Audio:", + "aspectRatio": "beeldverhouding" + }, + "framesAndDetections": "Frames / Detecties", + "label": { + "camera": "camera", + "detect": "detectie", + "skipped": "overgeslagen", + "ffmpeg": "FFmpeg", + "capture": "registratie", + "overallSkippedDetectionsPerSecond": "totaal aantal overgeslagen detecties per seconde", + "overallFramesPerSecond": "totale aantal frames per seconde", + "overallDetectionsPerSecond": "totale aantal detecties per seconde", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} frames per seconde", + "cameraDetectionsPerSecond": "{{camName}} detecties per seconde", + "cameraSkippedDetectionsPerSecond": "{{camName}} overgeslagen detecties per seconde", + "cameraCapture": "{{camName}} opname", + "cameraDetect": "{{camName}} detecteren" + }, + "toast": { + "success": { + "copyToClipboard": "Uitvraag gegevens naar klembord gekopieerd." + }, + "error": { + "unableToProbeCamera": "Kan camera niet uitvragen: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Voor het laatst vernieuwd: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} zorgt voor hoge FFmpeg CPU belasting ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} zorgt voor hoge detectie CPU belasting ({{detectAvg}}%)", + "healthy": "Geen problemen", + "reindexingEmbeddings": "Herindexering van inbeddingen ({{processed}}% compleet)", + "detectIsSlow": "{{detect}} is traag ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} is erg traag ({{speed}} ms)", + "cameraIsOffline": "{{camera}} is offline", + "shmTooLow": "Vergroot de /dev/shm toewijzing van {{total}} MB naar minimaal {{min}} MB." + }, + "enrichments": { + "title": "Verrijkingen", + "infPerSecond": "Interferenties Per Seconde", + "embeddings": { + "image_embedding_speed": "Afbeelding Inplaatsings Snelheid", + "face_embedding_speed": "Gezicht Inplaatsings Snelheid", + "text_embedding_speed": "Text Inplaatsing Snelheid", + "plate_recognition_speed": "Kentekenplaat Herkenning Snelheid", + "face_recognition_speed": "Snelheid van gezichtsherkenning", + "image_embedding": "Afbeelding Inbedden", + "text_embedding": "Tekstinbedden", + "face_recognition": "Gezichtsherkenning", + "yolov9_plate_detection_speed": "YOLOv9 Kentekenplaat Detectiesnelheid", + "yolov9_plate_detection": "YOLOv9 Kentekenplaatdetectie", + "plate_recognition": "Kentekenherkenning", + "review_description": "Beoordelingsbeschrijving", + "review_description_speed": "Snelheid beoordelingsbeschrijving", + "review_description_events_per_second": "Beoordelingsbeschrijving", + "object_description": "Objectbeschrijving", + "object_description_speed": "Objectbeschrijvingssnelheid", + "object_description_events_per_second": "Objectbeschrijving" + }, + "averageInf": "Gemiddelde inferentietijd" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/audio.json b/sam2-cpu/frigate-dev/web/public/locales/peo/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/audio.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/common.json b/sam2-cpu/frigate-dev/web/public/locales/peo/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/common.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/input.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/peo/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/components/player.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/objects.json b/sam2-cpu/frigate-dev/web/public/locales/peo/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/objects.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/events.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/live.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/search.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/peo/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/peo/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/peo/views/system.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/audio.json b/sam2-cpu/frigate-dev/web/public/locales/pl/audio.json new file mode 100644 index 0000000..62cd7b4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "Mowa", + "babbling": "Gaworzenie", + "yell": "Krzyk", + "bellow": "Ryk", + "whoop": "Okrzyk", + "whispering": "Szept", + "laughter": "Śmiech", + "snicker": "Chichot", + "crying": "Płacz", + "sigh": "Westchnienie", + "singing": "Śpiewanie", + "choir": "Chór", + "yodeling": "Jodłowanie", + "chant": "Skandowanie", + "mantra": "Mantra", + "child_singing": "Śpiew dziecka", + "synthetic_singing": "Śpiew syntetyczny", + "rapping": "Rapowanie", + "humming": "Nucenie", + "groan": "Jęk", + "grunt": "Chrząknięcie", + "whistling": "Gwizdanie", + "breathing": "Oddychanie", + "wheeze": "Świszczący oddech", + "snoring": "Chrapanie", + "gasp": "Sapnięcie", + "pant": "Dyszenie", + "snort": "Prychnięcie", + "cough": "Kaszel", + "throat_clearing": "Odchrząkiwanie", + "sneeze": "Kichnięcie", + "sniff": "Pociągnięcie nosem", + "run": "Bieg", + "shuffle": "Szuranie", + "footsteps": "Kroki", + "chewing": "Żucie", + "biting": "Gryzienie", + "gargling": "Płukanie gardła", + "stomach_rumble": "Burczenie w brzuchu", + "burping": "Bekanie", + "hiccup": "Czkawka", + "pets": "Zwierzęta domowe", + "finger_snapping": "Pstrykanie palcami", + "heartbeat": "Bicie serca", + "dog": "Pies", + "heart_murmur": "Szmer serca", + "chatter": "Gwar", + "children_playing": "Bawiące się dzieci", + "animal": "Zwierzę", + "applause": "Oklaski", + "cheering": "Wiwatowanie", + "bark": "Szczekanie", + "fart": "Pierdnięcie", + "hands": "Dłonie", + "clapping": "Klaskanie", + "crowd": "Tłum", + "hiss": "Syczenie", + "purr": "Mruczenie", + "yip": "Poszczekiwanie", + "howl": "Wycie", + "growling": "Warczenie", + "meow": "Miauczenie", + "bow_wow": "Hau hau", + "whimper_dog": "Skomlenie psa", + "cat": "Kot", + "caterwaul": "Koci wrzask", + "livestock": "Zwierzęta hodowlane", + "horse": "Koń", + "clip_clop": "Stukot kopyt", + "neigh": "Rżenie", + "cattle": "Bydło", + "moo": "Muczenie", + "cowbell": "Krowi dzwonek", + "goat": "Koza", + "bleat": "Beczenie", + "pig": "Świnia", + "oink": "Chrumkanie", + "sheep": "Owca", + "fowl": "Drób", + "chicken": "Kura", + "cluck": "Gdakanie", + "cock_a_doodle_doo": "Kukuryku", + "turkey": "Indyk", + "gobble": "Gulgotanie", + "duck": "Kaczka", + "quack": "Kwakanie", + "goose": "Gęś", + "honk": "Gęganie", + "wild_animals": "Dzikie zwierzęta", + "roaring_cats": "Ryczące koty", + "roar": "Ryk", + "bird": "Ptak", + "chirp": "Ćwierkanie", + "squawk": "Skrzeczenie", + "pigeon": "Gołąb", + "coo": "Gruchanie", + "crow": "Wrona", + "caw": "Krakanie", + "owl": "Sowa", + "hoot": "Pohukiwanie", + "flapping_wings": "Trzepot skrzydeł", + "insect": "Owad", + "cricket": "Świerszcz", + "mosquito": "Komar", + "fly": "Mucha", + "buzz": "Bzyczenie", + "frog": "Żaba", + "croak": "Kumkanie", + "snake": "Wąż", + "rattle": "Grzechotanie", + "whale_vocalization": "Wokalizacja wieloryba", + "music": "Muzyka", + "plucked_string_instrument": "Instrument strunowy szarpany", + "guitar": "Gitara", + "electric_guitar": "Gitara elektryczna", + "bass_guitar": "Gitara basowa", + "acoustic_guitar": "Gitara akustyczna", + "steel_guitar": "Gitara stalowa", + "tapping": "Tapowanie", + "strum": "Szarpnięcie strun", + "sitar": "Sitar", + "mandolin": "Mandolina", + "zither": "Cytra", + "ukulele": "Ukulele", + "keyboard": "Klawiatura", + "rimshot": "Rimshot", + "drum_roll": "Werbel (tremolo)", + "bass_drum": "Bęben basowy", + "timpani": "Kotły", + "tabla": "Tabla", + "cymbal": "Talerz", + "hi_hat": "Hi-hat", + "wood_block": "Pudełko drewniane", + "tambourine": "Tamburyn", + "maraca": "Marakasy", + "gong": "Gong", + "brass_instrument": "Instrument dęty blaszany", + "french_horn": "Waltornia", + "tubular_bells": "Dzwony rurowe", + "mallet_percussion": "Instrumenty perkusyjne pałkowe", + "marimba": "Marimba", + "glockenspiel": "Dzwonki", + "vibraphone": "Wibrafon", + "steelpan": "Instrument z beczek", + "orchestra": "Orkiestra", + "trumpet": "Trąbka", + "trombone": "Puzon", + "bowed_string_instrument": "Instrument strunowy smyczkowy", + "string_section": "Sekcja smyczkowa", + "violin": "Skrzypce", + "pizzicato": "Pizzicato", + "double_bass": "Kontrabas", + "wind_instrument": "Instrument dęty", + "saxophone": "Saksofon", + "clarinet": "Klarnet", + "harp": "Harfa", + "bell": "Dzwonek", + "church_bell": "Dzwon kościelny", + "jingle_bell": "Dzwoneczek", + "bicycle_bell": "Dzwonek rowerowy", + "tuning_fork": "Kamerton", + "chime": "Dzwonki", + "wind_chime": "Dzwonki wietrzne", + "harmonica": "Harmonijka", + "accordion": "Akordeon", + "bagpipes": "Dudy", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Misa dźwiękowa", + "scratching": "Scratching", + "pop_music": "Muzyka pop", + "hip_hop_music": "Muzyka hip-hopowa", + "beatboxing": "Beatbox", + "rock_music": "Muzyka rockowa", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk rock", + "grunge": "Grunge", + "progressive_rock": "Rock progresywny", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Rock psychodeliczny", + "rhythm_and_blues": "Rhythm and blues", + "soul_music": "Muzyka soul", + "reggae": "Reggae", + "country": "Country", + "swing_music": "Muzyka swingowa", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Muzyka folkowa", + "middle_eastern_music": "Muzyka bliskowschodnia", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Muzyka klasyczna", + "opera": "Opera", + "electronic_music": "Muzyka elektroniczna", + "house_music": "Muzyka house", + "electronica": "Electronica", + "electronic_dance_music": "Muzyka elektroniczna taneczna", + "ambient_music": "Muzyka ambient", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and bass", + "trance_music": "Muzyka trance", + "music_of_latin_america": "Muzyka Latynowska", + "salsa_music": "Muzyka salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Muzyka dla dzieci", + "new-age_music": "Muzyka new age", + "vocal_music": "Muzyka wokalna", + "a_capella": "A cappella", + "music_of_africa": "Muzyka afrykańska", + "afrobeat": "Afrobeat", + "christian_music": "Muzyka chrześcijańska", + "gospel_music": "Muzyka gospel", + "music_of_asia": "Muzyka azjatycka", + "carnatic_music": "Muzyka karnatycka", + "music_of_bollywood": "Muzyka bollywood", + "ska": "Ska", + "independent_music": "Muzyka niezależna", + "song": "Piosenka", + "background_music": "Muzyka tła", + "theme_music": "Muzyka tematyczna", + "jingle": "Dżingiel", + "soundtrack_music": "Muzyka filmowa", + "lullaby": "Kołysanka", + "video_game_music": "Muzyka z gier wideo", + "christmas_music": "Muzyka świąteczna", + "dance_music": "Muzyka taneczna", + "wedding_music": "Muzyka weselna", + "happy_music": "Muzyka radosna", + "sad_music": "Muzyka smutna", + "tender_music": "Muzyka łagodna", + "exciting_music": "Muzyka ekscytująca", + "angry_music": "Muzyka gniewna", + "scary_music": "Muzyka straszna", + "wind": "Wiatr", + "rustling_leaves": "Szeleszczące liście", + "wind_noise": "Szum wiatru", + "thunderstorm": "Burza", + "thunder": "Grzmot", + "water": "Woda", + "rain": "Deszcz", + "raindrop": "Kropla deszczu", + "rain_on_surface": "Deszcz na powierzchni", + "stream": "Strumień", + "waterfall": "Wodospad", + "ocean": "Ocean", + "fire": "Ogień", + "boat": "Łódź", + "sailboat": "Żaglówka", + "motorboat": "Motorówka", + "ship": "Statek", + "motor_vehicle": "Pojazd silnikowy", + "car": "Samochód", + "toot": "Klakson", + "car_alarm": "Alarm samochodowy", + "power_windows": "Elektryczne szyby", + "skidding": "Poślizg", + "tire_squeal": "Pisk opon", + "ambulance": "Karetka", + "fire_engine": "Wóz strażacki", + "motorcycle": "Motocykl", + "traffic_noise": "Hałas uliczny", + "rail_transport": "Transport kolejowy", + "train": "Pociąg", + "train_whistle": "Gwizd pociągu", + "train_horn": "Sygnał dźwiękowy pociągu", + "railroad_car": "Wagon kolejowy", + "train_wheels_squealing": "Pisk kół pociągu", + "subway": "Metro", + "aircraft": "Statek powietrzny", + "aircraft_engine": "Silnik samolotu", + "jet_engine": "Silnik odrzutowy", + "propeller": "Śmigło", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Samolot", + "bicycle": "Rower", + "skateboard": "Deskorolka", + "engine": "Silnik", + "light_engine": "Lekki silnik", + "dental_drill's_drill": "Wiertło dentystyczne", + "lawn_mower": "Kosiarka do trawy", + "chainsaw": "Piła łańcuchowa", + "medium_engine": "Średni silnik", + "heavy_engine": "Ciężki silnik", + "engine_knocking": "Stukanie silnika", + "engine_starting": "Uruchamianie silnika", + "idling": "Praca na biegu jałowym", + "accelerating": "Przyspieszanie", + "door": "Drzwi", + "doorbell": "Dzwonek do drzwi", + "ding-dong": "Ding-dong", + "sliding_door": "Drzwi przesuwne", + "slam": "Trzaśnięcie", + "knock": "Pukanie", + "tap": "Stukanie", + "frying": "Smażenie", + "microwave_oven": "Kuchenka mikrofalowa", + "blender": "Blender", + "water_tap": "Kran", + "sink": "Zlew", + "bathtub": "Wanna", + "hair_dryer": "Suszarka do włosów", + "toilet_flush": "Spłuczka toalety", + "toothbrush": "Szczoteczka do zębów", + "electric_toothbrush": "Elektryczna szczoteczka do zębów", + "vacuum_cleaner": "Odkurzacz", + "zipper": "Zamek błyskawiczny", + "keys_jangling": "Brzęk kluczy", + "coin": "Moneta", + "shuffling_cards": "Tasowanie kart", + "typing": "Pisanie na klawiaturze", + "typewriter": "Maszyna do pisania", + "computer_keyboard": "Klawiatura komputerowa", + "writing": "Pisanie", + "alarm": "Alarm", + "telephone": "Telefon", + "telephone_bell_ringing": "Dzwonek telefonu", + "ringtone": "Dzwonek", + "telephone_dialing": "Wybieranie numeru", + "dial_tone": "Sygnał wybierania", + "busy_signal": "Sygnał zajętości", + "alarm_clock": "Budzik", + "siren": "Syrena", + "civil_defense_siren": "Syrena obrony cywilnej", + "buzzer": "Brzęczyk", + "smoke_detector": "Czujnik dymu", + "fire_alarm": "Alarm pożarowy", + "foghorn": "Sygnał mgłowy", + "whistle": "Gwizdek", + "steam_whistle": "Gwizdek parowy", + "drum_machine": "Automat perkusyjny", + "rats": "Szczury", + "harpsichord": "Klawesyn", + "musical_instrument": "Instrument muzyczny", + "organ": "Organy", + "dogs": "Psy", + "piano": "Fortepian", + "synthesizer": "Syntezator", + "sampler": "Sampler", + "electronic_organ": "Organy elektroniczne", + "patter": "Tupot", + "drum": "Bęben", + "banjo": "Banjo", + "snare_drum": "Werbel", + "mouse": "Mysz", + "electric_piano": "Pianino elektryczne", + "percussion": "Perkusja", + "hammond_organ": "Organy Hammonda", + "drum_kit": "Zestaw perkusyjny", + "air_brake": "Hamulec pneumatyczny", + "flute": "Flet", + "rowboat": "Łódź wiosłowa", + "squeak": "Skrzypienie", + "cupboard_open_or_close": "Otwieranie lub zamykanie szafki", + "chopping": "Siekanie", + "cello": "Wiolonczela", + "dishes": "Naczynia", + "cutlery": "Sztućce", + "car_passing_by": "Przejeżdżający samochód", + "ice_cream_truck": "Samochód z lodami", + "waves": "Fale", + "race_car": "Samochód wyścigowy", + "steam": "Para", + "reversing_beeps": "Sygnał cofania", + "police_car": "Radiowóz", + "bus": "Autobus", + "emergency_vehicle": "Pojazd uprzywilejowany", + "drawer_open_or_close": "Otwieranie lub zamykanie szuflady", + "traditional_music": "Muzyka tradycyjna", + "electric_shaver": "Elektryczna golarka", + "gurgling": "Bulgotanie", + "crackle": "Trzask", + "vehicle": "Pojazd", + "air_horn": "Klakson pneumatyczny", + "truck": "Ciężarówka", + "scissors": "Nożyczki", + "mechanisms": "Mechanizmy", + "ratchet": "Zapadka", + "tick-tock": "Tik-tak", + "gears": "Przekładnie", + "sewing_machine": "Maszyna do szycia", + "mechanical_fan": "Wentylator mechaniczny", + "air_conditioning": "Klimatyzacja", + "cash_register": "Kasa fiskalna", + "printer": "Drukarka", + "camera": "Kamera", + "single-lens_reflex_camera": "Lustrzanka jednooobiektywowa", + "tools": "Narzędzia", + "hammer": "Młotek", + "jackhammer": "Młot pneumatyczny", + "sawing": "Piłowanie", + "filing": "Pilnikowanie", + "power_tool": "Elektronarzędzie", + "drill": "Wiertarka", + "explosion": "Eksplozja", + "gunshot": "Strzał", + "machine_gun": "Karabin maszynowy", + "fusillade": "Kanonada", + "artillery_fire": "Ogień artyleryjski", + "cap_gun": "Pistolet na kapiszony", + "fireworks": "Fajerwerki", + "firecracker": "Petarda", + "burst": "Wybuch", + "eruption": "Erupcja", + "boom": "Huk", + "wood": "Drewno", + "chop": "Rąbanie", + "splinter": "Drzazga", + "crack": "Pęknięcie", + "glass": "Szkło", + "chink": "Brzęk", + "shatter": "Rozbicie", + "silence": "Cisza", + "sound_effect": "Efekt dźwiękowy", + "environmental_noise": "Hałas otoczenia", + "static": "Szum", + "white_noise": "Biały szum", + "pink_noise": "Różowy szum", + "television": "Telewizor", + "radio": "Radio", + "field_recording": "Nagranie terenowe", + "scream": "Wrzask", + "pulleys": "Bloczki", + "sanding": "Szlifowanie", + "clock": "Zegar", + "tick": "Tykanie" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/common.json b/sam2-cpu/frigate-dev/web/public/locales/pl/common.json new file mode 100644 index 0000000..0c68e18 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/common.json @@ -0,0 +1,309 @@ +{ + "time": { + "12hours": "12 godzin", + "1hour": "1 godzina", + "24hours": "24 godziny", + "last7": "Ostatnie 7 dni", + "last14": "Ostatnie 14 dni", + "last30": "Ostatnie 30 dni", + "thisWeek": "Ten tydzień", + "lastWeek": "Ostatni tydzień", + "thisMonth": "Ten miesiąc", + "lastMonth": "Ostatni miesiąc", + "5minutes": "5 minut", + "10minutes": "10 minut", + "30minutes": "30 minut", + "untilForTime": "Do {{time}}", + "untilForRestart": "Do czasu restartu Frigate.", + "untilRestart": "Do restartu", + "ago": "{{timeAgo}} temu", + "justNow": "Właśnie teraz", + "today": "Dzisiaj", + "yesterday": "Wczoraj", + "pm": "po południu", + "am": "przed południem", + "yr": "{{time}}r.", + "year_one": "{{time}} rok", + "year_few": "{{time}} lata", + "year_many": "{{time}} lat", + "mo": "{{time}}m.", + "d": "{{time}}d.", + "day_one": "{{time}} dzień", + "day_few": "{{time}} dni", + "day_many": "{{time}} dni", + "h": "{{time}}godz.", + "m": "{{time}}min.", + "s": "{{time}}s.", + "month_one": "{{time}} miesiąc", + "month_few": "{{time}} miesiące", + "month_many": "{{time}} miesięcy", + "hour_one": "{{time}} godzina", + "hour_few": "{{time}} godziny", + "hour_many": "{{time}} godzin", + "minute_one": "{{time}} minuta", + "minute_few": "{{time}} minuty", + "minute_many": "{{time}} minut", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "second_one": "{{time}} sekunda", + "second_few": "{{time}} sekundy", + "second_many": "{{time}} sekund", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMMM yyyy", + "24hour": "d MMMM yyyy" + }, + "inProgress": "W trakcie", + "invalidStartTime": "Nieprawidłowy czas rozpoczęcia", + "invalidEndTime": "Nieprawidłowy czas zakończenia" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "stopy", + "meters": "metry" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/godz.", + "mbph": "MB/godz.", + "gbph": "GB/godz." + } + }, + "label": { + "back": "Wróć", + "hide": "Ukryj {{item}}", + "show": "Pokaż {{item}}", + "ID": "ID", + "none": "Brak", + "all": "Wszystko" + }, + "button": { + "apply": "Zastosuj", + "reset": "Resetuj", + "done": "Gotowe", + "enabled": "Włączone", + "enable": "Włącz", + "disable": "Wyłącz", + "save": "Zapisz", + "history": "Historia", + "fullscreen": "Pełny ekran", + "exitFullscreen": "Wyjdź z pełnego ekranu", + "pictureInPicture": "Obraz w obrazie", + "delete": "Usuń", + "yes": "Tak", + "no": "Nie", + "download": "Pobierz", + "info": "Informacje", + "suspended": "Wstrzymane", + "unsuspended": "Wznów", + "play": "Odtwórz", + "unselect": "Odznacz", + "export": "Eksportuj", + "deleteNow": "Usuń teraz", + "next": "Dalej", + "disabled": "Wyłączone", + "back": "Wstecz", + "saving": "Zapisywanie…", + "on": "WŁĄCZ", + "cancel": "Anuluj", + "twoWayTalk": "Komunikacja dwustronna", + "close": "Zamknij", + "copy": "Kopiuj", + "cameraAudio": "Dźwięk kamery", + "off": "WYŁĄCZ", + "edit": "Edytuj", + "copyCoordinates": "Kopiuj współrzędne", + "continue": "Kontynuuj" + }, + "menu": { + "system": "System", + "systemMetrics": "Metryki systemowe", + "configuration": "Konfiguracja", + "systemLogs": "Logi systemowe", + "languages": "Języki", + "language": { + "en": "English (Angielski)", + "zhCN": "简体中文 (Uproszczony Chiński)", + "withSystem": { + "label": "Użyj ustawień systemowych dla języka" + }, + "es": "Español (Hiszpański)", + "cs": "Čeština (Czeski)", + "pl": "Polski (Polski)", + "hi": "हिन्दी (hinduski)", + "ar": "العربية (arabski)", + "fr": "Français (francuski)", + "pt": "Português (portugalski)", + "ru": "Русский (rosyjski)", + "de": "Deutsch (niemiecki)", + "ja": "日本語 (japoński)", + "tr": "Türkçe (Turecki)", + "it": "Italiano (Włoski)", + "nl": "Nederlands (Holenderski)", + "sv": "Svenska (Szwedzki)", + "nb": "Norsk Bokmål (Norweski)", + "ko": "한국어 (Koreański)", + "fa": "فارسی (Perski)", + "uk": "Українська (Ukraiński)", + "he": "עברית (Hebrajski)", + "el": "Ελληνικά (Grecki)", + "hu": "Magyar (Węgierski)", + "da": "Dansk (Duński)", + "sk": "Slovenčina (Słowacki)", + "vi": "Tiếng Việt (Wietnamski)", + "ro": "Română (Rumuński)", + "fi": "Suomi (Fiński)", + "yue": "粵語 (Kantoński)", + "th": "ไทย (Tajski)", + "ca": "Català (Kataloński)", + "ptBR": "Português brasileiro (portugalski - Brazylia)", + "sr": "Српски (Serbski)", + "sl": "Slovenščina (Słowacki)", + "lt": "Lietuvių (Litewski)", + "bg": "Български (Bułgarski)", + "gl": "Galego (Galicyjski)", + "id": "Bahasa Indonesia (Indonezyjski)", + "ur": "اردو (Urdu)" + }, + "appearance": "Wygląd", + "darkMode": { + "label": "Tryb ciemny", + "light": "Jasny", + "dark": "Ciemny", + "withSystem": { + "label": "Użyj ustawień systemowych dla trybu jasnego/ciemnego" + } + }, + "withSystem": "System", + "theme": { + "label": "Motyw", + "blue": "Niebieski", + "green": "Zielony", + "contrast": "Wysoki kontrast", + "default": "Domyślny", + "red": "Czerwony", + "nord": "Nordycki", + "highcontrast": "Wysoki kontrast" + }, + "restart": "Uruchom ponownie Frigate", + "live": { + "title": "Na żywo", + "allCameras": "Wszystkie kamery", + "cameras": { + "title": "Kamery", + "count_one": "{{count}} Kamera", + "count_few": "{{count}} Kamer", + "count_many": "{{count}} Kamery" + } + }, + "review": "Przegląd", + "uiPlayground": "Plac testowy UI", + "faceLibrary": "Biblioteka twarzy", + "user": { + "account": "Konto", + "current": "Aktualny użytkownik: {{user}}", + "anonymous": "anonimowy", + "logout": "Wyloguj", + "setPassword": "Ustaw hasło", + "title": "Użytkownik" + }, + "documentation": { + "title": "Dokumentacja", + "label": "Dokumentacja Frigate" + }, + "explore": "Przeglądaj", + "configurationEditor": "Edytor konfiguracji", + "help": "Pomoc", + "settings": "Ustawienia", + "export": "Eksportuj", + "classification": "Klasyfikacja" + }, + "role": { + "viewer": "Przeglądający", + "desc": "Administratorzy mają pełny dostęp do wszystkich funkcji w interfejsie Frigate. Przeglądający mają ograniczony dostęp tylko do podglądu kamer, przeglądania nagrań i historycznych materiałów w interfejsie.", + "title": "Rola", + "admin": "Administrator" + }, + "pagination": { + "label": "paginacja", + "previous": { + "title": "Poprzednia", + "label": "Przejdź do poprzedniej strony" + }, + "next": { + "title": "Następna", + "label": "Przejdź do następnej strony" + }, + "more": "Więcej stron" + }, + "accessDenied": { + "title": "Dostęp Zabroniony", + "desc": "Nie masz uprawnień do wyświetlenia tej strony.", + "documentTitle": "Dostęp Zabroniony - Frigate" + }, + "notFound": { + "title": "404", + "desc": "Strona nie znaleziona", + "documentTitle": "Nie Znaleziono - Frigate" + }, + "selectItem": "Wybierz {{item}}", + "toast": { + "copyUrlToClipboard": "Skopiowano URL do schowka.", + "save": { + "error": { + "title": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}", + "noMessage": "Nie udało się zapisać zmian konfiguracji" + }, + "title": "Zapisz" + } + }, + "readTheDocumentation": "Przeczytaj dokumentację", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} i {{1}}", + "many": "{{items}}, oraz {{last}}" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/auth.json new file mode 100644 index 0000000..12aba0f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nazwa użytkownika", + "password": "Hasło", + "login": "Login", + "errors": { + "usernameRequired": "Nazwa użytkownika jest wymagana", + "passwordRequired": "Hasło jest wymagane", + "loginFailed": "Logowanie nieudane", + "unknownError": "Nieznany błąd. Sprawdź logi.", + "webUnknownError": "Nieznany błąd. Sprawdź konsolę.", + "rateLimit": "Przekroczono limit częstotliwości. Spróbuj ponownie później." + }, + "firstTimeLogin": "Próbujesz się zalogować po raz pierwszy? Dane logowania są dostępne w logach Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/camera.json new file mode 100644 index 0000000..f673261 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupy kamer", + "add": "Dodaj grupę kamer", + "edit": "Edytuj grupę kamer", + "delete": { + "label": "Usuń grupę kamer", + "confirm": { + "title": "Potwierdź usuwanie", + "desc": "Czy jesteś pewny że chcesz usunąć grupę kamer {{name}}?" + } + }, + "name": { + "placeholder": "Wprowadź nazwę…", + "label": "Nazwa", + "errorMessage": { + "mustLeastCharacters": "Nazwa grupy kamer musi mieć co najmniej 2 znaki.", + "exists": "Grupa kamer o takiej nazwie już istnieje.", + "nameMustNotPeriod": "Nazwa grupy kamer nie może zawierać kropki.", + "invalid": "Niepoprawna nazwa grupy kamer." + } + }, + "cameras": { + "label": "Kamery", + "desc": "Wykierz kamery dla tej grupy." + }, + "icon": "Ikona", + "success": "Grupa kamer ({{name}}) została zapisana.", + "camera": { + "setting": { + "audio": { + "tips": { + "document": "Czytaj dokumentację ", + "title": "Dźwięk musi być wysyłany z kamery i skonfigurowany w go2rtc dla tego strumienia." + } + }, + "label": "Ustawienia Strumieniowania Kamery", + "title": "Ustawienia Strumieniowania {{cameraName}}", + "desc": "Zmień opcje strumieniowania na żywo dla pulpitu tej grupy kamer. Te ustawienia są specyficzne dla urządzenia/przeglądarki.", + "audioIsAvailable": "Dźwięk jest dostępny dla tego strumienia", + "audioIsUnavailable": "Dźwięk jest niedostępny dla tego strumienia", + "streamMethod": { + "label": "Metoda Strumieniowania", + "method": { + "noStreaming": { + "label": "Brak Strumieniowania", + "desc": "Obrazy z kamery będą aktualizowane tylko raz na minutę, bez strumieniowania na żywo." + }, + "smartStreaming": { + "label": "Inteligentne Strumieniowanie (zalecane)", + "desc": "Inteligentne strumieniowanie aktualizuje obraz kamery raz na minutę gdy nie wykryto aktywności, aby oszczędzać przepustowość i zasoby. Gdy aktywność zostanie wykryta, obraz płynnie przełącza się na transmisję na żywo." + }, + "continuousStreaming": { + "desc": { + "title": "Obraz kamery będzie zawsze strumieniowany na żywo, gdy jest widoczny na pulpicie, nawet jeśli nie wykryto aktywności.", + "warning": "Ciągłe strumieniowanie może powodować wysokie zużycie przepustowości i problemy z wydajnością. Używaj ostrożnie." + }, + "label": "Ciągłe Strumieniowanie" + } + }, + "placeholder": "Wybierz sposób strumieniowania" + }, + "compatibilityMode": { + "label": "Tryb kompatybilności", + "desc": "Włącz tę opcję tylko jeśli transmisja na żywo z kamery wyświetla artefakty kolorów i ma ukośną linię po prawej stronie obrazu." + }, + "placeholder": "Wybierz strumień", + "stream": "Strumień" + }, + "birdseye": "Widok z lotu ptaka" + } + }, + "debug": { + "options": { + "label": "Ustawienia", + "title": "Opcje", + "showOptions": "Pokaż Opcje", + "hideOptions": "Ukryj Opcje" + }, + "timestamp": "Znacznik czasu", + "zones": "Strefy", + "mask": "Maski", + "regions": "Regiony", + "motion": "Ruch", + "boundingBox": "Ramka Ograniczająca" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/dialog.json new file mode 100644 index 0000000..b9a4d9a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/dialog.json @@ -0,0 +1,135 @@ +{ + "restart": { + "title": "Czy na pewno chcesz ponownie uruchomić Frigate?", + "button": "Uruchom ponownie", + "restarting": { + "title": "Frigate uruchamia się ponownie", + "content": "Strona odświeży się za {{countdown}} sekund.", + "button": "Wymuś odświeżenie" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Wyślij do Frigate+", + "desc": "Obiekty w miejscach, których chcesz unikać, nie są fałszywymi alarmami. Zgłaszanie ich jako fałszywe alarmy zdezorientuje model." + }, + "review": { + "true": { + "label": "Potwierdź tę etykietę dla Frigate Plus", + "true_one": "To jest {{label}}", + "true_few": "To są {{label}}", + "true_many": "To są {{label}}" + }, + "false": { + "label": "Nie potwierdzaj tej etykiety dla Frigate Plus", + "false_one": "To nie jest {{label}}", + "false_few": "To nie są {{label}}", + "false_many": "To nie są {{label}}" + }, + "state": { + "submitted": "Przesłano" + }, + "question": { + "ask_a": "Czy ten obiekt to {{label}}?", + "ask_an": "Czy ten obiekt to {{label}}?", + "ask_full": "Czy ten obiekt to {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potwierdź tę etykietę dla Frigate Plus" + } + } + }, + "video": { + "viewInHistory": "Zobacz w Historii" + } + }, + "export": { + "time": { + "lastHour_one": "Ostatnia godzina", + "lastHour_few": "Ostatnie {{count}} godziny", + "lastHour_many": "Ostatnie {{count}} godzin", + "fromTimeline": "Wybierz z Osi Czasu", + "custom": "Niestandardowy", + "start": { + "title": "Czas Rozpoczęcia", + "label": "Wybierz Czas Rozpoczęcia" + }, + "end": { + "title": "Czas Zakończenia", + "label": "Wybierz Czas Zakończenia" + } + }, + "name": { + "placeholder": "Nazwij Eksport" + }, + "select": "Wybierz", + "export": "Eksportuj", + "selectOrExport": "Wybierz lub Eksportuj", + "toast": { + "success": "Pomyślnie rozpoczęto eksport. Zobacz plik na stronie eksportów.", + "error": { + "failed": "Nie udało się rozpocząć eksportu: {{error}}", + "endTimeMustAfterStartTime": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia", + "noVaildTimeSelected": "Nie wybrano prawidłowego zakresu czasu" + }, + "view": "Widok" + }, + "fromTimeline": { + "saveExport": "Zapisz Eksport", + "previewExport": "Podgląd Eksportu" + } + }, + "recording": { + "button": { + "markAsReviewed": "Oznacz jako sprawdzone", + "deleteNow": "Usuń teraz", + "export": "Eksportuj", + "markAsUnreviewed": "Oznacz jako niesprawdzone" + }, + "confirmDelete": { + "title": "Potwierdź Usunięcie", + "desc": { + "selected": "Czy na pewno chcesz usunąć wszystkie nagrane wideo powiązane z tym elementem recenzji?

    Przytrzymaj klawisz Shift, aby pominąć to okno dialogowe w przyszłości." + }, + "toast": { + "success": "Nagrania wideo powiązane z wybranymi elementami przeglądu zostały pomyślnie usunięte.", + "error": "Nie udało się usunąć: {{error}}" + } + } + }, + "streaming": { + "label": "Strumień", + "restreaming": { + "disabled": "Restreaming nie jest włączony dla tej kamery.", + "desc": { + "title": "Skonfiguruj go2rtc dla dodatkowych opcji podglądu na żywo i dźwięku dla tej kamery.", + "readTheDocumentation": "Przeczytaj dokumentację" + } + }, + "showStats": { + "label": "Pokaż statystyki strumienia", + "desc": "Włącz tę opcję, aby wyświetlać statystyki strumienia jako nakładkę na obrazie z kamery." + }, + "debugView": "Widok Debugowania" + }, + "search": { + "saveSearch": { + "label": "Zapisz Wyszukiwanie", + "desc": "Podaj nazwę dla tego zapisanego wyszukiwania.", + "placeholder": "Wprowadź nazwę dla swojego wyszukiwania", + "overwrite": "{{searchName}} już istnieje. Zapisanie nadpisze istniejącą wartość.", + "success": "Wyszukiwanie ({{searchName}}) zostało zapisane.", + "button": { + "save": { + "label": "Zapisz to wyszukiwanie" + } + } + } + }, + "imagePicker": { + "selectImage": "Wybierz miniaturkę śledzonego obiektu", + "search": { + "placeholder": "Wyszukaj po etykiecie (label) lub etykiecie potomnej (sub label)..." + }, + "noImages": "Brak miniatur dla tej kamery" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/filter.json new file mode 100644 index 0000000..b604c98 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtr", + "labels": { + "label": "Etykiety", + "all": { + "title": "Wszystkie Etykiety", + "short": "Etykiety" + }, + "count_one": "{{count}} Etykieta", + "count_other": "{{count}} Etykiet(y)" + }, + "zones": { + "label": "Strefy", + "all": { + "title": "Wszystkie Strefy", + "short": "Strefy" + } + }, + "subLabels": { + "all": "Wszystkie Podetykiety", + "label": "Podetykiety" + }, + "score": "Wynik", + "estimatedSpeed": "Szacowana Prędkość ({{unit}})", + "features": { + "label": "Funkcje", + "hasSnapshot": "Posiada stop klatkę", + "hasVideoClip": "Posiada klip wideo", + "submittedToFrigatePlus": { + "label": "Przesłano do Frigate+", + "tips": "Musisz najpierw filtrować obiekty śledzone, które mają stop klatkę.

    Obiekty śledzone bez stop klatki nie mogą być przesłane do Frigate+." + } + }, + "sort": { + "dateDesc": "Data (Malejąco)", + "scoreAsc": "Wynik Obiektu (Rosnąco)", + "scoreDesc": "Wynik Obiektu (Malejąco)", + "speedAsc": "Szacowana Prędkość (Rosnąco)", + "speedDesc": "Szacowana Prędkość (Malejąco)", + "label": "Sortuj", + "dateAsc": "Data (Rosnąco)", + "relevance": "Trafność" + }, + "explore": { + "settings": { + "gridColumns": { + "desc": "Wybierz liczbę kolumn w widoku siatki.", + "title": "Kolumny Siatki" + }, + "searchSource": { + "label": "Źródło Wyszukiwania", + "desc": "Wybierz, czy przeszukiwać miniatury czy opisy śledzonych obiektów.", + "options": { + "description": "Opis", + "thumbnailImage": "Obraz Miniatury" + } + }, + "defaultView": { + "title": "Domyślny Widok", + "summary": "Podsumowanie", + "unfilteredGrid": "Niefiltrowana Siatka", + "desc": "Gdy nie wybrano filtrów, wyświetl podsumowanie najnowszych śledzonych obiektów dla każdej etykiety lub wyświetl niefiltrowaną siatkę." + }, + "title": "Ustawienia" + }, + "date": { + "selectDateBy": { + "label": "Wybierz datę do filtrowania" + } + } + }, + "logSettings": { + "label": "Filtruj poziom logów", + "filterBySeverity": "Filtruj logi według ważności", + "loading": { + "title": "Ładowanie", + "desc": "Gdy panel logów jest przewinięty do dołu, nowe logi są automatycznie strumieniowane w miarę ich dodawania." + }, + "disableLogStreaming": "Wyłącz strumieniowanie logów", + "allLogs": "Wszystkie logi" + }, + "recognizedLicensePlates": { + "loading": "Ładowanie rozpoznanych tablic rejestracyjnych…", + "placeholder": "Wpisz, aby wyszukać tablice rejestracyjne…", + "noLicensePlatesFound": "Nie znaleziono tablic rejestracyjnych.", + "title": "Rozpoznane Tablice Rejestracyjne", + "loadFailed": "Nie udało się załadować rozpoznanych tablic rejestracyjnych.", + "selectPlatesFromList": "Wybierz jedną lub więcej tablic z listy.", + "selectAll": "Wybierz wszystko", + "clearAll": "Wyczyść wszystko" + }, + "dates": { + "all": { + "title": "Wszystkie Daty", + "short": "Daty" + }, + "selectPreset": "Wybierz ustawienie…" + }, + "more": "Więcej Filtrów", + "reset": { + "label": "Resetuj filtry do wartości domyślnych" + }, + "timeRange": "Zakres Czasu", + "cameras": { + "label": "Filtr Kamer", + "all": { + "title": "Wszystkie Kamery", + "short": "Kamery" + } + }, + "review": { + "showReviewed": "Pokaż Przejrzane" + }, + "motion": { + "showMotionOnly": "Pokaż Tylko Ruch" + }, + "trackedObjectDelete": { + "toast": { + "success": "Śledzone obiekty zostały pomyślnie usunięte.", + "error": "Nie udało się usunąć śledzonych obiektów: {{errorMessage}}" + }, + "title": "Potwierdź Usunięcie", + "desc": "Usunięcie tych {{objectLength}} śledzonych obiektów usuwa stop klatki, wszelkie zapisane osadzenia i wszystkie powiązane wpisy cyklu życia obiektu. Nagrane materiały tych śledzonych obiektów w widoku Historii NIE zostaną usunięte.

    Czy na pewno chcesz kontynuować?

    Przytrzymaj klawisz Shift, aby pominąć to okno dialogowe w przyszłości." + }, + "zoneMask": { + "filterBy": "Filtruj według maski strefy" + }, + "classes": { + "label": "Klasy", + "all": { + "title": "Wszystkie Klasy" + }, + "count_one": "{{count}} Klasa", + "count_other": "{{count}} Klas(y)" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/icons.json new file mode 100644 index 0000000..35cbdc1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Wybierz ikonę", + "search": { + "placeholder": "Wyszukaj ikonę…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/input.json new file mode 100644 index 0000000..1216c7a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Pobierz Wideo", + "toast": { + "success": "Rozpoczęto pobieranie nagrania do przeglądu." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/pl/components/player.json new file mode 100644 index 0000000..227813f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Nie znaleziono żadnych nagrań w tym czasie", + "cameraDisabled": "Kamera jest wyłączona", + "stats": { + "latency": { + "title": "Opóźnienie:", + "value": "{{seconds}} sekund", + "short": { + "title": "Opóźnienie", + "value": "{{seconds}} s" + } + }, + "totalFrames": "Całkowita liczba klatek:", + "streamType": { + "title": "Typ transmisji:", + "short": "Typ" + }, + "bandwidth": { + "title": "Przepustowość:", + "short": "Przepustowość" + }, + "droppedFrames": { + "title": "Porzucone klatki:", + "short": { + "title": "Porzucone", + "value": "{{droppedFrames}} klatek" + } + }, + "decodedFrames": "Zdekodowane klatki:", + "droppedFrameRate": "Współczynnik porzuconych klatek:" + }, + "noPreviewFound": "Nie znaleziono podglądu", + "noPreviewFoundFor": "Nie znaleziono podglądu dla {{cameraName}}", + "submitFrigatePlus": { + "title": "Wyślij tę klatkę do Frigate+?", + "submit": "Wyślij" + }, + "livePlayerRequiredIOSVersion": "Wymagana wersja iOS 17.1 lub nowsza dla tego typu transmisji na żywo.", + "streamOffline": { + "title": "Transmisja offline", + "desc": "Nie otrzymano klatek na strumieniu {{cameraName}} detect, sprawdź logi błędów" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Pomyślnie wysłano klatkę do Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nie udało się wysłać klatki do Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/objects.json b/sam2-cpu/frigate-dev/web/public/locales/pl/objects.json new file mode 100644 index 0000000..3923ec7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/objects.json @@ -0,0 +1,120 @@ +{ + "bird": "Ptak", + "boat": "Łódź", + "car": "Samochód", + "bus": "Autobus", + "motorcycle": "Motocykl", + "train": "Pociąg", + "bicycle": "Rower", + "sheep": "Owca", + "mouse": "Mysz", + "keyboard": "Klawiatura", + "door": "Drzwi", + "blender": "Blender", + "sink": "Zlew", + "vehicle": "Pojazd", + "hair_dryer": "Suszarka do włosów", + "toothbrush": "Szczoteczka do zębów", + "scissors": "Nożyczki", + "goat": "Koza", + "skateboard": "Deskorolka", + "dog": "Pies", + "cat": "Kot", + "horse": "Koń", + "clock": "Zegar", + "animal": "Zwierzę", + "bark": "Szczekanie", + "person": "Osoba", + "airplane": "Samolot", + "traffic_light": "Światła Uliczne", + "fire_hydrant": "Hydrant", + "street_sign": "Znak Drogowy", + "stop_sign": "Znak Stopu", + "parking_meter": "Parkometr", + "bench": "Ławka", + "cow": "Krowa", + "bear": "Niedźwiedź", + "giraffe": "Żyrafa", + "backpack": "Plecak", + "umbrella": "Parasolka", + "shoe": "But", + "eye_glasses": "Okulary Przeciwsłoneczne", + "tie": "Krawat", + "skis": "Narty", + "tennis_racket": "Rakieta Tenisowa", + "bottle": "Butelka", + "plate": "Tależ", + "wine_glass": "Kieliszek do Wina", + "cup": "Kubek", + "fork": "Widelec", + "banana": "Banan", + "apple": "Jabłko", + "carrot": "Marchewka", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "cake": "Ciastko", + "chair": "Krzesło", + "bed": "Łóżko", + "mirror": "Lustro", + "dining_table": "Stół Jadalny", + "window": "Okno", + "toilet": "Toaleta", + "tv": "Telewizor", + "laptop": "Laptop", + "remote": "Pilot", + "toaster": "Toster", + "refrigerator": "Lodówka", + "book": "Książka", + "vase": "Waza", + "hair_brush": "Szczotka do Włosów", + "squirrel": "Wiewiórka", + "fox": "Lis", + "waste_bin": "Kosz na Śmieci", + "face": "Twarz", + "license_plate": "Tablica Rejestracyjna", + "package": "Paczka", + "bbq_grill": "Grill", + "amazon": "Amazon", + "dhl": "DHL", + "gls": "GLS", + "dpd": "DPD", + "baseball_glove": "Rękawica Bejsbolowa", + "baseball_bat": "Kij Bejsbolowy", + "bowl": "Miska", + "spoon": "Łyżka", + "sandwich": "Kanapka", + "zebra": "Zebra", + "snowboard": "Snowboard", + "knife": "Nóż", + "broccoli": "Brokuł", + "elephant": "Śłoń", + "desk": "Biurko", + "orange": "Pomarańcza", + "cell_phone": "Telefon Komórkowy", + "microwave": "Mikrofalówka", + "oven": "Piekarnik", + "hat": "Kapelusz", + "handbag": "Torebka", + "suitcase": "Walizka", + "sports_ball": "Piłka sportowa", + "kite": "Latawiec", + "surfboard": "Deska surfingowa", + "donut": "Pączek", + "couch": "Kanapa", + "potted_plant": "Roślina doniczkowa", + "teddy_bear": "Miś pluszowy", + "deer": "Jeleń", + "rabbit": "Królik", + "raccoon": "Szop pracz", + "robot_lawnmower": "Robot koszący", + "usps": "USPS (Poczta Amerykańska)", + "on_demand": "Na żądanie", + "ups": "UPS", + "fedex": "FedEx", + "an_post": "An Post (Poczta Irlandzka)", + "purolator": "Purolator", + "postnl": "PostNL (Poczta Holenderska)", + "nzpost": "NZPost (Poczta Nowozelandzka)", + "postnord": "PostNord (Poczta Skandynawska)", + "frisbee": "Frisbee" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/classificationModel.json new file mode 100644 index 0000000..0b63b91 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/classificationModel.json @@ -0,0 +1,119 @@ +{ + "documentTitle": "Modele klasyfikacji - Frigate", + "button": { + "deleteClassificationAttempts": "Usuń obrazy klasyfikacyjne", + "renameCategory": "Zmień nazwę klasy", + "deleteCategory": "Usuń klasyfikację", + "deleteImages": "Usuń obrazy", + "trainModel": "Przeszkol model", + "addClassification": "Dodaj klasyfikację", + "deleteModels": "Usuń modele", + "editModel": "Edytuj model" + }, + "details": { + "scoreInfo": "Wynik przedstawia średnią pewność klasyfikacji wszystkich wykryć danego obiektu." + }, + "toast": { + "success": { + "deletedCategory": "Usunięte klasy", + "deletedImage": "Usunięte obrazy", + "deletedModel_one": "Pomyślenie usunięto {{count}} model", + "deletedModel_few": "Pomyślenie usunięto {{count}} modele", + "deletedModel_many": "Pomyślenie usunięto {{count}} modeli", + "categorizedImage": "Obraz pomyślnie sklasyfikowany", + "trainedModel": "Model pomyślnie wytrenowany", + "trainingModel": "Pomyślnie uruchomiono trenowanie modelu.", + "updatedModel": "Pomyślnie zaktualizowane ustawienia modelu", + "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}" + }, + "error": { + "deleteImageFailed": "Nie udało się usunąć: {{errorMessage}}", + "deleteCategoryFailed": "Nie udało się usunąć klasy: {{errorMessage}}", + "deleteModelFailed": "Nie udało się usunąć modelu: {{errorMessage}}", + "categorizeFailed": "Nie udało się skategoryzować obrazka: {{errorMessage}}", + "trainingFailed": "Trening modelu zakończył się niepowodzeniem. Sprawdź logi Frigate aby uzyskać więcej informacji.", + "updateModelFailed": "Nie udało się zaktualizować modelu: {{errorMessage}}", + "trainingFailedToStart": "Nie udało się rozpocząć trenowania modelu: {{errorMessage}}", + "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Usuń klasę", + "desc": "Czy na pewno chcesz usunąć klasę {{name}}? Spowoduje to trawałe usunięcie wszystkich powiązanych obrazków i konieczność ponownego trenowania modelu.", + "minClassesTitle": "Nie można usunąć kategorii", + "minClassesDesc": "Model klasyfikacyjny musi posiadać co najmniej dwie kategorie. Dodaj inną kategorię aby możliwe było usunięcie tej kategorii." + }, + "deleteModel": { + "title": "Usuń model klasyfikacji", + "single": "Czy na pewno chcesz usunąć {{name}}? Spowoduje to trwałe usunięcie wszystkich powiązanych data włącznie z obrazkami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_one": "Czy na pewno chcesz usunąć {{count}} model? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_few": "Czy na pewno chcesz usunąć {{count}} modele? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji.", + "desc_many": "Czy na pewno chcesz usunąć {{count}} modeli? Spowoduje to trwałe usunięcie wszystkich powiązanych danych, włącznie z obrazami i danymi treningowymi. Nie można cofnąć tej operacji." + }, + "edit": { + "title": "Edytuj model klasyfikacji", + "descriptionObject": "Zmień typ obiektu i kryteria dla tego modelu klasyfikacji.", + "stateClassesInfo": "Uwaga: Zmiana typu klasyfikacji wymaga treningu nowego modelu.", + "descriptionState": "Edycja klas dla tego modelu klasyfikacji stanu. Zmiany będą wymagały przekwalifikowania modelu." + }, + "tooltip": { + "trainingInProgress": "Trwa trenowanie modelu", + "modelNotReady": "Mode nie jest gotowy do trenowania", + "noChanges": "Brak zmian w zbiorze danych od czasu ostatniego treningu.", + "noNewImages": "Nie ma więcej obrazów do trenowania. Zaklasyfikuj więcej obrazów do zbioru danych." + }, + "deleteDatasetImages": { + "title": "Usuń obrazy z puli danych", + "desc_one": "Czy na pewno chcesz usunąć {{count}} obraz z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu.", + "desc_few": "Czy na pewno chcesz usunąć obrazy {{count}} z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu.", + "desc_many": "Czy na pewno chcesz usunąć obrazy {{count}} z {{dataset}}? Ta akcja nie może zostać cofnięta i będzie wymagała przetrenowania modelu." + }, + "renameCategory": { + "title": "Zmień nazwę klasy", + "desc": "Wprowadź nową nazwę dla {{name}}. Zastosowanie tej zmiany wymagać będzie treningu nowego modelu." + }, + "description": { + "invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki." + }, + "train": { + "title": "Ostatnie Klasyfikacje", + "titleShort": "Najnowsze", + "aria": "Wybierz Najnowsze Klasyfikacje" + }, + "createCategory": { + "new": "Stwórz nową klasyfikację" + }, + "deleteTrainImages": { + "title": "Usuń obrazy pociągów", + "desc_one": "Czy na pewno chcesz usunąć {{count}} obraz? Tego działania nie da się cofnąć.", + "desc_few": "Czy na pewno chcesz usunąć obrazy {{count}}? Tego działania nie da się cofnąć.", + "desc_many": "Czy na pewno chcesz usunąć obrazy {{count}}? Tego działania nie da się cofnąć." + }, + "categories": "Zajęcia", + "none": "Nic", + "categorizeImageAs": "Klasyfikuj Obraz Jako:", + "categorizeImage": "Klasyfikuj obraz", + "menu": { + "objects": "Obiekty", + "states": "Stany" + }, + "noModels": { + "object": { + "title": "Brak modeli klasyfikacji obiektów", + "description": "Utwórz model niestandardowy do klasyfikacji wykrytych obiektów.", + "buttonText": "Tworzenie modelu obiektu" + }, + "state": { + "title": "Brak państwowych modeli klasyfikacji", + "description": "Utwórz niestandardowy model do monitorowania i klasyfikacji zmian stanu w określonych obszarach kamery.", + "buttonText": "Utwórz model stanu" + } + }, + "wizard": { + "title": "Tworzenie nowej klasyfikacji", + "steps": { + "nameAndDefine": "Nazwij i zdefiniuj", + "stateArea": "Obszar stanu" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/configEditor.json new file mode 100644 index 0000000..a8c3740 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Edytor konfiguracji - Frigate", + "configEditor": "Edytor konfiguracji", + "copyConfig": "Skopiuj konfigurację", + "toast": { + "success": { + "copyToClipboard": "Konfiguracja skopiowana do schowka." + }, + "error": { + "savingError": "Błąd podczas zapisywanie konfiguracji" + } + }, + "saveOnly": "Tylko zapisz", + "saveAndRestart": "Zapisz i uruchom ponownie", + "confirm": "Zamknąć bez zapisywania?", + "safeConfigEditor": "Edytor Konfiguracji (tryb bezpieczny)", + "safeModeDescription": "Frigate jest w trybie bezpiecznym przez błąd walidacji konfiguracji." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/events.json new file mode 100644 index 0000000..cc7b258 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/events.json @@ -0,0 +1,61 @@ +{ + "camera": "Kamera", + "alerts": "Alerty", + "detections": "Wykrycia", + "motion": { + "label": "Ruch", + "only": "Tylko ruch" + }, + "allCameras": "Wszystkie kamery", + "empty": { + "alert": "Brak alertów do przejrzenia", + "detection": "Brak detekcji do przejrzenia", + "motion": "Nie znaleziono danych o ruchu" + }, + "timeline": "Oś czasu", + "timeline.aria": "Wybierz oś czasu", + "events": { + "label": "Zdarzenia", + "aria": "Wybierz zdarzenia", + "noFoundForTimePeriod": "Brak zdarzeń w tym okresie czasu." + }, + "documentTitle": "Przegląd - Frigate", + "recordings": { + "documentTitle": "Nagrania - Frigate" + }, + "markAsReviewed": "Oznacz jako przejrzane", + "markTheseItemsAsReviewed": "Oznacz te elementy jako przejrzane", + "calendarFilter": { + "last24Hours": "Ostatnie 24 godziny" + }, + "newReviewItems": { + "label": "Zobacz nowe elementy do przeglądu", + "button": "Nowe elementy do przeglądu" + }, + "selected_one": "{{count}} wybrane", + "selected_other": "{{count}} wybrane", + "detected": "wykryto", + "suspiciousActivity": "Podejrzana aktywność", + "threateningActivity": "Niebezpieczne działania", + "zoomIn": "Przybliż", + "zoomOut": "Oddal", + "detail": { + "label": "Szczegóły", + "noDataFound": "Brak szczegółów do przejrzenia", + "aria": "Przełącz widok szczegółów", + "trackedObject_one": "{{count}} obiekt", + "trackedObject_other": "{{count}} obiekty", + "noObjectDetailData": "Brak danych szczegółowych dla obiektu.", + "settings": "Ustawienia widoku szczegółów", + "alwaysExpandActive": { + "title": "Zawsze rozwiń aktywne", + "desc": "Zawsze rozwijaj szczegóły aktywnego obiektu, jeżeli są dostępne." + } + }, + "objectTrack": { + "trackedPoint": "Śledzony punkt", + "clickToSeek": "Kliknij aby przewinąć do tego miejsca" + }, + "needsReview": "Wymaga manualnego sprawdzenia", + "normalActivity": "Normalne" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/explore.json new file mode 100644 index 0000000..5132a2a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/explore.json @@ -0,0 +1,264 @@ +{ + "generativeAI": "Generatywna SI", + "documentTitle": "Eksploruj - Frigate", + "details": { + "timestamp": "Znacznik czasu", + "item": { + "desc": "Szczegóły elementu do przeglądu", + "title": "Szczegóły Elementu do Przeglądu", + "button": { + "share": "Udostępnij ten element", + "viewInExplore": "Zobacz w Eksploracji" + }, + "tips": { + "hasMissingObjects": "Dostosuj swoją konfigurację, jeśli chcesz, aby Frigate zapisywał śledzone obiekty dla następujących etykiet: {{objects}}", + "mismatch_one": "{{count}} niedostępny obiekt został wykryty i uwzględniony w tym elemencie przeglądu. Ten obiekt albo nie kwalifikował się jako alert lub detekcja, albo został już wyczyszczont/usunięty.", + "mismatch_few": "{{count}} niedostępne obiekty zostały wykryte i uwzględnione w tym elemencie przeglądu. Te obiekty albo nie kwalifikowały się jako alert lub detekcja, albo zostały już wyczyszczone/usunięte.", + "mismatch_many": "{{count}} niedostępnych obiektów zostało wykrytych i uwzględnionych w tym elemencie przeglądu. Te obiekty albo nie kwalifikowały się jako alert lub detekcja, albo zostały już wyczyszczone/usunięte." + }, + "toast": { + "success": { + "regenerate": "Zażądano nowego opisu od {{provider}}. W zależności od szybkości twojego dostawcy, wygenerowanie nowego opisu może zająć trochę czasu.", + "updatedSublabel": "Pomyślnie zaktualizowano podetykietę.", + "updatedLPR": "Pomyślnie zaktualizowano tablicę rejestracyjną.", + "audioTranscription": "Wysłano prośbę o audio transkrypcję." + }, + "error": { + "regenerate": "Nie udało się wezwać {{provider}} dla nowego opisu: {{errorMessage}}", + "updatedSublabelFailed": "Nie udało się zaktualizować podetykiety: {{errorMessage}}", + "updatedLPRFailed": "Nie udało się zaktualizować tablicy rejestracyjnej: {{errorMessage}}", + "audioTranscription": "Nie udało się włączyć audio transkrypcji: {{errorMessage}}" + } + } + }, + "topScore": { + "info": "Najwyższy wynik to najwyższa mediana wyniku dla śledzonego obiektu, więc może się różnić od wyniku pokazanego na miniaturze wyników wyszukiwania.", + "label": "Najwyższy wynik" + }, + "editSubLabel": { + "descNoLabel": "Wprowadź nową podetykietę dla tego śledzonego obiektu", + "title": "Edytuj podetykietę", + "desc": "Wprowadź nową podetykietę dla tego {{label}}" + }, + "estimatedSpeed": "Szacowana prędkość", + "label": "Etykieta", + "button": { + "regenerate": { + "title": "Regeneruj", + "label": "Regeneruj opis śledzonego obiektu" + }, + "findSimilar": "Znajdź Podobne" + }, + "objects": "Obiekty", + "camera": "Kamera", + "zones": "Strefy", + "expandRegenerationMenu": "Rozwiń menu regeneracji", + "description": { + "label": "Opis", + "placeholder": "Opis śledzonego obiektu", + "aiTips": "Frigate nie poprosi o opis od twojego dostawcy AI, dopóki cykl życia śledzonego obiektu nie dobiegnie końca." + }, + "editLPR": { + "title": "Edytuj tablicę rejestracyjną", + "desc": "Wprowadź nową wartość tablicy rejestracyjnej dla tego {{label}}", + "descNoLabel": "Wprowadź nową wartość tablicy rejestracyjnej dla tego śledzonego obiektu" + }, + "tips": { + "descriptionSaved": "Pomyślnie zapisano opis", + "saveDescriptionFailed": "Nie udało się zaktualizować opisu: {{errorMessage}}" + }, + "recognizedLicensePlate": "Rozpoznana tablica rejestracyjna", + "regenerateFromSnapshot": "Regeneruj ze zrzutu ekranu", + "regenerateFromThumbnails": "Regeneruj z miniatur", + "snapshotScore": { + "label": "Wynik zrzutu" + }, + "score": { + "label": "Wynik" + } + }, + "objectLifecycle": { + "annotationSettings": { + "title": "Ustawienia adnotacji", + "showAllZones": { + "title": "Pokaż wszystkie strefy", + "desc": "Zawsze pokazuj strefy na klatkach, w których obiekty weszły do strefy." + }, + "offset": { + "desc": "Te dane pochodzą z kanału detekcji kamery, ale są nakładane na obrazy z kanału nagrywania. Mało prawdopodobne, aby oba strumienie były idealnie zsynchronizowane. W rezultacie ramka ograniczająca i nagranie mogą nie być idealnie dopasowane. Jednak pole annotation_offset może być użyte do regulacji tego.", + "documentation": "Przeczytaj dokumentację ", + "label": "Przesunięcie adnotacji", + "millisecondsToOffset": "Milisekundy do przesunięcia adnotacji detekcji. Domyślnie: 0", + "tips": "WSKAZÓWKA: Wyobraź sobie, że istnieje klip zdarzenia z osobą idącą od lewej do prawej. Jeśli na osi czasu zdarzenia ramka ograniczająca jest konsekwentnie na lewo od osoby, wartość powinna być zmniejszona. Podobnie, jeśli osoba idzie od lewej do prawej, a ramka ograniczająca jest konsekwentnie przed osobą, wartość powinna być zwiększona.", + "toast": { + "success": "Przesunięcie adnotacji dla {{camera}} zostało zapisane w pliku konfiguracyjnym. Zrestartuj Frigate, aby zastosować zmiany." + } + } + }, + "title": "Cykl życia obiektu", + "noImageFound": "Nie znaleziono obrazu dla tego znacznika czasu.", + "scrollViewTips": "Przewiń, aby zobaczyć kluczowe momenty cyklu życia tego obiektu.", + "autoTrackingTips": "Pozycje ramek ograniczających mogą być niedokładne dla kamer z automatycznym śledzeniem.", + "lifecycleItemDesc": { + "visible": "{{label}} wykryty", + "entered_zone": "{{label}} wszedł w strefę {{zones}}", + "active": "{{label}} stał się aktywny", + "stationary": "{{label}} stał się nieruchomy", + "attribute": { + "faceOrLicense_plate": "{{attribute}} wykryty dla {{label}}", + "other": "{{label}} rozpoznany jako {{attribute}}" + }, + "gone": "{{label}} zniknął", + "heard": "{{label}} usłyszany", + "external": "{{label}} wykryty", + "header": { + "ratio": "Współczynnik", + "area": "Obszar", + "zones": "Strefy" + } + }, + "carousel": { + "previous": "Poprzedni slajd", + "next": "Następny slajd" + }, + "createObjectMask": "Utwórz maskę obiektu", + "adjustAnnotationSettings": "Dostosuj ustawienia adnotacji", + "trackedPoint": "Punkt Śledzenia", + "count": "{{first}} z {{second}}" + }, + "exploreIsUnavailable": { + "title": "Eksploracja jest niedostępna", + "embeddingsReindexing": { + "context": "Eksploracja będzie dostępna po zakończeniu ponownego indeksowania osadzenia śledzonych obiektów.", + "startingUp": "Uruchamianie…", + "estimatedTime": "Szacowany pozostały czas:", + "finishingShortly": "Zaraz się zakończy", + "step": { + "thumbnailsEmbedded": "Osadzone miniatury: ", + "descriptionsEmbedded": "Osadzone opisy: ", + "trackedObjectsProcessed": "Przetworzone śledzone obiekty: " + } + }, + "downloadingModels": { + "context": "Frigate pobiera niezbędne modele osadzenia do obsługi funkcji wyszukiwania semantycznego. Może to potrwać kilka minut, w zależności od prędkości Twojego połączenia sieciowego.", + "setup": { + "visionModelFeatureExtractor": "Ekstraktor cech modelu wizyjnego", + "textModel": "Model tekstowy", + "visionModel": "Model wizyjny", + "textTokenizer": "Tokenizer tekstu" + }, + "tips": { + "context": "Po pobraniu modeli warto ponownie zindeksować osadzenia śledzonych obiektów.", + "documentation": "Przeczytaj dokumentację" + }, + "error": "Wystąpił błąd. Sprawdź logi Frigate." + } + }, + "trackedObjectDetails": "Szczegóły śledzonego obiektu", + "type": { + "details": "szczegóły", + "snapshot": "zrzut ekranu", + "video": "wideo", + "object_lifecycle": "cykl życia obiektu", + "thumbnail": "miniaturka", + "tracking_details": "szczegóły śledzenia" + }, + "itemMenu": { + "downloadSnapshot": { + "aria": "Pobierz zrzut ekranu", + "label": "Pobierz zrzut ekranu" + }, + "viewObjectLifecycle": { + "label": "Wyświetl cykl życia obiektu", + "aria": "Pokaż cykl życia obiektu" + }, + "downloadVideo": { + "label": "Pobierz wideo", + "aria": "Pobierz wideo" + }, + "findSimilar": { + "label": "Znajdź podobne", + "aria": "Znajdź podobne śledzone obiekty" + }, + "submitToPlus": { + "label": "Prześlij do Frigate+", + "aria": "Prześlij do Frigate Plus" + }, + "viewInHistory": { + "label": "Wyświetl w Historii", + "aria": "Wyświetl w Historii" + }, + "deleteTrackedObject": { + "label": "Usuń ten śledzony obiekt" + }, + "addTrigger": { + "label": "Dodaj wyzwalacz", + "aria": "Dodaj wyzwalacz dla tego śledzonego obiektu" + }, + "audioTranscription": { + "label": "Rozpisz", + "aria": "Poproś o audiotranskrypcję" + } + }, + "trackedObjectsCount_one": "{{count}} śledzony obiekt ", + "trackedObjectsCount_few": "{{count}} śledzone obiekty ", + "trackedObjectsCount_many": "{{count}} śledzonych obiektów ", + "noTrackedObjects": "Nie znaleziono śledzonych obiektów", + "dialog": { + "confirmDelete": { + "desc": "Usunięcie tego śledzonego obiektu usuwa zrzut ekranu, wszelkie zapisane osadzenia i wszystkie powiązane wpisy cyklu życia obiektu. Nagrany materiał tego śledzonego obiektu w widoku Historii NIE zostanie usunięty.

    Czy na pewno chcesz kontynuować?", + "title": "Potwierdź usunięcie" + } + }, + "fetchingTrackedObjectsFailed": "Błąd pobierania śledzonych obiektów: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Śledzony obiekt usunięty pomyślnie.", + "error": "Nie udało się usunąć śledzonego obiektu: {{errorMessage}}" + } + }, + "tooltip": "Pasuje do {{type}} z pewnością {{confidence}}%" + }, + "exploreMore": "Odkryj więcej obiektów typu {{label}}", + "aiAnalysis": { + "title": "Analiza SI" + }, + "concerns": { + "label": "Obawy" + }, + "trackingDetails": { + "title": "Szczegóły śledzenia", + "noImageFound": "Nie znaleziono obrazka dla podanego czasu.", + "createObjectMask": "Utwórz maskę obiektu", + "adjustAnnotationSettings": "Dostosuj ustawienia adnotacji", + "scrollViewTips": "Kliknij, aby zobaczyć najważniejsze momenty cyklu życia tego obiektu.", + "count": "{{first}} z {{second}}", + "autoTrackingTips": "Pozycja znacznika obiektu jest niedokładna dla kamer z automatycznym śledzeniem.", + "lifecycleItemDesc": { + "visible": "Wykryto {{label}}", + "entered_zone": "{{label}} pojawił się w {{zones}}", + "active": "{{label}} poruszył się", + "stationary": "{{label}} zatrzymał się", + "attribute": { + "faceOrLicense_plate": "Wykryto {{attribute}} dla obiektu {{label}}", + "other": "{{label}} został rozpoznany jako {{attribute}}" + }, + "gone": "Utracono śledzenie dla {{label}}", + "external": "Wykryto {{label}}", + "header": { + "zones": "Strefy", + "area": "Powierzchnia", + "score": "Wynik" + } + }, + "annotationSettings": { + "title": "Ustawienia adnotacji", + "showAllZones": { + "title": "Pokaż wszystkie strefy", + "desc": "Pokazuj linie stref w momencie wejścia obiektu w strefę." + } + }, + "trackedPoint": "Śledzony Punkt" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/exports.json new file mode 100644 index 0000000..b0d41bb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Szukaj", + "documentTitle": "Eksportuj - Frigate", + "noExports": "Nie znaleziono eksportów", + "deleteExport": "Usuń eksport", + "deleteExport.desc": "Czy na pewno chcesz usunąć {{exportName}}?", + "editExport": { + "title": "Zmień nazwę eksportu", + "desc": "Wprowadź nową nazwę dla tego eksportu.", + "saveExport": "Zapisz eksport" + }, + "toast": { + "error": { + "renameExportFailed": "Nie udało się zmienić nazwy eksportu: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Udostępnij eksport", + "downloadVideo": "Pobierz wideo", + "editName": "Edytuj nazwę", + "deleteExport": "Usuń eksport" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/faceLibrary.json new file mode 100644 index 0000000..5edfcba --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "selectItem": "Wybierz {{item}}", + "description": { + "addFace": "Dodaj nową kolekcję do biblioteki twarzy, przesyłając swoje pierwsze zdjęcie.", + "placeholder": "Wprowadź nazwę tej kolekcji", + "invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki." + }, + "details": { + "person": "Osoba", + "confidence": "Pewność", + "face": "Szczegóły twarzy", + "faceDesc": "Szczegóły śledzonego obiektu, który wygenerował tę twarz", + "timestamp": "Znacznik czasu", + "subLabelScore": "Ocena podetykiety", + "scoreInfo": "Ocena podetykiety to ważona ocena uwzględniająca wszystkie rozpoznane twarze i ich poziom rozpoznania , dlatego może się różnić od oceny pokazanej na stopklatce.", + "unknown": "Nieznany" + }, + "documentTitle": "Biblioteka twarzy - Frigate", + "uploadFaceImage": { + "title": "Wgraj zdjęcie twarzy", + "desc": "Wgraj obraz do skanowania twarzy i dołącz do {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Utwórz kolekcję", + "desc": "Utwórz nową kolekcję", + "new": "Utwórz nową twarz", + "nextSteps": "Aby zbudować solidną podstawę:
  • Użyj zakładki Ostatnie rozpoznania, aby wybrać i trenować na obrazach dla każdej wykrytej osoby.
  • Skup się na zdjęciach twarzy na wprost dla najlepszych wyników; unikaj trenowania na zdjęciach, które pokazują twarze pod kątem.
  • " + }, + "train": { + "aria": "Wybierz ostatnio rozpoznane", + "title": "Ostatnie rozpoznania", + "empty": "Nie podjęto ostatnio żadnych prób rozpoznawania twarzy" + }, + "selectFace": "Wybierz twarz", + "deleteFaceLibrary": { + "title": "Usuń nazwę", + "desc": "Czy na pewno chcesz usunąć kolekcję {{name}}? Spowoduje to trwałe usunięcie wszystkich powiązanych twarzy." + }, + "button": { + "addFace": "Dodaj twarz", + "uploadImage": "Wgraj obraz", + "reprocessFace": "Przetwórz twarz ponownie", + "deleteFaceAttempts": "Usuń twarze", + "renameFace": "Zmień nazwę twarzy", + "deleteFace": "Usuń twarz" + }, + "imageEntry": { + "validation": { + "selectImage": "Proszę wybrać plik obrazu." + }, + "dropActive": "Upuść obraz tutaj…", + "dropInstructions": "Przeciągnij i upuść obraz tutaj lub kliknij, aby wybrać", + "maxSize": "Maksymalny rozmiar: {{size}}MB" + }, + "toast": { + "success": { + "deletedName_one": "{{count}} twarz została pomyślnie usunięta.", + "deletedName_few": "{{count}} twarze zostały pomyślnie usunięte.", + "deletedName_many": "{{count}} twarzy zostało pomyślnie usuniętych.", + "deletedFace_one": "Pomyślnie usunięto {{count}} twarz.", + "deletedFace_few": "Pomyślnie usunięto {{count}} twarze.", + "deletedFace_many": "Pomyślnie usunięto {{count}} twarzy.", + "uploadedImage": "Pomyślnie wgrano obraz.", + "addFaceLibrary": "{{name}} został pomyślnie dodany do Biblioteki Twarzy!", + "trainedFace": "Pomyślnie wytrenowano twarz.", + "updatedFaceScore": "Pomyślnie zaktualizowano wynik twarzy do {{name}} {{score}}.", + "renamedFace": "Pomyślnie zmieniono nazwę twarzy na {{name}}" + }, + "error": { + "addFaceLibraryFailed": "Nie udało się ustawić nazwy twarzy: {{errorMessage}}", + "deleteFaceFailed": "Nie udało się usunąć: {{errorMessage}}", + "deleteNameFailed": "Nie udało się usunąć nazwy: {{errorMessage}}", + "trainFailed": "Nie udało się przeprowadzić treningu: {{errorMessage}}", + "updateFaceScoreFailed": "Nie udało się zaktualizować wyniku twarzy: {{errorMessage}}", + "uploadingImageFailed": "Nie udało się wgrać obrazu: {{errorMessage}}", + "renameFaceFailed": "Nie udało się zmienić nazwy twarzy: {{errorMessage}}" + } + }, + "readTheDocs": "Przeczytaj dokumentację", + "trainFaceAs": "Trenuj twarz jako:", + "trainFace": "Trenuj twarz", + "steps": { + "faceName": "Wprowadź nazwę twarzy", + "uploadFace": "Prześlij obraz twarzy", + "nextSteps": "Kolejne kroki", + "description": { + "uploadFace": "Prześlij zdjęcie {{name}}, na którym twarz jest widoczna z przodu. Obraz nie musi być przycięty wyłącznie do twarzy." + } + }, + "renameFace": { + "title": "Zmień nazwę twarzy", + "desc": "Wprowadź nową nazwę dla {{name}}" + }, + "collections": "Kolekcje", + "deleteFaceAttempts": { + "title": "Usuń twarze", + "desc_one": "Czy na pewno chcesz usunąć {{count}} twarz? Tej czynności nie można cofnąć.", + "desc_few": "Czy na pewno chcesz usunąć {{count}} twarze? Tej czynności nie można cofnąć.", + "desc_many": "Czy na pewno chcesz usunąć {{count}} twarzy? Tej czynności nie można cofnąć." + }, + "nofaces": "Brak dostępnych twarzy", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/live.json new file mode 100644 index 0000000..417354f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Na żywo - Frigate", + "documentTitle.withCamera": "{{camera}}- Na żywo - Frigate", + "lowBandwidthMode": "Tryb niskiej przepustowości", + "twoWayTalk": { + "enable": "Włącz komunikację dwukierunkową", + "disable": "Wyłącz komunikację dwukierunkową" + }, + "cameraAudio": { + "enable": "Włącz dźwięk kamery", + "disable": "Wyłącz dźwięk kamery" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknij w ramce, aby wyśrodkować kamerę", + "enable": "Włącz kliknięcie do przesuwania", + "disable": "Wyłącz kliknięcie do przesuwania" + }, + "left": { + "label": "Przesuń kamerę PTZ w lewo" + }, + "right": { + "label": "Przesuń kamerę PTZ w prawo" + }, + "down": { + "label": "Przesuń kamerę PTZ w dół" + }, + "up": { + "label": "Przesuń kamerę PTZ w górę" + } + }, + "zoom": { + "in": { + "label": "Przybliż kamerę PTZ" + }, + "out": { + "label": "Oddal kamerę PTZ" + } + }, + "frame": { + "center": { + "label": "Kliknij w ramce, aby wyśrodkować kamerę PTZ" + } + }, + "presets": "Presety kamery PTZ", + "focus": { + "in": { + "label": "Zmniejsz ostrość kamery PTZ" + }, + "out": { + "label": "Wyostrz kamerę PTZ" + } + } + }, + "recording": { + "enable": "Włącz nagrywanie", + "disable": "Wyłącz nagrywanie" + }, + "snapshots": { + "enable": "Włącz zrzuty ekranu", + "disable": "Wyłącz zrzuty ekranu" + }, + "streamStats": { + "disable": "Ukryj statystyki strumienia", + "enable": "Pokaż statystyki strumienia" + }, + "manualRecording": { + "title": "Nagrywanie na żądanie", + "tips": "Ręcznie rozpocznij zdarzenie w oparciu o ustawienia przechowywania nagrań tej kamery.", + "playInBackground": { + "label": "Odtwarzaj w tle", + "desc": "Włącz tę opcję, aby kontynuować transmisję, gdy odtwarzacz jest ukryty." + }, + "showStats": { + "label": "Pokaż statystyki", + "desc": "Włącz tę opcję, aby pokazać statystyki strumienia jako nakładkę na podgląd kamery." + }, + "debugView": "Widok debugowania", + "start": "Rozpocznij nagrywanie na żądanie", + "started": "Rozpoczęto ręczne nagrywanie na żądanie.", + "failedToStart": "Nie udało się rozpocząć ręcznego nagrywania na żądanie.", + "recordDisabledTips": "Ponieważ nagrywanie jest wyłączone lub ograniczone w konfiguracji tej kamery, zostanie zapisany tylko zrzut ekranu.", + "end": "Zakończ nagrywanie na żądanie", + "ended": "Zakończono ręczne nagrywanie na żądanie.", + "failedToEnd": "Nie udało się zakończyć ręcznego nagrywania na żądanie." + }, + "notifications": "Powiadomienia", + "audio": "Dźwięk", + "suspend": { + "forTime": "Zawieś na: " + }, + "stream": { + "title": "Strumień", + "audio": { + "tips": { + "title": "Dźwięk musi być wysyłany z kamery i skonfigurowany w go2rtc dla tego strumienia.", + "documentation": "Przeczytaj dokumentację " + }, + "available": "Dźwięk jest dostępny dla tego strumienia", + "unavailable": "Dźwięk nie jest dostępny dla tego strumienia" + }, + "twoWayTalk": { + "tips.documentation": "Przeczytaj dokumentację ", + "tips": "Twoje urządzenie musi obsługiwać tę funkcję, a WebRTC musi być skonfigurowany dla komunikacji dwukierunkowej.", + "unavailable": "Komunikacja dwukierunkowa jest niedostępna dla tego strumienia", + "available": "Komunikacja dwukierunkowa jest dostępna dla tego strumienia" + }, + "lowBandwidth": { + "resetStream": "Zresetuj strumień", + "tips": "Podgląd na żywo jest w trybie niskiej przepustowości z powodu buforowania lub błędów strumienia." + }, + "playInBackground": { + "tips": "Włącz tę opcję, aby kontynuować transmisję, gdy odtwarzacz jest ukryty.", + "label": "Odtwarzaj w tle" + }, + "debug": { + "picker": "Wybór strumienia jest niedostępny w trybie debug. Widok w trybie debug zawsze pokazuje strumień przypisany do detektora." + } + }, + "cameraSettings": { + "title": "Ustawienia {{camera}}", + "cameraEnabled": "Kamera włączona", + "objectDetection": "Wykrywanie obiektów", + "recording": "Nagrywanie", + "snapshots": "Zrzuty ekranu", + "audioDetection": "Wykrywanie dźwięku", + "autotracking": "Automatyczne śledzenie", + "transcription": "Stenogram" + }, + "effectiveRetainMode": { + "modes": { + "all": "Wszystkie", + "active_objects": "Aktywne obiekty", + "motion": "Ruch" + }, + "notAllTips": "Twoja konfiguracja przechowywania nagrań {{source}} jest ustawiona na tryb: {{effectiveRetainMode}}, więc to nagrywanie na żądanie zachowa tylko segmenty z {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Edytuj układ", + "group": { + "label": "Edytuj grupę kamer" + }, + "exitEdit": "Zakończ edycję" + }, + "muteCameras": { + "enable": "Wycisz wszystkie kamery", + "disable": "Wyłącz wyciszenie wszystkich kamer" + }, + "camera": { + "disable": "Wyłącz kamerę", + "enable": "Włącz kamerę" + }, + "autotracking": { + "enable": "Włącz automatyczne śledzenie", + "disable": "Wyłącz automatyczne śledzenie" + }, + "detect": { + "disable": "Wyłącz wykrywanie", + "enable": "Włącz wykrywanie" + }, + "audioDetect": { + "enable": "Włącz wykrywanie dźwięku", + "disable": "Wyłącz wykrywanie dźwięku" + }, + "streamingSettings": "Ustawienia transmisji", + "history": { + "label": "Pokaż nagrania archiwalne" + }, + "transcription": { + "enable": "Włącz audio transkrypcję na żywo", + "disable": "Wyłącz audio transkrypcję na żywo" + }, + "noCameras": { + "buttonText": "Dodaj kamerę", + "description": "Zacznij od podłączenia kamery do Frigate.", + "title": "Nie skonfigurowano żadnej kamery", + "restricted": { + "title": "Brak dostępnych kamer", + "description": "Nie masz uprawnień aby przeglądać kamery w tej grupie." + } + }, + "snapshot": { + "takeSnapshot": "Pobierz miniaturę", + "captureFailed": "Nie udało się wykonać migawki.", + "downloadStarted": "Pobieranie migawki rozpoczęte.", + "noVideoSource": "Brak źródeł video dostępnych do wykonania migawki." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/recording.json new file mode 100644 index 0000000..dfaf0c3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtr", + "export": "Eksportuj", + "calendar": "Kalendarz", + "filters": "Filtry", + "toast": { + "error": { + "noValidTimeSelected": "Nie wybrano poprawnego zakresu czasu", + "endTimeMustAfterStartTime": "Czas końca musi być po czasie początku" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/search.json new file mode 100644 index 0000000..175b42a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "Szukaj", + "savedSearches": "Zapisane wyszukiwania", + "searchFor": "Szukaj {{inputValue}}", + "button": { + "clear": "Wyczyść wyszukiwanie", + "save": "Zapisz wyszukiwanie", + "delete": "Usuń zapisane wyszukiwanie", + "filterInformation": "Informacje o filtrze", + "filterActive": "Aktywne filtry" + }, + "trackedObjectId": "ID śledzonego obiektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Etykiety", + "zones": "Strefy", + "sub_labels": "Podetykiety", + "min_score": "Min. wynik", + "max_score": "Maks. wynik", + "min_speed": "Min. prędkość", + "max_speed": "Maks. prędkość", + "recognized_license_plate": "Rozpoznana tablica rejestracyjna", + "has_clip": "Posiada klip", + "has_snapshot": "Posiada zrzut ekranu", + "after": "Po", + "search_type": "Typ wyszukiwania", + "time_range": "Zakres czasu", + "before": "Przed" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data 'przed' musi być późniejsza niż data 'po'.", + "afterDatebeEarlierBefore": "Data 'po' musi być wcześniejsza niż data 'przed'.", + "minScoreMustBeLessOrEqualMaxScore": "'Min. wynik' musi być mniejszy lub równy 'maks. wynikowi'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'Maks. wynik' musi być większy lub równy 'min. wynikowi'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Min. prędkość' musi być mniejsza lub równa 'maks. prędkości'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Maks. prędkość' musi być większa lub równa 'min. prędkości'." + } + }, + "tips": { + "title": "Jak używać filtrów tekstowych", + "desc": { + "text": "Filtry pomagają zawęzić wyniki wyszukiwania. Oto jak używać ich w polu wejściowym:", + "example": "Przykład: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Wpisz nazwę filtra, a następnie dwukropek (np., \"kamery:\").
    • Wybierz wartość z sugestii lub wpisz własną.
    • Używaj wielu filtrów, dodając je jeden po drugim z odstępem pomiędzy.
    • Filtry dat (przed: i po:) używają formatu {{DateFormat}}.
    • Filtr zakresu czasu używa formatu {{exampleTime}}.
    • Usuwaj filtry klikając 'x' obok nich.
    ", + "step2": "Wybierz wartość z sugestii lub wpisz własną.", + "step3": "Użyj kilku filtrów dodając jeden po drugim i oddzielając je spacją.", + "exampleLabel": "Przykład:", + "step1": "Wprowadź nazwę filtra z dwukropkiem (np.: \"cameras:\").", + "step4": "Filtry dat (before: i after:) użyj formatu {{DateFormat}}.", + "step5": "Filtr zakresu czasu wykorzystuje format {{exampleTime}}.", + "step6": "Usuń filtry klikając 'x' obok nich." + } + }, + "header": { + "currentFilterType": "Wartości filtrów", + "noFilters": "Filtry", + "activeFilters": "Aktywne filtry" + } + }, + "similaritySearch": { + "title": "Wyszukiwanie podobieństw", + "active": "Wyszukiwanie podobieństw aktywne", + "clear": "Wyczyść wyszukiwanie podobieństw" + }, + "placeholder": { + "search": "Szukaj…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/settings.json new file mode 100644 index 0000000..956eb5f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/settings.json @@ -0,0 +1,1135 @@ +{ + "menu": { + "users": "Użytkownicy", + "notifications": "Powiadomienia", + "ui": "Interfejs Użytkownika", + "classification": "Klasyfikacja", + "cameras": "Ustawienia Kamery", + "frigateplus": "Frigate+", + "masksAndZones": "Maski / Strefy", + "motionTuner": "Konfigurator Ruchu", + "debug": "Debugowanie", + "enrichments": "Wzbogacenia", + "triggers": "Wyzwalacze", + "roles": "Role", + "cameraManagement": "Zarządzanie", + "cameraReview": "Przegląd" + }, + "dialog": { + "unsavedChanges": { + "title": "Masz niezapisane zmiany.", + "desc": "Czy chcesz zapisać swoje zmiany przed kontynuowaniem?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Brak Kamery" + }, + "general": { + "title": "Ustawienia interfejsu użytkownika", + "storedLayouts": { + "title": "Zapisane Układy", + "clearAll": "Wyczyść Wszystkie Układy", + "desc": "Układ kamer w grupie można przeciągać/zmieniać rozmiar. Pozycje są zapisywane w lokalnej pamięci przeglądarki." + }, + "calendar": { + "title": "Kalendarz", + "firstWeekday": { + "label": "Pierwszy dzień tygodnia", + "sunday": "Niedziela", + "monday": "Poniedziałek", + "desc": "Dzień od którego zaczyna się kalendarz przeglądu." + } + }, + "liveDashboard": { + "automaticLiveView": { + "label": "Automatyczny Podgląd na Żywo", + "desc": "Automatycznie przełącz na podgląd na żywo kamery, gdy wykryta zostanie aktywność. Wyłączenie tej opcji spowoduje, że statyczne obrazy kamer na panelu Na Żywo będą aktualizowane tylko raz na minutę." + }, + "title": "Panel Na Żywo", + "playAlertVideos": { + "label": "Odtwarzaj Filmy Alarmowe", + "desc": "Domyślnie, ostatnie alerty na panelu Na Żywo są odtwarzane jako małe zapętlone filmy. Wyłącz tę opcję, aby pokazywać tylko statyczny obraz ostatnich alertów na tym urządzeniu/przeglądarce." + }, + "displayCameraNames": { + "label": "Zawsze pokazuj nazwy kamer", + "desc": "Zawsze pokazuj nazwę kamery w widoku wielu kamer." + }, + "liveFallbackTimeout": { + "label": "Przekroczono czas oczekiwania dla strumienia", + "desc": "W wypadku utraty strumienia wysokiej jakości, użyj trybu niskiej przepustowości po X sekund od utracenia połączenia. Sugerowana wartość: 3." + } + }, + "cameraGroupStreaming": { + "title": "Ustawienia Strumieniowania Grup Kamer", + "desc": "Ustawienia strumieniowania dla każdej grupy kamer są przechowywane w lokalnej pamięci przeglądarki.", + "clearAll": "Wyczyść Wszystkie Ustawienia Strumieniowania" + }, + "recordingsViewer": { + "title": "Przeglądarka Nagrań", + "defaultPlaybackRate": { + "label": "Domyślna Prędkość Odtwarzania", + "desc": "Domyślna prędkość odtwarzania dla odtwarzania nagrań." + } + }, + "toast": { + "success": { + "clearStoredLayout": "Wyczyszczono zapisany układ dla {{cameraName}}", + "clearStreamingSettings": "Wyczyszczono ustawienia strumieniowania dla wszystkich grup kamer." + }, + "error": { + "clearStoredLayoutFailed": "Nie udało się wyczyścić zapisanego układu: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nie udało się wyczyścić ustawień strumieniowania: {{errorMessage}}" + } + } + }, + "documentTitle": { + "default": "Ustawienia - Frigate", + "camera": "Ustawienia Kamery - Frigate", + "masksAndZones": "Edytor Masek i Stref - Frigate", + "frigatePlus": "Ustawienia Frigate+ - Frigate", + "classification": "Ustawienia Klasyfikacji - Frigate", + "general": "Ustawienia Interfejsu - Frigate", + "authentication": "Ustawienia Uwierzytelniania - Frigate", + "motionTuner": "Konfigurator Ruchu - Frigate", + "object": "Debug - Frigate", + "notifications": "Ustawienia powiadomień - Frigate", + "enrichments": "Ustawienia wzbogacania - Frigate", + "cameraManagement": "Zarządzanie kamerami – Frigate", + "cameraReview": "Ustawienia przeglądu kamer - Frigate" + }, + "classification": { + "title": "Ustawienia Klasyfikacji", + "semanticSearch": { + "modelSize": { + "small": { + "title": "mały", + "desc": "Używanie małego modelu wykorzystuje skwantyzowaną wersję, która zużywa mniej pamięci RAM i działa szybciej na procesorze przy bardzo nieznacznej różnicy w jakości osadzeń." + }, + "large": { + "title": "duży", + "desc": "Używanie dużego modelu wykorzystuje pełny model Jina i automatycznie uruchomi się na GPU, jeśli jest dostępny." + }, + "desc": "Rozmiar modelu używanego do osadzeń wyszukiwania semantycznego.", + "label": "Rozmiar Modelu" + }, + "title": "Wyszukiwanie Semantyczne", + "desc": "Wyszukiwanie semantyczne w Frigate pozwala znaleźć śledzone obiekty w elementach przeglądu za pomocą samego obrazu, zdefiniowanego przez użytkownika opisu tekstowego lub automatycznie wygenerowanego opisu.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "reindexNow": { + "label": "Przeindeksuj Teraz", + "desc": "Przeindeksowanie wygeneruje ponownie osadzenia dla wszystkich śledzonych obiektów. Ten proces działa w tle i może maksymalnie obciążyć procesor oraz zająć sporo czasu w zależności od liczby śledzonych obiektów.", + "confirmButton": "Przeindeksuj", + "success": "Przeindeksowanie rozpoczęte pomyślnie.", + "alreadyInProgress": "Przeindeksowanie jest już w toku.", + "error": "Nie udało się rozpocząć przeindeksowania: {{errorMessage}}", + "confirmTitle": "Potwierdź Przeindeksowanie", + "confirmDesc": "Czy na pewno chcesz przeindeksować osadzenia wszystkich śledzonych obiektów? Ten proces będzie działał w tle, ale może maksymalnie obciążyć procesor i zająć sporo czasu. Możesz śledzić postęp na stronie Eksploruj." + } + }, + "faceRecognition": { + "title": "Rozpoznawanie Twarzy", + "desc": "Rozpoznawanie twarzy pozwala na przypisanie imion do osób przez Frigate jako podrzędna etykieta. Ta informacja jest zawarta w interfejsie użytkownika, filtrach jak i w powiadomieniach.", + "modelSize": { + "label": "Rozmiar Modelu", + "desc": "Rozmiar modelu używanego do rozpoznawania twarzy.", + "small": { + "title": "mały", + "desc": "Używanie małego modelu wykorzystuje model osadzania twarzy FaceNet, który działa efektywnie na większości procesorów." + }, + "large": { + "title": "duży", + "desc": "Używanie dużego modelu wykorzystuje model osadzania twarzy ArcFace i automatycznie uruchomi się na GPU, jeśli jest dostępny." + } + }, + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "licensePlateRecognition": { + "title": "Rozpoznawanie Tablic Rejestracyjnych", + "desc": "Frigate może rozpoznawać tablice rejestracyjne pojazdów i automatycznie dodawać wykryte znaki do pola recognized_license_plate albo znaną nazwę jako etykietę podrzędną do obiektów typu samochód. Częstym przypadkiem użycia może być odczytywanie tablic rejestracyjnych samochodów wjeżdżających na podjazd albo przejeżdżających ulicą.", + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "toast": { + "error": "Nie udało się zapisać zmian w konfiguracji: {{errorMessage}}", + "success": "Ustawienia klasyfikacji zostały zapisane. Uruchom ponownie Frigate aby wprowadzić swoje zmiany." + }, + "birdClassification": { + "desc": "Klasyfikacja ptaków identyfikuje znane ptaki przy użyciu skwantyzowanego modelu Tensorflow. Gdy znany ptak zostanie rozpoznany, jego popularna nazwa zostanie dodana jako sub_label. Ta informacja jest uwzględniana w interfejsie użytkownika, filtrach oraz powiadomieniach.", + "title": "Klasyfikacja Ptaków" + }, + "restart_required": "Wymagane ponowne uruchomienie (Zmienione ustawienia klasyfikacji)", + "unsavedChanges": "Niezapisane zmiany w ustawieniach klasyfikacji" + }, + "camera": { + "title": "Ustawienia Kamery", + "reviewClassification": { + "noDefinedZones": "Brak stref zdefiniowanych dla tej kamery.", + "selectAlertsZones": "Wybierz strefy dla Alertów", + "title": "Klasyfikacja Przeglądu", + "desc": "Frigate kategoryzuje elementy przeglądu jako Alerty i Wykrycia. Domyślnie wszystkie obiekty osoba i samochód są traktowane jako Alerty. Możesz doprecyzować kategoryzację elementów przeglądu, konfigurując dla nich wymagane strefy.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "objectAlertsTips": "Wszystkie obiekty {{alertsLabels}} na {{cameraName}} będą wyświetlane jako Alerty.", + "zoneObjectAlertsTips": "Wszystkie obiekty {{alertsLabels}} wykryte w strefie {{zone}} na {{cameraName}} będą wyświetlane jako Alerty.", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Wszystkie obiekty {{detectionsLabels}} wykryte w strefie {{zone}} na {{cameraName}}, które nie są skategoryzowane jako Alerty, będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "regardlessOfZoneObjectDetectionsTips": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} na {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "text": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} w strefie {{zone}} na {{cameraName}} będą wyświetlane jako Wykrycia." + }, + "toast": { + "success": "Konfiguracja klasyfikacji przeglądu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + }, + "objectDetectionsTips": "Wszystkie nieskategoryzowane obiekty {{detectionsLabels}} na {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy, w której się znajdują.", + "limitDetections": "Ogranicz wykrycia do określonych stref", + "selectDetectionsZones": "Wybierz strefy dla Wykryć", + "unsavedChanges": "Niezapisane ustawienia klasyfikacji przeglądu dla {{camera}}" + }, + "review": { + "alerts": "Alerty ", + "title": "Przegląd", + "detections": "Wykrycia ", + "desc": "Tymczasowo włącz/wyłącz alerty i wykrywania dla tej kamery do czasu restartu Frigate. Po wyłączeniu nie będą generowane nowe elementy do przeglądu. " + }, + "streams": { + "desc": "Tymczasowo wyłącz kamerę dopóki Frigate nie uruchomi się ponownie. Wyłączenie kamery całkowicie zatrzymuje przetwarzanie strumieni tej kamery przez Frigate. Wykrywanie, nagrywanie i debugowanie będą niedostępne.
    Uwaga: Nie wyłącza to przekazywania strumieni go2rtc.", + "title": "Strumienie" + }, + "object_descriptions": { + "title": "Opisy obiektów wygenerowane przez Sztuczną Inteligencję", + "desc": "Tymczasowo włącz/wyłącz opisy obiektów generowane przez SI. Gdy zostanie to wyłączone, prośby o opis śledzonych obiektów dla tej kamery nie będzie przesyłany do SI." + }, + "review_descriptions": { + "title": "Opis recenzji od SI", + "desc": "Tymczasowo włącz/wyłącz recenzje opisów SI dla tej kamery. Gdy wyłączone prośby o wykonanie opisów nie zostaną przekazane do SI dla tej kamery." + }, + "addCamera": "Dodaj nową kamerę", + "editCamera": "Edytuj kamerę:", + "selectCamera": "Wybierz kamerę", + "backToSettings": "Powrót do ustawień kamery", + "cameraConfig": { + "add": "Dodaj kamerę", + "edit": "Edytuj kamerę", + "description": "Konfiguracja ustawień kamery wraz ze strumieniem wejściowym i rolami.", + "name": "Nazwa kamery", + "nameRequired": "Nazwa kamery jest wymagana", + "nameLength": "Nazwa kamery musi być krótsza niż 24 znaki.", + "namePlaceholder": "np. drzwi_wejsciowe", + "enabled": "Włączony", + "ffmpeg": { + "inputs": "Strumienie wejściowe", + "path": "Ścieżka do strumienia", + "pathRequired": "Ścieżka do strumienia jest wymagana", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Przynajmniej jedna rola jest wymagana", + "rolesUnique": "Każda z ról (audio, detect, record) może być przypisana tylko do jednego strumienia", + "addInput": "Dodaj strumień wejściowy", + "removeInput": "Usuń strumień wejściowy", + "inputsRequired": "Przynajmniej jeden strumień wejściowy jest wymagany" + }, + "toast": { + "success": "Konfiguracja kamery {{cameraName}} została zapisana" + } + } + }, + "masksAndZones": { + "filter": { + "all": "Wszystkie Maski i Strefy" + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Nazwa strefy musi mieć co najmniej 2 znaki.", + "mustNotBeSameWithCamera": "Nazwa strefy nie może być taka sama jak nazwa kamery.", + "alreadyExists": "Strefa z tą nazwą już istnieje dla tej kamery.", + "hasIllegalCharacter": "Nazwa strefy zawiera niedozwolone znaki.", + "mustNotContainPeriod": "Nazwa strefy nie może zawierać kropki." + } + }, + "distance": { + "error": { + "text": "Odległość musi być większa lub równa 0.1.", + "mustBeFilled": "Wszystkie pola odległości muszą być wypełnione, aby używać szacowania prędkości." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Bezwładność musi być większa od 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Czas przebywania musi być większy lub równy 0." + } + }, + "polygonDrawing": { + "snapPoints": { + "false": "Nie przyciągaj punktów", + "true": "Przyciągaj punkty" + }, + "removeLastPoint": "Usuń ostatni punkt", + "reset": { + "label": "Wyczyść wszystkie punkty" + }, + "delete": { + "title": "Potwierdź usunięcie", + "success": "{{name}} został usunięty.", + "desc": "Czy na pewno chcesz usunąć {{type}} {{name}}?" + }, + "error": { + "mustBeFinished": "Rysowanie wielokąta musi być zakończone przed zapisaniem." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Próg prędkości musi być większy lub równy 0,1." + } + } + }, + "zones": { + "label": "Strefy", + "documentTitle": "Edytuj Strefę - Frigate", + "desc": { + "title": "Strefy pozwalają na zdefiniowanie konkretnych obszarów kadru, dzięki czemu można sprawdzić, czy obiekt znajduje się w danym obszarze.", + "documentation": "Dokumentacja" + }, + "add": "Dodaj Strefę", + "clickDrawPolygon": "Kliknij aby narysować wielokąt na obrazie.", + "edit": "Edytuj Strefę", + "name": { + "title": "Nazwa", + "inputPlaceHolder": "Wprowadź nazwę…", + "tips": "Nazwa musi mieć co najmniej 2 znaki i nie może być taka sama jak nazwa kamery lub innej strefy." + }, + "objects": { + "title": "Obiekty", + "desc": "Lista obiektów dla tej strefy." + }, + "allObjects": "Wszystkie Obiekty", + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "inertia": { + "title": "Bezwładność", + "desc": "Określa, przez ile klatek obiekt musi znajdować się w strefie, zanim zostanie uznany za będący w strefie. Domyślnie: 3" + }, + "loiteringTime": { + "title": "Czas przebywania", + "desc": "Ustala minimalny czas w sekundach, przez który obiekt musi znajdować się w strefie, aby ją aktywować. Domyślnie: 0" + }, + "speedThreshold": { + "title": "Próg prędkości ({{unit}})", + "desc": "Określa minimalną prędkość, przy której obiekty są uwzględniane w tej strefie.", + "toast": { + "error": { + "loiteringTimeError": "Strefy z czasem przebywania większym niż 0 nie powinny być używane z szacowaniem prędkości.", + "pointLengthError": "Szacowanie prędkości zostało wyłączone dla tej strefy. Strefy z szacowaniem prędkości muszą mieć dokładnie 4 punkty." + } + } + }, + "speedEstimation": { + "title": "Szacowanie prędkości", + "desc": "Włącz szacowanie prędkości dla obiektów w tej strefie. Strefa musi mieć dokładnie 4 punkty.", + "docs": "Przeczytaj dokumentację", + "lineADistance": "Odległość linii A ({{unit}})", + "lineBDistance": "Odległość linii B ({{unit}})", + "lineCDistance": "Odległość linii C ({{unit}})", + "lineDDistance": "Odległość linii D ({{unit}})" + }, + "toast": { + "success": "Strefa ({{zoneName}}) została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + } + }, + "motionMasks": { + "desc": { + "documentation": "Dokumentacja", + "title": "Maski ruchu służą do zapobiegania wykrywaniu niepożądanych typów ruchu. Zbyt intensywne maskowanie utrudni śledzenie obiektów." + }, + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "clickDrawPolygon": "Kliknij aby narysować wielokąt na obrazie.", + "documentTitle": "Edytuj maskę ruchu - Frigate", + "add": "Nowa Maska Ruchu", + "polygonAreaTooLarge": { + "tips": "Maski ruchu nie zapobiegają wykrywaniu obiektów. Powinieneś użyć wymaganej strefy zamiast tego.", + "documentation": "Przeczytaj dokumentację", + "title": "Maska ruchu pokrywa {{polygonArea}}% ramki kamery. Duże maski ruchu nie są zalecane." + }, + "toast": { + "success": { + "title": "{{polygonName}} został zapisany. Uruchom ponownie Frigate, aby zastosować zmiany.", + "noName": "Maska Ruchu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + } + }, + "label": "Maska ruchu", + "edit": "Edytuj Maskę Ruchu", + "context": { + "documentation": "Przeczytaj dokumentację", + "title": "Maski ruchu są używane do zapobiegania niepożądanym typom ruchu przed wyzwalaniem detekcji (przykład: gałęzie drzew, znaczniki czasowe kamery). Maski ruchu powinny być używane bardzo oszczędnie, nadmierne maskowanie utrudni śledzenie obiektów." + } + }, + "objectMasks": { + "toast": { + "success": { + "title": "{{polygonName}} został zapisany. Uruchom ponownie Frigate aby wprowadzić zmiany.", + "noName": "Maska Obiektu została zapisana. Uruchom ponownie Frigate, aby zastosować zmiany." + } + }, + "objects": { + "title": "Obiekty", + "allObjectTypes": "Wszystkie typy obiektów", + "desc": "Typ obiektu, który ma zastosowanie do tej maski obiektu." + }, + "point_one": "{{count}} punkt", + "point_few": "{{count}} punkty", + "point_many": "{{count}} punktów", + "label": "Maski Obiektów", + "documentTitle": "Edytuj Maskę Obiektu - Frigate", + "desc": { + "title": "Maski filtrujące obiekty są używane do filtrowania fałszywych detekcji dla danego typu obiektu na podstawie lokalizacji.", + "documentation": "Dokumentacja" + }, + "add": "Dodaj Maskę Obiektu", + "edit": "Edytuj Maskę Obiektu", + "clickDrawPolygon": "Kliknij, aby narysować wielokąt na obrazie.", + "context": "Maski filtrujące obiekty są używane do filtrowania fałszywych detekcji dla danego typu obiektu na podstawie lokalizacji." + }, + "toast": { + "success": { + "copyCoordinates": "Skopiowano współrzędne dla {{polyName}} do schowka." + }, + "error": { + "copyCoordinatesFailed": "Nie udało się skopiować współrzędnych do schowka." + } + }, + "restart_required": "Wymagane ponowne uruchomienie (maski/strefy zmienione)", + "motionMaskLabel": "Maska Ruchu {{number}}", + "objectMaskLabel": "Maska Obiektu {{number}} ({{label}})" + }, + "debug": { + "objectList": "Lista Obiektów", + "debugging": "Debugowanie", + "title": "Debugowanie", + "detectorDesc": "Frigate używa twoich detektorów ({{detectors}}) do wykrywania obiektów w strumieniu wideo kamery.", + "boundingBoxes": { + "desc": "Pokaż ramki ograniczające wokół śledzonych obiektów", + "colors": { + "label": "Kolory Ramek Ograniczających Obiekty", + "info": "
  • Przy uruchomieniu, różne kolory zostaną przypisane do każdej etykiety obiektu
  • Ciemnoniebieska cienka linia oznacza, że obiekt nie jest wykrywany w tym momencie
  • Szara cienka linia oznacza, że obiekt jest wykrywany jako nieruchomy
  • Gruba linia oznacza, że obiekt jest przedmiotem automatycznego śledzenia (gdy włączone)
  • " + }, + "title": "Ramki ograniczające" + }, + "zones": { + "desc": "Pokaż kontur zdefiniowanych stref", + "title": "Strefy" + }, + "desc": "Widok debugowania pokazuje podgląd śledzonych obiektów w czasie rzeczywistym i ich statystyki. Lista obiektów pokazuje opóźnione podsumowanie wykrytych obiektów.", + "noObjects": "Brak obiektów", + "timestamp": { + "title": "Znacznik czasu", + "desc": "Nałóż znacznik czasu na obraz" + }, + "mask": { + "title": "Maski ruchu", + "desc": "Pokaż wielokąty maski ruchu" + }, + "motion": { + "title": "Ramki ruchu", + "desc": "Pokaż ramki wokół obszarów, gdzie wykryto ruch", + "tips": "

    Ramki Ruchu


    Czerwone ramki będą nakładane na obszary kadru, gdzie aktualnie wykrywany jest ruch

    " + }, + "regions": { + "title": "Regiony", + "desc": "Pokaż ramkę regionu zainteresowania wysyłanego do detektora obiektów", + "tips": "

    Ramki Regionów


    Jasnozielone ramki będą nakładane na obszary zainteresowania w kadrze, które są wysyłane do detektora obiektów.

    " + }, + "objectShapeFilterDrawing": { + "document": "Przeczytaj dokumentację ", + "title": "Rysowanie Filtra Kształtu Obiektu", + "ratio": "Proporcja", + "score": "Wynik", + "tips": "Włącz tę opcję, aby narysować prostokąt na obrazie kamery w celu pokazania jego obszaru i proporcji. Te wartości mogą być następnie użyte do ustawienia parametrów filtra kształtu obiektu w twojej konfiguracji.", + "desc": "Narysuj prostokąt na obrazie, aby zobaczyć szczegóły obszaru i proporcji", + "area": "Obszar" + }, + "openCameraWebUI": "Otwórz interfejs kamery {{camera}}", + "audio": { + "title": "Audio", + "noAudioDetections": "Nie wykryto dźwięku", + "score": "wynik", + "currentRMS": "Bieżąca moc RMS", + "currentdbFS": "Bieżące dbFS" + }, + "paths": { + "title": "Ścieżki", + "desc": "Pokaż punkty znaczące ścieżki dla śledzonego obiektu", + "tips": "

    Ścieżki


    Linie i koła wskażą punkty znaczące po których poruszał się obiekt podczas śledzenia.

    " + } + }, + "motionDetectionTuner": { + "title": "Tuner Wykrywania Ruchu", + "desc": { + "title": "Frigate używa wykrywania ruchu jako pierwszej linii sprawdzenia, czy w kadrze dzieje się coś wartego sprawdzenia przez detekcję obiektów.", + "documentation": "Przeczytaj Przewodnik Dostrajania Ruchu" + }, + "Threshold": { + "desc": "Wartość progowa określa, jak duża zmiana jasności piksela jest wymagana, aby uznać ją za ruch. Domyślnie: 30", + "title": "Próg" + }, + "contourArea": { + "title": "Obszar Konturu", + "desc": "Wartość obszaru konturu służy do określenia, które grupy zmienionych pikseli kwalifikują się jako ruch. Domyślnie: 10" + }, + "improveContrast": { + "desc": "Popraw kontrast dla ciemniejszych scen. Domyślnie: WŁĄCZONE", + "title": "Popraw Kontrast" + }, + "toast": { + "success": "Ustawienia ruchu zostały zapisane." + }, + "unsavedChanges": "Niezapisane zmiany w korektorze ruchu ({{camera}})" + }, + "users": { + "addUser": "Dodaj Użytkownika", + "updatePassword": "Aktualizuj Hasło", + "toast": { + "success": { + "createUser": "Użytkownik {{user}} został utworzony pomyślnie", + "deleteUser": "Użytkownik {{user}} został usunięty pomyślnie", + "updatePassword": "Hasło zaktualizowane pomyślnie.", + "roleUpdated": "Rola zaktualizowana dla {{user}}" + }, + "error": { + "setPasswordFailed": "Nie udało się zapisać hasła: {{errorMessage}}", + "createUserFailed": "Nie udało się utworzyć użytkownika: {{errorMessage}}", + "deleteUserFailed": "Nie udało się usunąć użytkownika: {{errorMessage}}", + "roleUpdateFailed": "Nie udało się zaktualizować roli: {{errorMessage}}" + } + }, + "table": { + "username": "Nazwa użytkownika", + "actions": "Akcje", + "role": "Rola", + "noUsers": "Nie znaleziono użytkowników.", + "changeRole": "Zmień rolę użytkownika", + "password": "Hasło", + "deleteUser": "Usuń użytkownika" + }, + "dialog": { + "form": { + "user": { + "title": "Nazwa użytkownika", + "desc": "Dozwolone są tylko litery, cyfry, kropki i podkreślenia.", + "placeholder": "Wprowadź nazwę użytkownika" + }, + "password": { + "strength": { + "strong": "Silne", + "title": "Siła hasła: ", + "weak": "Słabe", + "medium": "Średnie", + "veryStrong": "Bardzo silne" + }, + "match": "Hasła pasują", + "confirm": { + "placeholder": "Potwierdź hasło", + "title": "Potwierdź hasło" + }, + "title": "Hasło", + "placeholder": "Wprowadź hasło", + "notMatch": "Hasła nie pasują" + }, + "newPassword": { + "placeholder": "Wprowadź nowe hasło", + "title": "Nowe hasło", + "confirm": { + "placeholder": "Wprowadź ponownie nowe hasło" + } + }, + "usernameIsRequired": "Nazwa użytkownika jest wymagana", + "passwordIsRequired": "Hasło jest wymagane" + }, + "changeRole": { + "desc": "Aktualizuj uprawnienia dla {{username}}", + "roleInfo": { + "intro": "Wybierz właściwą rolę dla tego użytkownika:", + "admin": "Admin", + "adminDesc": "Pełny dostęp do wszystkich funkcjonalności.", + "viewerDesc": "Ograniczony wyłącznie do pulpitów na żywo, przeglądania, eksploracji i eksportu.", + "viewer": "Przeglądający", + "customDesc": "Własna rola z dedykowanym dostępem do kamery." + }, + "title": "Zmień rolę użytkownika", + "select": "Wybierz role" + }, + "createUser": { + "title": "Utwórz nowego użytkownika", + "desc": "Dodaj nowe konto użytkownika i określ rolę dla dostępu do obszarów interfejsu Frigate.", + "usernameOnlyInclude": "Nazwa użytkownika może zawierać tylko litery, cyfry lub znak _", + "confirmPassword": "Proszę potwierdź swoje hasło" + }, + "deleteUser": { + "title": "Usuń użytkownika", + "desc": "Tej akcji nie można cofnąć. Spowoduje to trwałe usunięcie konta użytkownika i wszystkich powiązanych danych.", + "warn": "Czy na pewno chcesz usunąć {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Aktualizuj hasło dla {{username}}", + "setPassword": "Ustaw hasło", + "desc": "Utwórz silne hasło, aby zabezpieczyć to konto.", + "cannotBeEmpty": "Hasło nie może być puste", + "doNotMatch": "Hasła nie pasują do siebie" + } + }, + "management": { + "title": "Zarządzanie Użytkownikami", + "desc": "Zarządzaj kontami użytkowników tej instancji Frigate." + }, + "title": "Użytkownicy" + }, + "notification": { + "title": "Powiadomienia", + "notificationSettings": { + "title": "Ustawienia powiadomień", + "desc": "Frigate może wysyłać natywne powiadomienia push na twoje urządzenie, gdy działa w przeglądarce lub jest zainstalowany jako PWA.", + "documentation": "Przeczytaj dokumentację" + }, + "notificationUnavailable": { + "title": "Powiadomienia niedostępne", + "desc": "Powiadomienia push w przeglądarce wymagają bezpiecznego kontekstu (https://…). To jest ograniczenie przeglądarki. Uzyskaj dostęp do Frigate przez bezpieczne połączenie, aby korzystać z powiadomień.", + "documentation": "Przeczytaj dokumentację" + }, + "globalSettings": { + "title": "Ustawienia globalne", + "desc": "Tymczasowo wstrzymaj powiadomienia dla określonych kamer na wszystkich zarejestrowanych urządzeniach." + }, + "suspendTime": { + "12hours": "Zawieś na 12 godzin", + "24hours": "Zawieś na 24 godziny", + "untilRestart": "Zawieś do restartu", + "1hour": "Zawieś na 1 godzinę", + "5minutes": "Zawieś na 5 minut", + "10minutes": "Zawieś na 10 minut", + "30minutes": "Zawieś na 30 minut", + "suspend": "Wstrzymaj" + }, + "cancelSuspension": "Anuluj zawieszenie", + "toast": { + "error": { + "registerFailed": "Nie udało się zapisać rejestracji powiadomień." + }, + "success": { + "settingSaved": "Ustawienia powiadomień zostały zapisane.", + "registered": "Rejestracja powiadomień zakończona powodzeniem. Przed wysłaniem jakichkolwiek powiadomień (włącznie z testowym) wymagane jest ponowne uruchomienie Frigate." + } + }, + "email": { + "title": "Email", + "placeholder": "np. przyklad@email.com", + "desc": "Wymagany jest prawidłowy adres email, który będzie używany do powiadamiania Cię w przypadku problemów z usługą push." + }, + "cameras": { + "title": "Kamery", + "noCameras": "Brak dostępnych kamer", + "desc": "Wybierz kamery, dla których chcesz włączyć powiadomienia." + }, + "deviceSpecific": "Ustawienia specyficzne dla urządzenia", + "registerDevice": "Zarejestruj to urządzenie", + "active": "Powiadomienia aktywne", + "suspended": "Powiadomienia zawieszone {{time}}", + "unregisterDevice": "Wyrejestruj to urządzenie", + "sendTestNotification": "Wyślij testowe powiadomienie", + "unsavedRegistrations": "Niezapisane ustawienia rejestracji powiadomień", + "unsavedChanges": "Niezapisane zmiany ustawień powiadomień" + }, + "frigatePlus": { + "title": "Ustawienia Frigate+", + "apiKey": { + "title": "Klucz API Frigate+", + "validated": "Klucz API Frigate+ został wykryty i zweryfikowany", + "plusLink": "Dowiedz się więcej o Frigate+", + "notValidated": "Klucz API Frigate+ nie został wykryty lub nie został zweryfikowany", + "desc": "Klucz API Frigate+ umożliwia integrację z usługą Frigate+." + }, + "snapshotConfig": { + "title": "Konfiguracja zrzutów ekranu", + "documentation": "Przeczytaj dokumentację", + "desc": "Aby wysyłać dane do Frigate+, w konfiguracji muszą być włączone zarówno zwykłe zrzuty ekranu, jak i zrzuty typu clean_copy.", + "table": { + "snapshots": "Zrzuty ekranu", + "cleanCopySnapshots": "Zrzuty ekranu clean_copy", + "camera": "Kamera" + }, + "cleanCopyWarning": "Niektóre kamery mają włączone zrzuty ekranu, ale mają wyłączoną funkcję czystej kopii. Musisz włączyć clean_copy w konfiguracji zrzutów ekranu, aby móc przesyłać obrazy z tych kamer do Frigate+." + }, + "modelInfo": { + "title": "Informacje o modelu", + "modelType": "Typ modelu", + "trainDate": "Data treningu", + "supportedDetectors": "Wspierane detektory", + "dimensions": "Wymiary", + "cameras": "Kamery", + "loading": "Ładowanie informacji o modelu…", + "error": "Nie udało się załadować informacji o modelu", + "availableModels": "Dostępne modele", + "loadingAvailableModels": "Ładowanie dostępnych modeli…", + "baseModel": "Model bazowy", + "modelSelect": "Tutaj możesz wybrać swoje dostępne modele w Frigate+. Pamiętaj, że można wybrać tylko modele kompatybilne z Twoją aktualną konfiguracją detektora.", + "plusModelType": { + "baseModel": "Model bazowy", + "userModel": "Dostrojony" + } + }, + "toast": { + "success": "Ustawienia Frigate+ zostały zapisane. Uruchom ponownie Frigate, aby zastosować zmiany.", + "error": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}" + }, + "restart_required": "Wymagane ponowne uruchomienie (Zmieniony model Frigate+)", + "unsavedChanges": "Niezapisane zmiany ustawień Frigate+" + }, + "enrichments": { + "faceRecognition": { + "title": "Rozpoznawanie twarzy", + "desc": "Rozpoznawanie twarzy pozwala na przypisywanie imion osobom, a gdy ich twarz zostanie rozpoznana, Frigate przypisze imię osoby jako sub label. Ta informacja jest wyświetlana w interfejsie, filtrach oraz powiadomieniach.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "modelSize": { + "label": "Rozmiar modelu", + "desc": "Rozmiar modelu używanego do rozpoznawania twarzy.", + "small": { + "title": "mały", + "desc": "Użycie opcji mały wykorzystuje model osadzeń twarzy FaceNet, który działa wydajnie na większości procesorów." + }, + "large": { + "title": "duży", + "desc": "Użycie opcji duży wykorzystuje model osadzeń twarzy ArcFace i automatycznie uruchomi się na karcie graficznej, jeśli jest dostępna." + } + } + }, + "title": "Ustawienia wzbogaceń", + "unsavedChanges": "Niezapisane zmiany ustawień wzbogacania", + "birdClassification": { + "title": "Klasyfikacja ptaków", + "desc": "Klasyfikacja ptaków identyfikuje znane gatunki przy użyciu skwantyzowanego modelu Tensorflow. Gdy rozpoznany zostanie znany ptak, jego zwyczajowa nazwa zostanie dodana jako sub_label. Ta informacja jest wyświetlana w interfejsie, filtrach oraz powiadomieniach." + }, + "semanticSearch": { + "title": "Wyszukiwanie semantyczne", + "desc": "Wyszukiwanie semantyczne w Frigate pozwala na znajdowanie śledzonych obiektów w elementach przeglądowych za pomocą samego obrazu, opisu tekstowego zdefiniowanego przez użytkownika lub automatycznie wygenerowanego.", + "readTheDocumentation": "Przeczytaj Dokumentację", + "reindexNow": { + "label": "Reindeksuj teraz", + "desc": "Reindeksowanie zregeneruje osadzenia dla wszystkich śledzonych obiektów. Ten proces działa w tle i może maksymalnie obciążyć procesor oraz zająć sporo czasu w zależności od liczby śledzonych obiektów.", + "confirmTitle": "Potwierdź reindeksowanie", + "confirmDesc": "Czy na pewno chcesz reindeksować wszystkie osadzenia śledzonych obiektów? Ten proces będzie działał w tle, ale może maksymalnie obciążyć procesor i zająć sporo czasu. Postęp możesz śledzić na stronie Eksploruj.", + "confirmButton": "Reindeksuj", + "success": "Reindeksowanie zostało pomyślnie uruchomione.", + "alreadyInProgress": "Reindeksowanie już trwa.", + "error": "Nie udało się uruchomić reindeksowania: {{errorMessage}}" + }, + "modelSize": { + "label": "Rozmiar modelu", + "desc": "Rozmiar modelu używanego do osadzeń wyszukiwania semantycznego.", + "small": { + "title": "mały", + "desc": "Użycie opcji mały wykorzystuje skwantyzowaną wersję modelu, która zużywa mniej pamięci RAM i działa szybciej na procesorze przy zaniedbywalnej różnicy w jakości osadzeń." + }, + "large": { + "title": "duży", + "desc": "Użycie opcji duży wykorzystuje pełny model Jina i automatycznie uruchomi się na karcie graficznej, jeśli jest dostępna." + } + } + }, + "licensePlateRecognition": { + "title": "Rozpoznawanie tablic rejestracyjnych", + "desc": "Frigate może rozpoznawać tablice rejestracyjne na pojazdach i automatycznie dodawać wykryte znaki do pola recognized_license_plate lub znaną nazwę jako sub_label do obiektów typu samochód. Typowy przypadek użycia to odczytywanie tablic rejestracyjnych samochodów wjeżdżających na podjazd lub przejeżdżających ulicą.", + "readTheDocumentation": "Przeczytaj Dokumentację" + }, + "restart_required": "Wymagany restart (zmieniono ustawienia wzbogacania)", + "toast": { + "success": "Ustawienia wzbogacania zostały zapisane. Uruchom ponownie Frigate, aby zastosować zmiany.", + "error": "Nie udało się zapisać zmian konfiguracji: {{errorMessage}}" + } + }, + "roles": { + "management": { + "title": "Zarządzanie rolami podglądu", + "desc": "Zarządzaj własnymi rolami podglądu i ich dostępem do kamer dla tej instancji Frigate." + }, + "addRole": "Dodaj rolę", + "table": { + "role": "Rola", + "cameras": "Kamery", + "actions": "Akcje", + "noRoles": "Brak własnych ról.", + "editCameras": "Edytuj kamery", + "deleteRole": "Usuń rolę" + }, + "toast": { + "success": { + "createRole": "Utworzono rolę {{role}}", + "updateCameras": "Zaktualizowano kamery dla roli {{role}}", + "deleteRole": "Rola {{role}} została usunięta", + "userRolesUpdated_one": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer.", + "userRolesUpdated_few": "", + "userRolesUpdated_many": "" + }, + "error": { + "createRoleFailed": "Nie udało się utworzyć roli: {{errorMessage}}", + "updateCamerasFailed": "Nie udało się zaktualizować kamery: {{errorMessage}}", + "deleteRoleFailed": "Nie udało się usunąć roli: {{errorMessage}}", + "userUpdateFailed": "Nie udało się zaktualizować ról użytkownika: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Dodaj nową rolę", + "desc": "Dodaj nową rolę i określ prawa dostępu do kamer." + }, + "editCameras": { + "title": "Edytuj kamery roli", + "desc": "Aktualizuj dostęp do kamer dla roli {{role}}." + }, + "deleteRole": { + "title": "Usuń rolę", + "desc": "Ta akcja nie może zostać wycofana. To usunie rolę na stałe i przypisze jej użytkowników do roli 'viewer' która ma dostęp do wszystkich kamer.", + "warn": "Czy na pewno chcesz usunąć rolę {{role}}?", + "deleting": "Usuwanie..." + }, + "form": { + "role": { + "title": "Nazwa roli", + "placeholder": "Wprowadź nazwę roli", + "desc": "Tylko litery, liczby, kropki i podkreślenie są dozwolone.", + "roleIsRequired": "Nazwa roli jest wymagana", + "roleOnlyInclude": "Nazwa roli może zawierać litery, liczby, . albo _", + "roleExists": "Taka rola już istnieje." + }, + "cameras": { + "title": "Kamery", + "desc": "Wybierz do jakich kamer ta rola ma dostęp. Wymagana jest przynajmniej jedna kamera.", + "required": "Przynajmniej jedna kamera musi zostać wybrana." + } + } + } + }, + "triggers": { + "documentTitle": "Wyzwalacze", + "management": { + "title": "Zarządzanie wyzwalaczami", + "desc": "Zarządzaj wyzwalaczami dla kamery {{camera}}. Użyj typu miniatury, aby aktywować miniatury podobne do wybranego śledzonego obiektu, i typu opisu, aby aktywować opisy podobne do określonego tekstu." + }, + "addTrigger": "Dodaj wyzwalacz", + "table": { + "name": "Nazwa", + "type": "Typ", + "content": "Zawartość", + "threshold": "Próg", + "actions": "Akcje", + "noTriggers": "Brak wyzwalaczy dla tej kamery.", + "edit": "Edytuj", + "deleteTrigger": "Usuń wyzwalacz", + "lastTriggered": "Ostatnio wyzwolony" + }, + "type": { + "thumbnail": "Miniaturka", + "description": "Opis" + }, + "actions": { + "alert": "Oznacz jako alarm", + "notification": "Wyślij powiadomienie" + }, + "dialog": { + "createTrigger": { + "title": "Utwórz wyzwalacz", + "desc": "Utwórz wyzwalacz dla kamery {{camera}}" + }, + "editTrigger": { + "title": "Edytuj wyzwalacz", + "desc": "Edytuj ustawienia wyzwalacza na kamerze {{camera}}" + }, + "deleteTrigger": { + "title": "Usuń wyzwalacz", + "desc": "Czy na pewno chcesz usunąć wyzwalacz {{triggerName}}? To działanie jest nieodwracalne." + }, + "form": { + "name": { + "title": "Nazwa", + "placeholder": "Wprowadź nazwę wyzwalacza", + "error": { + "minLength": "Nazwa musi mieć co najmniej 2 znaki.", + "invalidCharacters": "Nazwa może zawierać jedynie litery, liczby, podkreślenie i myślniki.", + "alreadyExists": "Wyzwalacz o tej nazwie istnieje już dla tej kamery." + } + }, + "enabled": { + "description": "Włącz lub wyłącz ten wyzwalacz" + }, + "type": { + "title": "Typ", + "placeholder": "Wybierz typ wyzwalacza" + }, + "content": { + "title": "Zawartość", + "imagePlaceholder": "Wybierz obraz", + "textPlaceholder": "Wprowadź treść", + "imageDesc": "Wybierz obraz, aby uruchomić tę akcję po wykryciu podobnego obrazu.", + "textDesc": "Wprowadź tekst, który spowoduje uruchomienie tej akcji po wykryciu podobnego opisu śledzonego obiektu.", + "error": { + "required": "Zawartość jest wymagana." + } + }, + "threshold": { + "title": "Próg", + "error": { + "min": "Próg musi wynosić co najmniej 0", + "max": "Próg nie może być większy niż 1" + } + }, + "actions": { + "title": "Akcje", + "desc": "Domyślnie Frigate wysyła wiadomość MQTT dla wszystkich wyzwalaczy. Wybierz dodatkową akcję, która ma zostać wykonana po uruchomieniu tego wyzwalacza.", + "error": { + "min": "Musisz wybrać co najmniej jedną akcję." + } + }, + "friendly_name": { + "title": "Przyjazna nazwa", + "placeholder": "Nazwij lub opisz ten trigger", + "description": "Opcjonalna przyjazna nazwa lub opis tego triggera." + } + } + }, + "toast": { + "success": { + "createTrigger": "Utworzono wyzwalacz {{name}}.", + "updateTrigger": "Zaktualizowano wyzwalacz {{name}}.", + "deleteTrigger": "Usunięto wyzwalacz {{name}}." + }, + "error": { + "createTriggerFailed": "Nie udało się utworzyć wyzwalacza: {{errorMessage}}", + "updateTriggerFailed": "Nie udało się zaktualizować wyzwalacza: {{errorMessage}}", + "deleteTriggerFailed": "Nie udało się usunąć wyzwalacza: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Wyszukiwanie semantyczne jest zablokowane", + "desc": "Wyszukiwanie semantyczne musi być włączone, aby korzystać z triggerów." + } + }, + "cameraWizard": { + "title": "Dodaj kamerę", + "steps": { + "streamConfiguration": "Konfiguracja strumienia", + "nameAndConnection": "Nazwa i połączenie", + "probeOrSnapshot": "Sonda lub migawka", + "validationAndTesting": "Walidacja i testowanie" + }, + "save": { + "success": "Zapisano ustawienia nowej kamery {{cameraName}}.", + "failure": "Błąd zapisu {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rozdzielczość", + "fps": "kl./s", + "video": "Wideo", + "audio": "Audio" + }, + "commonErrors": { + "noUrl": "Podaj poprawny adres URL", + "testFailed": "Negatywny wynik testu strumienia: {{error}}" + }, + "step1": { + "cameraName": "Nazwa kamery", + "cameraNamePlaceholder": "np. drzwi_frontowe lub Ogród", + "host": "Host/Adres IP", + "port": "Port", + "username": "Nazwa użytkownika", + "usernamePlaceholder": "Opcjonalne", + "password": "Hasło", + "passwordPlaceholder": "Opcjonalne", + "selectTransport": "Wybierz protokół warstwy transportowej", + "cameraBrand": "Marka Kamery", + "selectBrand": "Wybierz markę kamery aby dostosować wzór adresu URL", + "customUrl": "Niestandardowy adres URL strumienia", + "brandInformation": "Informacje o marce", + "brandUrlFormat": "Dla kamer z formatem RTSP, formatuj URL jako: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nazwa_użytkownika:hasło@host:port/scieżka", + "connectionSettings": "Ustawienia Połączenia", + "detectionMethod": "Metoda wykrywania strumienia", + "onvifPort": "Port ONVIF", + "manualMode": "Ręczny wybór", + "onvifPortDescription": "Dla kamer wspierających protokół ONVIF, port to zazwyczaj 80 lub 8080.", + "errors": { + "brandOrCustomUrlRequired": "Wybierz markę kamery oraz host/adres IP lub wybierz 'Inny' i podaj niestandardowy adres URL", + "nameRequired": "Wymagana nazwa kamery", + "nameLength": "Nazwa kamery musi mieć 64 lub mniej znaków", + "invalidCharacters": "Nazwa kamery zawiera niepoprawne znaki", + "nameExists": "Nazwa kamery jest już zajęta", + "customUrlRtspRequired": "Niestandardowe adresy URL muszą zaczynać się od \"rtsp://\". Ręczna konfiguracja wymagana jest dla strumieniów innych niż RTSP." + }, + "description": "Wprowadź szczegóły kamery i wybierz autodetekcję lub ręcznie wybierz firmę.", + "probeMode": "Wykryj kamerę", + "detectionMethodDescription": "Wykryj kamerę za pomocą ONVIF (jeśli wspierane) by znaleźć adresy strumieni lub wybierz ręcznie markę kamery by wybrać predefiniowane adresy. By wpisać własny adres strumienia RTSP użyj ręcznej metody i wybierz \"Inne\".", + "useDigestAuth": "Użyj przesyłania skrótu autentykacji", + "useDigestAuthDescription": "Użyj przesyłania skrótu logowania HTTP dla ONVIF. Niektóre kamery mogą wymagać dedykowanego użytkownika i hasła ONVIF zamiast standardowego konta admin." + }, + "step2": { + "testSuccess": "Test połączenia udany!", + "testFailed": "Test połączenia nieudany. Sprawdź adres źródła obrazu i spróbuj ponownie.", + "testFailedTitle": "Test Nieudany", + "streamDetails": "Szczegóły Strumienia", + "testing": { + "fetchingSnapshot": "Przygotowywanie migawki kamery...", + "probingMetadata": "Wykrywanie metadanych kamery..." + }, + "deviceInfo": "Informacje o urządzeniu", + "manufacturer": "Producent", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profile", + "ptzSupport": "Wsparcie PTZ", + "autotrackingSupport": "Wsparcie auto-śledzenia", + "uriCopy": "Kopiuj", + "uriCopied": "Adres URL skopiowano do schowka", + "testConnection": "Przetestuj połączenie", + "errors": { + "hostRequired": "Wymagany jest Host/Adres IP" + }, + "description": "Wykryj dostępne strumienie kamery lub skonfiguruj ręcznie ustawienia na podstawie wybranej metody detekcji.", + "probing": "Wykrywanie kamery...", + "retry": "Ponów", + "probeFailed": "Błąd wykrywania kamery: {{error}}", + "probingDevice": "Wykrywanie urządzenia...", + "probeSuccessful": "Wykrywanie udane", + "probeError": "Błąd wykrywania", + "probeNoSuccess": "Niepowodzenie wykrywania", + "presets": "Ustawienia wstępne" + }, + "step3": { + "streamTitle": "Strumień numer: {{number}}", + "streamUrl": "URL strumienia", + "streamUrlPlaceholder": "rtsp://nazwa_użytkownika:hasło@host:port/scieżka", + "selectStream": "Wybierz strumień", + "noStreamFound": "Nie znaleziono żadnego strumienia", + "url": "adres URL", + "resolution": "Rozdzielczość", + "selectResolution": "Wybierz rozdzielczość", + "quality": "Jakość", + "selectQuality": "wybierz jakość", + "roles": "Role", + "roleLabels": { + "detect": "Wykrywanie obiektów", + "record": "Nagrywanie", + "audio": "Dźwięk" + }, + "testStream": "Przetestuj połączenie", + "testSuccess": "Test strumienia udany!", + "testFailed": "Test strumienia nieudany", + "testFailedTitle": "Test nieudany", + "connected": "Połączono", + "notConnected": "Nie połączono", + "featuresTitle": "Funkcje", + "go2rtc": "Ogranicz połączenia do kamery", + "detectRoleWarning": "Przynajmniej jeden strumień musi mieć rolę \"detect\".", + "rolesPopover": { + "title": "Role strumienia", + "detect": "Główny strumień służący do wykrywania obiektów." + }, + "featuresPopover": { + "title": "Funkcje strumienia" + } + }, + "step4": { + "description": "Końcowa walidacja i analiza przed zapisaniem ustawień nowej kamery. Połącz się z każdym strumieniem przed zapisaniem.", + "validationTitle": "Walidacja strumienia", + "reconnectionSuccess": "Ponowna próba połączenia udana.", + "streamUnavailable": "Podgląd strumienia niedostępny", + "connecting": "Łączenie...", + "streamTitle": "Strumień numer: {{number}}", + "valid": "Poprawny", + "connectingStream": "Łączenie", + "disconnectStream": "Rozłącz", + "estimatedBandwidth": "Przewidywana przepustowość", + "roles": "Role", + "ffmpegModuleDescription": "Jeżeli po kilku próbach strumień nadal nie ładuje się, uruchom ten tryb. Gdy włączony jest ten tryb Frigate będzie używać modułu ffmpeg z go2rtc. Może to zapewnić lepszą kompatybilność z niektórymi typami strumieniów.", + "none": "Brak", + "error": "Błąd", + "streamValidated": "Strumień numer: {{number}} przeszedł test pozytywnie.", + "streamValidationFailed": "Strumień numer: {{number}} test nieudany", + "saveAndApply": "Zapisz nową kamerę", + "saveError": "Nieprawidłowa konfiguracja. Sprawdź ustawienia.", + "issues": { + "title": "Walidacja strumienia", + "audioCodecGood": "Kodek dźwięku to {{codec}}.", + "resolutionHigh": "Rozdzielczość {{resolution}} może spowodować większe zużycie zasobów.", + "resolutionLow": "Rozdzielczość {{resolution}} może okazać się za mała aby poprawnie wykrywać małe obiekty.", + "noAudioWarning": "Nie wykryto dźwięku dla tego strumienia, nagrania również nie będą zawierać dźwięku.", + "audioCodecRecordError": "Kodek AAC jest wymagany aby uwzględnić dźwięk w nagraniach.", + "audioCodecRequired": "Strumień audio jest wymagany aby umożliwić wykrywanie dźwięku.", + "restreamingWarning": "Ograniczenie ilości połączeń do strumienia nagrań może delikatnie zwiększyć użycie procesora", + "brands": { + "reolink-rtsp": "Strumień RTSP dla kamer firmy Reolink nie jest rekomendowany. Uruchom strumień HTTP w oprogramowaniu kamery i uruchom kreator jeszcze raz." + } + } + }, + "description": "Wykonaj poniższe kroki aby dodać nową kamerę do Frigate." + }, + "cameraManagement": { + "title": "Zarządzaj kamerami", + "addCamera": "Dodaj nową kamerę", + "editCamera": "Edytuj kamerę:", + "selectCamera": "Wybierz kamerę", + "backToSettings": "Powrót do ustawień kamery", + "streams": { + "title": "Włącz / Wyłącz kamery" + }, + "cameraConfig": { + "add": "Dodaj kamerę", + "edit": "Edytuj kamerę", + "description": "Skonfiguruj ustawienia kamery, wliczając strumienie wejściowe i ich role.", + "name": "Nazwa kamery", + "nameRequired": "Wymagana nazwa kamery", + "nameLength": "Nazwa kamery musi mieć 64 lub mniej znaków.", + "namePlaceholder": "np. drzwi_frontowe lub Ogród", + "enabled": "Włączone", + "ffmpeg": { + "inputs": "Strumienie wejściowe", + "path": "Ścieżka strumienia", + "pathRequired": "Ścieżka strumienia jest wymagana", + "pathPlaceholder": "rtsp://...", + "roles": "Role", + "rolesRequired": "Wymagana jest przynajmniej jedna rola", + "rolesUnique": "Każda rola ('audio', 'detect', 'record') może zostać przypisana tylko raz", + "addInput": "Dodaj strumień wejściowy", + "removeInput": "Usuń strumień wejściowy", + "inputsRequired": "Wymagany jest przynajmniej jeden strumień wejściowy" + }, + "go2rtcStreams": "Strumienie go2rtc", + "streamUrls": "Adresy URL strumieni", + "addUrl": "Dodaj adres URL", + "addGo2rtcStream": "Dodaj strumień go2rtc", + "toast": { + "success": "Zapisano poprawnie kamerę {{cameraName}}" + } + } + }, + "cameraReview": { + "review": { + "alerts": "Alerty ", + "detections": "Wykrycia " + }, + "reviewClassification": { + "title": "Przegląd klasyfikacji", + "noDefinedZones": "Nie zdefiniowano żadnych stref dla tej kamery.", + "objectDetectionsTips": "Wszystkie obiekty w kategorii {{detectionsLabels}} wykryte przez kamerę {{cameraName}} będą wyświetlane jako Wykrycia niezależnie od strefy w której zostały wykryte.", + "zoneObjectDetectionsTips": { + "text": "Wszystkie obiekty w kategorii {{detectionsLabels}} nieskategoryzowane w strefie {{zone}} kamery {{cameraName}} będą wyświetlane jako Wykrycia.", + "notSelectDetections": "Wszystkie obiekty w kategorii {{detectionsLabels}} wykryte w strefie {{zone}} kamery {{cameraName}} nieskategoryzowane jako Alerty będą wyświetlane jako Wykrycia, niezależnie w której strefie zostaną wykryte.", + "regardlessOfZoneObjectDetectionsTips": "Wszystkie obiekty w kategorii {{detectionsLabels}} nieskategoryzowane dla kamery {{cameraName}} będą wyświetlane jako Wykrycia niezależnie w której strefie zostaną wykryte." + }, + "unsavedChanges": "Niezapisane ustawienia klasyfikacji przeglądu dla kamery {{camera}}", + "selectAlertsZones": "Wybierz strefę dla Alertów", + "selectDetectionsZones": "Wybierz strefę dla Wykryć", + "limitDetections": "Ogranicz detekcje do konkretnych stref" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pl/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/pl/views/system.json new file mode 100644 index 0000000..ba82ea9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pl/views/system.json @@ -0,0 +1,190 @@ +{ + "documentTitle": { + "cameras": "Statystyki kamer - Frigate", + "storage": "Statystyki Magazynowania - Frigate", + "general": "Statystyki Ogólne - Frigate", + "enrichments": "Statystyki Wzbogacania - Frigate", + "logs": { + "frigate": "Logi Frigate - Frigate", + "go2rtc": "Logi Go2RTC - Frigate", + "nginx": "Logi Nginx - Frigate" + } + }, + "general": { + "hardwareInfo": { + "gpuInfo": { + "vainfoOutput": { + "title": "Wynik Vainfo", + "returnCode": "Kod zwrotny: {{code}}", + "processOutput": "Wynik procesu:", + "processError": "Błąd procesu:" + }, + "nvidiaSMIOutput": { + "title": "Wynik Nvidia SMI", + "name": "Nazwa: {{name}}", + "driver": "Sterownik: {{driver}}", + "cudaComputerCapability": "Możliwości obliczeniowe CUDA: {{cuda_compute}}", + "vbios": "Informacje VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Zamknij informacje o GPU" + }, + "copyInfo": { + "label": "Kopiuj informacje o GPU" + }, + "toast": { + "success": "Skopiowano informacje o GPU do schowka" + } + }, + "title": "Informacje o sprzęcie", + "gpuEncoder": "Enkoder GPU", + "gpuDecoder": "Dekoder GPU", + "gpuMemory": "Pamięć GPU", + "gpuUsage": "Użycie GPU", + "npuUsage": "Użycie NPU", + "npuMemory": "Pamięć NPU", + "intelGpuWarning": { + "message": "Statystyki układu graficznego niedostępne", + "description": "W narzędziach telemetrii i statystyki układów graficznych firmy Intel (intel_gpu_top) znajduje się znany błąd powodujący raportowanie użycia układu graficznego wynoszące 0%, nawet gdy akceleracja sprzętowa i wykrywanie obiektów działa prawidłowo korzystając ze zintegrowanego układu graficznego. To nie jest błąd oprogramowania Frigate. Restart hosta może chwilowo rozwiązać problem i pozwolić na weryfikację działania układu graficznego. Ten bład nie wpływa na wydajność systemu," + } + }, + "title": "Ogólne", + "detector": { + "title": "Detektory", + "inferenceSpeed": "Szybkość wnioskowania detektora", + "cpuUsage": "Użycie CPU przez detektor", + "memoryUsage": "Użycie pamięci przez detektor", + "temperature": "Temperatura detektora", + "cpuUsageInformation": "Procesor został użyty w przygotowaniu wejścia i obsłudze danych do i z modeli wykrywających. Ta wartość nie mierzy czasu wnioskowania, nawet jeśli został użyty akcelerator lub GPU." + }, + "otherProcesses": { + "title": "Inne procesy", + "processCpuUsage": "Użycie CPU przez proces", + "processMemoryUsage": "Użycie pamięci przez proces" + } + }, + "cameras": { + "info": { + "stream": "Strumień {{idx}}", + "cameraProbeInfo": "{{camera}} Informacje o sondowaniu kamery", + "streamDataFromFFPROBE": "Dane strumienia są pozyskiwane za pomocą ffprobe.", + "video": "Wideo:", + "codec": "Kodek:", + "resolution": "Rozdzielczość:", + "fps": "FPS:", + "unknown": "Nieznany", + "audio": "Audio:", + "error": "Błąd: {{error}}", + "tips": { + "title": "Informacje o sondowaniu kamery" + }, + "fetching": "Pobieranie danych kamery", + "aspectRatio": "proporcje" + }, + "toast": { + "success": { + "copyToClipboard": "Skopiowano dane sondowania do schowka." + }, + "error": { + "unableToProbeCamera": "Nie można sondować kamery: {{errorMessage}}" + } + }, + "title": "Kamery", + "overview": "Przegląd", + "framesAndDetections": "Klatki / Detekcje", + "label": { + "camera": "kamera", + "detect": "wykryj", + "skipped": "pominięte", + "ffmpeg": "FFmpeg", + "capture": "przechwytywanie", + "overallSkippedDetectionsPerSecond": "łączna liczba pominiętych detekcji na sekundę", + "cameraSkippedDetectionsPerSecond": "{{camName}} liczba pominiętych detekcji na sekundę", + "overallFramesPerSecond": "łączna liczba klatek na sekundę", + "overallDetectionsPerSecond": "łączna liczba detekcji na sekundę", + "cameraCapture": "{{camName}} przechwytywanie", + "cameraDetect": "{{camName}} detekcja", + "cameraFramesPerSecond": "{{camName}} liczba klatek na sekundę", + "cameraDetectionsPerSecond": "{{camName}} liczba detekcji na sekundę", + "cameraFfmpeg": "{{camName}} FFmpeg" + } + }, + "storage": { + "cameraStorage": { + "unused": { + "title": "Niewykorzystane", + "tips": "Ta wartość może niedokładnie przedstawiać wolne miejsce dostępne dla Frigate, jeśli masz inne pliki przechowywane na dysku poza nagraniami Frigate. Frigate nie śledzi wykorzystania magazynu poza swoimi nagraniami." + }, + "title": "Magazyn kamery", + "camera": "Kamera", + "storageUsed": "Wykorzystany magazyn", + "percentageOfTotalUsed": "Procent całości", + "bandwidth": "Przepustowość", + "unusedStorageInformation": "Informacja o niewykorzystanym magazynie" + }, + "title": "Magazyn", + "overview": "Przegląd", + "recordings": { + "title": "Nagrania", + "tips": "Ta wartość reprezentuje całkowite miejsce zajmowane przez nagrania w bazie danych Frigate. Frigate nie śledzi wykorzystania magazynu dla wszystkich plików na twoim dysku.", + "earliestRecording": "Najwcześniejsze dostępne nagranie:" + }, + "shm": { + "title": "Wykorzystanie pamięci współdzielonej SHM", + "warning": "Obecny rozmiar pamięci współdzielonej SHM {{total}}MB jest za mały. Zwiększ shm_size do co najmniej {{min_shm}}MB." + } + }, + "logs": { + "copy": { + "error": "Nie udało się skopiować logów do schowka", + "label": "Kopiuj do Schowka", + "success": "Skopiowano logi do schowka" + }, + "download": { + "label": "Pobierz Logi" + }, + "type": { + "label": "Typ", + "timestamp": "Znacznik czasu", + "tag": "Tag", + "message": "Wiadomość" + }, + "tips": "Logi są przesyłane strumieniowo z serwera", + "toast": { + "error": { + "fetchingLogsFailed": "Błąd pobierania logów: {{errorMessage}}", + "whileStreamingLogs": "Błąd podczas strumieniowania logów: {{errorMessage}}" + } + } + }, + "title": "System", + "metrics": "Metryki systemowe", + "lastRefreshed": "Ostatnie odświeżenie: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez detekcję ({{detectAvg}}%)", + "healthy": "System jest sprawny", + "reindexingEmbeddings": "Ponowne indeksowanie osadzeń ({{processed}}% ukończone)", + "detectIsSlow": "{{detect}} jest wolne ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} jest bardzo wolne ({{speed}} ms)", + "cameraIsOffline": "{{camera}} jest niedostępna", + "shmTooLow": "przydział {{total}} MB dla /dev/shm powinien zostać zwiększony do przynajmniej {{min}} MB." + }, + "enrichments": { + "title": "Wzbogacenia", + "infPerSecond": "Wnioskowania na sekundę", + "embeddings": { + "image_embedding_speed": "Szybkość osadzania obrazów", + "face_embedding_speed": "Szybkość osadzania twarzy", + "plate_recognition_speed": "Szybkość rozpoznawania tablic rejestracyjnych", + "text_embedding_speed": "Szybkość osadzania tekstu", + "face_recognition_speed": "Szybkość rozpoznawania twarzy", + "image_embedding": "Osadzenie obrazu", + "plate_recognition": "Rozpoznawanie rejestracji samochodowych", + "yolov9_plate_detection_speed": "Prędkość detekcji rejestracji samochodowych YOLOv9", + "yolov9_plate_detection": "Detekcja rejestracji samochodowych YOLOv9", + "text_embedding": "Osadzenie tekstu", + "face_recognition": "Rozpoznawanie twarzy" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/audio.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/audio.json new file mode 100644 index 0000000..b36f099 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/audio.json @@ -0,0 +1,433 @@ +{ + "mantra": "Mantra", + "child_singing": "Criança cantando", + "speech": "Fala", + "yell": "Gritar", + "chant": "Canto", + "babbling": "Balbuciando", + "bellow": "Abaixo", + "whoop": "Grito de Felicidade", + "whispering": "Sussurrando", + "laughter": "Risada", + "snicker": "Risada", + "crying": "Choro", + "sigh": "Suspirar", + "singing": "Cantoria", + "choir": "Coro", + "yodeling": "Cantando", + "bicycle": "Bicicleta", + "car": "Carro", + "motorcycle": "Moto", + "bus": "Ônibus", + "train": "Trem", + "boat": "Barco", + "bird": "Pássaro", + "cat": "Gato", + "dog": "Cachorro", + "rapping": "Cantando rap", + "horse": "Cavalo", + "humming": "Cantarolando", + "sheep": "Ovelha", + "synthetic_singing": "Canto Sintético", + "groan": "Gemido", + "grunt": "Grunhido", + "whistling": "Assobio", + "breathing": "Respiração", + "camera": "Câmera", + "wheeze": "Chiado", + "snoring": "Ronco", + "gasp": "Respiração Ofegante", + "pant": "Arfado", + "snort": "Bufado", + "cough": "Tosse", + "throat_clearing": "Pigarro", + "sneeze": "Espirro", + "sniff": "Fungado", + "run": "Executar", + "shuffle": "Embaralhar", + "footsteps": "Passos", + "chewing": "Mastigação", + "biting": "Mordida", + "gargling": "Gargarejo", + "stomach_rumble": "Ronco de Estômago", + "burping": "Arroto", + "skateboard": "Skate", + "hiccup": "Soluço", + "fart": "Flatulência", + "hands": "Mãos", + "finger_snapping": "Estalar de Dedos", + "clapping": "Palmas", + "heartbeat": "Batida de Coração", + "heart_murmur": "Sopro Cardíaco", + "cheering": "Comemoração", + "applause": "Aplausos", + "chatter": "Conversa", + "crowd": "Multidão", + "children_playing": "Crianças Brincando", + "animal": "Animal", + "pets": "Animais de Estimação", + "bark": "Latido", + "yip": "Latido / Grito Agudo", + "howl": "Uivado", + "bow_wow": "Latido", + "growling": "Rosnado", + "whimper_dog": "Choro de Cachorro", + "purr": "Ronronado", + "meow": "Miado", + "hiss": "Sibilo", + "caterwaul": "Lamúria", + "livestock": "Animais de Criação", + "clip_clop": "Galope", + "neigh": "Relincho", + "door": "Porta", + "cattle": "Gado", + "moo": "Mugido", + "cowbell": "Sino de Vaca", + "mouse": "Rato", + "pig": "Porco", + "oink": "Grunhido de Porco", + "keyboard": "Teclado", + "goat": "Cabra", + "bleat": "Balido", + "fowl": "Ave", + "chicken": "Galinha", + "sink": "Pia", + "cluck": "Cacarejo", + "cock_a_doodle_doo": "Cacarejado", + "blender": "Liquidificador", + "turkey": "Peru", + "gobble": "Deglutição", + "clock": "Relógio", + "duck": "Pato", + "quack": "Grasnado", + "scissors": "Tesouras", + "goose": "Ganso", + "honk": "Buzina", + "hair_dryer": "Secador de Cabelo", + "wild_animals": "Animais Selvagens", + "toothbrush": "Escova de Dentes", + "roaring_cats": "Felinos Rugindo", + "roar": "Rugido", + "vehicle": "Veículo", + "chirp": "Piado", + "squawk": "Guincho Animal", + "pigeon": "Pombo", + "dogs": "Cachorros", + "rats": "Ratos", + "coo": "Arrulhado de Pombo", + "crow": "Corvo", + "caw": "Grasnado de Corvo", + "owl": "Coruja", + "hoot": "Chirriado de Coruja", + "flapping_wings": "Bater de Asas", + "patter": "Passos Leves", + "insect": "Inseto", + "cricket": "Grilo", + "mosquito": "Mosquito", + "fly": "Mosca", + "buzz": "Zumbido", + "frog": "Sapo", + "croak": "Coaxado", + "snake": "Cobra", + "rattle": "Chocalho", + "whale_vocalization": "Vocalização de Baleia", + "music": "Música", + "musical_instrument": "Instrumento Musical", + "plucked_string_instrument": "Instrumento de Cordas Dedilhadas", + "guitar": "Violão", + "electric_guitar": "Guitarra", + "bass_guitar": "Baixo", + "acoustic_guitar": "Violão Acústico", + "steel_guitar": "Guitarra Havaiana", + "tapping": "Batidas Leves", + "strum": "Dedilhado", + "banjo": "Banjo", + "sitar": "Sitar", + "mandolin": "Bandolim", + "zither": "Cítara", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Piano Elétrico", + "organ": "Órgão", + "electronic_organ": "Órgão Eletrônico", + "hammond_organ": "Órgão Hammond", + "synthesizer": "Sintetizador", + "sampler": "Sampler", + "harpsichord": "Cravo (Instrumento Musical)", + "percussion": "Percussão", + "drum_kit": "Kit de Baterias", + "drum_machine": "Bateria Eletrônica", + "drum": "Tambor", + "snare_drum": "Caixa Clara", + "rimshot": "Rimshot", + "drum_roll": "Tambores Rufando", + "bass_drum": "Bumbo", + "timpani": "Tímpanos (Instrumento Musical)", + "tabla": "Tabla", + "wood_block": "Bloco de Madeira", + "bagpipes": "Gaita de Fole", + "pop_music": "Música Pop", + "grunge": "Grunge", + "middle_eastern_music": "Música do Oriente Médio", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música Clássica", + "opera": "Ópera", + "electronic_music": "Música Eletrónica", + "house_music": "Música House", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Eletrónica", + "cymbal": "Címbalo", + "hi_hat": "Chimbau", + "tambourine": "Pandeiro", + "maraca": "Maraca", + "gong": "Gongo", + "tubular_bells": "Sinos Tubulares", + "mallet_percussion": "Percussão de Martelo", + "marimba": "Marimba", + "glockenspiel": "Glockenspiel", + "vibraphone": "Vibrafone", + "steelpan": "Panela de Aço", + "orchestra": "Orquestra", + "brass_instrument": "Instrumento de Metal", + "french_horn": "Trompa Francesa", + "trumpet": "Trombeta", + "trombone": "Trombone", + "bowed_string_instrument": "Instrumento de Cordas Friccionadas", + "string_section": "Seção de Cordas", + "violin": "Violino", + "pizzicato": "Pizzicato", + "cello": "Violoncelo", + "double_bass": "Contrabaixo", + "wind_instrument": "Instrumento de Sopro", + "flute": "Flauta", + "saxophone": "Saxofone", + "clarinet": "Clarinete", + "harp": "Harpa", + "bell": "Sino", + "church_bell": "Sino de Igreja", + "jingle_bell": "Guizo", + "bicycle_bell": "Campainha de Bicicleta", + "tuning_fork": "Diapasão", + "chime": "Carrilhão", + "wind_chime": "Sinos de Vento", + "harmonica": "Gaita", + "accordion": "Acordeão", + "didgeridoo": "Didjeridu", + "theremin": "Teremim", + "scratching": "Arranhado", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatbox", + "rock_music": "Rock", + "heavy_metal": "Heavy Metal", + "punk_rock": "Punk Rock", + "progressive_rock": "Rock Progressivo", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Rock Psicodélico", + "rhythm_and_blues": "Rhythm and Blues", + "soul_music": "Música Soul", + "music_of_latin_america": "Music of Latin America", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para Crianças", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A Capella", + "music_of_africa": "Music of Africa", + "afrobeat": "Afrobeat", + "christian_music": "Música Cristã", + "gospel_music": "Música Gospel", + "music_of_asia": "Music of Asia", + "carnatic_music": "Música Carnática", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "traditional_music": "Música Tradicional", + "independent_music": "Música Independente", + "song": "Música", + "thunderstorm": "Tempestade", + "thunder": "Trovão", + "water": "Água", + "rain": "Chuva", + "raindrop": "Gota de Chuva", + "rain_on_surface": "Chuva na Superfície", + "stream": "Transmissão", + "waterfall": "Cachoeira", + "ocean": "Oceano", + "waves": "Ondas", + "steam": "Vapor", + "gurgling": "Borbulhado", + "fire": "Fogo", + "crackle": "Estalo", + "sailboat": "Veleiro", + "rowboat": "Barco a Remo", + "motorboat": "Lancha", + "ship": "Navio", + "motor_vehicle": "Veículo Motorizado", + "toot": "Buzinado", + "car_alarm": "Alarme de Carro", + "power_windows": "Vidros Elétricos", + "skidding": "Derrapado", + "singing_bowl": "Tigela Tibetana", + "reggae": "Reggae", + "country": "País", + "swing_music": "Música Swing", + "bluegrass": "Música Bluegrass", + "funk": "Funk", + "folk_music": "Música Folk", + "electronic_dance_music": "Música Eletrônica", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "background_music": "Música de Fundo", + "theme_music": "Música Tema", + "jingle": "Jingle", + "soundtrack_music": "Música de Trilha Sonora", + "lullaby": "Canção de Ninar", + "video_game_music": "Música de Video Game", + "christmas_music": "Música Natalina", + "dance_music": "Música Dance", + "wedding_music": "Música de Casamento", + "happy_music": "Música Feliz", + "sad_music": "Música Triste", + "tender_music": "Música Suave", + "exciting_music": "Música Empolgante", + "angry_music": "Música Raivosa", + "scary_music": "Música Assustadora", + "wind": "Vento", + "rustling_leaves": "Folhas Farfalhantes", + "fixed-wing_aircraft": "Aeronave de Asa Fixa", + "engine": "Motor", + "light_engine": "Motor Leve", + "dental_drill's_drill": "Broca Odontológica", + "lawn_mower": "Cortador de Grama", + "chainsaw": "Motosserra", + "medium_engine": "Motor Médio", + "heavy_engine": "Motor Pesado", + "engine_knocking": "Motor Batendo", + "engine_starting": "Motor Partindo", + "idling": "Marcha Lenta", + "chopping": "Cortando", + "frying": "Fritando", + "microwave_oven": "Forno Microondas", + "water_tap": "Torneira de Água", + "bathtub": "Banheira", + "toilet_flush": "Descarga de Vaso Sanitário", + "computer_keyboard": "Teclado de Computador", + "writing": "Escrita", + "alarm": "Alarme", + "telephone": "Telefone", + "telephone_bell_ringing": "Telefone Tocando", + "ringtone": "Toque de Celular", + "telephone_dialing": "Telefone Discando", + "dial_tone": "Tom de Discagem", + "busy_signal": "Sinal de Ocupado", + "alarm_clock": "Despertador", + "siren": "Sirene", + "civil_defense_siren": "Sirene de Defesa Civil", + "wind_noise": "Ruído de Vento", + "tire_squeal": "Pneus Cantando", + "car_passing_by": "Carro Passando", + "race_car": "Carro de Corrida", + "truck": "Pickup / Caminhão", + "air_brake": "Freios a Ar", + "air_horn": "Buzina a Ar", + "reversing_beeps": "Alarme de Ré", + "ice_cream_truck": "Carro de Sorvete", + "emergency_vehicle": "Veículo de Emergência", + "police_car": "Carro de Polícia", + "ambulance": "Ambulância", + "fire_engine": "Caminhão de Bombeiros", + "traffic_noise": "Barulho de Tráfego", + "rail_transport": "Transporte Ferroviário", + "train_whistle": "Apito de Trem", + "train_horn": "Buzina de Trem", + "railroad_car": "Vagão de Trem", + "train_wheels_squealing": "Rodas de Trem Rangendo", + "subway": "Metrô", + "aircraft": "Aeronave", + "aircraft_engine": "Motor de Aeronave", + "jet_engine": "Motor a Jato", + "propeller": "Hélice", + "helicopter": "Helicóptero", + "accelerating": "Acelerando", + "doorbell": "Campainha", + "ding-dong": "Toque de Campainha", + "sliding_door": "Porta de Correr", + "slam": "Batida Forte", + "knock": "Batida na Porta", + "burst": "Estouro / Rajada", + "eruption": "Erupção", + "boom": "Estrondo", + "wood": "Madeira", + "chop": "Barulho de Corte", + "splinter": "Lascado", + "crack": "Rachado", + "glass": "Vidro", + "chink": "Fenda", + "shatter": "Estilhaçado", + "silence": "Silêncio", + "sound_effect": "Efeito Sonoro", + "environmental_noise": "Ruido Ambiente", + "static": "Estático", + "white_noise": "Ruido Branco", + "pink_noise": "Ruido Rosa", + "television": "Televisão", + "radio": "Rádio", + "field_recording": "Gravação de Campo", + "scream": "Grito", + "tap": "Toque", + "squeak": "Rangido", + "cupboard_open_or_close": "Cristaleira Abrindo ou Fechando", + "drawer_open_or_close": "Gaveteiro Abrindo ou Fechando", + "dishes": "Pratos", + "cutlery": "Talheres", + "electric_toothbrush": "Escova de Dentes Elétrica", + "vacuum_cleaner": "Aspirador de Pó", + "zipper": "Zíper", + "keys_jangling": "Chaves Chacoalhando", + "coin": "Moeda", + "electric_shaver": "Barbeador Elétrico", + "shuffling_cards": "Embaralhar de Cartas", + "typing": "Digitação", + "typewriter": "Máquina de Escrever", + "buzzer": "Zumbador", + "smoke_detector": "Detector de Fumaça", + "fire_alarm": "Alarme de Incêndio", + "foghorn": "Buzina de Nevoeiro", + "whistle": "Apito", + "steam_whistle": "Apito a Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Catraca", + "tick": "Tique", + "tick-tock": "Tique-Toque", + "gears": "Engrenagens", + "pulleys": "Polias", + "sewing_machine": "Máquina de Costura", + "mechanical_fan": "Ventilador Mecânico", + "air_conditioning": "Ar-Condicionado", + "cash_register": "Caixa Registradora", + "printer": "Impressora", + "single-lens_reflex_camera": "Câmera Single-Lens Reflex", + "tools": "Ferramentas", + "hammer": "Martelo", + "jackhammer": "Britadeira", + "sawing": "Som de Serra", + "filing": "Som de Lima", + "sanding": "Lixamento", + "power_tool": "Ferramenta Elétrica", + "drill": "Furadeira", + "explosion": "Explosão", + "gunshot": "Tiro", + "machine_gun": "Metralhadora", + "fusillade": "Fuzilamento", + "artillery_fire": "Fogo de Artilharia", + "cap_gun": "Espoleta", + "fireworks": "Fogos de Artifício", + "firecracker": "Rojões", + "noise": "Ruído", + "distortion": "Distorção", + "cacophony": "Cacofonia", + "vibration": "Vibração" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/common.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/common.json new file mode 100644 index 0000000..e1ab1e5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/common.json @@ -0,0 +1,285 @@ +{ + "time": { + "untilForTime": "Até {{time}}", + "untilForRestart": "Até o Frigate reiniciar.", + "untilRestart": "Até reiniciar", + "ago": "{{timeAgo}} atrás", + "justNow": "Agora mesmo", + "today": "Hoje", + "yesterday": "Ontem", + "last7": "Últimos 7 dias", + "last14": "Últimos 14 dias", + "last30": "Últimos 30 dias", + "thisWeek": "Essa semana", + "lastWeek": "Semana passada", + "thisMonth": "Este mês", + "lastMonth": "Mês passado", + "5minutes": "5 minutos", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "1hour": "1 hora", + "12hours": "12 horas", + "24hours": "24 horas", + "pm": "pm", + "am": "am", + "yr": "{{time}}ano", + "year_one": "{{time}} ano", + "year_many": "{{time}} anos", + "year_other": "{{time}} anos", + "mo": "{{time}}mês", + "month_one": "{{time}} mês", + "month_many": "{{time}} meses", + "month_other": "{{time}} meses", + "d": "{{time}} dia", + "day_one": "{{time}} dia", + "day_many": "{{time}} dias", + "day_other": "{{time}} dias", + "h": "{{time}}h", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "m": "{{time}}m", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "s": "{{time}}s", + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "formattedTimestamp": { + "12hour": "d MMM,h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-hh-mm-ss", + "24hour": "dd-MM-yy-HH-mm-ss" + } + }, + "selectItem": "Selecionar {{item}}", + "unit": { + "speed": { + "mph": "mi/h", + "kph": "km/h" + }, + "length": { + "feet": "pés", + "meters": "metros" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/hora", + "mbph": "MB/hora", + "gbph": "GB/hora" + } + }, + "label": { + "back": "Voltar" + }, + "button": { + "apply": "Aplicar", + "reset": "Resetar", + "done": "Concluído", + "enabled": "Habilitado", + "enable": "Habilitar", + "disabled": "Desativado", + "disable": "Desativar", + "save": "Salvar", + "saving": "Salvando…", + "cancel": "Cancelar", + "close": "Fechar", + "copy": "Copiar", + "back": "Voltar", + "history": "Histórico", + "fullscreen": "Tela Inteira", + "exitFullscreen": "Sair da Tela Inteira", + "pictureInPicture": "Miniatura Flutuante", + "twoWayTalk": "Áudio Bidirecional", + "cameraAudio": "Áudio da Câmera", + "on": "LIGADO", + "off": "DESLIGADO", + "edit": "Editar", + "copyCoordinates": "Copiar coordenadas", + "delete": "Deletar", + "yes": "Sim", + "no": "Não", + "download": "Baixar", + "info": "Informação", + "suspended": "Suspenso", + "unsuspended": "Não Suspenso", + "play": "Reproduzir", + "unselect": "Deselecionar", + "export": "Exportar", + "deleteNow": "Deletar Agora", + "next": "Próximo" + }, + "menu": { + "system": "Sistema", + "systemMetrics": "Métricas de sistema", + "configuration": "Configuração", + "language": { + "hi": "हिन्दी (Hindi)", + "fr": "Français (Francês)", + "en": "English (Inglês)", + "es": "Español (Espanhol)", + "zhCN": "简体中文 (Chinês Simplificado)", + "ar": "العربية (Arábico)", + "pt": "Português (Português)", + "ru": "Русский (Russo)", + "de": "Deustch (Alemão)", + "ja": "日本語 (Japonês)", + "tr": "Türkçe (Turco)", + "it": "Italiano (Italiano)", + "nl": "Nederlands (Holandês)", + "sv": "Svenska (Sueco)", + "cs": "Čeština (Checo)", + "nb": "Norsk Bokmål (Bokmål Norueguês)", + "ko": "한국어 (Coreano)", + "vi": "Tiếng Việt (Vietnamita)", + "fa": "فارسی (Persa)", + "pl": "Polski (Polonês)", + "uk": "Українська (Ucraniano)", + "he": "עברית (Hebraico)", + "el": "Ελληνικά (Grego)", + "ro": "Română (Romeno)", + "hu": "Magyar (Húngaro)", + "fi": "Suomi (Finlandês)", + "da": "Dansk (Dinamarquês)", + "sk": "Slovenčina (Eslovaco)", + "yue": "粵語 (Cantonês)", + "th": "ไทย (Tailandês)", + "ca": "Català (Catalão)", + "withSystem": { + "label": "Usar as configurações de sistema para o idioma" + }, + "ptBR": "Português Brasileiro (Português Brasileiro)", + "sr": "Српски (Sérvio)", + "sl": "Slovenščina (Esloveno)", + "lt": "Lietuvių (Lituano)", + "bg": "Български (Búlgaro)", + "gl": "Galego (Galego)", + "id": "Bahasa Indonesia (Indonésio)", + "ur": "اردو (Urdu)" + }, + "systemLogs": "Logs de sistema", + "settings": "Configurações", + "configurationEditor": "Editor de Configuração", + "languages": "Idiomas", + "appearance": "Aparência", + "darkMode": { + "label": "Modo Escuro", + "light": "Claro", + "dark": "Escuro", + "withSystem": { + "label": "Use as configurações do sistema para modo claro ou escuro" + } + }, + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "nord": "Nord", + "red": "Vermelho", + "highcontrast": "Alto Contraste", + "default": "Padrão" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentação", + "label": "Documentação do Frigate" + }, + "restart": "Reiniciar o Frigate", + "live": { + "title": "Ao Vivo", + "allCameras": "Todas as câmeras", + "cameras": { + "title": "Câmeras", + "count_one": "{{count}} Câmera", + "count_many": "{{count}} Câmeras", + "count_other": "{{count}} Câmeras" + } + }, + "review": "Revisar", + "explore": "Explorar", + "export": "Exportar", + "uiPlayground": "Playground da UI", + "faceLibrary": "Biblioteca de Rostos", + "user": { + "title": "Usuário", + "account": "Conta", + "current": "Usuário Atual: {{user}}", + "anonymous": "anônimo", + "logout": "Sair", + "setPassword": "Definir Senha" + } + }, + "toast": { + "copyUrlToClipboard": "URL copiada para a área de transferência.", + "save": { + "title": "Salvar", + "error": { + "title": "Falha ao salvar as alterações de configuração: {{errorMessage}}", + "noMessage": "Falha ao salvar as alterações de configuração" + } + } + }, + "role": { + "title": "Papel", + "admin": "Administrador", + "viewer": "Espectador", + "desc": "Administradores possuem acesso total a todos os recursos da interface do Frigate. Espectadores são limitados a ver as câmeras, revisar itens, e filmagens históricas na interface." + }, + "pagination": { + "label": "paginação", + "previous": { + "title": "Anterior", + "label": "Ir para a página anterior" + }, + "next": { + "title": "Próximo", + "label": "Ir para a próxima página" + }, + "more": "Mais páginas" + }, + "accessDenied": { + "documentTitle": "Acesso Negado - Frigate", + "title": "Acesso Negado", + "desc": "Você não possui permissão para visualizar essa página." + }, + "notFound": { + "documentTitle": "Não Encontrado - Frigate", + "title": "404", + "desc": "Página não encontrada" + }, + "readTheDocumentation": "Leia a documentação", + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/auth.json new file mode 100644 index 0000000..2777581 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nome de Usuário", + "password": "Senha", + "login": "Login", + "errors": { + "usernameRequired": "Nome de usuário é necessário", + "passwordRequired": "Senha necessária", + "rateLimit": "Limite de taxa excedido. Tente novamente mais tarde.", + "loginFailed": "Falha no Login", + "unknownError": "Erro desconhecido. Checar registros.", + "webUnknownError": "Erro desconhecido. Verifique os logs do console." + }, + "firstTimeLogin": "Fazendo login pela primeira vez? As credenciais estão escritas nos logs do Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/camera.json new file mode 100644 index 0000000..03ee52b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupo de Câmeras", + "add": "Adicionar Grupo de Câmeras", + "edit": "Edição Grupo de Câmera", + "delete": { + "label": "Deletar Grupo de Câmera", + "confirm": { + "title": "Confirmar Apagar", + "desc": "Você tem certeza que quer apagar o grupo de câmera {{name}}?" + } + }, + "name": { + "label": "Nome", + "placeholder": "Digite um nome…", + "errorMessage": { + "mustLeastCharacters": "O nome do grupo de câmeras deve ter pelo menos 2 caracteres.", + "exists": "O nome do grupo de câmeras já existe.", + "nameMustNotPeriod": "O nome do grupo de câmeras não deve conter ponto.", + "invalid": "Nome de grupo de câmeras inválido." + } + }, + "cameras": { + "label": "Câmeras", + "desc": "Selecione as câmeras para este grupo." + }, + "icon": "Ícone", + "success": "O grupo de câmeras {{name}} foi salvo.", + "camera": { + "setting": { + "label": "Configurações de Streaming da Câmera", + "title": "Configurações de streaming da câmera {{cameraName}}", + "audioIsAvailable": "Áudio está disponível para esta transmissão", + "audioIsUnavailable": "Áudio indisponível para esta transmissão", + "desc": "Alterar as opções de transmissão ao vivo para o painel desse grupo de câmera. Esses ajustes são específicos para esse dispositivo/navegador.", + "audio": { + "tips": { + "title": "O audio deve ter a sua saída da câmera e configurado em go2rtc para essa transmissão.", + "document": "Leia a documentação " + } + }, + "stream": "Transmissão", + "placeholder": "Selecionar transmissão ao vivo", + "streamMethod": { + "label": "Método de Transmissão", + "placeholder": "Selecione um método de transmissão", + "method": { + "noStreaming": { + "label": "Sem Transmissão", + "desc": "Imagens da câmera atualizarão apenas uma vez por minuto e não haverá transmissão ao vivo." + }, + "smartStreaming": { + "label": "Transmissão Inteligente (recomendado)", + "desc": "O streaming inteligente atualizará a imagem da câmera uma vez por minuto quando não houver atividade detectável para economizar largura de banda e recursos. Quando alguma atividade for detectada, a imagem automáticamente mudará para a transmissão ao vivo." + }, + "continuousStreaming": { + "label": "Transmissão Contínua", + "desc": { + "title": "A imagem da câmera será sempre uma transmissão ao vivo quando visível no painel, mesmo que não haja atividade sendo detectada.", + "warning": "A transmissão contínua pode causar alta utilização de banda e problemas de performance. Use com cuidado." + } + } + } + }, + "compatibilityMode": { + "label": "Modo de compatibilidade", + "desc": "Habilite essa opção somente se a transmissão ao vivo da sua câmera estiver exibindo artefatos de cor e possui uma linha diagonal no canto esquerdo da imagem." + } + }, + "birdseye": "Visão Panorâmica" + } + }, + "debug": { + "options": { + "label": "Configurações", + "title": "Opções", + "showOptions": "Exibir Opções", + "hideOptions": "Ocultar Opções" + }, + "zones": "Zonas", + "mask": "Máscara", + "motion": "Movimento", + "regions": "Regiões", + "boundingBox": "Caixa Delimitadora", + "timestamp": "Timestamp" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/dialog.json new file mode 100644 index 0000000..6f15f98 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/dialog.json @@ -0,0 +1,122 @@ +{ + "restart": { + "title": "Você tem certeza que deseja reiniciar o Frigate?", + "button": "Reiniciar", + "restarting": { + "title": "Frigate está Reiniciando", + "content": "Essa página vai recarregar em {{countdown}} segundos.", + "button": "Forçar Recarregar Agora" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Enviar para Frigate+", + "desc": "Objetos nos lugares que você quer evitar não são falsos positivos. Enviá-los como falsos positivos confundirá o modelo." + }, + "review": { + "question": { + "label": "Confirmar esse rótulo para Frigate Plus", + "ask_a": "Este objeto é um {{label}}?", + "ask_an": "Este objeto é um{{label}}?", + "ask_full": "Este objeto é um{{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Enviado" + } + } + }, + "video": { + "viewInHistory": "Ver no Histórico" + } + }, + "export": { + "time": { + "fromTimeline": "Selecione da Linha do tempo", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas", + "custom": "Personalizado", + "start": { + "title": "Hora de Início", + "label": "Selecione a Hora de Início" + }, + "end": { + "title": "Hora de Término", + "label": "Selecione a Hora de Término" + } + }, + "name": { + "placeholder": "Nomeie a Exportação" + }, + "select": "Selecionar", + "export": "Exportar", + "selectOrExport": "Selecionar ou Exportar", + "toast": { + "success": "Exportação iniciada com sucesso. Veja o arquivo na pasta /exports.", + "error": { + "failed": "Falha em iniciar exportação: {{error}}", + "endTimeMustAfterStartTime": "Tempo de finalização deve ser após tempo de início", + "noVaildTimeSelected": "Nenhuma faixa de tempo válida selecionada" + } + }, + "fromTimeline": { + "saveExport": "Salvar Exportação", + "previewExport": "Pré-Visualizar Exportação" + } + }, + "streaming": { + "label": "Transmissão", + "restreaming": { + "disabled": "A retransmissão não está habilitada para essa câmera.", + "desc": { + "title": "Configurar o go2rtc para opções de visualização ao vivo e audio adicional para essa câmera.", + "readTheDocumentation": "Leia a documentação" + } + }, + "showStats": { + "label": "Exibir estatísticas da transmissão", + "desc": "Habilite essa opção para exibir as estatísticas de transmissão como uma sobreposição na transmissão da câmera." + }, + "debugView": "Visualização do Depurador" + }, + "search": { + "saveSearch": { + "label": "Salvar Busca", + "desc": "Indique um nome para essa pesquisa salva.", + "placeholder": "Dê um nome para a sua busca", + "overwrite": "{{searchName}} já existe. Salvar substituirá o valor existente.", + "success": "A pesquisa ({{searchName}}) foi salva.", + "button": { + "save": { + "label": "Salvar esta pesquisa" + } + } + } + }, + "recording": { + "confirmDelete": { + "desc": { + "selected": "Tem certeza de que deseja excluir todos os vídeos gravados associados a este item de revisão?

    Segure a tecla Shift para ignorar esta caixa de diálogo no futuro." + }, + "toast": { + "success": "As filmagens associadas aos itens de revisão selecionados foram excluídas com sucesso.", + "error": "Falha ao deletar: {{error}}" + }, + "title": "Confirmar Exclusão" + }, + "button": { + "markAsReviewed": "Marcar como revisado", + "export": "Exportar", + "deleteNow": "Deletar Agora", + "markAsUnreviewed": "Marcar como não revisado" + } + }, + "imagePicker": { + "selectImage": "Selecionar a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por rótulo ou sub-rótulo…" + }, + "noImages": "Nenhuma miniatura encontrada para essa câmera" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/filter.json new file mode 100644 index 0000000..ee84e75 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtro", + "labels": { + "label": "Rótulos", + "all": { + "title": "Todos os Rótulos", + "short": "Rótulos" + }, + "count_one": "{{count}} Rótulo", + "count_other": "{{count}} Rótulos" + }, + "zones": { + "label": "Zonas", + "all": { + "title": "Todas as Zonas", + "short": "Zonas" + } + }, + "dates": { + "selectPreset": "Selecione uma predefinição…", + "all": { + "title": "Todas as datas", + "short": "Datas" + } + }, + "more": "Mais Filtros", + "reset": { + "label": "Resetar filtros para valores padrão" + }, + "timeRange": "Intervalo de Tempo", + "subLabels": { + "label": "Sub-Rótulos", + "all": "Todos os Sub-Rótulos" + }, + "score": "Pontuação", + "estimatedSpeed": "Velocidade Estimada {{unit}}", + "features": { + "hasSnapshot": "Tem um snapshot", + "label": "Características", + "hasVideoClip": "Possui videoclipe", + "submittedToFrigatePlus": { + "label": "Enviado ao Frigate+", + "tips": "Você deve filtrar primeiro objetos que possuem capturas de imagem.

    Objetos rastreados sem capturas de imagem não serão enviados ao Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendente)", + "dateDesc": "Data (Descendente)", + "scoreAsc": "Pontuação do Objeto (Ascendente)", + "scoreDesc": "Pontuação de Objeto (Descendente)", + "speedAsc": "Velocidade Estimada (Ascendente)", + "speedDesc": "Velocidade Estimada (Descendente)", + "relevance": "Relevância" + }, + "cameras": { + "label": "Filtro de Câmeras", + "all": { + "title": "Todas as Câmeras", + "short": "Câmeras" + } + }, + "review": { + "showReviewed": "Exibir Revisados" + }, + "motion": { + "showMotionOnly": "Exibir Movimento Apenas" + }, + "explore": { + "settings": { + "title": "Configurações", + "defaultView": { + "title": "Visualização Padrão", + "desc": "Quando nenhum filtro é selecionado, exibe um sumário dos objetos mais recentes rastreados por rótulo, ou exibe uma grade sem filtro.", + "summary": "Sumário", + "unfilteredGrid": "Grade Sem Filtros" + }, + "gridColumns": { + "desc": "Selecione o número de colunas na visualização em grade.", + "title": "Colunas de Grade" + }, + "searchSource": { + "desc": "Escolha se deseja pesquisar nas miniaturas ou descrições dos seus objetos rastreados.", + "label": "Buscar Fonte", + "options": { + "thumbnailImage": "Imagem da Miniatura", + "description": "Descrição" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selecione uma data para filtrar" + } + } + }, + "logSettings": { + "label": "Nível de filtro de log", + "filterBySeverity": "Filtrar logs por severidade", + "loading": { + "title": "Carregando", + "desc": "Quando o painel de log é rolado para baixo, novos logs são transmitidos automaticamente conforme são adicionados." + }, + "disableLogStreaming": "Desativar o log de tranmissão", + "allLogs": "Todos os logs" + }, + "trackedObjectDelete": { + "title": "Confirmar Exclusão", + "desc": "Deletar esses {{objectLength}} objetos rastreados remove as capturas de imagem, quaisquer embeddings salvos, e quaisquer entradas do ciclo de vida associadas do objeto. Gravações desses objetos rastreados na visualização de Histórico NÃO irão ser deletadas.

    Tem certeza que quer proceder?

    Segure a tecla Shift para pular esse diálogo no futuro.", + "toast": { + "success": "Objetos rastreados deletados com sucesso.", + "error": "Falha ao deletar objeto rastreado: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "recognizedLicensePlates": { + "title": "Placas de Identificação Reconhecidas", + "loadFailed": "Falha ao carregar placas de identificação reconhecidas.", + "loading": "Carregando placas de identificação reconhecidas…", + "placeholder": "Digite para pesquisar por placas de identificação…", + "noLicensePlatesFound": "Nenhuma placa de identificação encontrada.", + "selectPlatesFromList": "Seleciona uma ou mais placas da lista.", + "selectAll": "Selecionar todos", + "clearAll": "Limpar todos" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/icons.json new file mode 100644 index 0000000..c038a02 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecione um ícone", + "search": { + "placeholder": "Buscar por um ícone…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/input.json new file mode 100644 index 0000000..25a8190 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Baixar Video", + "toast": { + "success": "Sua análise do item de vídeo começou a ser baixado." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/player.json new file mode 100644 index 0000000..370565b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Nenhuma gravação encontrada para este horário", + "noPreviewFound": "Nenhuma pré-visualização encontrada", + "noPreviewFoundFor": "Nenhuma Pré-Visualização Encontrada em {{cameraName}}", + "submitFrigatePlus": { + "title": "Enviar esse frame para Frigate+?", + "submit": "Enviar" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para esse tipo de transmissão ao vivo.", + "streamOffline": { + "title": "Stream Offiline", + "desc": "Nenhum quadro foi recebido na stream {{cameraName}}detect, checar registros de erros" + }, + "cameraDisabled": "A câmera está desativada", + "stats": { + "streamType": { + "title": "Tipo de fluxo:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Largura de banda:", + "short": "Largura de banda" + }, + "latency": { + "title": "Latência:", + "value": "{{seconds}} segundos", + "short": { + "title": "Latência", + "value": "{{seconds}} s" + } + }, + "totalFrames": "Total de Quadros:", + "droppedFrames": { + "title": "Quadros perdidos:", + "short": { + "title": "Perdidos", + "value": "{{droppedFrames}} quadros" + } + }, + "decodedFrames": "Quadros Decodificados:", + "droppedFrameRate": "Taxa de Quadros Perdidos:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Quadro enviado ao Frigate+ com sucesso" + }, + "error": { + "submitFrigatePlusFailed": "Falha em submeter quadro ao Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/objects.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/objects.json new file mode 100644 index 0000000..48283d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Pessoa", + "bicycle": "Bicicleta", + "car": "Carro", + "motorcycle": "Moto", + "airplane": "Avião", + "bus": "Ônibus", + "train": "Trem", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Hidrante", + "street_sign": "Placa de rua", + "stop_sign": "Sinal de parada", + "parking_meter": "Parquímetro", + "bench": "Banco", + "bird": "Pássaro", + "cat": "Gato", + "dog": "Cachorro", + "horse": "Cavalo", + "sheep": "Ovelha", + "cow": "Vaca", + "elephant": "Elefante", + "bear": "Urso", + "zebra": "Zebra", + "giraffe": "Girafa", + "hat": "Chapéu", + "backpack": "Mochila", + "umbrella": "Guarda-Chuva", + "shoe": "Sapato", + "eye_glasses": "Óculos", + "handbag": "Bolsa", + "tie": "Gravata", + "suitcase": "Mala", + "frisbee": "Frisbe", + "skis": "Esquis", + "snowboard": "Snowboard", + "sports_ball": "Bola de Esportes", + "kite": "Pipa", + "baseball_bat": "Taco de Basebol", + "baseball_glove": "Luva de Basebol", + "skateboard": "Skate", + "plate": "Placa", + "surfboard": "Prancha de Surfe", + "tennis_racket": "Raquete de Tênis", + "bottle": "Garrafa", + "wine_glass": "Garrafa de Vinho", + "cup": "Copo", + "fork": "Garfo", + "knife": "Faca", + "spoon": "Colher", + "bowl": "Tigela", + "banana": "Banana", + "apple": "Maçã", + "animal": "Animal", + "sandwich": "Sanduíche", + "orange": "Laranja", + "broccoli": "Brócolis", + "bark": "Latido", + "carrot": "Cenoura", + "hot_dog": "Cachorro-Quente", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Bolo", + "chair": "Cadeira", + "couch": "Sofá", + "potted_plant": "Planta em Vaso", + "bed": "Cama", + "mirror": "Espelho", + "dining_table": "Mesa de Jantar", + "window": "Janela", + "desk": "Mesa", + "toilet": "Vaso Sanitário", + "door": "Porta", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Rato", + "remote": "Controle Remoto", + "keyboard": "Teclado", + "goat": "Cabra", + "cell_phone": "Celular", + "microwave": "Microondas", + "oven": "Forno", + "toaster": "Torradeira", + "sink": "Pia", + "refrigerator": "Geladeira", + "blender": "Liquidificador", + "book": "Livro", + "clock": "Relógio", + "vase": "Vaso", + "scissors": "Tesouras", + "teddy_bear": "Ursinho de Pelúcia", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "hair_brush": "Escova de Cabelo", + "vehicle": "Veículo", + "squirrel": "Esquilo", + "deer": "Veado", + "on_demand": "Sob Demanda", + "face": "Rosto", + "fox": "Raposa", + "rabbit": "Coelho", + "raccoon": "Guaxinim", + "robot_lawnmower": "Cortador de Grama Robô", + "waste_bin": "Lixeira", + "license_plate": "Placa de Identificação", + "package": "Pacote", + "bbq_grill": "Grelha de Churrasco", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/classificationModel.json new file mode 100644 index 0000000..c905298 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/classificationModel.json @@ -0,0 +1,55 @@ +{ + "documentTitle": "Modelos de Classificação", + "button": { + "deleteClassificationAttempts": "Apagar Imagens de Classificação", + "renameCategory": "Renomear Classe", + "deleteCategory": "Apagar Classe", + "deleteImages": "Apagar Imagens", + "trainModel": "Treinar Modelo", + "addClassification": "Adicionar classificação", + "deleteModels": "Excluir modelos", + "editModel": "Editar Modelo" + }, + "toast": { + "success": { + "deletedCategory": "Classe Apagada", + "deletedImage": "Imagens Apagadas", + "categorizedImage": "Imagem Classificada com Sucesso", + "trainedModel": "Modelo treinado com sucesso.", + "trainingModel": "Treinamento do modelo iniciado com sucesso.", + "deletedModel_one": "{{count}} modelo excluído com sucesso", + "deletedModel_many": "{{count}} modelos excluídos com sucesso", + "deletedModel_other": "{{count}} modelos excluídos com sucesso", + "updatedModel": "Configuração do modelo atualizada com sucesso", + "renamedCategory": "Classe renomeada para {{name}} com sucesso" + }, + "error": { + "deleteImageFailed": "Falha ao deletar:{{errorMessage}}", + "deleteCategoryFailed": "Falha ao deletar classe:{{errorMessage}}", + "categorizeFailed": "Falha ao categorizar imagem:{{errorMessage}}", + "deleteModelFailed": "Falha ao excluir o modelo: {{errorMessage}}", + "trainingFailed": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "trainingFailedToStart": "Falha ao iniciar o treinamento do modelo: {{errorMessage}}", + "updateModelFailed": "Falha ao atualizar modelo: {{errorMessage}}", + "renameCategoryFailed": "Falha ao renomear classe: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Excluir Classe", + "desc": "Tem certeza de que deseja excluir a classe {{name}}? Isso excluirá permanentemente todas as imagens associadas e exigirá o treinamento do modelo novamente.", + "minClassesTitle": "Não é possível apagar a classe" + }, + "deleteModel": { + "title": "Deletar modelo de classificação", + "single": "Tem certeza de que deseja excluir {{name}}? Isso excluirá permanentemente todos os dados associados, incluindo imagens e dados de treinamento. Esta ação não pode ser desfeita." + }, + "details": { + "scoreInfo": "A pontuação representa a média de confiança da classificação de todas as detecções deste objeto." + }, + "tooltip": { + "trainingInProgress": "O modelo está sendo treinado", + "noNewImages": "Nenhuma nova imagem para treinar. Classifique mais imagens para treinar mais.", + "noChanges": "Nenhuma alteração ao conjunto de dados desde o último treinamento.", + "modelNotReady": "O modelo não está pronto para treinamento" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/configEditor.json new file mode 100644 index 0000000..46c4808 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de Configuração - Frigate", + "configEditor": "Editor de configuração", + "copyConfig": "Copiar Configuração", + "saveAndRestart": "Salvar & Reiniciar", + "saveOnly": "Salvar apenas", + "confirm": "Sair sem salvar?", + "toast": { + "success": { + "copyToClipboard": "Configuração copiada para a área de transferência." + }, + "error": { + "savingError": "Erro ao salvar configuração" + } + }, + "safeConfigEditor": "Editor de Configuração (Modo Seguro)", + "safeModeDescription": "O Frigate está no modo seguro devido a um erro de validação de configuração." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/events.json new file mode 100644 index 0000000..37785ab --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/events.json @@ -0,0 +1,58 @@ +{ + "alerts": "Alertas", + "detections": "Detecções", + "motion": { + "label": "Movimento", + "only": "Somente Movimento" + }, + "allCameras": "Todas as Câmeras", + "empty": { + "alert": "Não existe nenhum alerta para revisar", + "detection": "Não há nenhuma detecção para revisar", + "motion": "Nenhum dado de movimento encontrado" + }, + "timeline": "Linha do tempo", + "timeline.aria": "Selecione a linha do tempo", + "events": { + "label": "Eventos", + "aria": "Selecione eventos", + "noFoundForTimePeriod": "Nenhum evento encontrado neste período." + }, + "recordings": { + "documentTitle": "Gravações - Frigate" + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markTheseItemsAsReviewed": "Marque estes itens como revisados", + "newReviewItems": { + "button": "Novos Itens para Revisar", + "label": "Ver novos itens para revisão" + }, + "selected_one": "{{count}} selecionado(s)", + "documentTitle": "Revisar - Frigate", + "markAsReviewed": "Marcar como Revisado", + "selected_other": "{{count}} selecionado(s)", + "camera": "Câmera", + "detected": "detectado", + "suspiciousActivity": "Atividade Suspeita", + "threateningActivity": "Atividade de Ameaça", + "detail": { + "noDataFound": "Nenhum dado de detalhe para revisar", + "aria": "Alternar visualização de detalhe", + "trackedObject_one": "{{count}} objeto(s)", + "trackedObject_other": "{{count}} objetos", + "noObjectDetailData": "Nenhum dado de detalhe de objeto disponível.", + "label": "Detalhe", + "settings": "Configurações de visualização detalhada", + "alwaysExpandActive": { + "title": "Expandir sempre o modo ativo" + } + }, + "objectTrack": { + "trackedPoint": "Ponto rastreado", + "clickToSeek": "Clique para ir para esse horário" + }, + "zoomIn": "Ampliar", + "zoomOut": "Diminuir o zoom" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/explore.json new file mode 100644 index 0000000..bb3e6fd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/explore.json @@ -0,0 +1,229 @@ +{ + "documentTitle": "Explorar - Frigate", + "generativeAI": "IA Generativa", + "exploreMore": "Explorar mais objetos {{label}}", + "exploreIsUnavailable": { + "title": "A seção Explorar está indisponível", + "embeddingsReindexing": { + "context": "O menu explorar pode ser usado após os embeddings de objetos rastreados terem terminado de reindexar.", + "startingUp": "Iniciando…", + "estimatedTime": "Tempo estimado restante:", + "finishingShortly": "Finalizando em breve", + "step": { + "thumbnailsEmbedded": "Miniaturas embedded: ", + "descriptionsEmbedded": "Descrições embedded: ", + "trackedObjectsProcessed": "Objetos rastreados processados: " + } + }, + "downloadingModels": { + "context": "Frigate está baixando os modelos de embeddings necessários para oferecer suporte ao recurso de Pesquisa Semântica. Isso pode levar vários minutos, dependendo da velocidade da sua conexão de rede.", + "setup": { + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de Texto", + "visionModel": "Modelo de visão", + "visionModelFeatureExtractor": "Extrator de características do modelo de visão" + }, + "tips": { + "context": "Você pode querer reindexar os embeddings de seus objetos rastreados uma vez que os modelos forem baixados.", + "documentation": "Leia a documentação" + }, + "error": "Um erro ocorreu. Verifique os registos do Frigate." + } + }, + "details": { + "timestamp": "Carimbo de data e hora", + "item": { + "title": "Rever Detalhe dos itens", + "desc": "Revisar os detalhes do item", + "button": { + "share": "Compartilhar esse item revisado", + "viewInExplore": "Ver em Explorar" + }, + "tips": { + "mismatch_one": "{{count}} objeto indisponível foi detectado e incluido nesse item de revisão. Esse objeto ou não se qualifica para um alerta ou detecção, ou já foi limpo/deletado.", + "mismatch_many": "{{count}} objetos indisponíveis foram detectados e incluídos nesse item de revisão. Esses objetos ou não se qualificam para um alerta ou detecção, ou já foram limpos/deletados.", + "mismatch_other": "{{count}} objetos indisponíveis foram detectados e incluídos nesse item de revisão. Esses objetos ou não se qualificam para um alerta ou detecção, ou já foram limpos/deletados.", + "hasMissingObjects": "Ajustar a sua configuração se quiser que o Frigate salve objetos rastreados com os seguintes rótulos: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Uma nova descrição foi solicitada do {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para regenerar.", + "updatedSublabel": "Sub-rótulo atualizado com sucesso.", + "updatedLPR": "Placa de identificação atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio requisitada com sucesso." + }, + "error": { + "regenerate": "Falha ao ligar para {{provider}} para uma descrição nova: {{errorMessage}}", + "updatedSublabelFailed": "Falha ao atualizar sub-rótulo: {{errorMessage}}", + "updatedLPRFailed": "Falha ao atualizar placa de identificação: {{errorMessage}}", + "audioTranscription": "Falha ao requisitar transcrição de áudio: {{errorMessage}}" + } + } + }, + "label": "Rótulo", + "editSubLabel": { + "title": "Editar sub-rótulo", + "desc": "Nomeie um novo sub-rótulo para esse(a) {{label}}", + "descNoLabel": "Nomeie um sub-rótulo para esse objeto rastreado" + }, + "editLPR": { + "title": "Editar placa de identificação", + "desc": "Entre um valor de placa de identificação para esse(a) {{label}}", + "descNoLabel": "Entre um novo valor de placa de identificação para esse objeto rastrado" + }, + "snapshotScore": { + "label": "Pontuação da Captura de Imagem" + }, + "topScore": { + "label": "Pontuação Mais Alta", + "info": "A pontuação mais alta é a pontuação mediana mais alta para o objeto rastreado, então pode ser diferente da pontuação mostrada na miniatura dos resultados de busca." + }, + "recognizedLicensePlate": "Placa de Identificação Reconhecida", + "estimatedSpeed": "Velocidade Estimada", + "objects": "Objetos", + "camera": "Câmera", + "zones": "Zonas", + "button": { + "findSimilar": "Encontrar Semelhante", + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descrição de objetos rastreados" + } + }, + "description": { + "label": "Descrição", + "placeholder": "Descrição do objeto rastreado", + "aiTips": "O Frigate não solicitará a descrição do seu fornecedor de IA Generativa até que o ciclo de vida do objeto rastreado tenha finalizado." + }, + "expandRegenerationMenu": "Expandir menu de regeneração", + "regenerateFromSnapshot": "Regenerar a partir de Captura de Imagem", + "regenerateFromThumbnails": "Regenerar a partir de Miniaturas", + "tips": { + "descriptionSaved": "Descrição salva com sucesso", + "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Pontuação" + } + }, + "trackedObjectDetails": "Detalhes do Objeto Rastreado", + "type": { + "details": "detalhes", + "snapshot": "captura de imagem", + "video": "vídeo", + "object_lifecycle": "ciclo de vida do objeto", + "thumbnail": "thumbnail" + }, + "objectLifecycle": { + "title": "Ciclo de Vida do Objeto", + "noImageFound": "Nenhuma imagem encontrada nessa marcação de horário.", + "createObjectMask": "Criar Máscara de Objeto", + "adjustAnnotationSettings": "Ajustar configurações de anotação", + "scrollViewTips": "Role a tela para ver momentos significantes do ciclo de vida desse objeto.", + "autoTrackingTips": "As posições da caixa delimitadora será inacurada para cameras com rastreamento automático.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Ponto Rastreado", + "lifecycleItemDesc": { + "visible": "{{label}} detectado", + "entered_zone": "{{label}} entrou em {{zones}}", + "active": "{{label}} se tornou ativo", + "stationary": "{{label}} se tornou estacionário", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectado para {{label}}", + "other": "{{label}} reconhecido como {{attribute}}" + }, + "gone": "{{label}} esquerda", + "heard": "{{label}} escutado(a)", + "header": { + "zones": "Zonas", + "area": "Área", + "ratio": "Proporção" + }, + "external": "{{label}} detectado(a)" + }, + "annotationSettings": { + "title": "Configurações de anotação", + "showAllZones": { + "title": "Mostrar todas as zonas", + "desc": "Sempre exibir zonas nos quadros em que objetos entraram em uma zona." + }, + "offset": { + "label": "Deslocamento da Anotação", + "desc": "Esses dados vem do feed de detecção da sua câmera, porém estão sobrepondo imagens da gravação. É improvável que duas transmissões estejam perfeitamente sincronizadas. Como resultado, as caixas delimitadoras e a gravação não se alinharam perfeitamente. Porém, o campo annotation_offset pode ser utilizado para ajustar isso.", + "documentation": "Leia a documentação. ", + "millisecondsToOffset": "Milisegundos para separar detecções de anotações.Default: 0", + "tips": "DICA: Imagine que haja um clipe de evento com uma pessoa caminhando da esquerda para a direita. Se a caixa delimitadora da linha do tempo do evento está consistentemente à esquerda da pessoa, então o valor deve ser reduzido. Similarmente, se a pessoa está caminhando da esquerda para a direita e a caixa delimitadora está consistentemente à frente da pessoa, então o valor deve ser aumentado.", + "toast": { + "success": "O deslocamento de anotação para a câmera {{camera}} foi salvo no arquivo de configuração. Reinicie o Frigate para aplicar as alterações." + } + } + }, + "carousel": { + "previous": "Slide anterior", + "next": "Próximo slide" + } + }, + "itemMenu": { + "findSimilar": { + "aria": "Encontrar objetos rastreados similares", + "label": "Encontrar similar" + }, + "submitToPlus": { + "label": "Enviar ao Frigate+", + "aria": "Enviar ao Frigate Plus" + }, + "downloadVideo": { + "label": "Baixar vídeo", + "aria": "Baixar vídeo" + }, + "downloadSnapshot": { + "label": "Baixar captura de imagem", + "aria": "Baixar captura de imagem" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida do objeto", + "aria": "Exibir o ciclo de vida do objeto" + }, + "viewInHistory": { + "label": "Ver no Histórico", + "aria": "Ver no Histórico" + }, + "deleteTrackedObject": { + "label": "Deletar esse objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicionar um gatilho para esse objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmar Exclusão", + "desc": "Deletar esse objeto rastreado remove a captura de imagem, quaisquer embeddings salvos, e quaisquer entradas de ciclo de vida de objeto associadas. Gravações desse objeto rastreado na visualização de Histórico NÃO serão deletadas.

    Tem certeza que quer prosseguir?" + } + }, + "noTrackedObjects": "Nenhum Objeto Rastreado Encontrado", + "fetchingTrackedObjectsFailed": "Erro ao buscar por objetos rastreados: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "{{count}} objetos rastreados ", + "searchResult": { + "tooltip": "Correspondência com {{type}} de {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado deletado com sucesso.", + "error": "Falha ao detectar objeto rastreado {{errorMessage}}" + } + } + }, + "aiAnalysis": { + "title": "Análise de IA" + }, + "concerns": { + "label": "Preocupações" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/exports.json new file mode 100644 index 0000000..12a6dce --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Buscar", + "noExports": "Nenhuma exportação encontrada", + "deleteExport": "Deletar Exportação", + "deleteExport.desc": "Você tem certeza que quer apagar {{exportName}}?", + "editExport": { + "title": "Exportar Renomear", + "desc": "Entre um novo nome para essa exportação.", + "saveExport": "Salvar exportação" + }, + "toast": { + "error": { + "renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Compartilhar exportação", + "downloadVideo": "Baixar vídeo", + "editName": "Editar nome", + "deleteExport": "Apagar exportação" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/faceLibrary.json new file mode 100644 index 0000000..ee3ccde --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "details": { + "person": "Pessoa", + "unknown": "Desconhecido", + "face": "Detalhes do Rosto", + "subLabelScore": "Pontuação do Sub-Rótulo", + "scoreInfo": "A pontuação do sub-rótulo é a pontuação ponderada de todas as confidências faciais reconhecidas, então a pontuação pode ser diferente da mostrada na foto instantânea.", + "faceDesc": "Detalhes do objeto rastreado que gerou este rosto", + "timestamp": "Carimbo de data e hora" + }, + "selectItem": "Selecione {{item}}", + "imageEntry": { + "validation": { + "selectImage": "Por favor selecione um arquivo de imagem." + }, + "maxSize": "Tamanho máximo: {{size}}MB", + "dropActive": "Solte a imagem aqui…", + "dropInstructions": "Arraste e solte ou cole uma imagem aqui ou clique para selecionar" + }, + "deleteFaceLibrary": { + "title": "Apagar Nome", + "desc": "Tem certeza que quer deletar a coleção {{name}}? Isso deletará permanentemente todos os rostos associados." + }, + "button": { + "addFace": "Adicionar Rosto", + "renameFace": "Renomear Rosto", + "deleteFace": "Remover Rosto", + "deleteFaceAttempts": "Remover Rostos", + "reprocessFace": "Reprocessar Rosto", + "uploadImage": "Enviar Imagem" + }, + "createFaceLibrary": { + "new": "Criar Novo Rosto", + "title": "Criar Coleção", + "desc": "Criar uma nova coleção", + "nextSteps": "Para construir uma base forte:
  • Use a aba Reconhecimentos Recentes para selecionar e treinar em imagens para cada pessoa detectada.
  • Foque em imagens retas para melhores resultados; evite treinar imagens que capturam rostos em um ângulo.
  • " + }, + "deleteFaceAttempts": { + "title": "Apagar Rostos", + "desc_one": "Você tem certeza que quer deletar {{count}} rosto? Essa ação não pode ser desfeita.", + "desc_many": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita.", + "desc_other": "Você tem certeza que quer deletar os {{count}} rostos? Essa ação não pode ser desfeita." + }, + "renameFace": { + "title": "Renomear Rosto", + "desc": "Entre com o novo nome para {{name}}" + }, + "nofaces": "Nenhum rosto disponível", + "pixels": "{{area}}px", + "readTheDocs": "Leia a documentação", + "steps": { + "nextSteps": "Próximos Passos", + "faceName": "Digite o Nome do Rosto", + "uploadFace": "Enviar Imagem de Rosto", + "description": { + "uploadFace": "Faça o upload de uma imagem de {{name}} que mostre seu rosto visto de frente. A imagem não precisa estar recortada apenas com o rosto." + } + }, + "description": { + "placeholder": "Informe um nome para esta coleção", + "addFace": "Adicione uma nova coleção à Biblioteca Facial subindo a sua primeira imagem.", + "invalidName": "Nome inválido. Nomes podem incluir apenas letras, números, espaços, apóstrofos, sublinhados e hífenes." + }, + "documentTitle": "Biblioteca de rostos - Frigate", + "uploadFaceImage": { + "title": "Carregar imagem facial", + "desc": "Envie uma imagem para escanear por faces e incluir em {{pageToggle}}" + }, + "collections": "Coleções", + "train": { + "title": "Reconhecimentos Recentes", + "aria": "Selecionar reconhecimentos recentes", + "empty": "Não há tentativas recentes de reconhecimento facial" + }, + "selectFace": "Selecionar Rosto", + "trainFaceAs": "Treinar Rosto como:", + "trainFace": "Treinar Rosto", + "toast": { + "success": { + "uploadedImage": "Imagens enviadas com sucesso.", + "addFaceLibrary": "{{name}} foi adicionado com sucesso à Biblioteca de Rostos!", + "deletedFace_one": "{{count}} rosto apagado com sucesso.", + "deletedFace_many": "{{count}} rostos apagados com sucesso.", + "deletedFace_other": "{{count}} rostos apagados com sucesso.", + "trainedFace": "Rosto treinado com sucesso.", + "updatedFaceScore": "Pontuação de rosto atualizada com sucesso.", + "renamedFace": "O rosto foi renomeado com sucesso para {{name}}", + "deletedName_one": "{{count}} rosto foi deletado com sucesso.", + "deletedName_many": "{{count}} rostos foram deletados com sucesso.", + "deletedName_other": "{{count}} rostos foram deletados com sucesso." + }, + "error": { + "uploadingImageFailed": "Falha ao enviar a imagem: {{errorMessage}}", + "addFaceLibraryFailed": "Falha ao definir o nome do rosto: {{errorMessage}}", + "deleteFaceFailed": "Falha em deletar: {{errorMessage}}", + "deleteNameFailed": "Falha ao deletar nome: {{errorMessage}}", + "renameFaceFailed": "Falha ao renomear rosto: {{errorMessage}}", + "trainFailed": "Falha ao treinar: {{errorMessage}}", + "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/live.json new file mode 100644 index 0000000..8fb79a8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/live.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Ao Vivo - Frigate", + "documentTitle.withCamera": "{{camera}} - Ao vivo - Frigate", + "lowBandwidthMode": "Modo de baixa largura de banda", + "twoWayTalk": { + "enable": "Habilitar Fala em Dois Sentidos", + "disable": "Desabilitar Fala em Dois Sentidos" + }, + "cameraAudio": { + "enable": "Habilitar Áudio da Câmera", + "disable": "Desabilitar Audio da Câmera" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Clique no quadro para centralizar a câmera", + "enable": "Ativar clique para mover", + "disable": "Desativar clique para mover" + }, + "left": { + "label": "Mova a câmera PTZ para a esquerda" + }, + "up": { + "label": "Mova a câmera PTZ para cima" + }, + "down": { + "label": "Mova a câmera PTZ para baixo" + }, + "right": { + "label": "Mova a câmera PTZ para a direita" + } + }, + "frame": { + "center": { + "label": "Clique no quadro para centralizar a câmera PTZ" + } + }, + "presets": "Predefinições de câmera PTZ", + "zoom": { + "in": { + "label": "Aumentar Zoom na câmera PTZ" + }, + "out": { + "label": "Diminuir Zoom na câmera PTZ" + } + }, + "focus": { + "in": { + "label": "Aumentar foco da câmera PTZ" + }, + "out": { + "label": "Tirar foco da câmera PTZ" + } + } + }, + "camera": { + "enable": "Ativar Câmera", + "disable": "Desabilitar Câmera" + }, + "muteCameras": { + "enable": "Silenciar Todas as Câmeras", + "disable": "Ativar Áudio de Todas as Câmeras" + }, + "detect": { + "enable": "Ativar Detecção", + "disable": "Desativar Detecção" + }, + "recording": { + "enable": "Ativar Gravação", + "disable": "Desativar Gravação" + }, + "snapshots": { + "enable": "Permitir Capturas de Imagem", + "disable": "Desativar Capturas de Imagem" + }, + "audioDetect": { + "enable": "Ativar Detecção de Áudio", + "disable": "Desabilitar Detecção de Áudio" + }, + "autotracking": { + "enable": "Habilitar Rastreamento Automático", + "disable": "Desabilitar Rastreamento Automático" + }, + "streamStats": { + "enable": "Exibir Estatísticas de Transmissão", + "disable": "Ocultar Estatísticas de Transmissão" + }, + "manualRecording": { + "title": "Sob Demanda", + "tips": "Baixe uma captura de tela instantânea ou Inicie um evento manual baseado nas configurações de retenção de gravação dessa câmera.", + "playInBackground": { + "label": "Reproduzir em segundo plano", + "desc": "Habilite essa opção para continuar transmitindo quando o reprodutor estiver oculto." + }, + "showStats": { + "label": "Exibir Estatísticas", + "desc": "Habilite esta opção para exibir as estatísticas da transmissão como uma sobreposição no feed da câmera." + }, + "start": "Iniciar gravação sob demanda", + "started": "Iniciou a gravação manual sob demanda.", + "failedToStart": "Falha ao iniciar a gravação manual sob demanda.", + "recordDisabledTips": "Como a gravação está desabilitada ou restrita na configuração desta câmera, apenas um instantâneo será salvo.", + "end": "Fim da gravação sob demanda", + "failedToEnd": "Falha ao finalizar a gravação manual sob demanda.", + "debugView": "Visualização de Depuração", + "ended": "Gravação manual sob demanda finalizada." + }, + "streamingSettings": "Configurações de Transmissão", + "notifications": "Notificações", + "audio": "Áudio", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmissão", + "audio": { + "tips": { + "title": "O áudio deve sair da sua câmera e configurado no go2rtc para essa transmissão.", + "documentation": "Leia da documentação. " + }, + "available": "Áudio disponível para essa transmissão", + "unavailable": "O áudio não está disponível para essa transmissão" + }, + "twoWayTalk": { + "tips": "O seu dispostivio precisa suportar esse recurso e o WebRTC precisa estar configurado para áudio bidirecional.", + "tips.documentation": "Leia a documentação. ", + "available": "Áudio bidirecional está disponível para essa transmissão", + "unavailable": "Áudio bidirecional está indisponível para essa transmissão" + }, + "lowBandwidth": { + "tips": "A transmissão ao vivo está em modo de economia de dados devido a erros de buffering ou de transmissão.", + "resetStream": "Resetar transmissão" + }, + "playInBackground": { + "label": "Reproduzir em segundo plano", + "tips": "Habilitar essa opção para continuar a transmissão quando o reprodutor estiver oculto." + }, + "debug": { + "picker": "A seleção da transmissão fica indisponível em modo de depuração. A visualização de depuração sempre usa o papel de detecção atribuído à transmissão." + } + }, + "cameraSettings": { + "title": "Configurações de {{camera}}", + "cameraEnabled": "Câmera Habilitada", + "objectDetection": "Detecção de Objeto", + "recording": "Gravação", + "snapshots": "Capturas de Imagem", + "audioDetection": "Detecção de Áudio", + "autotracking": "Auto Rastreamento", + "transcription": "Transcrição de Áudio" + }, + "history": { + "label": "Exibir gravação histórica" + }, + "effectiveRetainMode": { + "modes": { + "all": "Todos", + "motion": "Movimento", + "active_objects": "Objetos Ativos" + }, + "notAllTips": "A configuração de retenção da sua gravação do(a) {{source}} está definida para o modo: {{effectiveRetainMode}}, então essa gravação sob demanda irá manter somente os segmentos com o {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar Layout", + "group": { + "label": "Editar Grupo de Câmera" + }, + "exitEdit": "Sair da Edição" + }, + "transcription": { + "enable": "Habilitar Transcrição de Áudio em Tempo Real", + "disable": "Desabilitar Transcrição de Áudio em Tempo Real" + }, + "noCameras": { + "title": "Nenhuma Câmera Configurada", + "description": "Inicie conectando uma câmera ao Frigate", + "buttonText": "Adicionar Câmera" + }, + "snapshot": { + "takeSnapshot": "Baixar captura de imagem instantânea", + "noVideoSource": "Nenhuma fonte de vídeo disponível para captura de imagem.", + "captureFailed": "Falha ao capturar imagem.", + "downloadStarted": "Download de capturas de imagem iniciado." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/recording.json new file mode 100644 index 0000000..fd7cf6e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtro", + "export": "Exportar", + "calendar": "Calendário", + "filters": "Filtros", + "toast": { + "error": { + "noValidTimeSelected": "Nenhum intervalo de tempo selecionado", + "endTimeMustAfterStartTime": "O tempo de término deve ser após o tempo de início" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/search.json new file mode 100644 index 0000000..19bf1a2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Buscar", + "savedSearches": "Buscas Salvas", + "searchFor": "Procurar por {{inputValue}}", + "button": { + "clear": "Limpar procurar", + "save": "Salvar pesquisa", + "delete": "Apagar procura salva", + "filterInformation": "Filtrar informação", + "filterActive": "Filtros ativos" + }, + "trackedObjectId": "ID do objeto rastreado", + "filter": { + "label": { + "cameras": "Câmeras", + "labels": "Rótulos", + "zones": "Zonas", + "before": "Antes", + "after": "Depois", + "min_score": "Pontuação Mínima", + "max_score": "Pontuação Máxima", + "min_speed": "Velocidade Mínima", + "max_speed": "Velocidade Máxima", + "sub_labels": "Sub-Rótulos", + "search_type": "Tipo de Busca", + "time_range": "Intervalo de Tempo", + "recognized_license_plate": "Placa de Carro Reconhecida", + "has_clip": "Possui Clipe", + "has_snapshot": "Possui Captura de Imagem" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A data 'antes' deve ser depois da data 'após'.", + "afterDatebeEarlierBefore": "A data 'após' deve ser antes da data 'antes'.", + "minScoreMustBeLessOrEqualMaxScore": "A 'pontuação_min' deve ser menor ou igual a 'pontuação_max'.", + "maxScoreMustBeGreaterOrEqualMinScore": "A 'pontuação_max' deve ser maior ou igual a 'pontuação_min'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "A 'velocidad_min' deve ser menor ou igual a 'velocidad_max'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "A 'velocidad_max' deve ser maior ou igual a 'velocidade_min'." + } + }, + "tips": { + "title": "Como utilizar filtros de texto", + "desc": { + "text": "Filtros ajudam a refinar os resultados da busca. Veja como utilizá-los na campo de pesquisa:", + "step1": "Digite um nome de chave de filtro seguido por \":\" (ex., \"câmeras:\").", + "step2": "Selecione um valor a partir das sugestões ou digite a sua própria.", + "step3": "Usar filtros múltiplos adicionando um após o outro com um espaço entre eles.", + "step4": "Filtros de data (antes: e após:) usam o formato {{DateFormat}}.", + "step5": "Filtros de tempo usam o formato {{exampleTime}}.", + "step6": "Remova os filtros clicando no \"x\" ao lado deles.", + "exampleLabel": "Exemplo:" + } + }, + "header": { + "noFilters": "Filtros", + "activeFilters": "Filtros ativos", + "currentFilterType": "Valores de Filtros" + } + }, + "similaritySearch": { + "active": "Pesquisa por similaridade ativa", + "title": "Buscar por Similaridade", + "clear": "Limpar buscar por similaridade" + }, + "placeholder": { + "search": "Pesquisar…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/settings.json new file mode 100644 index 0000000..541fd8b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/settings.json @@ -0,0 +1,903 @@ +{ + "documentTitle": { + "default": "Configurações - Frigate", + "authentication": "Configurações de Autenticação - Frigate", + "camera": "Configurações de Câmera - Frigate", + "enrichments": "Configurações de Enriquecimento - Frigate", + "masksAndZones": "Editor de Máscara e Zona - Frigate", + "motionTuner": "Ajuste de Movimento - Frigate", + "object": "Debug - Frigate", + "general": "Configurações de Interface de Usuário - Frigate", + "frigatePlus": "Frigate+ Configurações- Frigate", + "notifications": "Configurações de notificação - Frigate", + "cameraManagement": "Gerenciar Câmeras - Frigate", + "cameraReview": "Configurações de Revisão de Câmera - Frigate" + }, + "menu": { + "ui": "UI", + "cameras": "Configurações da câmera", + "masksAndZones": "Máscaras / Zonas", + "users": "Usuários", + "notifications": "Notificações", + "frigateplus": "Frigate+", + "motionTuner": "Ajuste de Movimento", + "debug": "Depurar", + "enrichments": "Enriquecimentos", + "triggers": "Gatilhos", + "roles": "Papéis", + "cameraManagement": "Gerenciamento", + "cameraReview": "Revisar" + }, + "dialog": { + "unsavedChanges": { + "title": "Você tem alterações não salvas.", + "desc": "Você deseja salvar as alterações antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Câmera", + "noCamera": "Sem Câmera" + }, + "general": { + "title": "Opções Gerais", + "liveDashboard": { + "title": "Painel em Tempo Real", + "automaticLiveView": { + "label": "Visualização em Tempo Real Automática", + "desc": "Automaticamente alterar para a visualização em tempo real da câmera quando alguma atividade for detectada. Desativar essa opção faz com que as imagens estáticas da câmera no Painel em Tempo Real atualizem apenas uma vez por minuto." + }, + "playAlertVideos": { + "label": "Reproduzir Alertas de Video", + "desc": "Por padrão, alertas recentes no Painel em Tempo Real são reproduzidos como vídeos em loop. Desative essa opção para mostrar apenas a imagens estáticas de alertas recentes nesse dispositivo / navegador." + } + }, + "storedLayouts": { + "title": "Layouts Salvos", + "desc": "O layout das câmeras em um grupo de câmeras pode ser arrastado/redimensionado. As posições são salvas no armazenamento local do seu navegador.", + "clearAll": "Apagar Todos os Layouts" + }, + "cameraGroupStreaming": { + "title": "Opções de Streaming de Grupo de Câmeras", + "desc": "Os ajustes de streaming para cada grupo de câmera são salvos no armazenamento local do seu navegador.", + "clearAll": "Apagar Todos os Ajustes de Streaming" + }, + "recordingsViewer": { + "title": "Visualizador de Gravações", + "defaultPlaybackRate": { + "label": "Velocidade Padrão de Reprodução", + "desc": "Velocidade padrão de reprodução para gravações." + } + }, + "calendar": { + "title": "Calendário", + "firstWeekday": { + "label": "Primeiro Dia da Semana", + "desc": "Dia em que as semanas no calendário de revisão iniciam.", + "sunday": "Domingo", + "monday": "Segunda-Feira" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Layout deletado para {{cameraName}}", + "clearStreamingSettings": "Ajustes de streaming para todos os grupos de câmera limpados." + }, + "error": { + "clearStoredLayoutFailed": "Não foi possível apagar o layout:{{errorMessage}}", + "clearStreamingSettingsFailed": "Não foi possível apagar os ajustes de streaming:{{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Configurações de Enriquecimento", + "unsavedChanges": "Alterações de configurações de Enriquecimento não salvas", + "birdClassification": { + "title": "Classificação de Pássaros", + "desc": "A classificação de pássaros identifica pássaros conhecidos usando o modelo Tensorflow quantizado. Quando um pássaro é reconhecido, o seu nome comum será adicionado como um sub-rótulo. Essa informação é incluida na UI, filtros e notificações." + }, + "semanticSearch": { + "title": "Busca Semântica", + "desc": "A Busca Semântica no Frigate permite você encontrar objetos rastreados dentro dos seus itens revisados, usando ou a imagem em si, uma descrição de texto definida pelo usuário ou uma gerada automaticamente.", + "readTheDocumentation": "Leia a Documentação", + "reindexNow": { + "label": "Reindexar Agora", + "desc": "A reindexação irá regenerar os embeddings para todos os objetos rastreados. Esse processo roda em segundo plano e pode demandar 100% da CPU e levar um tempo considerável dependendo do número de objetos rastreados que você possui.", + "confirmTitle": "Confirmar Reindexação", + "confirmDesc": "Tem certeza que quer reindexar todos os embeddings de objetos rastreados? Esse processo rodará em segundo plano porém utilizará 100% da CPU e levará uma quantidade de tempo considerável. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "success": "A reindexação iniciou com sucesso.", + "alreadyInProgress": "A reindexação já está em progresso.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}" + }, + "modelSize": { + "label": "Tamanho do Modelo", + "desc": "O tamanho do modelo usado para embeddings de pesquisa semântica.", + "small": { + "title": "pequeno", + "desc": "Usando pequeno emprega a versão quantizada do modelo que utiliza menos RAM e roda mais rápido na CPU, com diferenças negligíveis na qualidade dos embeddings." + }, + "large": { + "title": "grande", + "desc": "Usar grande emprega o modelo Jina completo e roda na GPU automáticamente caso aplicável." + } + } + }, + "faceRecognition": { + "title": "Reconhecimento Facial", + "desc": "O reconhecimento facial permite que pessoas sejam associadas a nomes e quando seus rostos forem reconhecidos, o Frigate associará o nome da pessoa como um sub-rótulo. Essa informação é inclusa na UI, filtros e notificações.", + "readTheDocumentation": "Leia a Documentação", + "modelSize": { + "label": "Tamanho do Modelo", + "desc": "O tamanho do modelo usado para reconhecimento facial.", + "small": { + "title": "pequeno", + "desc": "Usar o pequeno emprega o modelo de embedding de rosto FaceNet, que roda de maneira eficiente na maioria das CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar o grande emprega um modelo de embedding de rosto ArcFace e irá automáticamente rodar pela GPU se aplicável." + } + } + }, + "licensePlateRecognition": { + "title": "Reconhecimento de Placa de Identificação", + "desc": "O Frigate pode reconhecer placas de identificação em veículos e automáticamente adicionar os caracteres detectados ao campo placas_de_identificação_reconhecidas ou um nome conhecido como um sub-rótulo a objetos que são do tipo carro. Um uso típico é ler a placa de carros entrando em uma garagem ou carros passando pela rua.", + "readTheDocumentation": "Leia a Documentação" + }, + "restart_required": "Necessário reiniciar (configurações de enriquecimento foram alteradas)", + "toast": { + "success": "As regras de enriquecimento foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar alterações de configurações: {{errorMessage}}" + } + }, + "camera": { + "title": "Configurações de Câmera", + "streams": { + "title": "Transmissões", + "desc": "Temporáriamente desativa a câmera até o Frigate reiniciar. Desativar a câmera completamente impede o processamento da transmissão dessa câmera pelo Frigate. Detecções, gravações e depuração estarão indisponíveis.
    Nota: Isso não desativa as retransmissões do go2rtc." + }, + "review": { + "title": "Revisar", + "desc": "Temporariamente habilita/desabilita alertas e detecções para essa câmera até o Frigate reiniciar. Quando desabilitado, nenhum novo item de revisão será gerado. ", + "alerts": "Alertas ", + "detections": "Detecções " + }, + "reviewClassification": { + "title": "Classificação de Revisões", + "desc": "O Frigate categoriza itens de revisão como Alertas e Detecções. Por padrão, todas as pessoas e carros são considerados alertas. Você pode refinar a categorização dos seus itens revisados configurando as zonas requeridas para eles.", + "readTheDocumentation": "Leia a Documentação", + "noDefinedZones": "Nenhuma zona definida para essa câmera.", + "selectAlertsZones": "Selecionar as zonas para Alertas", + "selectDetectionsZones": "Selecionar as zonas para Detecções", + "objectAlertsTips": "Todos os objetos {{alertsLabels}} em {{cameraName}} serão exibidos como Alertas.", + "zoneObjectAlertsTips": "Todos os {{alertsLabels}} objetos detectados em {{zone}} em {{cameraName}} serão exibidos como Alertas.", + "objectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados em {{cameraName}} serão exibidos como Detecções independente de qual zona eles estiverem.", + "zoneObjectDetectionsTips": { + "text": "Todos os objetos de {{detectionsLabels}} não categorizados em {{zone}} em {{cameraName}} serão exibidos como Detecções.", + "notSelectDetections": "Todos os objetos {{detectionsLabels}} detectados em {{zone}} em {{cameraName}} não categorizados como Alertas serão exibidos como Detecções independente da zona em que estiverem.", + "regardlessOfZoneObjectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados em {{cameraName}} serão exibidos como Detecções independente de quais zonas estiverem." + }, + "unsavedChanges": "Configurações de Classificação de Revisões Não Salvas para {{camera}}", + "limitDetections": "Limitar detecções a zonas específicas", + "toast": { + "success": "A configuração de Revisão de Classificação foi salva. Reinicie o Frigate para aplicar as mudanças." + } + }, + "object_descriptions": { + "title": "Descrições de Objeto por IA Generativa", + "desc": "Habilitar descrições por IA Generativa temporariamente para essa câmera. Quando desativada, as descrições geradas por IA não serão requisitadas para objetos rastreados para essa câmera." + }, + "review_descriptions": { + "title": "Revisar Descrições de IA Generativa", + "desc": "Habilitar/desabilitar temporariamente descrições de revisão de IA Generativa para essa câmera. Quando desativada, as descrições de IA Generativa não serão solicitadas para revisão para essa câmera." + }, + "addCamera": "Adicionar Câmera Nova", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurções de Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as opções da câmera incluindo as de transmissão e papéis.", + "name": "Nome da Câmera", + "nameRequired": "Nome para a câmera é requerido", + "nameInvalid": "O nome da câmera deve contar apenas letras, números, sublinhado ou hífens", + "namePlaceholder": "ex: porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Transmissões de Entrada", + "path": "Caminho da Transmissão", + "pathRequired": "Um caminho para a transmissão é requerido", + "pathPlaceholder": "rtsp://...", + "roles": "Regras", + "rolesRequired": "Ao menos um papel é requerido", + "rolesUnique": "Cada papel (áudio, detecção, gravação) pode ser atribuído a uma única transmissão", + "addInput": "Adicionar Transmissão de Entrada", + "removeInput": "Remover Transmissão de Entrada", + "inputsRequired": "Ao menos uma transmissão de entrada é requerida" + }, + "toast": { + "success": "Câmera {{cameraName}} salva com sucesso" + }, + "nameLength": "O nome da câmera deve ter ao menos 24 caracteres." + } + }, + "masksAndZones": { + "filter": { + "all": "Todas as Máscaras e Zonas" + }, + "restart_required": "Reinicialização requerida (máscaras/zonas foram alteradas)", + "toast": { + "success": { + "copyCoordinates": "Coordenadas copiadas para {{polyName}} para a área de transferência." + }, + "error": { + "copyCoordinatesFailed": "Não foi possível copiar as coordenadas para a área de transferência." + } + }, + "motionMaskLabel": "Máscara de Movimento {{number}}", + "objectMaskLabel": "Máscara de Objeto {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "O nome da zona deve conter ao menos 2 caracteres.", + "mustNotBeSameWithCamera": "O nome da zona não pode ser igual ao nome da câmera.", + "alreadyExists": "Uma zona com esse noma já existe para essa câmera.", + "mustNotContainPeriod": "O nome da zona não pode conter ponto final.", + "hasIllegalCharacter": "O nome da zona contém caracteres ilegais." + } + }, + "distance": { + "error": { + "text": "A distância deve sair maior ou igual a 0.1.", + "mustBeFilled": "Todos os campos de distância devem ser preenchidos para utilizar a estimativa de velocidade." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A inércia deve ser maior que 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "O tempo de permanência deve ser maior ou igual a zero." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "O limiar de velocidade deve ser maior ou igual a 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Remover o ultimo ponto", + "reset": { + "label": "Limpar todos os pontos" + }, + "snapPoints": { + "true": "Pontos de encaixe", + "false": "Não encaixar os ponts" + }, + "delete": { + "title": "Confirmar Deletar", + "desc": "Tem certeza que quer deletar o {{type}} {{name}}?", + "success": "{{name}} foi deletado." + }, + "error": { + "mustBeFinished": "O desenho do polígono deve ser finalizado antes de salvar." + } + } + }, + "zones": { + "label": "Zonas", + "documentTitle": "Editar Zona - Frigate", + "desc": { + "title": "Zonas permitem que você defina uma área específica do quadro para que você possa determinar se um objeto está ou não em uma área em particular.", + "documentation": "Documentação" + }, + "add": "Adicionar Zona", + "edit": "Editar Zona", + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "name": { + "title": "Nome", + "inputPlaceHolder": "Digite um nome…", + "tips": "O nome deve ter no mínimo 2 caracteres e não pode ter o nome de uma câmera ou outra zona." + }, + "inertia": { + "title": "Inércia", + "desc": "Especifica por quantos quadros um objeto deve permanecer em uma zona para que seja considerado na zona. Padrão: 3" + }, + "loiteringTime": { + "title": "Tempo de Permanência", + "desc": "Define o tempo mínimo em segundos que o objeto deve estar na zona para ser ativado. Padrão: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que se aplicam a essa zona." + }, + "allObjects": "Todos os Objetos", + "speedEstimation": { + "title": "Estimativa de Velocidade", + "desc": "Habilitar estimativa de velocidade para objetos nesta zona. A zona deve ter exatamente 4 pontos.", + "docs": "Leia a documentação", + "lineADistance": "Distância da linha A ({{unit}})", + "lineBDistance": "Distância da Linha B ({{unit}})", + "lineCDistance": "Distância da linha C ({{unit}})", + "lineDDistance": "Distância da linha D ({{unit}})" + }, + "speedThreshold": { + "title": "Limiar de Velocidade ({{unit}})", + "desc": "Especifique a velocidade mínima para o objeto ser considerado nessa zona.", + "toast": { + "error": { + "pointLengthError": "A estimativa de velocidade foi desativada para essa zona. Zonas com estimação de velocidade devem ter exatamente 4 pontos.", + "loiteringTimeError": "Zonas com tempo de permanência acima de 0 não devem ser usadas com estimativa de velocidade." + } + } + }, + "toast": { + "success": "A zona ({{zoneName}}) foi salva. Reinicie o Frigate para aplicar as mudanças." + } + }, + "objectMasks": { + "objects": { + "allObjectTypes": "Todos os tipos de objetos", + "title": "Objetos", + "desc": "O tipo de objeto que se aplica para essa máscara de objeto." + }, + "toast": { + "success": { + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações.", + "noName": "A máscara de objeto foi salva. Reinicie o Frigate para aplicar as alterações." + } + }, + "label": "Máscaras de Objeto", + "documentTitle": "Editar Máscara de Objeto - Frigate", + "desc": { + "title": "Máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto baseado na localização.", + "documentation": "Documentação" + }, + "add": "Adicionar Máscara de Objeto", + "edit": "Editar Máscara de Objeto", + "context": "Filtro de máscaras de objeto são usados para filtrar falsos positivos para um dado tipo de objeto baseado na localização.", + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem." + }, + "motionMasks": { + "label": "Máscara de Movimento", + "documentTitle": "Editar Máscara de Movimento - Frigate", + "desc": { + "title": "Máscaras de movimento são usadas para prevenir tipos de movimento de ativarem uma detecção. Excesso de mascaramento tornará mais difícil que objetos sejam rastreados.", + "documentation": "Documentação" + }, + "add": "Nova Máscara de Movimento", + "edit": "Editar Máscara de Movimento", + "context": { + "title": "Máscaras de movimento são usadas para prevenir typos de movimento não desejados de ativarem uma detecção (exemplo: galhos de árvores, timestamps de câmeras). Máscaras de movimento devem ser usadas com muita parcimônia, excesso de mascaramento tornará mais difícil de objetos serem rastreados.", + "documentation": "Leia a documentação" + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "polygonAreaTooLarge": { + "title": "A máscara de movimento está cobrindo {{polygonArea}}% do quadro da câmera. Máscaras de movimento grandes não são recomendadas.", + "tips": "Máscaras de movimento não previnem objetos de serem detectados. Em vez disso você deve usar uma zona obrigatória.", + "documentation": "Leia a documentação" + }, + "toast": { + "success": { + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações.", + "noName": "Máscara de Movimento salva. Reinicie o Frigate para aplicar as alterações." + } + } + } + }, + "motionDetectionTuner": { + "desc": { + "title": "O Frigate usa a detecção de movimento como uma verificação de primeira linha para ver se há algo acontecendo no quadro que valha a pena verificar com a detecção de objetos.", + "documentation": "Leia o Guia de Ajuste de Movimento" + }, + "Threshold": { + "title": "Limiar", + "desc": "O valor do limiar dita o quanto de mudança na luminância de um pixel é requerida para ser considerada movimento. Padrão: 30" + }, + "contourArea": { + "title": "Área de contorno", + "desc": "O valor da área de contorno é usado para decidir quais grupos de mudança de pixel se qualificam como movimento. Padrão: 10" + }, + "improveContrast": { + "title": "Melhorar o contraste", + "desc": "Melhorar contraste para cenas escuras. Padrão: Ativado" + }, + "toast": { + "success": "As configurações de movimento foram salvas." + }, + "title": "Ajuste de Detecção de Movimento", + "unsavedChanges": "Alterações do Ajuste de Movimento Não Salvas ({{camera}})" + }, + "debug": { + "detectorDesc": "O Frigate usa seus detectores ({{detectors}}) para detectar objetos no fluxo de vídeo da sua câmera.", + "desc": "A visualização de depuração mostra uma visão em tempo real dos objetos rastreados e suas estatísticas. A lista de objetos mostra um resumo com atraso de tempo dos objetos detectados.", + "objectList": "Lista de Objetos", + "boundingBoxes": { + "desc": "Mostrar caixas delimitadoras ao redor de objetos rastreados", + "colors": { + "label": "Cores da caixa delimitadora de objetos", + "info": "
  • Na inicialização, cores diferentes serão atribuídas a cada rótulo de objeto
  • Uma linha fina azul escura indica que o objeto não foi detectado neste momento
  • Uma linha fina cinza indica que o objeto foi detectado como estacionário
  • Uma linha grossa indica que o objeto está sujeito ao rastreamento automático (quando ativado)
  • " + }, + "title": "Caixas delimitadoras" + }, + "zones": { + "title": "Zonas", + "desc": "Mostrar um esboço de quaisquer zonas definidas" + }, + "mask": { + "title": "Máscaras de movimento", + "desc": "Mostrar polígonos de máscara de movimento" + }, + "motion": { + "title": "Caixas de movimento", + "desc": "Mostrar caixas ao redor das áreas onde o movimento é detectado", + "tips": "

    Caixas de movimento


    Caixas vermelhas serão sobrepostas em áreas do quadro onde o movimento está sendo detectado

    " + }, + "regions": { + "title": "Regiões", + "desc": "Mostrar uma caixa da região de interesse enviada ao detector de objetos", + "tips": "

    Caixas de Região


    Caixas verdes claras serão sobrepostas em áreas de interesse no quadro que está sendo enviado ao detector de objetos.

    " + }, + "title": "Depuração", + "debugging": "Depuração", + "objectShapeFilterDrawing": { + "desc": "Desenhe um retângulo na imagem para ver os detalhes da área e proporções", + "tips": "Habilite essa opção para desenhar um retângulo na imagem da camera para mostrar a sua área e proporção. Esses valores podem ser usados para estabelecer parâmetros de filtro de formato de objetos na sua configuração.", + "document": "Leia a documentação ", + "score": "Pontuação", + "ratio": "Proporção", + "area": "Área", + "title": "Desenho de Filtro de Formato de Objeto" + }, + "noObjects": "Nenhum Objeto", + "timestamp": { + "title": "Timestamp", + "desc": "Sobrepor um timestamp na imagem" + }, + "paths": { + "title": "Caminho", + "desc": "Mostrar pontos significantes do caminho do objeto rastreado", + "tips": "

    Caminhos


    Linhas e círculos indicarão pontos significantes por onde o objeto rastreado se moveu durante o seu ciclo de vida.

    " + }, + "audio": { + "title": "Áudio", + "noAudioDetections": "Nenhuma detecção de áudio", + "score": "pontuanção", + "currentRMS": "RMS Atual", + "currentdbFS": "dbFS Atual" + }, + "openCameraWebUI": "Abrir a Interface Web de {{camera}}" + }, + "users": { + "title": "Usuários", + "management": { + "title": "Gerenciamento de Usuário", + "desc": "Gerenciar as contas de usuário dessa instância do Frigate." + }, + "addUser": "Adicionar Usuário", + "updatePassword": "Atualizar Senha", + "toast": { + "success": { + "createUser": "Usuário {{user}} criado com sucesso", + "deleteUser": "Usuário {{user}} deletado com sucesso", + "updatePassword": "Senha atualizada com sucesso.", + "roleUpdated": "Papel atualizado para {{user}}" + }, + "error": { + "setPasswordFailed": "Falha ao salvar a senha: {{errorMessage}}", + "createUserFailed": "Falha ao criar usuário: {{errorMessage}}", + "deleteUserFailed": "Falha ao deletar usuário: {{errorMessage}}", + "roleUpdateFailed": "Falha ao atualizar papel: {{errorMessage}}" + } + }, + "dialog": { + "form": { + "password": { + "match": "As senhas correspondem", + "notMatch": "As senhas são diferentes", + "title": "Senha", + "placeholder": "Digita a senha", + "confirm": { + "title": "Confirmar Senha", + "placeholder": "Confirmar Senha" + }, + "strength": { + "title": "Nível de segurança da senha: ", + "weak": "Fraca", + "medium": "Mediana", + "strong": "Forte", + "veryStrong": "Muito Forte" + } + }, + "newPassword": { + "title": "Senha Nova", + "placeholder": "Digite uma senha nova", + "confirm": { + "placeholder": "Digite a senha novamente" + } + }, + "usernameIsRequired": "Nome de usuário requerido", + "passwordIsRequired": "Senha requerida", + "user": { + "title": "Nome de Usuário", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "placeholder": "Digite o nome de usuário" + } + }, + "createUser": { + "title": "Criar Novo Usuário", + "desc": "Adicionar um novo usuário e especificar um papel para acesso às áreas da interface do Frigate.", + "usernameOnlyInclude": "O nome de usuário pode conter apenas letras, números, . ou _", + "confirmPassword": "Por favor confirme a sua senha" + }, + "deleteUser": { + "title": "Deletar Usuário", + "desc": "Essa ação não pode ser desfeita. Isso irá deletar permanentemente a conta do usuário e remover todos os dados associados.", + "warn": "Tem certeza que quer deletar {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "A senha não pode estar vazia", + "doNotMatch": "As senhas não correspondem", + "updatePassword": "Atualizar Senha para {{username}}", + "setPassword": "Definir Senha", + "desc": "Crie uma senha forte para proteger essa conta." + }, + "changeRole": { + "title": "Alterar Papel do Usuário", + "select": "Selecionar um papel", + "desc": "Atualizar permissões para {{username}}", + "roleInfo": { + "intro": "Selecione o papel apropriado para esse usuário:", + "admin": "Administrador", + "adminDesc": "Acesso total a todos os recursos.", + "viewer": "Espectador", + "viewerDesc": "Limitado aos Painéis ao Vivo, Revisar, Explorar, e Exportar somente.", + "customDesc": "Papel customizado com acesso a câmeras específicas." + } + } + }, + "table": { + "username": "Nome de Usuário", + "actions": "Ações", + "role": "Papel", + "noUsers": "Nenhum usuário encontrado.", + "changeRole": "Mudar papel do usuário", + "password": "Senha", + "deleteUser": "Deletar usuário" + } + }, + "notification": { + "suspendTime": { + "10minutes": "Suspender por 10 minutos", + "30minutes": "Suspender por 30 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "24hours": "Suspender por 24 horas", + "untilRestart": "Suspender até reiniciar", + "suspend": "Suspender", + "5minutes": "Suspender por 5 minutos" + }, + "cancelSuspension": "Cancelar Suspensão", + "toast": { + "success": { + "registered": "Registrados para notificações com sucesso. É necessário reiniciar o Frigate para que as notificações possam ser enviadas (incluindo a notificação de teste).", + "settingSaved": "As configurações de notificações foram salvas." + }, + "error": { + "registerFailed": "Falha ao salvar o registro para notificações." + } + }, + "title": "Notificações", + "notificationSettings": { + "title": "Configurações de Notificação", + "desc": "O Frigate pode enviar notificações push nativamente ao seu dispositivo quando estiver sendo executado no navegador ou instalado como um PWA.", + "documentation": "Leia a Documentação" + }, + "notificationUnavailable": { + "title": "Notificações Indisponíveis", + "desc": "Notificações push da Web exigem um contexto seguro (https://…). Essa é uma limitação do navegador. Acesse o Frigate com seguraça para usar as notificações.", + "documentation": "Leia a Documentação" + }, + "globalSettings": { + "title": "Configurações Globais", + "desc": "Suspender as notificações temporáriamente para câmeras específicas em todos os dispositivos registrados." + }, + "email": { + "title": "Email", + "placeholder": "ex: exemplo@email.com", + "desc": "Um email válido é requerido e será usado para notificar você caso haja algum problema com o serviço push." + }, + "cameras": { + "title": "Câmeras", + "noCameras": "Nenhuma câmera disponível", + "desc": "Selecionar para quais câmeras habilitar as notificações." + }, + "deviceSpecific": "Configurações Específicas do Dispositivo", + "registerDevice": "Registre Esse Dispositivo", + "unregisterDevice": "Cancelar Registro Desse Dispositivo", + "sendTestNotification": "Enviar uma notificação de teste", + "unsavedRegistrations": "Registros de Notificações Não Salvos", + "unsavedChanges": "Alterações de Notificações Não Salvas", + "active": "Notificações Ativas", + "suspended": "Notificações suspensas {{time}}" + }, + "frigatePlus": { + "title": "Configurações do Frigate+", + "apiKey": { + "title": "Chave de API do Frigate+", + "validated": "A chave de API do Frigate+ detectada e validada", + "notValidated": "Chave de API do Frigate+ não detectada ou não validada", + "desc": "A chave de API do Frigate+ habilita a integração com o serviço do Frigate+.", + "plusLink": "Leia mais sobre o Frigate+" + }, + "modelInfo": { + "plusModelType": { + "baseModel": "Modelo Base", + "userModel": "Ajuste Refinado" + }, + "supportedDetectors": "Detectores Suportados", + "cameras": "Câmeras", + "loading": "Carregando informações do modelo…", + "error": "Falha ao carregar as informações do modelo", + "availableModels": "Modelos Disponíveis", + "loadingAvailableModels": "Carregando modelos disponíveis…", + "title": "Informação do Modelo", + "modelType": "Tipo de Modelo", + "trainDate": "Data do Treinamento", + "baseModel": "Modelo Base", + "modelSelect": "Os seus modelos disponíveis no Frigate+ podem ser selecionados aqui. Note que apenas modelos compatíveis com a sua configuração atual de detector podem ser selecionados." + }, + "snapshotConfig": { + "title": "Configuração de Captura de Imagem", + "desc": "Envios ao Frigate+ requerem tanto a captura de imagem normais quanto a captura de imagem clean_copy estarem habilitadas na sua configuração.", + "documentation": "Leia a documentação", + "cleanCopyWarning": "Algumas câmeras possuem captura de imagem habilitada porém têm a cópia limpa desabilitada. Você precisa habilitar a clean_copy nas suas configurações de captura de imagem para poder submeter imagems dessa câmera ao Frigate+.", + "table": { + "camera": "Câmera", + "snapshots": "Capturas de Imagem", + "cleanCopySnapshots": "Capturas de Imagem clean_copy" + } + }, + "unsavedChanges": "Alterações de configurações do Frigate+ não salvas", + "restart_required": "Reinicialização necessária (modelo do Frigate+ foi alterado)", + "toast": { + "success": "As configurações do Frigate+ foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar as alterações de configuração: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Gatilhos", + "management": { + "title": "Gerenciamento de Gatilhos", + "desc": "Gerenciar gatilhos para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes para os seus objetos rastreados selecionados, e o tipo de descrição para acionar descrições semelhantes para textos que você especifica." + }, + "addTrigger": "Adicionar Gatilho", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limiar", + "actions": "Ações", + "noTriggers": "Nenhum gatilho configurado para essa câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Gatilho", + "lastTriggered": "Acionado pela última vez" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Gatilho", + "desc": "Criar gatilho para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Gatilho", + "desc": "Editar as configurações de gatilho na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Gatilho", + "desc": "Tem certeza que quer deletar o gatilho {{triggerName}}? Essa ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Digite o nome do gatilho", + "error": { + "minLength": "O nome precisa ter no mínimo 2 caracteres.", + "invalidCharacters": "O nome pode contar apenas letras, números, sublinhados, e hífens.", + "alreadyExists": "Um gatilho com esse nome já existe para essa câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar esse gatilho" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecionar o tipo de gatilho" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecionar uma imagem", + "textPlaceholder": "Digitar conteúdo do texto", + "imageDesc": "Selecionar uma imagem para acionar essa ação quando uma imagem semelhante for detectada.", + "textDesc": "Digite o texto para ativar essa ação quando uma descrição semelhante de objeto rastreado for detectada.", + "error": { + "required": "Um conteúdo é requerido." + } + }, + "threshold": { + "title": "Limiar", + "error": { + "min": "O limitar deve ser no mínimo 0", + "max": "O limiar deve ser no mínimo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate dispara uma mensagem MQTT para todos os gatilhos. Escolha uma ação adicional para realizar quando uma ação for disparada.", + "error": { + "min": "Ao menos uma ação deve ser selecionada." + } + }, + "friendly_name": { + "title": "Nome Amigável", + "placeholder": "Nomeie ou descreva esse gatilho", + "description": "Um nome amigável ou descritivo opcional para esse gatilho." + } + } + }, + "toast": { + "success": { + "createTrigger": "Gatilho {{name}} criado com sucesso.", + "updateTrigger": "Gatilho {{name}} atualizado com sucesso.", + "deleteTrigger": "Gatilho {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar gatilho: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar gatilho: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar gatilho: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Busca Semântica desativada", + "desc": "Busca Semântica deve estar habilitada para usar os Gatilhos." + } + }, + "roles": { + "management": { + "title": "Gerenciamento do Papel de Visualizador", + "desc": "Gerenciar papéis de visualizador customizados e suas permissões de acesso para essa instância do Frigate." + }, + "addRole": "Adicionar Papel", + "table": { + "role": "Papel", + "cameras": "Câmeras", + "actions": "Ações", + "noRoles": "Nenhum papel customizado encontrado.", + "editCameras": "Editar Câmeras", + "deleteRole": "Apagar Papel" + }, + "toast": { + "success": { + "createRole": "Papel {{role}} criado com sucesso", + "updateCameras": "Câmeras atualizados para o papel {{role}}", + "deleteRole": "Papel {{role}} apagado com sucesso", + "userRolesUpdated_one": "{{count}} usuário(os) atribuídos a esse papel foram atualizados para 'visualizador', que possui acesso a todas as câmeras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Falha ao criar papel: {{errorMessage}}", + "updateCamerasFailed": "Falha ao atualizar câmeras: {{errorMessage}}", + "deleteRoleFailed": "Falha ao apagar papel: {{errorMessage}}", + "userUpdateFailed": "Falha ao atualizar papel do usuário: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Criar Novo Papel", + "desc": "Adicionar um novo papel e especificar permissões de acesso." + }, + "editCameras": { + "title": "Editar Câmeras de Papéis", + "desc": "Atualizar acesso da câmera para o papel {{role}}." + }, + "deleteRole": { + "title": "Deletar Papel", + "desc": "Essa ação não pode ser desfeita. Isso irá apagar permanentemente o papel e atribuir a quaisquer usuários com esse papel como 'visualizador', o que dará acesso de visualização para todas as câmeras.", + "warn": "Tem certeza que quer apagar {{role}}?", + "deleting": "Apagando…" + }, + "form": { + "role": { + "title": "Nome do Papel", + "placeholder": "Digitar nome do papel", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "roleIsRequired": "Nome para o papel é requerido", + "roleOnlyInclude": "O nome do papel pode conter apenas letras, números, pontos ou sublinhados", + "roleExists": "Um papel com esse nome já existe." + }, + "cameras": { + "title": "Câmeras", + "desc": "Selecione as câmeras que esse papel terá acesso. Ao menos uma câmera é requerida.", + "required": "Ao menos uma câmera deve ser selecionada." + } + } + } + }, + "cameraWizard": { + "title": "Adicionar Câmera", + "description": "Siga os passos abaixo para adicionar uma câmera nova no seu Frigate.", + "steps": { + "nameAndConnection": "Nome e Conexão", + "streamConfiguration": "Configuração de Stream", + "validationAndTesting": "Validação e Teste" + }, + "save": { + "success": "Nova câmera {{cameraName}} salva com sucesso.", + "failure": "Erro ao salvar {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolução", + "video": "Vídeo", + "audio": "Áudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Favor fornecer uma URL de stream válida", + "testFailed": "Teste de stream falhou: {{error}}" + }, + "step1": { + "description": "Adicione os detalhes da sua câmera e teste a conexão.", + "cameraName": "Nome da Câmera", + "cameraNamePlaceholder": "ex., porta_entrada ou Visão Geral do Quintal", + "host": "Host/Endereço IP", + "port": "Porta", + "username": "Nome de Usuário", + "usernamePlaceholder": "Opcional", + "password": "Senha", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecionar protocolo de transporte", + "cameraBrand": "Marca da Câmera", + "selectBrand": "Selecione a marca da câmera para template de URL", + "customUrl": "URL Customizada de Stream", + "brandInformation": "Informação da marca", + "brandUrlFormat": "Para câmeras com o formato de URL RTSP como: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomedeusuario:senha@host:porta/caminho", + "testConnection": "Testar Conexão", + "testSuccess": "Teste de conexão bem sucedido!", + "testFailed": "Teste de conexão falhou. Favor verifique os dados e tente novamente.", + "streamDetails": "Detalhes do Stream", + "warnings": { + "noSnapshot": "Não foi possível adquirir uma captura de imagem do stream configurado." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecione a marca da câmera com o host/IP or selecione 'Outro' com uma URL customizada", + "nameRequired": "Nome para a câmera requerido", + "nameLength": "O nome da câmera deve ter 64 caracteres ou menos" + }, + "testing": { + "probingMetadata": "Inferindo o metadata da câmera...", + "fetchingSnapshot": "Buscando a captura de imagem da câmera..." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/system.json new file mode 100644 index 0000000..4875d80 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt-BR/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "Estatísticas das Câmeras - Frigate", + "storage": "Status de Armazenamento - Frigate", + "general": "Estatísticas Gerais - Frigate", + "enrichments": "Estatísticas de Enriquecimento - Frigate", + "logs": { + "frigate": "Registros Frigate - Frigate", + "go2rtc": "Registros GoRTC - Frigate", + "nginx": "Registros Nginx - Frigate" + } + }, + "title": "Sistema", + "metrics": "Métricas do sistema", + "logs": { + "download": { + "label": "Baixar registros" + }, + "copy": { + "label": "Copiar para a área de transferência", + "success": "Registros copiados para a área de transferência", + "error": "Não foi possível copiar os registros para a área de transferência" + }, + "type": { + "label": "Tipo", + "timestamp": "Marca temporal", + "tag": "Marcador", + "message": "Mensagem" + }, + "tips": "Os Registros estão sendo transmitidos do servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Erro ao buscar registros: {{errorMessage}}", + "whileStreamingLogs": "Erro ao transmitir registros: {{errorMessage}}" + } + } + }, + "general": { + "title": "Geral", + "detector": { + "title": "Detectores", + "inferenceSpeed": "Velocidade de Inferência do Detector", + "temperature": "Detector Temperatura", + "cpuUsage": "Utilização de CPU de Detecção", + "memoryUsage": "Utilização de Memória do Detector", + "cpuUsageInformation": "CPU utilizado para preparar os dados de entrada e saída de/para os modelos de detecção. Esse valor não mede a utilização da inferência, mesmo se estiver usando um GPU ou acelerador." + }, + "hardwareInfo": { + "title": "Informações de Hardware", + "gpuUsage": "Utilização de GPU", + "gpuMemory": "Memória da GPU", + "gpuEncoder": "Codificador da GPU", + "gpuDecoder": "Decodificador da GPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Saída do Vainfo", + "returnCode": "Código de Retorno: {{code}}", + "processOutput": "Saída do Processo:", + "processError": "Erro de Processo:" + }, + "nvidiaSMIOutput": { + "title": "Saída SMI da Nvidia", + "name": "Nome: {{name}}", + "driver": "Motorista: {{driver}}", + "cudaComputerCapability": "Capacidade de Computação CUDA: {{cuda_compute}}", + "vbios": "Informação de VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Fechar informações da GPU" + }, + "copyInfo": { + "label": "Copiar informações da GPU" + }, + "toast": { + "success": "Informações da GPU copiadas para a área de transferência" + } + }, + "npuUsage": "Uso da NPU", + "npuMemory": "Memória da NPU" + }, + "otherProcesses": { + "title": "Outros processos", + "processCpuUsage": "Uso de Processamento da CPU", + "processMemoryUsage": "Uso de Memória de Processos" + } + }, + "storage": { + "title": "Armazenamento", + "overview": "Visão Geral", + "recordings": { + "title": "Gravações", + "earliestRecording": "Gravação mais recente disponível:", + "tips": "Esse valor representa o armazenamento total usado pelas gravações no banco de dados do Frigate. O Frigate não rastreia o uso de armazenamento para todos os arquivos do seu disco." + }, + "cameraStorage": { + "title": "Armazenamento da Câmera", + "camera": "Câmera", + "unusedStorageInformation": "Informação de Armazenamento Não Utilizado", + "storageUsed": "Armazenamento", + "percentageOfTotalUsed": "Porcentagem do Total", + "bandwidth": "Largura de Banda", + "unused": { + "title": "Não Utilizado", + "tips": "Esse valor por não representar com precisão o espaço livre disponí®el para o Frigate se você possui outros arquivos armazenados no seu drive além das gravações do Frigate. O Frigate não rastreia a utilização do armazenamento além de suas próprias gravações." + } + }, + "shm": { + "title": "Alocação de memória compartilhada (SHM)", + "warning": "O tamanho de {{total}}MB de memória compartilhada (SHM) é insuficiente. Aumente para ao menos {{min_shm}}MB." + } + }, + "cameras": { + "title": "Câmeras", + "overview": "Visão Geral", + "info": { + "aspectRatio": "proporção", + "cameraProbeInfo": "{{camera}} Informação de Probe da Câmera", + "streamDataFromFFPROBE": "Os dados da tranmissão são obtidos com o ffprobe.", + "fetching": "Buscando Dados da Câmera", + "stream": "Transmissão {{idx}}", + "video": "Vídeo:", + "codec": "Codec:", + "resolution": "Resolução:", + "fps": "FPS:", + "unknown": "Desconhecido", + "audio": "Áudio:", + "error": "Erro: {{error}}", + "tips": { + "title": "Informação de Probe de Câmera" + } + }, + "framesAndDetections": "Quadros / Detecções", + "label": { + "camera": "câmera", + "detect": "detectar", + "skipped": "ignoradas", + "ffmpeg": "FFmpeg", + "capture": "captura", + "overallFramesPerSecond": "quadros por segundo em geral", + "overallDetectionsPerSecond": "detecções por segundo em geral", + "overallSkippedDetectionsPerSecond": "detecções puladas por segundo em geral", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} captura", + "cameraDetect": "{{camName}} detectar", + "cameraFramesPerSecond": "{{camName}} quadros por segundo", + "cameraDetectionsPerSecond": "{{camName}} detecções por segundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} detecções puladas por segundo" + }, + "toast": { + "success": { + "copyToClipboard": "Dados do probe copiados para a área de transferência." + }, + "error": { + "unableToProbeCamera": "Não foi possível fazer o probe da câmera: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Atualizado pela última vez: ", + "stats": { + "detectIsVerySlow": "{{detect}} está muito lento ({{speed}} ms)", + "ffmpegHighCpuUsage": "{{camera}} possui alta utilização de CPU para FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} possui alta utilização de CPU para detecção ({{detectAvg}}%)", + "healthy": "O sistema está saudável", + "cameraIsOffline": "{{camera}} está offline", + "reindexingEmbeddings": "Reindexando os embeddings ({{processed}}% completado)", + "detectIsSlow": "{{detect}} está lento ({{speed}} ms)", + "shmTooLow": "A alocação ({{total}} MB) para a pasta /dev/shm deve ser aumentada para ao menos {{min}} MB." + }, + "enrichments": { + "title": "Enriquecimentos", + "infPerSecond": "Inferências por Segundo", + "embeddings": { + "face_recognition": "Reconhecimento Facial", + "plate_recognition": "Reconhecimento de Placa", + "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", + "text_embedding_speed": "Velocidade de Embeddings de Texto", + "yolov9_plate_detection_speed": "Velocidade de Reconhecimento de Placas do YOLOv9", + "yolov9_plate_detection": "Detecção de Placas do YOLOv9", + "image_embedding": "Embeddings de Imagens", + "text_embedding": "Embeddings de Texto", + "image_embedding_speed": "Velocidade de Embeddings de Imagens", + "face_embedding_speed": "Velocidade de Embedding de Rostos", + "face_recognition_speed": "Velocidade de Reconhecimento de Rostos" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/audio.json b/sam2-cpu/frigate-dev/web/public/locales/pt/audio.json new file mode 100644 index 0000000..3bf1ba6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/audio.json @@ -0,0 +1,429 @@ +{ + "babbling": "Falador", + "speech": "Discurso", + "whoop": "Grito de Alegria", + "bellow": "Debaixo", + "yell": "Gritar", + "whispering": "Sussurrar", + "child_singing": "Criança a Cantar", + "crying": "Choro", + "singing": "Canto", + "laughter": "Rir", + "breathing": "Respirar", + "applause": "Aplausos", + "meow": "Miau", + "run": "Correr", + "sheep": "Ovelha", + "motorcycle": "Mota", + "car": "Carro", + "cat": "Gato", + "horse": "Cavalo", + "bus": "Autocarro", + "boat": "Barco", + "bicycle": "Bicicleta", + "skateboard": "Skate", + "door": "Porta", + "bird": "Pássaro", + "train": "Comboio", + "dog": "Cão", + "mantra": "Mantra", + "humming": "Cantarolar", + "sigh": "Suspiro", + "grunt": "Grunhido", + "whistling": "Assobiar", + "wheeze": "Chiadeira", + "gasp": "Ofegar", + "cough": "Tossir", + "sneeze": "Espirrar", + "footsteps": "Passos", + "chewing": "Mastigar", + "biting": "Morder", + "gargling": "Gargarejar", + "stomach_rumble": "Ronco de Estômago", + "burping": "Arroto", + "hiccup": "Soluço", + "fart": "Pum", + "hands": "Mãos", + "finger_snapping": "Estalar os Dedos", + "clapping": "Palmas", + "heartbeat": "Batimento Cardíaco", + "heart_murmur": "Sopro Cardíaco", + "cheering": "Aplausos Entusiásticos", + "chatter": "Conversar", + "crowd": "Multidão", + "snoring": "Ressonar", + "choir": "Coro", + "yodeling": "Iodel", + "chant": "Cântico", + "synthetic_singing": "Canto Sintético", + "rapping": "Rap", + "groan": "Gemido", + "snicker": "Risinho", + "animal": "Animal", + "pets": "Animais de Estimação", + "bark": "Latido", + "howl": "Uivar", + "bow_wow": "Au-Au", + "growling": "Rosnar", + "whimper_dog": "Choro de Cão", + "pig": "Porco", + "goat": "Cabra", + "fowl": "Aves de Capoeira", + "chicken": "Galinha", + "turkey": "Peru", + "duck": "Pato", + "quack": "Quá-Quá", + "goose": "Ganso", + "wild_animals": "Animais Selvagens", + "pigeon": "Pombo", + "dogs": "Cães", + "insect": "Inseto", + "cricket": "Grilo", + "mosquito": "Mosquito", + "fly": "Mosca", + "frog": "Rã", + "snake": "Cobra", + "rattle": "Chocalhar", + "music": "Música", + "musical_instrument": "Instrumento Musical", + "banjo": "Banjo", + "keyboard": "Teclado", + "piano": "Piano", + "organ": "Órgão", + "synthesizer": "Sintetizador", + "tambourine": "Pandeireta", + "clarinet": "Clarinete", + "harp": "Harpa", + "psychedelic_rock": "Rock Psicadélico", + "waterfall": "Cascata", + "ocean": "Oceano", + "fire": "Fogo", + "ship": "Navio", + "car_alarm": "Alarme de Carro", + "race_car": "Carro de Corridas", + "truck": "Camião", + "ice_cream_truck": "Carrinha de Gelados", + "emergency_vehicle": "Veículo de Emergência", + "police_car": "Carro da Polícia", + "ambulance": "Ambulância", + "helicopter": "Helicóptero", + "engine": "Motor", + "coin": "Moeda", + "scissors": "Tesouras", + "electric_shaver": "Barbeador Elétrico", + "computer_keyboard": "Teclado de Computador", + "alarm": "Alarme", + "telephone": "Telefone", + "siren": "Sirene", + "smoke_detector": "Detetor de Fumo", + "fire_alarm": "Alarme de Incêndio", + "whistle": "Assobio", + "clock": "Relógio", + "tools": "Ferramentas", + "camera": "Câmara", + "chink": "Estilhaçar", + "sound_effect": "Efeito Sonoro", + "static": "Estática", + "pink_noise": "Ruído Rosa", + "television": "Televisão", + "scream": "Grito Agudo", + "glass": "Vidro", + "wood": "Madeira", + "crack": "Rachar", + "silence": "Silêncio", + "steam": "Vapor", + "progressive_rock": "Rock Progressivo", + "white_noise": "Ruído Branco", + "maraca": "Maraca", + "percussion": "Percussão", + "rats": "Ratos", + "oink": "Oinc", + "waves": "Ondas", + "shatter": "Quebrar", + "radio": "Rádio", + "splinter": "Lasca", + "owl": "Coruja", + "mouse": "Rato", + "vehicle": "Veículo", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "sink": "Banca", + "blender": "Liquidificador", + "pant": "Ofegar", + "snort": "Resfolegar", + "throat_clearing": "Limpar a Garganta", + "sniff": "Cheirar", + "shuffle": "Embaralhar", + "children_playing": "Crianças a Brincar", + "purr": "Ronronar", + "livestock": "Gado", + "cattle": "Gado Bovino", + "cock_a_doodle_doo": "Cucurucu", + "coo": "Arrulhar", + "flapping_wings": "Bater de Asas", + "crow": "Corvo", + "hoot": "Piar", + "mandolin": "Mandolim", + "whale_vocalization": "Vocalização de Baleia", + "sitar": "Sitar", + "plucked_string_instrument": "Instrumento de Cordas Dedilhado", + "croak": "Coaxar", + "guitar": "Guitarra", + "electric_guitar": "Guitarra Elétrica", + "bass_guitar": "Baixo Elétrico", + "acoustic_guitar": "Guitarra Acústica", + "ukulele": "Ukulele", + "tapping": "Tocar com os Dedos", + "strum": "Dedilhar", + "drum_kit": "Bateria (Kit)", + "gong": "Gongo", + "orchestra": "Orquestra", + "flute": "Flauta", + "saxophone": "Saxofone", + "harmonica": "Harmónica", + "wind_instrument": "Instrumento de Sopro", + "trumpet": "Trompete", + "violin": "Violino", + "cello": "Violoncelo", + "double_bass": "Contrabaixo", + "church_bell": "Sino de Igreja", + "bicycle_bell": "Sino de Bicicleta", + "bagpipes": "Gaita de Foles", + "cowbell": "Sino de Vaca", + "hiss": "Sibilar", + "caterwaul": "Miado Forte", + "clip_clop": "Cavalgar", + "neigh": "Relincho", + "moo": "Mugido", + "gobble": "Gluglu (Peru)", + "cluck": "Cacarejar", + "caw": "Grasnido", + "chirp": "Piar (Passarinho)", + "yip": "Latido Agudo", + "bleat": "Balar", + "honk": "Buzina", + "roaring_cats": "Gatos a Ruge", + "roar": "Rugido", + "squawk": "Chilrear Estridente", + "patter": "Tamborilar", + "buzz": "Zumbido", + "steel_guitar": "Guitarra Steel", + "zither": "Cítara", + "electric_piano": "Piano Elétrico", + "electronic_organ": "Órgão Eletrónico", + "hammond_organ": "Órgão Hammond", + "sampler": "Sampler", + "harpsichord": "Cravo (Instrumento)", + "drum_machine": "Máquina de Ritmos", + "drum": "Tambor", + "snare_drum": "Tarola", + "rimshot": "Tocada de Caixa (Rimshot)", + "drum_roll": "Rufar de Tambor", + "bass_drum": "Bombo", + "timpani": "Tímpano", + "tabla": "Tabla", + "cymbal": "Prato de Bateria", + "hi_hat": "Prato Hi-Hat", + "wood_block": "Bloco de Madeira", + "tubular_bells": "Sinos Tubulares", + "mallet_percussion": "Percussão com Baquetas", + "glockenspiel": "Glockenspiel", + "electronic_dance_music": "Música de Dança Eletrónica", + "ambient_music": "Música Ambiente", + "trance_music": "Música Trance", + "music_of_latin_america": "Música da América Latina", + "salsa_music": "Música Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Música para Crianças", + "new-age_music": "Música New Age", + "vocal_music": "Música Vocal", + "a_capella": "A Capella", + "music_of_africa": "Música de África", + "lullaby": "Canção de Embalar", + "video_game_music": "Música de Videogame", + "christmas_music": "Música de Natal", + "dance_music": "Música de Dança", + "wedding_music": "Música de Casamento", + "happy_music": "Música Alegre", + "sad_music": "Música Triste", + "tender_music": "Música Suave", + "exciting_music": "Música Excitante", + "scary_music": "Música Assustadora", + "wind": "Vento", + "rustling_leaves": "Folhas a Mexer", + "wind_noise": "Ruído do Vento", + "thunderstorm": "Trovoada", + "thunder": "Trovão", + "water": "Água", + "rain": "Chuva", + "raindrop": "Gota de Chuva", + "rain_on_surface": "Chuva a Cair na Superfície", + "stream": "Torrente", + "gurgling": "Gorgolejar", + "train_whistle": "Apito de Comboio", + "train_horn": "Buzina de Comboio", + "railroad_car": "Carruagem Ferroviária", + "train_wheels_squealing": "Rodas de Comboio a Ranger", + "subway": "Metro", + "aircraft": "Aeronave", + "aircraft_engine": "Motor de Aeronave", + "jet_engine": "Motor a Jato", + "propeller": "Hélice", + "jackhammer": "Britadeira", + "sawing": "Serrar", + "filing": "Lixar", + "power_tool": "Ferramenta Elétrica", + "sanding": "Lixar Madeira", + "drill": "Berbequim", + "explosion": "Explosão", + "gunshot": "Disparo de Arma", + "machine_gun": "Metralhadora", + "fusillade": "Rajada de Disparos", + "artillery_fire": "Fogo de Artilharia", + "cap_gun": "Pistola de Brincar", + "fireworks": "Fogo de Artifício", + "firecracker": "Bombinha", + "burst": "Ruptura", + "typing": "Digitar", + "angry_music": "Música Zangada", + "typewriter": "Máquina de Escrever", + "marimba": "Marimba", + "vibraphone": "Vibrafone", + "steelpan": "Tambor de aço", + "brass_instrument": "Instrumento de Metal", + "french_horn": "Trompa", + "trombone": "Trombone", + "heavy_metal": "Heavy Metal", + "bowed_string_instrument": "Instrumento de Cordas com Arco", + "string_section": "Secção de Cordas", + "bell": "Sino", + "jingle_bell": "Sino de Natal", + "tuning_fork": "Diapasão", + "wind_chime": "Sino de Vento", + "pizzicato": "Pizzicato", + "chime": "Carrilhão", + "accordion": "Acordeão", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Tigela Sonora", + "scratching": "Raspar", + "hip_hop_music": "Música Hip-Hop", + "beatboxing": "Beatbox", + "rock_music": "Música Rock", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "rock_and_roll": "Rock and Roll", + "rhythm_and_blues": "Rhythm and Blues", + "pop_music": "Música Pop", + "soul_music": "Música Soul", + "reggae": "Reggae", + "country": "Country", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Música Folclórica", + "swing_music": "Música Swing", + "middle_eastern_music": "Música do Médio Oriente", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Música Clássica", + "opera": "Ópera", + "electronic_music": "Música Eletrónica", + "house_music": "Música House", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Eletrónica", + "afrobeat": "Afrobeat", + "christian_music": "Música Cristã", + "gospel_music": "Música Gospel", + "music_of_asia": "Música da Ásia", + "carnatic_music": "Música Carnática", + "music_of_bollywood": "Música de Bollywood", + "ska": "Ska", + "traditional_music": "Música Tradicional", + "independent_music": "Música Independente", + "song": "Canção", + "background_music": "Música de Fundo", + "theme_music": "Música de Tema", + "jingle": "Tilintar", + "soundtrack_music": "Música de Banda Sonora", + "air_brake": "Travão de Ar", + "air_horn": "Buzina de Ar", + "reversing_beeps": "Bip de Marcha-Atrás", + "crackle": "Estalidos", + "traffic_noise": "Ruído de Trânsito", + "power_windows": "Janelas Elétricas", + "skidding": "Derrapar", + "tire_squeal": "Guinada de Pneus", + "car_passing_by": "Carro a Passar", + "sailboat": "Veleiro", + "rowboat": "Barco a Remos", + "motorboat": "Barco a Motor", + "motor_vehicle": "Veículo Motorizado", + "fire_engine": "Carro dos Bombeiros", + "toot": "Buzina Curta", + "rail_transport": "Transporte Ferroviário", + "accelerating": "Aceleração", + "dental_drill's_drill": "Broca Dentária", + "lawn_mower": "Corta-relva", + "chainsaw": "Motosserra", + "medium_engine": "Motor Médio", + "engine_knocking": "Batidas do Motor", + "engine_starting": "Partida de Motor", + "idling": "Ao Ralenti", + "slam": "Bater Forte", + "light_engine": "Motor Leve", + "sliding_door": "Porta de Correr", + "knock": "Tocar à Porta", + "fixed-wing_aircraft": "Aeronave de Asa Fixa", + "doorbell": "Campainha de Porta", + "ding-dong": "Ding-Dong", + "heavy_engine": "Motor Pesado", + "squeak": "Ranger", + "cupboard_open_or_close": "Armário a Abrir ou Fechar", + "drawer_open_or_close": "Gaveta a Abrir ou Fechar", + "tap": "Toque", + "dishes": "Pratos", + "cutlery": "Talheres", + "chopping": "Cortar", + "frying": "Fritar", + "microwave_oven": "Forno Micro-ondas", + "water_tap": "Torneira", + "bathtub": "Banheira", + "keys_jangling": "Chaves a Tilintar", + "vacuum_cleaner": "Aspirador", + "zipper": "Fecho Éclair", + "shuffling_cards": "Embaralhar Cartas", + "toilet_flush": "Descarga de Sanita", + "electric_toothbrush": "Escova de Dentes Elétrica", + "writing": "Escrever", + "telephone_bell_ringing": "Campainha de Telefone", + "ringtone": "Toque de Telemóvel", + "telephone_dialing": "Discar Telefone", + "dial_tone": "Tom de Marcações", + "busy_signal": "Sinal de Ocupado", + "steam_whistle": "Apito a Vapor", + "mechanisms": "Mecanismos", + "ratchet": "Catraca", + "civil_defense_siren": "Sirene de Defesa Civil", + "buzzer": "Campainha", + "foghorn": "Buzina de Nevoeiro", + "alarm_clock": "Despertador", + "gears": "Engrenagens", + "pulleys": "Polias", + "sewing_machine": "Máquina de Costura", + "mechanical_fan": "Ventoinha Mecânica", + "air_conditioning": "Ar Condicionado", + "cash_register": "Caixa Registadora", + "printer": "Impressora", + "tick": "Tique-taque", + "tick-tock": "Tique-Taque", + "single-lens_reflex_camera": "Câmara Reflex de Lente Única", + "hammer": "Martelo", + "boom": "Estrondo", + "chop": "Corte", + "eruption": "Erupção", + "environmental_noise": "Ruído Ambiental", + "field_recording": "Gravação de Campo" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/common.json b/sam2-cpu/frigate-dev/web/public/locales/pt/common.json new file mode 100644 index 0000000..97d5438 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/common.json @@ -0,0 +1,284 @@ +{ + "time": { + "last30": "Últimos 30 dias", + "12hours": "12 horas", + "justNow": "Agora mesmo", + "yesterday": "Ontem", + "today": "Hoje", + "last7": "Últimos 7 dias", + "last14": "Últimos 14 dias", + "thisWeek": "Esta Semana", + "lastWeek": "Semana Passada", + "5minutes": "5 minutos", + "10minutes": "10 minutos", + "30minutes": "30 minutos", + "24hours": "24 horas", + "pm": "pm", + "am": "am", + "year_one": "{{time}} ano", + "year_many": "{{time}} de anos", + "year_other": "{{time}} anos", + "month_one": "{{time}} mes", + "month_many": "{{time}} meses", + "month_other": "", + "day_one": "{{time}} dia", + "day_many": "{{time}} dias", + "day_other": "{{time}} dias", + "thisMonth": "Este Mês", + "lastMonth": "Mês Passado", + "1hour": "1 hora", + "hour_one": "{{time}} hora", + "hour_many": "{{time}} horas", + "hour_other": "{{time}} horas", + "minute_one": "{{time}} minuto", + "minute_many": "{{time}} minutos", + "minute_other": "{{time}} minutos", + "second_one": "{{time}} segundo", + "second_many": "{{time}} segundos", + "second_other": "{{time}} segundos", + "untilForTime": "Até {{time}}", + "untilForRestart": "Até que o Frigate reinicie.", + "untilRestart": "Até reiniciar", + "ago": "há {{timeAgo}}", + "d": "{{time}}d", + "h": "{{time}}h", + "m": "{{time}}m", + "s": "{{time}}s", + "yr": "{{time}}ano", + "mo": "{{time}}mês", + "formattedTimestamp": { + "12hour": "%b %-d, %I:%M:%S %p", + "24hour": "%b %-d, %H:%M:%S" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ss a", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%b %-d, %I:%M %p", + "24hour": "%b %-d, %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%b %-d %Y, %I:%M %p", + "24hour": "%b %-d %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + } + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mph" + }, + "length": { + "feet": "pés", + "meters": "metros" + } + }, + "button": { + "enabled": "Ativado", + "enable": "Ativar", + "done": "Feito", + "reset": "Reiniciar", + "disabled": "Desativado", + "saving": "A guardar…", + "apply": "Aplicar", + "disable": "Desativar", + "save": "Guardar", + "copy": "Copiar", + "cancel": "Cancelar", + "close": "Fechar", + "history": "Histórico", + "back": "Voltar", + "fullscreen": "Ecrã Completo", + "exitFullscreen": "Sair do Ecrã Completo", + "twoWayTalk": "Conversa Bidirecional", + "cameraAudio": "Áudio da Câmera", + "edit": "Editar", + "off": "DESLIGADO", + "copyCoordinates": "Copiar coordenadas", + "on": "LIGADO", + "delete": "Eliminar", + "download": "Transferir", + "info": "Informação", + "no": "Não", + "suspended": "Suspenso", + "yes": "Sim", + "unselect": "Desselecionar", + "unsuspended": "Dessuspender", + "deleteNow": "Eliminar Agora", + "export": "Exportar", + "next": "Seguinte", + "play": "Tocar", + "pictureInPicture": "Imagem sobre Imagem" + }, + "label": { + "back": "Voltar" + }, + "menu": { + "user": { + "logout": "Terminar sessão", + "account": "Conta", + "current": "Utilizador atual: {{user}}", + "setPassword": "Definir Palavra-passe", + "title": "Utilizador", + "anonymous": "anónimo" + }, + "faceLibrary": "Biblioteca de Rostos", + "withSystem": "Sistema", + "theme": { + "label": "Tema", + "blue": "Azul", + "green": "Verde", + "red": "Vermelho", + "contrast": "Alto contraste", + "default": "Predefinição", + "highcontrast": "Alto Contraste", + "nord": "Nord" + }, + "system": "Sistema", + "systemMetrics": "Métricas do sistema", + "configuration": "Configuração", + "systemLogs": "Registos do sistema", + "settings": "Definições", + "configurationEditor": "Editor de Configuração", + "languages": "Idiomas", + "language": { + "en": "Inglês (EUA)", + "zhCN": "Chinês (Chinês Simplificado)", + "withSystem": { + "label": "Utilizar as definições do sistema para o idioma" + }, + "fr": "Francês (França)", + "es": "Espanhol (Espanha)", + "ru": "Russo", + "de": "Alemão (Alemanha)", + "ja": "Japonês", + "yue": "Cantonês", + "ar": "Árabe", + "uk": "Ucraniano", + "el": "Grego", + "hi": "Híndi (Índia)", + "pt": "Português (Portugal)", + "tr": "Turco (Turquia)", + "it": "Italiano (Itália)", + "nb": "Norueguês Bokmål", + "ko": "Coreano", + "vi": "Vietnamita", + "nl": "Holandês (Holanda)", + "sv": "Sueco", + "cs": "Checo", + "fa": "Persa", + "pl": "Polaco", + "he": "Hebraico", + "fi": "Finlandês", + "da": "Dinamarquês", + "ro": "Romeno", + "hu": "Húngaro", + "sk": "Eslovaco", + "th": "Tailandês", + "ca": "Catalão", + "ptBR": "Português (Brazil)", + "sr": "Sérvio", + "sl": "Esloveno", + "lt": "Lituano", + "bg": "Búlgaro", + "gl": "Galego", + "id": "Indonésio Bahasa", + "ur": "Urdu" + }, + "appearance": "Aparência", + "darkMode": { + "label": "Modo Escuro", + "withSystem": { + "label": "Utilizar as definições do sistema para o modo claro ou escuro" + }, + "light": "Claro", + "dark": "Escuro" + }, + "help": "Ajuda", + "documentation": { + "title": "Documentação", + "label": "Documentação do Frigate" + }, + "restart": "Reiniciar Frigate", + "live": { + "title": "Ao vivo", + "allCameras": "Todas as Câmaras", + "cameras": { + "title": "Câmaras", + "count_one": "{{count}} Câmera", + "count_many": "{{count}} Câmeras", + "count_other": "{{count}} Câmeras" + } + }, + "export": "Exportar", + "explore": "Explorar", + "review": "Rever", + "uiPlayground": "Área de Testes da IU" + }, + "pagination": { + "previous": { + "label": "Ir para a página anterior", + "title": "Anterior" + }, + "label": "paginação", + "next": { + "title": "Seguinte", + "label": "Ir para a página seguinte" + }, + "more": "Mais páginas" + }, + "role": { + "admin": "Administrador", + "viewer": "Visualizador", + "title": "Função", + "desc": "Os administradores têm acesso completo a todas as funcionalidades da IU do Frigate. Os visualizadores estão limitados a visualizar as câmeras, rever itens, e o histórico de gravaçoes na IU." + }, + "toast": { + "copyUrlToClipboard": "URL copiado para a área de transferência.", + "save": { + "title": "Guardar", + "error": { + "noMessage": "Não foi possível guardar as alterações da configuração", + "title": "Não foi possível guardar as alterações da configuração: {{errorMessage}}" + } + } + }, + "accessDenied": { + "documentTitle": "Frigate - Acesso Negado", + "title": "Acesso Negado", + "desc": "Não tem permissão para ver esta página." + }, + "notFound": { + "documentTitle": "Frigate - Não Encontrado", + "desc": "Página não encontrada", + "title": "404" + }, + "selectItem": "Selecionar {{item}}", + "readTheDocumentation": "Leia a documentação" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/auth.json new file mode 100644 index 0000000..fc00399 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "Nome do utilizador", + "login": "Iniciar sessão", + "errors": { + "usernameRequired": "O nome do utilizador é obrigatório", + "passwordRequired": "A palavra-passe é obrigatória", + "rateLimit": "Limite de taxa excedido. Tente novamente mais tarde.", + "loginFailed": "Autenticação falhou", + "unknownError": "Erro desconhecido. Verifique os registos.", + "webUnknownError": "Erro desconhecido. Verifique os registos da consola." + }, + "password": "Palavra-passe" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/camera.json new file mode 100644 index 0000000..3f7052c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupos de Câmaras", + "add": "Adicionar Gupo de Câmaras", + "edit": "Editar Grupo de Câmaras", + "delete": { + "label": "Eliminar Grupo de Câmaras", + "confirm": { + "title": "Confirmar Eliminar", + "desc": "Tem a certeza que deseja eliminar o grupo de câmaras {{name}}?" + } + }, + "name": { + "label": "Nome", + "placeholder": "Inserir um nome…", + "errorMessage": { + "exists": "O nome do grupo de câmaras já existe.", + "nameMustNotPeriod": "O nome do grupo de câmaras não deve conter pontos.", + "mustLeastCharacters": "O nome do grupo de câmaras deve ter pelo menos 2 carateres.", + "invalid": "Nome do grupo de câmaras inválido." + } + }, + "cameras": { + "desc": "Selecione as câmaras para este grupo.", + "label": "Câmaras" + }, + "icon": "Ícone", + "success": "O grupo de câmaras ({{name}}) foi guardado.", + "camera": { + "setting": { + "audioIsAvailable": "O áudio está disponível para esta transmissão", + "audioIsUnavailable": "O áudio não está disponível para esta transmissão", + "audio": { + "tips": { + "document": "Leia a documentação ", + "title": "O áudio deve ser emitido pela sua câmara e configurado no go2rtc para esta transmissão." + } + }, + "streamMethod": { + "label": "Método de Transmissão", + "method": { + "smartStreaming": { + "label": "Transmissão Inteligente (recomendado)", + "desc": "A transmissão inteligente atualizará a imagem da sua câmara uma vez por minuto quando não ocorrer nenhuma atividade detetável para conservar largura de banda e recursos. Quando a atividade é detetada, a imagem muda perfeitamente para uma transmissão ao vivo." + }, + "continuousStreaming": { + "label": "Transmissão Contínua", + "desc": { + "warning": "A transmissão contínua pode causar a utilização alta da largura de banda e problemas de desempenho. Utilize com precaução.", + "title": "A imagem da câmara será sempre uma transmissão ao vivo quando visível no painel, mesmo que não esteja a ser detetada nenhuma atividade." + } + }, + "noStreaming": { + "label": "Sem transmissão", + "desc": "As imagens da câmara serão atualizadas apenas uma vez por minuto e não haverá transmissão ao vivo." + } + }, + "placeholder": "Escolha um método de transmissão" + }, + "compatibilityMode": { + "label": "Modo de compatibilidade", + "desc": "Ative esta opção apenas se a transmissão ao vivo da sua câmara estiver a exibir artefatos de cor e tiver uma linha diagonal no lado direito da imagem." + }, + "label": "Definições de Transmissão da Câmara", + "desc": "Altere as opções de transmissão ao vivo para o painel deste grupo de câmaras. Estas definições são específicas do dispositivo/navegador.", + "title": "{{cameraName}} Definições de Transmissão", + "placeholder": "Escolha uma transmissão", + "stream": "Transmissão" + }, + "birdseye": "Vista Aérea" + } + }, + "debug": { + "options": { + "label": "Definições", + "title": "Opções", + "hideOptions": "Ocultar Opções", + "showOptions": "Mostrar Opções" + }, + "boundingBox": "Caixa Delimitadora", + "timestamp": "Carimbo de hora", + "zones": "Zonas", + "mask": "Máscara", + "motion": "Movimento", + "regions": "Regiões" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/dialog.json new file mode 100644 index 0000000..b1aeb06 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/dialog.json @@ -0,0 +1,133 @@ +{ + "restart": { + "button": "Reiniciar", + "restarting": { + "title": "Frigate está a reiniciar", + "content": "Esta página será recarregada em {{countdown}} segundos.", + "button": "Forçar Recarregar Agora" + }, + "title": "Tem a certeza que deseja reiniciar o Frigate?" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Submeter para Frigate+", + "desc": "Os objetos nas localizações que quer evitar não são falsos positivos. Submete-los como falsos positivos confundirá o modelo." + }, + "review": { + "true": { + "label": "Confirme esta etiqueta para Frigate Plus", + "true_one": "Este é um {{label}}", + "true_many": "Estes são muitos {{label}}", + "true_other": "Estão são {{label}}" + }, + "state": { + "submitted": "Submetido" + }, + "false": { + "label": "Não confirmar esta etiqueta para Frigate Plus", + "false_one": "Este não é um {{label}}", + "false_many": "Estes não são muitos {{label}}", + "false_other": "Estes não são {{label}}" + }, + "question": { + "label": "Confirme esta etiqueta para Frigate Plus", + "ask_a": "Este objeto é um {{label}}?", + "ask_an": "Este objeto é um {{label}}?", + "ask_full": "Este objeto é um {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Ver no Histórico" + } + }, + "export": { + "time": { + "fromTimeline": "Selecione na linha do tempo", + "start": { + "title": "Hora de início", + "label": "Selecione a hora de início" + }, + "end": { + "title": "Hora de término", + "label": "Selecione a hora de término" + }, + "custom": "Personalizado", + "lastHour_one": "Última hora", + "lastHour_many": "Últimas {{count}} horas", + "lastHour_other": "Últimas {{count}} horas" + }, + "export": "Exportar", + "toast": { + "success": "Exportação iniciada com sucesso. Veja o ficheiro na pasta de exportações.", + "error": { + "failed": "Não foi possível iniciar a exportação: {{error}}", + "endTimeMustAfterStartTime": "O horário de término deve ser posterior ao horário de início", + "noVaildTimeSelected": "Nenhum intervalo de tempo válido selecionado" + } + }, + "selectOrExport": "Selecionar ou Exportar", + "fromTimeline": { + "saveExport": "Guardar Exportação", + "previewExport": "Pré-visualizar Exportação" + }, + "select": "Selecionar", + "name": { + "placeholder": "Nome da Exportação" + } + }, + "streaming": { + "showStats": { + "label": "Mostrar estatísticas de transmissão", + "desc": "Ative esta opção para mostrar as estatísticas de transmissão como uma sobreposição na feed da câmara." + }, + "restreaming": { + "desc": { + "title": "Configure go2rtc para obter opções adicionais da visualização ao vivo e o áudio para esta câmara.", + "readTheDocumentation": "Leia a documentação" + }, + "disabled": "A retransmissão não está ativada para esta câmara." + }, + "label": "Transmissão", + "debugView": "Ver Depuração" + }, + "search": { + "saveSearch": { + "label": "Guardar Procura", + "overwrite": "{{searchName}} já existe. Ao guardar irá substituir o valor existente.", + "success": "A procura ({{searchName}}) foi guardada.", + "button": { + "save": { + "label": "Guardar esta procura" + } + }, + "placeholder": "Insira um nome para a sua procura", + "desc": "Forneça um nome para esta procura guardada." + } + }, + "recording": { + "confirmDelete": { + "title": "Confirmar Eliminar", + "desc": { + "selected": "Tem a certeza que deseja eliminar todos os vídeos guardados associados com este item de análise?

    Pressione a tecla Shift para ignorar esta janela no futuro." + }, + "toast": { + "success": "As imagens de vídeo associadas com os itens de análise selecionados foram elimiandos com sucesso.", + "error": "Não foi possível eliminar: {{error}}" + } + }, + "button": { + "export": "Exportar", + "markAsReviewed": "Marcar como analisado", + "deleteNow": "Eliminar Agora" + } + }, + "imagePicker": { + "selectImage": "Selecione a miniatura de um objeto rastreado", + "search": { + "placeholder": "Pesquisar por etiqueta ou sub-etiqueta..." + }, + "noImages": "Nenhuma miniatura encontrada para esta câmera" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/filter.json new file mode 100644 index 0000000..3f7fce7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "label": "Etiquetas", + "all": { + "short": "Etiquetas", + "title": "Todas as etiquetas" + }, + "count": "{{count}} etiquetas", + "count_one": "{{count}} Etiqueta", + "count_other": "{{count}} Etiquetas" + }, + "filter": "Filtro", + "zones": { + "label": "Zonas", + "all": { + "title": "Todas as Zonas", + "short": "Zonas" + } + }, + "dates": { + "all": { + "title": "Todas as Datas", + "short": "Datas" + }, + "selectPreset": "Selecionar um Pré-ajuste…" + }, + "more": "Mais Filtros", + "reset": { + "label": "Redefinir filtros para valores padrão" + }, + "subLabels": { + "label": "Sub etiquetas", + "all": "Todas sub etiquetas" + }, + "score": "Pontuação", + "features": { + "label": "Funcionalidades", + "hasSnapshot": "Tem uma captura", + "hasVideoClip": "Tem um videoclipe", + "submittedToFrigatePlus": { + "label": "Submetido para Frigate+", + "tips": "Primeiro, deve filtrar os objetos rastreados que têm uma captura.

    Os objetos rastreados sem uma captura não podem ser submetidos para Frigate+." + } + }, + "sort": { + "label": "Ordenar", + "dateAsc": "Data (Ascendente)", + "scoreAsc": "Pontuação do Objeto (Ascendente)", + "scoreDesc": "Pontuação do Objeto (Descendente)", + "speedDesc": "Velocidade Estimada (Descendente)", + "speedAsc": "Velocidade Estimada (Ascendente)", + "dateDesc": "Data (Decrescente)", + "relevance": "Relevância" + }, + "cameras": { + "label": "Filtro de Câmaras", + "all": { + "short": "Câmaras", + "title": "Todas as Câmaras" + } + }, + "review": { + "showReviewed": "Mostrar analisados" + }, + "motion": { + "showMotionOnly": "Mostrar apenas movimento" + }, + "explore": { + "settings": { + "title": "Definições", + "defaultView": { + "title": "Visualização Predefinida", + "summary": "Resumo", + "unfilteredGrid": "Grelha não Filtrada", + "desc": "Quando não for selecionado nenhum filtro, exiba um resumo dos objetos rastreados mais recentes por etiqueta, ou exiba uma grelha não filtrada." + }, + "gridColumns": { + "title": "Colunas da Grelha", + "desc": "Selecione o número de colunas na visualização em grelha." + }, + "searchSource": { + "label": "Procurar Fonte", + "desc": "Escolha se deseja procurar nas miniaturas ou descrições dos seus objetos rastreados.", + "options": { + "thumbnailImage": "Imagem em Miniatura", + "description": "Descrição" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selecione uma data para filtrar" + } + } + }, + "logSettings": { + "label": "Nível de registo do filtro", + "loading": { + "title": "A carregar", + "desc": "Quando desliza até ao fundo no painel de registos, os novos registos são apresentados automaticamente à medida que são adicionados." + }, + "filterBySeverity": "Filtrar registos por gravidade", + "disableLogStreaming": "Desativar transmissão de registos", + "allLogs": "Todos os registos" + }, + "estimatedSpeed": "Velocidade estimada ({{unit}})", + "timeRange": "Intervalo de tempo", + "zoneMask": { + "filterBy": "Filtrar por máscara de zona" + }, + "trackedObjectDelete": { + "title": "Confirmar Eliminar", + "toast": { + "success": "Objetos rastreados eliminados com sucesso.", + "error": "Não foi possível eliminar os objetos rastreados: {{errorMessage}}" + }, + "desc": "Ao eliminar estes {{objectLength}} objetos rastreados remove a captura de imagem, quaisquer integrações guardadas, e todas as entradas associadas ao ciclo de vida do objeto. As gravações desses objetos rastreados na visualização do Histórico NÃO serão eliminadas.

    Tem a certeza que deseja continuar?

    Mantenha pressionada a tecla Shift para ignorar esta janela no futuro." + }, + "recognizedLicensePlates": { + "title": "Matrículas Reconhecidas", + "noLicensePlatesFound": "Não foram encontradas matrículas.", + "selectPlatesFromList": "Selecione uma ou mais matrículas da lista.", + "loadFailed": "Não foi possível carregar as matrículas reconhecidas.", + "loading": "A carregar as matrículas reconhecidas…", + "placeholder": "Digite para procurar matrículas…", + "selectAll": "Selecionar tudo", + "clearAll": "Limpar tudo" + }, + "classes": { + "label": "Classes", + "all": { + "title": "Todas as Classes" + }, + "count_one": "{{count}} Classe", + "count_other": "{{count}} Classes" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/icons.json new file mode 100644 index 0000000..71b767a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selecione um ícone", + "search": { + "placeholder": "Procurar por um ícone…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/input.json new file mode 100644 index 0000000..1324ed1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Transferir Vídeo", + "toast": { + "success": "O vídeo do seu item de análise começou a ser transferido." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/pt/components/player.json new file mode 100644 index 0000000..741d37e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Nenhuma pré-visualização encontrada", + "noPreviewFoundFor": "Nenhuma pré-visualização encontrada para {{cameraName}}", + "submitFrigatePlus": { + "title": "Submeter esta imagem para Frigate+?", + "submit": "Submeter" + }, + "streamOffline": { + "title": "Transmissão Off-line", + "desc": "Nenhum quadro foi recebido na transmissão de detecção {{cameraName}}, verifique os logs de erro" + }, + "cameraDisabled": "A câmara está desativada", + "stats": { + "streamType": { + "title": "Tipo de transmissão:", + "short": "Tipo" + }, + "bandwidth": { + "title": "Largura de banda:", + "short": "Largura de banda" + }, + "latency": { + "value": "{{seconds}} segundos", + "short": { + "title": "Latência", + "value": "{{seconds}} seg" + }, + "title": "Latência:" + }, + "totalFrames": "Total de quadros:", + "droppedFrames": { + "title": "Imagens perdidas:", + "short": { + "title": "Perdida", + "value": "{{droppedFrames}} imagens" + } + }, + "decodedFrames": "Quadros decodificados:", + "droppedFrameRate": "Taxa de imagem perdida:" + }, + "noRecordingsFoundForThisTime": "Nenhuma gravação encontrada para este momento", + "livePlayerRequiredIOSVersion": "É necessário o iOS 17.1 ou superior para este tipo de transmissão ao vivo.", + "toast": { + "success": { + "submittedFrigatePlus": "Imagem submetida com sucesso para Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Não foi possível submeter a imagem para Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/objects.json b/sam2-cpu/frigate-dev/web/public/locales/pt/objects.json new file mode 100644 index 0000000..88762a7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/objects.json @@ -0,0 +1,120 @@ +{ + "giraffe": "Girafa", + "cup": "Chávena", + "person": "Pessoa", + "stop_sign": "Sinal de Parar", + "sheep": "Ovelha", + "sandwich": "Sande", + "carrot": "Cenoura", + "dining_table": "Mesa de Jantar", + "motorcycle": "Mota", + "bicycle": "Bicicleta", + "street_sign": "Sinal de Rua", + "pizza": "Pizza", + "parking_meter": "Parcómetro", + "skateboard": "Skate", + "bottle": "Garrafa", + "car": "Carro", + "airplane": "Avião", + "bus": "Autocarro", + "train": "Comboio", + "boat": "Barco", + "traffic_light": "Semáforo", + "fire_hydrant": "Boca de Incêndio", + "bird": "Pássaro", + "cat": "Gato", + "bench": "Banco de Jardim", + "elephant": "Elefante", + "hat": "Chapéu", + "backpack": "Mochila", + "shoe": "Sapato", + "handbag": "Carteira", + "tie": "Gravata", + "suitcase": "Mala de Viagem", + "frisbee": "Disco de Frisbee", + "skis": "Esquis", + "kite": "Papagaio de Papel", + "baseball_bat": "Taco de Basebol", + "tennis_racket": "Raquete de Ténis", + "plate": "Prato", + "wine_glass": "Copo de Vinho", + "fork": "Garfo", + "spoon": "Colher", + "bowl": "Tijela", + "banana": "Banana", + "apple": "Maça", + "hot_dog": "Cachorro Quente", + "donut": "Donut", + "cake": "Bolo", + "chair": "Cadeira", + "potted_plant": "Planta em Vaso", + "mirror": "Espelho", + "desk": "Escrivaninha", + "toilet": "Casa de Banho", + "door": "Porta", + "baseball_glove": "Luva de Basebol", + "surfboard": "Prancha de Surf", + "broccoli": "Brócolos", + "snowboard": "Snowboard", + "dog": "Cão", + "bear": "Urso", + "eye_glasses": "Óculos", + "umbrella": "Guarda-chuva", + "horse": "Cavalo", + "bed": "Cama", + "cow": "Vaca", + "zebra": "Zebra", + "sports_ball": "Bola", + "knife": "Faca", + "orange": "Laranja", + "window": "Janela", + "clock": "Relógio", + "keyboard": "Teclado", + "animal": "Animal", + "bark": "Latido", + "goat": "Cabra", + "vehicle": "Veículo", + "scissors": "Tesouras", + "mouse": "Rato", + "teddy_bear": "Urso de Peluche", + "hair_dryer": "Secador de Cabelo", + "toothbrush": "Escova de Dentes", + "hair_brush": "Escova de Cabelo", + "squirrel": "Esquilo", + "couch": "Sofá", + "tv": "TV", + "laptop": "Portátil", + "remote": "Comando", + "cell_phone": "Telemóvel", + "microwave": "Microondas", + "oven": "Forno", + "toaster": "Torradeira", + "sink": "Banca", + "refrigerator": "Frigorífico", + "blender": "Liquidificador", + "book": "Livro", + "vase": "Vaso", + "deer": "Veado", + "fox": "Raposa", + "rabbit": "Coelho", + "raccoon": "Guaxinim", + "robot_lawnmower": "Robô de Cortar Relva", + "waste_bin": "Contentor do Lixo", + "on_demand": "On Demand", + "face": "Rosto", + "license_plate": "Matrícula", + "package": "Pacote", + "bbq_grill": "Churrasqueira", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/configEditor.json new file mode 100644 index 0000000..fb1d337 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Editor de Configuração", + "copyConfig": "Copiar Configuração", + "saveAndRestart": "Guardar e Reiniciar", + "saveOnly": "Guardar Apenas", + "toast": { + "success": { + "copyToClipboard": "Configuração copiada para a área de transferência." + }, + "error": { + "savingError": "Erro ao guardar a configuração" + } + }, + "documentTitle": "Frigate - Editor de Configuração", + "confirm": "Sair sem guardar?", + "safeConfigEditor": "Editor de Configuração (Modo de Segurança)", + "safeModeDescription": "O Frigate está no modo de segurança devido a um erro de validação da configuração." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/events.json new file mode 100644 index 0000000..bb9b2e0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/events.json @@ -0,0 +1,40 @@ +{ + "detections": "Deteções", + "motion": { + "label": "Movimento", + "only": "Apenas movimento" + }, + "allCameras": "Todas as Câmaras", + "empty": { + "motion": "Nenhum dado de movimento encontrado", + "alert": "Não há alertas para análise", + "detection": "Não há detecções para análise" + }, + "timeline": "Linha do tempo", + "events": { + "aria": "Selecionar eventos", + "label": "Eventos", + "noFoundForTimePeriod": "Nenhum evento encontrado para este período." + }, + "timeline.aria": "Selecione a linha do tempo", + "alerts": "Alertas", + "documentTitle": "Análise - Frigate", + "recordings": { + "documentTitle": "Frigate - Gravações" + }, + "calendarFilter": { + "last24Hours": "Últimas 24 horas" + }, + "markAsReviewed": "Marcar como analisado", + "markTheseItemsAsReviewed": "Marque esses itens como analisados", + "newReviewItems": { + "label": "Ver novos itens para analisar", + "button": "Novos itens para analisar" + }, + "camera": "Câmara", + "detected": "detetado", + "selected_one": "{{count}} selecionado", + "selected_other": "{{count}} selecionados", + "suspiciousActivity": "Atividade Suspeita", + "threateningActivity": "Atividade Ameaçadora" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/explore.json new file mode 100644 index 0000000..7215081 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/explore.json @@ -0,0 +1,228 @@ +{ + "generativeAI": "IA Generativa", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "A iniciar…", + "estimatedTime": "Tempo restante estimado:", + "finishingShortly": "Terminando em breve", + "step": { + "thumbnailsEmbedded": "Miniaturas incorporadas: ", + "descriptionsEmbedded": "Descrições incorporadas: ", + "trackedObjectsProcessed": "Objetos rastreados processados: " + }, + "context": "O explorar pode ser usado depois que as incorporações de objetos rastreados terminarem de ser reindexadas." + }, + "downloadingModels": { + "setup": { + "visionModel": "Modelo de visão", + "textModel": "Modelo de texto", + "textTokenizer": "Tokenizador de texto", + "visionModelFeatureExtractor": "Extrator de funcionalidade de modelo de visão" + }, + "context": "O Frigate está a transferir os modelos de incorporação necessários para suportar a funcionalidade de \"Procura Semântica\". Isto pode levar vários minutos, dependendo da velocidade da sua ligação de rede.", + "tips": { + "context": "Talvez queira reindexar as incorporações dos seus objetos rastreados depois de os modelos serem transferidos.", + "documentation": "Leia a documentação" + }, + "error": "Ocorreu um erro. Verifique os registos do Frigate." + }, + "title": "Explorar não está disponível" + }, + "details": { + "timestamp": "Carimbo de hora", + "item": { + "title": "Analisar detalhes do item", + "desc": "Analisar detalhes do item", + "tips": { + "hasMissingObjects": "Ajuste sua configuração se você quiser que o Frigate guarde os objetos rastreados para os seguintes rótulos: {{objects}}", + "mismatch_one": "{{count}} objeto indisponível foi detectado e incluído neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos.", + "mismatch_many": "{{count}} objetos indisponíveis foram detectados e incluídos neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos.", + "mismatch_other": "{{count}} objetos indisponíveis foram detectados e incluídos neste item de análise. Esses objetos não qualificaram como alerta ou detecção ou já foram limpos/excluídos." + }, + "toast": { + "success": { + "regenerate": "Uma nova descrição foi solicitada pelo {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para ser regenerada.", + "updatedSublabel": "Sub-rotulo atualizado com sucesso.", + "updatedLPR": "Matrícula atualizada com sucesso.", + "audioTranscription": "Transcrição de áudio solicitada com sucesso." + }, + "error": { + "regenerate": "Falha ao chamar {{provider}} para uma nova descrição: {{errorMessage}}", + "updatedSublabelFailed": "Falha ao atualizar o sub-rotulo: {{errorMessage}}", + "updatedLPRFailed": "Falha ao atualizar a matrícula: {{errorMessage}}", + "audioTranscription": "Falha ao solicitar transcrição de áudio: {{errorMessage}}" + } + }, + "button": { + "share": "Compartilhe este item para análise", + "viewInExplore": "Ver no Explorar" + } + }, + "zones": "Zonas", + "description": { + "label": "Descrição", + "aiTips": "O Frigate não solicitará uma descrição do seu fornecedor de IA Generativa até que o ciclo de vida do objeto rastreado tenha terminado.", + "placeholder": "Descrição do objeto rastreado" + }, + "camera": "Câmera", + "snapshotScore": { + "label": "Pontuação da captura" + }, + "topScore": { + "label": "Maior pontuação", + "info": "A maior pontuação é a maior pontuação mediana para o objeto rastreado, portanto, isso pode diferir da pontuação exibida na miniatura do resultado da pesquisa." + }, + "button": { + "findSimilar": "Encontrar similar", + "regenerate": { + "title": "Regenerar", + "label": "Regenerar descrição do objeto rastreado" + } + }, + "label": "Rótulo", + "editSubLabel": { + "title": "Editar sub-rotulo", + "desc": "Digite um novo sub-rotulo para este {{label}}", + "descNoLabel": "Digite um novo sub-rotulo para este objeto rastreado" + }, + "editLPR": { + "title": "Editar matrícula", + "desc": "Digite um novo valor da matrícula para este {{label}}", + "descNoLabel": "Digite um novo valor da matrícula para este objeto rastreado" + }, + "recognizedLicensePlate": "Matrícula reconhecida", + "estimatedSpeed": "Velocidade estimada", + "objects": "Objetos", + "expandRegenerationMenu": "Expandir menu de regeneração", + "regenerateFromSnapshot": "Regenerar a partir da captura", + "regenerateFromThumbnails": "Regenerar a partir das miniaturas", + "tips": { + "descriptionSaved": "Descrição salva com sucesso", + "saveDescriptionFailed": "Falha ao atualizar a descrição: {{errorMessage}}" + }, + "score": { + "label": "Classificação" + } + }, + "documentTitle": "Frigate - Explorar", + "trackedObjectDetails": "Detalhes do objeto rastreado", + "type": { + "details": "detalhes", + "video": "vídeo", + "object_lifecycle": "ciclo de vida do objeto", + "snapshot": "captura de ecrã" + }, + "objectLifecycle": { + "title": "Ciclo de vida do objeto", + "lifecycleItemDesc": { + "attribute": { + "other": "{{label}} reconhecido como {{attribute}}", + "faceOrLicense_plate": "{{attribute}} detetado por {{label}}" + }, + "gone": "{{label}} saiu", + "heard": "{{label}} ouvido", + "visible": "{{label}} detectado", + "external": "{{label}} detetado", + "entered_zone": "{{label}} entrou em {{zones}}", + "active": "{{label}} se tornou ativo", + "stationary": "{{label}} se tornou estacionário", + "header": { + "zones": "Zonas", + "ratio": "Proporção", + "area": "Área" + } + }, + "annotationSettings": { + "title": "Definições de Anotação", + "offset": { + "documentation": "Leia a documentação ", + "desc": "Esses dados vêm do feed de detecção da sua câmara, mas são sobrepostos nas imagens do feed de gravação. É improvável que os dois streams estejam perfeitamente sincronizados. Como resultado, a caixa delimitadora e o vídeo não se alinharão perfeitamente. No entanto, o campo annotation_offset pode ser usado para ajustar isso.", + "tips": "DICA: Imagine que há um clipe de evento com uma pessoa a andar da esquerda para a direita. Se a caixa delimitadora da linha do tempo do evento estiver consistentemente à esquerda da pessoa, o valor deve ser diminuído. Da mesma forma, se uma pessoa estiver andando da esquerda para a direita e a caixa delimitadora estiver consistentemente à frente da pessoa, o valor deve ser aumentado.", + "label": "Deslocamento de Anotação", + "millisecondsToOffset": "Milissegundos para deslocar as anotações de detecção. Padrão: 0", + "toast": { + "success": "O deslocamento de anotação para {{camera}} foi salvo no arquivo de configuração. Reinicie o Frigate para aplicar as alterações." + } + }, + "showAllZones": { + "title": "Mostrar Todas as Zonas", + "desc": "Mostrar sempre as zonas nas imagens onde os objetos entraram numa zona." + } + }, + "carousel": { + "previous": "Slide anterior", + "next": "Próximo slide" + }, + "noImageFound": "Nenhuma imagem encontrada para este carimbo de data/hora.", + "createObjectMask": "Criar Máscara de Objeto", + "adjustAnnotationSettings": "Ajustar definições de anotação", + "autoTrackingTips": "As posições da caixa delimitadora serão imprecisas para as câmaras com rastreamento automático.", + "scrollViewTips": "Deslize para ver os momentos significativos do ciclo de vida deste objeto.", + "count": "{{first}} de {{second}}", + "trackedPoint": "Ponto Rastreado" + }, + "itemMenu": { + "downloadSnapshot": { + "aria": "Descarregar captura", + "label": "Descarregar captura" + }, + "viewObjectLifecycle": { + "label": "Ver ciclo de vida do objeto", + "aria": "Mostrar o ciclo de vida do objeto" + }, + "viewInHistory": { + "label": "Ver no Histórico", + "aria": "Ver no Histórico" + }, + "downloadVideo": { + "label": "Descarregar vídeo", + "aria": "Descarregar vídeo" + }, + "findSimilar": { + "label": "Encontrar similar", + "aria": "Encontrar objetos rastreados similares" + }, + "submitToPlus": { + "label": "Enviar para o Frigate+", + "aria": "Enviar para o Frigate Plus" + }, + "deleteTrackedObject": { + "label": "Excluir este objeto rastreado" + }, + "addTrigger": { + "label": "Adicionar gatilho", + "aria": "Adicione um gatilho para este objeto rastreado" + }, + "audioTranscription": { + "label": "Transcrever", + "aria": "Solicitar transcrição de áudio" + } + }, + "searchResult": { + "tooltip": "Encontrado {{type}} com {{confidence}}% de confiança", + "deleteTrackedObject": { + "toast": { + "success": "Objeto rastreado excluído com sucesso.", + "error": "Falha ao excluir objeto rastreado: {{errorMessage}}" + } + } + }, + "dialog": { + "confirmDelete": { + "desc": "Excluir este objeto rastreado remove a captura de imagem, quaisquer incorporações salvas e todas as entradas associadas ao ciclo de vida do objeto. As gravações desse objeto rastreado na visualização do Histórico NÃO serão excluídas.

    Tem certeza de que deseja continuar?", + "title": "Confirmar exclusão" + } + }, + "fetchingTrackedObjectsFailed": "Erro ao buscar objetos rastreados: {{errorMessage}}", + "noTrackedObjects": "Nenhum objeto rastreado encontrado", + "trackedObjectsCount_one": "{{count}} objeto rastreado ", + "trackedObjectsCount_many": "{{count}} objetos rastreados ", + "trackedObjectsCount_other": "", + "exploreMore": "Explora mais objetos {{label}}", + "aiAnalysis": { + "title": "Análise IA" + }, + "concerns": { + "label": "Preocupações" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/exports.json new file mode 100644 index 0000000..f1c441a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Exportar - Frigate", + "search": "Pesquisar", + "noExports": "Nenhuma exportação encontrada", + "deleteExport": "Excluir exportação", + "editExport": { + "title": "Renomear exportação", + "desc": "Digite um novo nome para esta exportação.", + "saveExport": "Salvar exportação" + }, + "toast": { + "error": { + "renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}" + } + }, + "deleteExport.desc": "Tem a certeza de que deseja excluir {{exportName}}?" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/faceLibrary.json new file mode 100644 index 0000000..057e015 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "description": { + "placeholder": "Digite um nome para esta coleção", + "addFace": "Veja como adicionar uma nova coleção à biblioteca de rostos.", + "invalidName": "Nome inválido. Os nomes podem incluir apenas letras, números, espaços, apóstrofos, sublinhados e hífens." + }, + "details": { + "person": "Pessoa", + "face": "Detalhes do rosto", + "faceDesc": "Detalhes do objeto encontrado que gerou esta cara", + "timestamp": "Carimbo de hora", + "confidence": "Confiança", + "scoreInfo": "A pontuação da subetiqueta é a pontuação ponderada de todas as confianças de rostos reconhecidos, portanto, ela pode ser diferente da pontuação exibida na captura de imagem.", + "subLabelScore": "Pontuação da Subetiqueta", + "unknown": "Desconhecido" + }, + "documentTitle": "Biblioteca de rostos - Frigate", + "uploadFaceImage": { + "title": "Carregar imagem do rosto", + "desc": "Carregue uma imagem para procurar rostos e incluir em {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Criar coleção", + "desc": "Criar uma nova coleção", + "new": "Criar novo rosto", + "nextSteps": "Para construir uma base sólida:
  • Use a aba Treinar para selecionar e treinar em imagens para cada pessoa detectada.
  • Concentre-se em imagens diretas para obter melhores resultados; evite imagens de treino que capturem rostos em ângulo.
  • " + }, + "train": { + "aria": "Selecionar treino", + "title": "Treinar", + "empty": "Não há tentativas recentes de reconhecimento facial" + }, + "selectItem": "Selecionar {{item}}", + "selectFace": "Selecionar rosto", + "deleteFaceLibrary": { + "title": "Excluir nome", + "desc": "Tem certeza de que deseja excluir a coleção {{name}}? Isso excluirá permanentemente todos os rostos associados." + }, + "button": { + "addFace": "Adicionar rosto", + "uploadImage": "Carregar imagem", + "deleteFaceAttempts": "Excluir rostos", + "reprocessFace": "Reprocessar Rosto", + "renameFace": "Renomear rosto", + "deleteFace": "Excluir rosto" + }, + "imageEntry": { + "validation": { + "selectImage": "Selecione um arquivo de imagem." + }, + "dropActive": "Solte a imagem aqui…", + "maxSize": "Tamanho máximo: {{size}}MB", + "dropInstructions": "Arraste e solte uma imagem aqui ou clique para selecionar" + }, + "trainFace": "Treinar rosto", + "toast": { + "success": { + "updatedFaceScore": "Pontuação facial atualizada com sucesso.", + "trainedFace": "Rosto treinado com sucesso.", + "deletedFace_one": "{{count}} rosto excluído com sucesso.", + "deletedFace_many": "{{count}} rostos excluídos com sucesso.", + "deletedFace_other": "{{count}} rostos excluídos com sucesso.", + "deletedName_one": "{{count}} rosto foi excluído com sucesso.", + "deletedName_many": "{{count}} rostos foram excluídos com sucesso.", + "deletedName_other": "{{count}} rostos foram excluídos com sucesso.", + "uploadedImage": "Imagem carregada com sucesso.", + "addFaceLibrary": "{{name}} foi adicionado com sucesso à biblioteca de rostos!", + "renamedFace": "Rosto renomeado com sucesso para {{name}}" + }, + "error": { + "uploadingImageFailed": "Falha ao carregar a imagem: {{errorMessage}}", + "deleteFaceFailed": "Falha ao excluir: {{errorMessage}}", + "deleteNameFailed": "Falha ao excluir nome: {{errorMessage}}", + "addFaceLibraryFailed": "Falhou ao definir nome do rosto: {{errorMessage}}", + "trainFailed": "Falhou ao treinar: {{errorMessage}}", + "updateFaceScoreFailed": "Falhou ao atualizar pontuação do rosto: {{errorMessage}}", + "renameFaceFailed": "Falha ao renomear o rosto: {{errorMessage}}" + } + }, + "readTheDocs": "Leia a documentação", + "trainFaceAs": "Treinar rosto como:", + "steps": { + "faceName": "Digite o Nome do Rosto", + "uploadFace": "Carregar imagem do rosto", + "nextSteps": "Próximos passos", + "description": { + "uploadFace": "Carregue uma imagem de {{name}} que mostre a cara de frente. A imagem não precisa de ser cortada para mostrar apenas a cara." + } + }, + "renameFace": { + "desc": "Entre com um novo nome para {{name}}", + "title": "Renomear rosto" + }, + "collections": "Coleções", + "deleteFaceAttempts": { + "title": "Excluir rostos", + "desc_one": "Tem a certeza que pretende apagar {{count}} cara? Esta ação não pode ser revertida.", + "desc_many": "Tem a certeza que pretende apagar {{count}} caras? Esta ação não pode ser revertida.", + "desc_other": "Tem a certeza que pretende apagar {{count}} caras? Esta ação não pode ser revertida." + }, + "nofaces": "Não tem caras disponíveis", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/live.json new file mode 100644 index 0000000..770028a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/live.json @@ -0,0 +1,171 @@ +{ + "documentTitle": "Ao vivo - Frigate", + "documentTitle.withCamera": "{{camera}} - Ao vivo - Frigate", + "twoWayTalk": { + "disable": "Desativar conversa bidirecional", + "enable": "Habilitar conversa bidirecional" + }, + "cameraAudio": { + "enable": "Habilitar áudio da câmara", + "disable": "Desativar áudio da câmara" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Clique no quadro para centralizar a câmara", + "enable": "Habilitar clique para mover", + "disable": "Desativar clique para mover" + }, + "down": { + "label": "Mover a câmara PTZ para baixo" + }, + "up": { + "label": "Mover a câmara PTZ para cima" + }, + "left": { + "label": "Mover a câmara PTZ para a esquerda" + }, + "right": { + "label": "Mover a câmara PTZ para a direita" + } + }, + "zoom": { + "in": { + "label": "Aumentar zoom da câmara PTZ" + }, + "out": { + "label": "Diminuir zoom da câmara PTZ" + } + }, + "presets": "Predefinições de câmara PTZ", + "frame": { + "center": { + "label": "Clique no quadro para centralizar a câmara PTZ" + } + }, + "focus": { + "in": { + "label": "Em foco da câmera PTZ" + }, + "out": { + "label": "Fora foco da câmera PTZ em" + } + } + }, + "lowBandwidthMode": "Modo de baixa largura de banda", + "camera": { + "enable": "Habilitar câmara", + "disable": "Desativar câmara" + }, + "muteCameras": { + "disable": "Ativar áudio de todas as câmaras", + "enable": "Silenciar todas as câmaras" + }, + "detect": { + "enable": "Habilitar detecção", + "disable": "Desativar detecção" + }, + "snapshots": { + "enable": "Habilitar snapshots", + "disable": "Desativar snapshots" + }, + "audioDetect": { + "enable": "Habilitar detecção de áudio", + "disable": "Desativar detecção de áudio" + }, + "autotracking": { + "enable": "Habilitar rastreamento automático", + "disable": "Desativar rastreamento automático" + }, + "streamStats": { + "enable": "Mostrar estatísticas de transmissão", + "disable": "Ocultar estatísticas de transmissão" + }, + "manualRecording": { + "tips": "Inicie um evento manual com base nas configurações de retenção de gravação desta câmara.", + "playInBackground": { + "label": "Reproduzir em segundo plano", + "desc": "Habilite esta opção para continuar a transmissão quando o player estiver oculto." + }, + "showStats": { + "label": "Mostrar estatísticas", + "desc": "Habilite esta opção para mostrar estatísticas de transmissão como uma sobreposição no feed da câmara." + }, + "start": "Iniciar gravação on-demand", + "recordDisabledTips": "Como a gravação está desabilitada ou restrita na configuração desta câmara, apenas um snapshot será salvo.", + "end": "Encerrar gravação on-demand", + "ended": "Fim da gravação manual on-demand.", + "failedToEnd": "Falha ao finalizar a gravação manual on-demand.", + "failedToStart": "Falha ao iniciar a gravação manual on-demand.", + "title": "Gravação on-demand", + "started": "Iniciou a gravação manual on-demand.", + "debugView": "Exibição de depuração" + }, + "streamingSettings": "Configurações de transmissão", + "notifications": "Notificações", + "suspend": { + "forTime": "Suspender por: " + }, + "stream": { + "title": "Transmissão", + "audio": { + "tips": { + "title": "O áudio deve ser emitido pela sua câmara e configurado no go2rtc para esta transmissão.", + "documentation": "Leia a documentação " + }, + "available": "O áudio está disponível para esta transmissão", + "unavailable": "O áudio não está disponível para esta transmissão" + }, + "twoWayTalk": { + "tips.documentation": "Leia a documentação ", + "unavailable": "Conversa bidirecional não está disponível para esta transmissão", + "tips": "Seu dispositivo deve suportar o recurso e o WebRTC deve ser configurado para conversa bidirecional.", + "available": "Conversa bidirecional está disponível para esta transmissão" + }, + "lowBandwidth": { + "tips": "A visualização ao vivo está em modo de baixa largura de banda devido a erros de buffer ou transmissão.", + "resetStream": "Reiniciar transmissão" + }, + "playInBackground": { + "label": "Reproduzir em segundo plano", + "tips": "Habilite esta opção para continuar a transmissão quando o player estiver oculto." + } + }, + "cameraSettings": { + "title": "{{camera}} configurações", + "cameraEnabled": "Câmara habilitada", + "objectDetection": "Detecção de objeto", + "recording": "Gravando", + "audioDetection": "Detecção de áudio", + "autotracking": "Rastreamento automático", + "snapshots": "Snapshots", + "transcription": "Transcrição de áudio" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Objetos ativos", + "motion": "Movimento", + "all": "Todos" + }, + "notAllTips": "Sua configuração de retenção de gravação {{source}} está definida como modo: {{effectiveRetainMode}}, portanto, esta gravação on-demand manterá apenas segmentos com {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editar layout", + "group": { + "label": "Editar grupo de câmaras" + }, + "exitEdit": "Sair da edição" + }, + "audio": "Áudio", + "recording": { + "enable": "Habilitar gravação", + "disable": "Desativar gravação" + }, + "history": { + "label": "Mostrar filmagens históricas" + }, + "transcription": { + "enable": "Habilitar transcrição de áudio ao vivo", + "disable": "Desabilitar transcrição de áudio ao vivo" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/recording.json new file mode 100644 index 0000000..a8b6e39 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Exportar", + "calendar": "Calendário", + "filter": "Filtro", + "filters": "Filtros", + "toast": { + "error": { + "endTimeMustAfterStartTime": "O horário de término deve ser posterior ao horário de início", + "noValidTimeSelected": "Nenhum intervalo de tempo válido selecionado" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/search.json new file mode 100644 index 0000000..4443f27 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "Pesquisar", + "savedSearches": "Pesquisas salvas", + "searchFor": "Pesquisar por {{inputValue}}", + "button": { + "save": "Salvar pesquisa", + "delete": "Excluir pesquisa salva", + "filterInformation": "Informação do filtro", + "filterActive": "Filtros ativos", + "clear": "Limpar pesquisa" + }, + "trackedObjectId": "ID do objeto rastreado", + "filter": { + "label": { + "sub_labels": "Sub etiquetas", + "zones": "Zonas", + "cameras": "Câmaras", + "labels": "Etiquetas", + "search_type": "Tipo de pesquisa", + "time_range": "Intervalo de tempo", + "before": "Antes", + "after": "Depois", + "min_score": "Pontuação mínima", + "max_score": "Pontuação máxima", + "min_speed": "Velocidade mínima", + "max_speed": "Velocidade máxima", + "recognized_license_plate": "Matrícula reconhecida", + "has_clip": "Tem Clipe", + "has_snapshot": "Tem Captura de Imagem" + }, + "searchType": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "header": { + "noFilters": "Filtros", + "activeFilters": "Filtros ativos", + "currentFilterType": "Valores de filtro" + }, + "tips": { + "desc": { + "text": "Os filtros ajudam você a restringir os resultados da sua pesquisa. Veja como usá-los no campo de entrada:", + "example": "Exemplo: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step": "
    • Digite um nome de filtro seguido de dois pontos (ex: \"cameras:\").
    • Selecione um valor entre as sugestões ou digite o seu próprio.
    • Use múltiplos filtros adicionando-os um após o outro com um espaço entre eles.
    • Filtros de data (before: e after:) usam o formato {{DateFormat}}.
    • O filtro de intervalo de tempo usa o formato {{exampleTime}}.
    • Remova filtros clicando no 'x' ao lado deles.
    ", + "step1": "Digite um nome para o filtro seguido de dois pontos (exemplo \"camaras:\").", + "step2": "Selecione um valor entre as sugestões ou digite o seu próprio.", + "step3": "Use vários filtros adicionando-os um após o outro com um espaço entre eles.", + "step5": "O filtro de intervalo de tempo usa o formato {{exampleTime}}.", + "step6": "Remova os filtros clicando no 'x' ao lado deles.", + "step4": "Os filtros de data (antes: e depois:) utilizam o formato {{DateFormat}}.", + "exampleLabel": "Exemplo:" + }, + "title": "Como usar filtros de texto" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "A data 'antes' deve ser posterior à data 'depois'.", + "minScoreMustBeLessOrEqualMaxScore": "O 'min_score' deve ser inferior ou igual ao 'max_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "O 'min_speed' deve ser inferior ou igual ao 'max_speed'.", + "afterDatebeEarlierBefore": "A data \"depois\" deve ser anterior à data \"antes\".", + "maxScoreMustBeGreaterOrEqualMinScore": "O 'max_score' deve ser maior ou igual ao 'min_score'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "O 'max_speed' deve ser maior ou igual ao 'min_speed'." + } + } + }, + "placeholder": { + "search": "Pesquisar…" + }, + "similaritySearch": { + "title": "Pesquisa de similaridade", + "active": "Busca de semelhança ativa", + "clear": "Pesquisa de semelhança clara" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/settings.json new file mode 100644 index 0000000..1bab92d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/settings.json @@ -0,0 +1,986 @@ +{ + "documentTitle": { + "camera": "Configurações da câmera - Frigate", + "classification": "Configurações de classificação - Frigate", + "masksAndZones": "Editor de máscara e zona - Frigate", + "motionTuner": "Ajuste de movimento - Frigate", + "object": "Depuração - Frigate", + "authentication": "Configurações de autenticação - Frigate", + "general": "Configurações gerais - Frigate", + "frigatePlus": "Configurações do Frigate+ - Frigate", + "default": "Configurações - Frigate", + "notifications": "Configuração de Notificações - Frigate", + "enrichments": "Configurações Avançadas - Frigate", + "cameraManagement": "Gerir Câmaras - Frigate", + "cameraReview": "Configurações de Revisão de Câmara - Frigate" + }, + "menu": { + "ui": "UI", + "masksAndZones": "Máscaras / Zonas", + "cameras": "Configurações da câmara", + "classification": "Classificação", + "motionTuner": "Ajuste de movimento", + "debug": "Depuração", + "users": "Utilizadores", + "notifications": "Notificações", + "frigateplus": "Frigate+", + "enrichments": "Avançado", + "triggers": "Gatilhos", + "cameraManagement": "Gestão", + "cameraReview": "Rever", + "roles": "Papéis" + }, + "dialog": { + "unsavedChanges": { + "title": "Você tem alterações não salvas.", + "desc": "Deseja salvar suas alterações antes de continuar?" + } + }, + "cameraSetting": { + "camera": "Câmara", + "noCamera": "Sem câmara" + }, + "general": { + "title": "Configurações gerais", + "liveDashboard": { + "title": "Painel ao vivo", + "automaticLiveView": { + "label": "Visualização ao vivo automática", + "desc": "Alternar automaticamente para a visualização ao vivo de uma câmara quando uma atividade for detectada. Desativar esta opção faz com que as imagens estáticas das câmaras no painel Ao Vivo sejam atualizadas apenas uma vez por minuto." + }, + "playAlertVideos": { + "label": "Reproduzir vídeos de alerta", + "desc": "Por padrão, alertas recentes no painel ao vivo são reproduzidos como vídeos curtos em loop. Desative esta opção para mostrar apenas uma imagem estática dos alertas recentes neste dispositivo/navegador." + } + }, + "calendar": { + "title": "Calendário", + "firstWeekday": { + "label": "Primeiro dia da semana", + "sunday": "Domingo", + "monday": "Segunda-feira", + "desc": "O dia em que as semanas do calendário de análise começam." + } + }, + "storedLayouts": { + "title": "Layouts armazenados", + "desc": "O layout das câmaras em um grupo de câmaras pode ser arrastado/redimensionado. As posições são armazenadas no armazenamento local do seu navegador.", + "clearAll": "Limpar todos os layouts" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Taxa de reprodução padrão", + "desc": "Taxa de reprodução padrão para a reprodução das gravações." + }, + "title": "Visualizador de gravações" + }, + "cameraGroupStreaming": { + "desc": "As configurações de transmissão de cada grupo de câmaras são armazenadas no armazenamento local do seu navegador.", + "title": "Configurações de transmissão do grupo de câmaras", + "clearAll": "Limpar todas as configurações de transmissão" + }, + "toast": { + "success": { + "clearStreamingSettings": "Configurações de transmissão para todos os grupos de câmaras limpas.", + "clearStoredLayout": "Limpo layout armazenado para {{cameraName}}" + }, + "error": { + "clearStoredLayoutFailed": "Falha ao limpar o layout armazenado: {{errorMessage}}", + "clearStreamingSettingsFailed": "Falha ao limpar as configurações de transmissão: {{errorMessage}}" + } + } + }, + "classification": { + "faceRecognition": { + "modelSize": { + "label": "Tamanho do modelo", + "small": { + "title": "pequeno", + "desc": "Usar pequeno utiliza um modelo de incorporação facial FaceNet que é eficiente na maioria dos CPUs." + }, + "large": { + "title": "grande", + "desc": "Usar grande utiliza um modelo de incorporação facial ArcFace e será executado automaticamente na GPU, se aplicável." + }, + "desc": "O tamanho do modelo utilizado para o reconhecimento facial." + }, + "readTheDocumentation": "Leia a documentação", + "title": "Reconhecimento facial", + "desc": "O reconhecimento facial permite que pessoas sejam atribuídas a nomes e, quando o rosto delas for reconhecido, o Frigate atribuirá o nome da pessoa como um sub-rótulo. Essa informação é incluída na interface do usuário, filtros e também nas notificações." + }, + "licensePlateRecognition": { + "readTheDocumentation": "Leia a documentação", + "title": "Reconhecimento de placas de veículo", + "desc": "O Frigate pode reconhecer placas de veículos e adicionar automaticamente os caracteres detectados ao campo placa_de_veículo_reconhecida ou um nome conhecido como sub_rótulo para objetos do tipo carro. Um caso de uso comum pode ser ler as placas de veículos que entram em uma garagem ou os carros que passam por uma rua." + }, + "semanticSearch": { + "readTheDocumentation": "Leia a documentação", + "title": "Pesquisa semântica", + "reindexNow": { + "label": "Reindexar agora", + "desc": "A reindexação irá regenerar as incorporações de todos os objetos rastreados. Esse processo é executado em segundo plano e pode sobrecarregar seu CPU e levar um bom tempo, dependendo do número de objetos rastreados que você possui.", + "confirmTitle": "Confirmar reindexação", + "confirmDesc": "Você tem certeza de que deseja reindexar todos as incorporações dos objetos rastreados? Esse processo será executado em segundo plano, mas pode sobrecarregar seu CPU e levar um bom tempo. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "alreadyInProgress": "A reindexação já está em andamento.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}", + "success": "Reindexação iniciada com sucesso." + }, + "modelSize": { + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo utilizado para as incorporações de pesquisa semântica.", + "small": { + "title": "pequeno", + "desc": "Usar pequeno utiliza uma versão quantizada do modelo, que usa menos RAM e executa mais rápido no CPU, com uma diferença insignificante na qualidade da incorporação." + }, + "large": { + "title": "grande", + "desc": "Usar grande utiliza o modelo completo do Jina e será executado automaticamente na GPU, se aplicável." + } + }, + "desc": "A Pesquisa Semântica no Frigate permite que você encontre objetos rastreados dentro dos itens de revisão usando a imagem em si, uma descrição de texto definida pelo usuário ou uma descrição gerada automaticamente." + }, + "title": "Configurações de classificação", + "toast": { + "success": "As configurações de classificação foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar as alterações de configuração: {{errorMessage}}" + }, + "birdClassification": { + "title": "Classificação de Pássaros", + "desc": "A classificação de aves/pássaros identifica aves conhecidas usando um modelo Tensorflow quantizado. Quando uma ave/ pássaro conhecida(o) for reconhecida(o), o seu nome comum será adicionado como um sub_rótulo. Estas informações estão incluídas na interface do utilizador, nos filtros e também nas notificações." + }, + "restart_required": "Reinício necessário (configurações de classificação alteradas)", + "unsavedChanges": "Alterações nas configurações de Classificação não estão salvas" + }, + "notification": { + "globalSettings": { + "title": "Configurações globais", + "desc": "Suspenda temporariamente as notificações para câmaras específicas em todos os dispositivos registados." + }, + "notificationSettings": { + "documentation": "Leia a documentação", + "title": "Configurações de notificação", + "desc": "O Frigate pode enviar notificações push para o seu dispositivo quando está a ser executado no browser ou instalado como um PWA." + }, + "notificationUnavailable": { + "documentation": "Leia a documentação", + "title": "Notificações indisponíveis", + "desc": "As notificações push da Web exigem um contexto seguro (https://…). Esta é uma limitação do navegador. Acesse o Frigate com segurança para usar as notificações." + }, + "cameras": { + "title": "Câmaras", + "noCameras": "Nenhuma câmara disponível", + "desc": "Selecione para que câmaras as notificações serão ativadas." + }, + "deviceSpecific": "Configurações específicas do dispositivo", + "registerDevice": "Registe este dispositivo", + "email": { + "placeholder": "por exemplo: exemplo@email.com", + "desc": "É necessário um e-mail válido que será utilizado para o notificar caso haja algum problema com o serviço push.", + "title": "E-mail" + }, + "title": "Notificações", + "unregisterDevice": "Cancelar o registro deste dispositivo", + "suspendTime": { + "5minutes": "Suspender por 5 minutos", + "1hour": "Suspender por 1 hora", + "12hours": "Suspender por 12 horas", + "untilRestart": "Suspender até reiniciar", + "10minutes": "Suspender por 10 minutos", + "suspend": "Suspender", + "30minutes": "Suspender por 30 minutos", + "24hours": "Suspender por 24 horas" + }, + "cancelSuspension": "Cancelar Suspensão", + "toast": { + "success": { + "registered": "Registo para notificações concluído com sucesso. É necessário reiniciar o Frigate antes que qualquer notificação (incluindo uma notificação de teste) possa ser enviada.", + "settingSaved": "As configurações de notificação foram salvas." + }, + "error": { + "registerFailed": "Falha ao guardar o registo das notificações." + } + }, + "sendTestNotification": "Envie uma notificação de teste", + "active": "Notificações ativas", + "suspended": "Notificações suspensas {{time}}", + "unsavedRegistrations": "Registros de notificação não salvos", + "unsavedChanges": "Registros de notificação não salvos" + }, + "frigatePlus": { + "snapshotConfig": { + "documentation": "Leia a documentação", + "table": { + "snapshots": "Snapshots", + "camera": "Câmara", + "cleanCopySnapshots": "clean_copy Snapshots" + }, + "title": "Configuração de snapshots", + "desc": "O envio para o Frigate+ requer que tanto os snapshots quanto os snapshots clean_copy estejam habilitados na sua configuração.", + "cleanCopyWarning": "Algumas câmaras têm snapshots habilitados, mas a cópia limpa está desabilitada. É necessário habilitar clean_copy na sua configuração de snapshot para poder enviar imagens dessas câmaras para o Frigate+." + }, + "toast": { + "success": "As definições do Frigate+ foram guardadas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao guardar alterações de configuração: {{errorMessage}}" + }, + "modelInfo": { + "modelType": "Tipo de modelo", + "trainDate": "Data do treino", + "title": "Informações do modelo", + "error": "Falha ao carregar informações do modelo", + "availableModels": "Modelos Disponíveis", + "baseModel": "Modelo Básico", + "plusModelType": { + "userModel": "Ajuste-Fino", + "baseModel": "Modelo Básico" + }, + "supportedDetectors": "Detectores Suportados", + "loading": "Carregando informações do modelo…", + "cameras": "Câmaras", + "loadingAvailableModels": "Carregando modelos disponíveis…", + "modelSelect": "Os modelos disponíveis no Frigate+ podem ser selecionados aqui. Observe que apenas modelos compatíveis com a configuração atual do seu detector podem ser selecionados." + }, + "title": "Configurações Frigate+", + "apiKey": { + "validated": "A chave da API do Frigate+ foi detectada e validada", + "notValidated": "A chave da API do Frigate+ não foi detectada ou não foi validada", + "desc": "A chave de API do Frigate+ permite a integração com o serviço Frigate+.", + "plusLink": "Saiba mais sobre o Frigate+", + "title": "Chave de API do Frigate+" + }, + "restart_required": "Reinicialização necessária (modelo Frigate+ alterado)", + "unsavedChanges": "Alterações nas configurações do Frigate+ não salvas" + }, + "masksAndZones": { + "motionMasks": { + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "context": { + "documentation": "Leia a documentação", + "title": "As máscaras de movimento são usadas para impedir que tipos indesejados de movimento acionem a detecção (exemplo: galhos de árvores, carimbos de data/hora da câmera). As máscaras de movimento devem ser usadas com moderação, o uso excessivo de máscaras dificultará o rastreamento de objetos." + }, + "polygonAreaTooLarge": { + "documentation": "Leia a documentação", + "tips": "As máscaras de movimento não impedem que objetos sejam detectados. Você deve usar uma zona obrigatória em vez disso.", + "title": "A máscara de movimento está cobrindo {{polygonArea}}% da área da câmara. Máscaras de movimento grandes não são recomendadas." + }, + "label": "Máscara de movimento", + "desc": { + "documentation": "Documentação", + "title": "As máscaras de movimento são usadas para impedir que tipos indesejados de movimento acionem a detecção. O uso excessivo de máscaras dificultará o rastreamento de objetos." + }, + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "toast": { + "success": { + "noName": "O filtro de movimento foi guardado. Reinicie o Frigate para aplicar as alterações.", + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações." + } + }, + "edit": "Editar Máscara de Movimento", + "documentTitle": "Editar Máscara de Movimento - Frigate", + "add": "Nova Máscara de Movimento" + }, + "zones": { + "label": "Zonas", + "documentTitle": "Editar Zona - Frigate", + "desc": { + "documentation": "Documentação", + "title": "As zonas permitem definir uma área específica do quadro para que você possa determinar se um objeto está ou não dentro de uma área particular." + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "add": "Adicionar Zona", + "edit": "Editar Zona", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "name": { + "title": "Nome", + "inputPlaceHolder": "Digite um nome…", + "tips": "O nome deve ter pelo menos 2 caracteres e não pode ser o nome de uma câmara ou de outra zona." + }, + "inertia": { + "title": "Inércia", + "desc": "Especifica quantos quadros um objeto deve estar em uma zona antes de ser considerado dentro da zona. Padrão: 3" + }, + "loiteringTime": { + "title": "Tempo de permanência", + "desc": "Define o tempo mínimo, em segundos, que o objeto deve permanecer na zona para que ela seja ativada. Padrão: 0" + }, + "objects": { + "title": "Objetos", + "desc": "Lista de objetos que se aplicam a esta zona." + }, + "allObjects": "Todos os objetos", + "speedEstimation": { + "title": "Estimativa de velocidade", + "desc": "Ativar estimativa de velocidade para objetos nesta zona. A zona deve ter exatamente 4 pontos.", + "docs": "Lê a documentação", + "lineBDistance": "Distância da Linha B ({{unit}})", + "lineCDistance": "Distância da Linha C ({{unit}})", + "lineDDistance": "Distância da Linha D ({{unit}})", + "lineADistance": "Distância da Linha A ({{unit}})" + }, + "speedThreshold": { + "title": "Limiar de velocidade ({{unit}})", + "desc": "Especifica uma velocidade mínima para que os objetos sejam considerados nesta zona.", + "toast": { + "error": { + "pointLengthError": "A estimativa de velocidade foi desativada para esta zona. Zonas com estimativa de velocidade devem ter exatamente 4 pontos.", + "loiteringTimeError": "Zonas com tempos de permanência maiores que 0 não devem ser usadas com estimativa de velocidade." + } + } + }, + "toast": { + "success": "A zona ({{zoneName}}) foi salva. Reinicie o Frigate para aplicar as alterações." + } + }, + "filter": { + "all": "Todas as Máscaras e Zonas" + }, + "toast": { + "success": { + "copyCoordinates": "Coordenadas de {{polyName}} copiadas para a área de transferência." + }, + "error": { + "copyCoordinatesFailed": "Não foi possível copiar as coordenadas para a área de transferência." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "O nome da zona deve ter pelo menos 2 caracteres.", + "mustNotContainPeriod": "O nome da zona não pode conter pontos.", + "hasIllegalCharacter": "O nome da zona contém caracteres ilegais.", + "mustNotBeSameWithCamera": "O nome da zona não pode ser o mesmo que o nome da câmara.", + "alreadyExists": "Já existe uma zona com esse nome para esta câmara." + } + }, + "distance": { + "error": { + "text": "A distância deve ser maior ou igual a 0,1.", + "mustBeFilled": "Todos os campos de distância devem ser preenchidos para usar a estimativa de velocidade." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "A inércia deve ser maior que 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "O tempo de permanência deve ser maior ou igual a 0." + } + }, + "polygonDrawing": { + "removeLastPoint": "Remover o último ponto", + "reset": { + "label": "Limpar todos os pontos" + }, + "snapPoints": { + "true": "Fixar pontos", + "false": "Não fixar pontos" + }, + "delete": { + "title": "Confirmar exclusão", + "success": "{{name}} foi excluído.", + "desc": "Você tem certeza de que deseja excluir o {{type}} {{name}}?" + }, + "error": { + "mustBeFinished": "A criação do polígono deve ser concluída antes de salvar." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "O limiar de velocidade deve ser igual ou superior a 0,1." + } + } + }, + "objectMasks": { + "label": "Máscaras de objetos", + "clickDrawPolygon": "Clique para desenhar um polígono na imagem.", + "desc": { + "documentation": "Documentação", + "title": "As máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto com base na localização." + }, + "point_one": "{{count}} ponto", + "point_many": "{{count}} pontos", + "point_other": "{{count}} pontos", + "objects": { + "allObjectTypes": "Todos os tipos de objeto", + "title": "Objetos", + "desc": "O tipo de objeto que se aplica a esta máscara de objeto." + }, + "add": "Adicionar filtro para objecto", + "edit": "Editar filtro de objecto", + "documentTitle": "Editar filtro de movimento - Frigate", + "toast": { + "success": { + "noName": "A máscara de objeto foi salva. Reinicie o Frigate para aplicar as alterações.", + "title": "{{polygonName}} foi salvo. Reinicie o Frigate para aplicar as alterações." + } + }, + "context": "As máscaras de filtro de objetos são usadas para filtrar falsos positivos para um determinado tipo de objeto com base na localização." + }, + "restart_required": "É necessário reiniciar (máscaras/zonas alteradas)", + "motionMaskLabel": "Mascara movimento {{number}}", + "objectMaskLabel": "Mascara de objecto {{number}} ({{label}})" + }, + "debug": { + "zones": { + "title": "Zonas", + "desc": "Mostrar um esboço de quaisquer zonas definidas" + }, + "timestamp": { + "title": "Carimbo de hora", + "desc": "Sobrepor um carimbo de data/hora na imagem" + }, + "title": "Depurar", + "detectorDesc": "O Frigate utiliza os seus detectores ({{detectors}}) para detectar objetos no fluxo de vídeo da sua câmara.", + "desc": "A vista de depuração apresenta uma vista em tempo real dos objetos localizados e das respectivas estatísticas. A lista de objetos apresenta um resumo dos objetos detectados em tempo real.", + "debugging": "A depurar", + "objectList": "Lista de Objetos", + "noObjects": "Sem Objetos", + "boundingBoxes": { + "title": "Caixas de contorno", + "desc": "Mostrar caixas de contorno à volta dos objetos seguidos", + "colors": { + "label": "Cores da caixa de contorno de objetos", + "info": "
  • Na inicialização, cores diferentes serão atribuídas a cada rótulo de objeto
  • Uma linha fina azul escura indica que o objeto não foi detectado neste momento
  • Uma linha fina cinza indica que o objeto foi detectado como estacionário
  • Uma linha grossa indica que o objeto está sujeito ao rastreamento automático (quando ativado)
  • " + } + }, + "objectShapeFilterDrawing": { + "tips": "Habilite esta opção para desenhar um retângulo na imagem da câmara para mostrar sua área e proporção. Esses valores podem ser usados para definir parâmetros de filtro de formato de objeto na sua configuração.", + "document": "Leia a documentação ", + "score": "Pontuação", + "ratio": "Razão", + "area": "Area", + "desc": "Desenhe um retângulo na imagem para visualizar detalhes da área e da proporção", + "title": "Desenho de filtro de forma de objeto" + }, + "regions": { + "title": "Regiões", + "desc": "Mostrar uma caixa da região de interesse enviada ao detector de objetos", + "tips": "

    Caixas de região


    Caixas verdes brilhantes serão sobrepostas em áreas de interesse no quadro que estão sendo enviadas ao detector de objetos.

    " + }, + "motion": { + "tips": "

    Caixas de movimento


    Caixas vermelhas serão sobrepostas em áreas do quadro onde o movimento está sendo detectado

    ", + "title": "Caixas de movimento", + "desc": "Mostrar caixas ao redor das áreas onde o movimento é detectado" + }, + "mask": { + "title": "Máscaras de movimento", + "desc": "Mostrar polígonos de máscara de movimento" + }, + "paths": { + "title": "Caminhos", + "desc": "Mostrar pontos significativos do caminho do objeto rastreado", + "tips": "

    Paths


    Linhas e círculos indicarão pontos significativos que o objeto rastreado moveu durante seu ciclo de vida.

    " + }, + "openCameraWebUI": "Abrir a Interface Web de {{camera}}", + "audio": { + "title": "Áudio", + "noAudioDetections": "Nenhuma detecção de áudio", + "score": "pontuanção", + "currentRMS": "RMS Atual", + "currentdbFS": "dbFS Atual" + } + }, + "camera": { + "reviewClassification": { + "readTheDocumentation": "Leia a documentação", + "title": "Classificação da Análise", + "noDefinedZones": "Nenhuma zona está definida para esta câmara.", + "objectAlertsTips": "Todos os objetos {{alertsLabels}} na câmara {{cameraName}} serão exibidos como Alertas.", + "zoneObjectDetectionsTips": { + "text": "Todos os objetos {{detectionsLabels}} não categorizados na zona {{zone}} na câmara {{cameraName}} serão exibidos como Detecções.", + "regardlessOfZoneObjectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados na câmara {{cameraName}} serão exibidos como Detecções, independentemente da zona em que se encontram.", + "notSelectDetections": "Todos os objetos {{detectionsLabels}} detectados na zona {{zone}} na câmara {{cameraName}} que não forem categorizados como Alertas serão exibidos como Detecções, independentemente da zona em que se encontram." + }, + "selectAlertsZones": "Selecionar zonas para Alertas", + "selectDetectionsZones": "Selecionar zonas para Detecções", + "limitDetections": "Limitar detecções a zonas específicas", + "desc": "O Frigate categoriza os itens de análise como Alertas e Detecções. Por padrão, todos os objetos do tipo pessoa e carro são considerados Alertas. Você pode refinar a categorização dos seus itens de análise configurando as zonas necessárias para eles.", + "objectDetectionsTips": "Todos os objetos {{detectionsLabels}} não categorizados na câmara {{cameraName}} serão exibidos como Detecções, independentemente da zona em que se encontram.", + "zoneObjectAlertsTips": "Todos os objetos {{alertsLabels}} detectados na zona {{zone}} na câmara {{cameraName}} serão exibidos como Alertas.", + "toast": { + "success": "A configuração de Classificação de análise foi salva. Reinicie o Frigate para aplicar as alterações." + }, + "unsavedChanges": "Configurações de classificação de análises não salvas para {{camera}}" + }, + "title": "Configurações da câmara", + "streams": { + "title": "Transmissões", + "desc": "Desativar uma câmara interrompe completamente o processamento das transmissões dessa câmara pelo Frigate. Detecção, gravação e depuração ficarão indisponíveis.
    Observação: Isso não desativa as retransmissões do go2rtc." + }, + "review": { + "title": "Análise", + "desc": "Ative ou desative alertas e detecções para esta câmara. Quando desativado, nenhum novo item de análise será gerado. ", + "alerts": "Alertas ", + "detections": "Detecções " + }, + "object_descriptions": { + "title": "Descrições de objetos de IA generativa", + "desc": "Ative/desative temporariamente as descrições de objetos de IA generativa para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para objetos rastreados nesta câmera." + }, + "review_descriptions": { + "title": "Descrições de análises de IA generativa", + "desc": "Ative/desative temporariamente as descrições de avaliação geradas por IA para esta câmera. Quando desativadas, as descrições geradas por IA não serão solicitadas para itens de avaliação nesta câmera." + }, + "addCamera": "Adicionar Nova Câmera", + "editCamera": "Editar Câmera:", + "selectCamera": "Selecione uma Câmera", + "backToSettings": "Voltar para as Configurações da Câmera", + "cameraConfig": { + "add": "Adicionar Câmera", + "edit": "Editar Câmera", + "description": "Configure as definições da câmera, incluindo entradas de transmissão e funções.", + "name": "Nome da Câmera", + "nameRequired": "O nome da câmera é obrigatório", + "nameInvalid": "O nome da câmera deve conter apenas letras, números, sublinhados ou hifens", + "namePlaceholder": "e.g., porta_da_frente", + "enabled": "Habilitado", + "ffmpeg": { + "inputs": "Entrada de Streams", + "path": "Caminho da Stream", + "pathRequired": "Caminho da Stream é obrigatória", + "pathPlaceholder": "rtsp://...", + "roles": "Funções", + "rolesRequired": "Pelo menos uma função é necessária", + "rolesUnique": "Cada função (áudio, detecção, gravação) só pode ser atribuída a uma stream", + "addInput": "Adicionar Entrada de Stream", + "removeInput": "Remover Entrada de Stream", + "inputsRequired": "É necessário pelo menos uma stream de entrada" + }, + "toast": { + "success": "Câmera {{cameraName}} guardada com sucesso" + }, + "nameLength": "O nome da câmara deve ter ao menos 24 caracteres." + } + }, + "motionDetectionTuner": { + "contourArea": { + "title": "Área de contorno", + "desc": "O valor da área de contorno é usado para decidir quais grupos de pixels alterados são qualificados como movimento. Valor padrão: 10" + }, + "improveContrast": { + "title": "Melhorar o contraste", + "desc": "Melhorar o contraste para cenas mais escuras. Defeito: ON" + }, + "Threshold": { + "title": "Limite", + "desc": "O valor do limiar determina quanto de alteração na luminância de um pixel é necessário para ser considerado movimento. Valor padrão: 30" + }, + "desc": { + "title": "O Frigate utiliza a detecção de movimento como uma primeira verificação para ver se há algo acontecendo no quadro que valha a pena ser verificado com a detecção de objetos.", + "documentation": "Leia o Guia de Ajuste de Movimento" + }, + "title": "Ajustador de Detecção de Movimento", + "unsavedChanges": "Alterações do Ajuste de Movimento não guardadas ({{camera}})", + "toast": { + "success": "Definições para Movimento foram salvas." + } + }, + "enrichments": { + "faceRecognition": { + "desc": "O reconhecimento facial permite que as pessoas recebam nomes e, quando o rosto delas for reconhecido, o Frigate atribuirá o nome da pessoa como um subrótulo. Essas informações estão incluídas na interface do utilizador, nos filtros e nas notificações.", + "modelSize": { + "small": { + "desc": "O uso de pequeno emprega um modelo de incorporação facial do FaceNet que funciona eficientemente na maioria dos CPUs.", + "title": "pequeno" + }, + "large": { + "desc": "O uso de grande emprega um modelo de incorporação de rostos do ArcFace e será executado automaticamente no GPU, se aplicável.", + "title": "grande" + }, + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo usado para reconhecimento facial." + }, + "title": "Reconhecimento facial", + "readTheDocumentation": "Leia a documentação" + }, + "semanticSearch": { + "modelSize": { + "small": { + "desc": "Usar pequeno emprega uma versão quantizada do modelo que usa menos RAM e roda mais rápido no CPU, com uma diferença muito insignificante na qualidade de incorporação.", + "title": "pequeno" + }, + "label": "Tamanho do modelo", + "desc": "O tamanho do modelo usado para incorporações de pesquisa semântica.", + "large": { + "title": "grande", + "desc": "Usar grande emprega o modelo Jina completo e será executado automaticamente no GPU, se aplicável." + } + }, + "reindexNow": { + "desc": "A reindexação regenerará os embeddings para todos os objetos rastreados. Esse processo é executado em segundo plano e pode sobrecarregar o seu CPU e levar um tempo considerável, dependendo do número de objetos rastreados.", + "label": "Reindexar agora", + "confirmTitle": "Confirmar reindexação", + "confirmDesc": "Tem certeza de que deseja reindexar todos os objetos incorporados rastreados? Este processo será executado em segundo plano, mas pode sobrecarregar o seu CPU e levar bastante tempo. Você pode acompanhar o progresso na página Explorar.", + "confirmButton": "Reindexar", + "success": "Reindexação iniciada com sucesso.", + "alreadyInProgress": "A reindexação já está em andamento.", + "error": "Falha ao iniciar a reindexação: {{errorMessage}}" + }, + "desc": "A Pesquisa Semântica no Frigate permite que você encontre objetos rastreados dentro dos seus itens de análise usando a própria imagem, uma descrição de texto definida pelo utilizador ou uma gerada automaticamente.", + "readTheDocumentation": "Leia a documentação", + "title": "Busca semântica" + }, + "licensePlateRecognition": { + "desc": "O Frigate pode reconhecer placas de veículos e adicionar automaticamente os caracteres detectados ao campo recognized_license_plate ou um nome conhecido como subrótulo para objetos do tipo carro. Um caso de uso comum pode ser a leitura de placas de carros entrando numa garagem ou de carros passando por uma rua.", + "title": "Reconhecimento de placas", + "readTheDocumentation": "Leia a documentação" + }, + "birdClassification": { + "desc": "A classificação de aves identifica aves conhecidas usando um modelo quantizado do Tensorflow. Quando uma ave conhecida é reconhecida, seu nome comum é adicionado como um sub_label. Essas informações são incluídas na interface do utilizador, nos filtros e nas notificações.", + "title": "Classificação de aves" + }, + "unsavedChanges": "Alterações nas configurações de enriquecimentos não salvos", + "title": "Configurações de enriquecimentos", + "restart_required": "Reinicialização necessária (configurações de enriquecimento alteradas)", + "toast": { + "success": "As configurações de enriquecimento foram salvas. Reinicie o Frigate para aplicar as alterações.", + "error": "Falha ao salvar alterações de configuração: {{errorMessage}}" + } + }, + "users": { + "dialog": { + "changeRole": { + "roleInfo": { + "admin": "Administrador", + "adminDesc": "Acesso total a todos os recursos.", + "viewer": "Visualização", + "viewerDesc": "Limitado apenas a painéis ao vivo, análise, exploração e exportações.", + "intro": "Selecione a função apropriada para este utilizador:", + "customDesc": "Papel customizado com acesso a câmaras específicas." + }, + "title": "Alterar função do utilizador", + "desc": "Atualizar permissões para {{username}}", + "select": "Selecione uma função" + }, + "deleteUser": { + "title": "Excluir utilizador", + "desc": "Esta ação não pode ser desfeita. Isso excluirá permanentemente a conta do utilizador e removerá todos os dados associados.", + "warn": "Tem certeza de que deseja excluir {{username}}?" + }, + "form": { + "user": { + "title": "Nome de utilizador", + "desc": "Somente letras, números, pontos e sublinhados são permitidos.", + "placeholder": "Digite o nome de utilizador" + }, + "password": { + "strength": { + "weak": "Fraco", + "medium": "Médio", + "strong": "Forte", + "title": "Força da senha: ", + "veryStrong": "Muito forte" + }, + "notMatch": "As senhas não correspondem", + "title": "Senha", + "placeholder": "Digite a senha", + "confirm": { + "title": "Digite uma senha", + "placeholder": "Confirme sua senha" + }, + "match": "Correspondência de senhas" + }, + "newPassword": { + "title": "Nova Senha", + "placeholder": "Digite a nova senha", + "confirm": { + "placeholder": "Digite novamente a nova senha" + } + }, + "usernameIsRequired": "O nome de utilizador é obrigatório", + "passwordIsRequired": "A senha é obrigatória" + }, + "createUser": { + "title": "Criar novo utilizador", + "desc": "Adicione uma nova conta de utilizador e especifique uma função para acesso a áreas da interface do utilizador do Frigate.", + "usernameOnlyInclude": "O nome de utilizador pode incluir apenas letras, números, . ou _", + "confirmPassword": "Por favor confirme sua senha" + }, + "passwordSetting": { + "setPassword": "Definir Senha", + "desc": "Crie uma senha forte para proteger esta conta.", + "updatePassword": "Atualizar senha para {{username}}", + "cannotBeEmpty": "A senha não pode ficar vazia", + "doNotMatch": "As senhas não correspondem" + } + }, + "management": { + "desc": "Gestão de utilizadores desta instância do Frigate.", + "title": "Gestão de Utilizadores" + }, + "table": { + "noUsers": "Nenhum utilizador encontrado.", + "password": "Senha", + "deleteUser": "Excluir utilizador", + "changeRole": "Alterar função do utilizador", + "username": "Nome de utilizador", + "actions": "Ações", + "role": "Papel" + }, + "title": "Utilizadores", + "addUser": "Adicionar utilizador", + "updatePassword": "Atualizar senha", + "toast": { + "success": { + "createUser": "Utilizador {{user}} criado com sucesso", + "deleteUser": "Utilizador {{user}} excluído com sucesso", + "updatePassword": "Senha atualizada com sucesso.", + "roleUpdated": "Função atualizada para {{user}}" + }, + "error": { + "setPasswordFailed": "Falha ao salvar a senha: {{errorMessage}}", + "createUserFailed": "Falha ao criar utilizador: {{errorMessage}}", + "deleteUserFailed": "Falha ao excluir o utilizador: {{errorMessage}}", + "roleUpdateFailed": "Falha ao atualizar a função: {{errorMessage}}" + } + } + }, + "triggers": { + "documentTitle": "Triggers (gatilhos)", + "management": { + "title": "Gestão de Triggers", + "desc": "Gira triggers para {{camera}}. Use o tipo de miniatura para acionar miniaturas semelhantes ao objeto rastreado selecionado e o tipo de descrição para acionar descrições semelhantes ao texto especificado." + }, + "addTrigger": "Adicionar Trigger", + "table": { + "name": "Nome", + "type": "Tipo", + "content": "Conteúdo", + "threshold": "Limite", + "actions": "Ações", + "noTriggers": "Nenhum trigger configurado para esta câmera.", + "edit": "Editar", + "deleteTrigger": "Apagar Trigger", + "lastTriggered": "Último acionado" + }, + "type": { + "thumbnail": "Miniatura", + "description": "Descrição" + }, + "actions": { + "alert": "Marcar como Alerta", + "notification": "Enviar Notificação" + }, + "dialog": { + "createTrigger": { + "title": "Criar Trigger", + "desc": "Crie um trigger para a câmera {{camera}}" + }, + "editTrigger": { + "title": "Editar Trigger", + "desc": "Editar as definições do trigger na câmera {{camera}}" + }, + "deleteTrigger": { + "title": "Apagar Trigger", + "desc": "Tem certeza de que deseja apagar o trigger {{triggerName}}? Esta ação não pode ser desfeita." + }, + "form": { + "name": { + "title": "Nome", + "placeholder": "Insira o nome do trigger", + "error": { + "minLength": "O nome deve ter pelo menos 2 caracteres.", + "invalidCharacters": "O nome só pode conter letras, números, sublinhados e hifens.", + "alreadyExists": "Já existe um trigger com este nome para esta câmera." + } + }, + "enabled": { + "description": "Habilitar ou desabilitar este trigger" + }, + "type": { + "title": "Tipo", + "placeholder": "Selecione o tipo de trigger" + }, + "content": { + "title": "Conteúdo", + "imagePlaceholder": "Selecione uma imagem", + "textPlaceholder": "Insira o conteúdo do texto", + "imageDesc": "Selecione uma imagem para acionar esta ação quando uma imagem semelhante for detectada.", + "textDesc": "Insira um texto para acionar esta ação quando uma descrição de objeto rastreado semelhante for detectada.", + "error": { + "required": "O Conteúdo é obrigatório." + } + }, + "threshold": { + "title": "Limite", + "error": { + "min": "Limite deve ser pelo menos 0", + "max": "Limite deve ser no máximo 1" + } + }, + "actions": { + "title": "Ações", + "desc": "Por padrão, o Frigate envia uma mensagem MQTT para todos os triggers. Escolha uma ação adicional a ser executada quando este trigger for disparado.", + "error": { + "min": "Pelo menos uma ação deve ser selecionada." + } + }, + "friendly_name": { + "title": "Nome Amigável", + "placeholder": "Nomeie ou descreva este gatilho", + "description": "Um nome amigável ou descritivo opcional para este gatilho." + } + } + }, + "toast": { + "success": { + "createTrigger": "Trigger {{name}} criado com sucesso.", + "updateTrigger": "Trigger {{name}} atualizado com sucesso.", + "deleteTrigger": "Trigger {{name}} apagado com sucesso." + }, + "error": { + "createTriggerFailed": "Falha ao criar trigger: {{errorMessage}}", + "updateTriggerFailed": "Falha ao atualizar o trigger: {{errorMessage}}", + "deleteTriggerFailed": "Falha ao apagar o trigger: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Pesquisa Semântica desativada", + "desc": "Pesquisa Semântica deve estar ativada para usar os Gatilhos." + } + }, + "roles": { + "management": { + "title": "Gestão do Papel de Visualizador", + "desc": "Gerir papéis de visualizador customizados e as suas permissões de acesso para esta instância do Frigate." + }, + "addRole": "Adicionar Papel", + "table": { + "role": "Papel", + "cameras": "Câmaras", + "actions": "Ações", + "noRoles": "Nenhum papel customizado encontrado.", + "editCameras": "Editar Câmaras", + "deleteRole": "Apagar Papel" + }, + "toast": { + "success": { + "createRole": "Papel {{role}} criado com sucesso", + "updateCameras": "Câmaras atualizados para o papel {{role}}", + "deleteRole": "Papel {{role}} apagado com sucesso", + "userRolesUpdated_one": "{{count}} utilizador(os) atribuídos a este papel foram atualizados para 'visualizador', que possui acesso a todas as câmaras.", + "userRolesUpdated_many": "", + "userRolesUpdated_other": "" + }, + "error": { + "createRoleFailed": "Falha ao criar papel: {{errorMessage}}", + "updateCamerasFailed": "Falha ao atualizar câmaras: {{errorMessage}}", + "deleteRoleFailed": "Falha ao apagar papel: {{errorMessage}}", + "userUpdateFailed": "Falha ao atualizar papel do utilizador: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Criar Novo Papel", + "desc": "Adicionar um novo papel e especificar permissões de acesso." + }, + "editCameras": { + "title": "Editar Câmaras de Papéis", + "desc": "Atualizar acesso da câmara para o papel {{role}}." + }, + "deleteRole": { + "title": "Apagar Papel", + "desc": "Esta ação não pode ser desfeita. Isto irá apagar permanentemente o papel e atribuir a quaisquer utilizadores com este papel como 'visualizador', o que dará acesso de visualização para todas as câmaras.", + "warn": "Tem certeza que quer apagar {{role}}?", + "deleting": "A apagar…" + }, + "form": { + "role": { + "title": "Nome do Papel", + "placeholder": "Digitar nome do papel", + "desc": "Apenas letras, números, pontos e sublinhados são permitidos.", + "roleIsRequired": "Nome para o papel é requerido", + "roleOnlyInclude": "O nome do papel pode conter apenas letras, números, pontos ou sublinhados", + "roleExists": "Um papel com este nome já existe." + }, + "cameras": { + "title": "Câmaras", + "desc": "Selecione as câmaras que este papel terá acesso. Ao menos uma câmara é requerida.", + "required": "Ao menos uma câmara deve ser selecionada." + } + } + } + }, + "cameraWizard": { + "title": "Adicionar Câmara", + "description": "Siga os passos abaixo para adicionar uma câmara nova no seu Frigate.", + "steps": { + "nameAndConnection": "Nome e Conexão", + "streamConfiguration": "Configuração de Stream", + "validationAndTesting": "Validação e Teste" + }, + "save": { + "success": "Nova câmara {{cameraName}} grava com sucesso.", + "failure": "Erro ao gravar {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolução", + "video": "Vídeo", + "audio": "Áudio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Favor fornecer uma URL de stream válida", + "testFailed": "Teste de stream falhou: {{error}}" + }, + "step1": { + "description": "Adicione os pormenores da sua câmara e teste a conexão.", + "cameraName": "Nome da Câmara", + "cameraNamePlaceholder": "ex., porta_entrada ou Visão Geral do Quintal", + "host": "Host/Endereço IP", + "port": "Porta", + "username": "Nome de Utilizador", + "usernamePlaceholder": "Opcional", + "password": "Palavra-passe", + "passwordPlaceholder": "Opcional", + "selectTransport": "Selecionar protocolo de transporte", + "cameraBrand": "Marca da Câmara", + "selectBrand": "Selecione a marca da câmara para template de URL", + "customUrl": "URL Customizada de Stream", + "brandInformation": "Informação da marca", + "brandUrlFormat": "Para câmaras com o formato de URL RTSP como: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://nomedeusuario:palavra-passe@host:porta/caminho", + "testConnection": "Testar Conexão", + "testSuccess": "Teste de conexão bem sucedido!", + "testFailed": "Teste de conexão falhou. Favor verifique os dados e tente novamente.", + "streamDetails": "Pormenores do Stream", + "warnings": { + "noSnapshot": "Não foi possível adquirir uma captura de imagem do stream configurado." + }, + "errors": { + "brandOrCustomUrlRequired": "Selecione a marca da câmara com o host/IP or selecione 'Outro' com uma URL customizada", + "nameRequired": "Nome para a câmara requerido" + } + }, + "step2": { + "url": "URL", + "roleLabels": { + "audio": "Áudio" + } + }, + "step3": { + "reload": "Recarregar", + "valid": "Válido", + "failed": "Falhou", + "none": "Nenhum", + "error": "Erro" + } + }, + "cameraManagement": { + "cameraConfig": { + "enabled": "Ativado", + "addUrl": "Adicionar URL" + } + }, + "cameraReview": { + "review": { + "title": "Rever" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/pt/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/pt/views/system.json new file mode 100644 index 0000000..9f90073 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/pt/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "storage": "Frigate - Estatísticas de Armazenamento", + "general": "Frigate - Estatísticas Gerais", + "enrichments": "Frigate - Estatísticas de Enriquecimento", + "logs": { + "frigate": "Frigate - Registos de Eventos do Frigate", + "go2rtc": "Frigate - Registos de Eventos do Go2RTC", + "nginx": "Frigate - Registos de Eventos do Nginx" + }, + "cameras": "Frigate - Estatísticas das Câmaras" + }, + "title": "Sistema", + "metrics": "Métricas do sistema", + "logs": { + "type": { + "label": "Tipo", + "timestamp": "Carimbo de hora", + "tag": "Etiqueta", + "message": "Mensagem" + }, + "copy": { + "success": "Registos copiados para a área de transferência", + "label": "Copiar para a Área de Transferência", + "error": "Não foi possível copiar os registos para a área de transferência" + }, + "download": { + "label": "Transferir Registos" + }, + "tips": "Os registos estão a ser transmitidos do servidor", + "toast": { + "error": { + "fetchingLogsFailed": "Erro ao obter os registos: {{errorMessage}}", + "whileStreamingLogs": "Erro enquanto transmitia os registos: {{errorMessage}}" + } + } + }, + "storage": { + "cameraStorage": { + "camera": "Câmara", + "storageUsed": "Armazenamento", + "percentageOfTotalUsed": "Porcentagem do total", + "bandwidth": "Largura de banda", + "unused": { + "tips": "Este valor pode não representar com precisão o espaço livre disponível para o Frigate se você tiver outros ficheiros armazenados em sua unidade além das gravações do Frigate. O Frigate não rastreia o uso de armazenamento fora de suas gravações.", + "title": "Não utilizado" + }, + "unusedStorageInformation": "Informações de armazenamento não utilizado", + "title": "Armazenamento da câmara" + }, + "title": "Armazenamento", + "overview": "Sinopse", + "recordings": { + "title": "Gravações", + "earliestRecording": "Primeira gravação disponível:", + "tips": "Este valor representa o armazenamento total utilizado pelas gravações na base de dados do Frigate. O Frigate não acompanha a utilização do armazenamento de todos os ficheiros no seu disco." + }, + "shm": { + "title": "Alocação SHM (memória partilhada)", + "warning": "A tamanho atual de SHM de {{total}} MB é muito pequeno. Aumente-o para pelo menos {{min_shm}} MB." + } + }, + "cameras": { + "title": "Câmaras", + "info": { + "video": "Vídeo:", + "unknown": "Desconhecido", + "error": "Erro: {{error}}", + "fetching": "Obtendo dados da câmara", + "resolution": "Resolução:", + "codec": "Codec:", + "fps": "FPS:", + "stream": "Transmissão {{idx}}", + "audio": "Áudio:", + "cameraProbeInfo": "{{camera}} Explorar informações da Camara", + "tips": { + "title": "Explorar informações da Camara" + }, + "streamDataFromFFPROBE": "Os dados de transmissão são obtidos com ffprobe.", + "aspectRatio": "relação de aspeto" + }, + "framesAndDetections": "Quadros / Detecções", + "label": { + "camera": "câmara", + "detect": "detectar", + "capture": "capturar", + "skipped": "ignorado", + "ffmpeg": "FFmpeg", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "imagens por segundo de {{camName}}", + "cameraCapture": "captura de {{camName}}", + "cameraDetectionsPerSecond": "deteções por segundo de {{camName}}", + "overallFramesPerSecond": "imagens por segundo totais (FPS)", + "overallDetectionsPerSecond": "deteções por segundo totais", + "overallSkippedDetectionsPerSecond": "deteções ignoradas por segundo totais", + "cameraDetect": "deteção de {{camName}}", + "cameraSkippedDetectionsPerSecond": "deteções ignoradas por segundo de {{camName}}" + }, + "overview": "Visão geral", + "toast": { + "success": { + "copyToClipboard": "Dados de exploração copiados para a área de transferência." + }, + "error": { + "unableToProbeCamera": "Não foi possível explorar a câmara: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Última atualização: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} tem alta utilização da CPU FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} tem alta utilização da CPU de deteção ({{detectAvg}}%)", + "healthy": "O sistema está saudável", + "reindexingEmbeddings": "Reindexando incorporações ({{processed}}% completo)", + "detectIsVerySlow": "{{detect}} está muito lento ({{speed}} ms)", + "cameraIsOffline": "{{camera}} está off-line", + "detectIsSlow": "{{detect}} está lento ({{speed}} ms)", + "shmTooLow": "/dev/shm alocação ({{total}} MB) deveria ser aumentada pelo menos {{min}} MB." + }, + "general": { + "title": "Geral", + "detector": { + "title": "Detetores", + "cpuUsage": "Utilização do CPU do Detetor", + "memoryUsage": "Utilização da Memória do Detetor", + "inferenceSpeed": "Velocidade de Inferência do Detetor", + "temperature": "Temperatura do Detetor", + "cpuUsageInformation": "CPU utilizada na preparação de dados de entrada e saída de/para os modelos de deteção. Este valor não mede oa utilização da inferência, mesmo se estiver a utilizar uma GPU ou acelerador." + }, + "hardwareInfo": { + "title": "Informação de Hardware", + "gpuUsage": "Utilização da GPU", + "gpuMemory": "Memória da GPU", + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "Controlador: {{driver}}", + "vbios": "Informação VBios: {{vbios}}", + "name": "Nome: {{name}}", + "cudaComputerCapability": "Capacidade de computação CUDA: {{cuda_compute}}", + "title": "Saída Nvidia SMI" + }, + "copyInfo": { + "label": "Copiar informação da GPU" + }, + "closeInfo": { + "label": "Fechar informação da GPU" + }, + "toast": { + "success": "Informação da GPU copiada para a área de transferência" + }, + "vainfoOutput": { + "title": "Saída do Vainfo", + "returnCode": "Código de retorno: {{code}}", + "processOutput": "Saída do processo:", + "processError": "Erro no processo:" + } + }, + "gpuEncoder": "Codificador da GPU", + "gpuDecoder": "Descodificador da GPU", + "npuUsage": "Utilização NPU", + "npuMemory": "Memória NPU" + }, + "otherProcesses": { + "title": "Outros Processos", + "processCpuUsage": "Utilização da CPU do Processo", + "processMemoryUsage": "Utilização da Memória do Processo" + } + }, + "enrichments": { + "title": "Enriquecimentos", + "infPerSecond": "Inferências por Segundo", + "embeddings": { + "image_embedding_speed": "Velocidade de Incorporação de Imagem", + "face_embedding_speed": "Velocidade de Incorporação Facial", + "plate_recognition_speed": "Velocidade de Reconhecimento de Placas", + "text_embedding_speed": "Velocidade de Incorporação de Texto", + "face_recognition_speed": "Velocidade de Reconhecimento Facial", + "plate_recognition": "Reconhecimento de Placas", + "image_embedding": "Incorporação de Imagem", + "text_embedding": "Incorporação de Texto", + "face_recognition": "Reconhecimento Facial", + "yolov9_plate_detection_speed": "Velocidade de Deteção de Placas YOLOv9", + "yolov9_plate_detection": "Deteção de Placas YOLOv9" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ro/audio.json new file mode 100644 index 0000000..56815c6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/audio.json @@ -0,0 +1,503 @@ +{ + "gunshot": "Foc de arma", + "machine_gun": "Mitraliera", + "speech": "Vorbire", + "babbling": "Murmur", + "yell": "Striga", + "bellow": "Sub", + "dog": "Câine", + "horse": "Cal", + "bird": "Pasare", + "sheep": "Oaie", + "boat": "Barcă", + "motorcycle": "Motocicletă", + "bus": "Autobuz", + "train": "Tren", + "skateboard": "Skateboard", + "camera": "Camera foto", + "bicycle": "Bicicletă", + "car": "Mașină", + "cat": "Pisică", + "animal": "Animal", + "goat": "Capra", + "keyboard": "Orga", + "vehicle": "Vehicul", + "sink": "Chiuveta", + "scissors": "Foarfeca", + "hair_dryer": "Uscator de Par", + "door": "Usa", + "blender": "Blender", + "mouse": "Soarece", + "clock": "Ceas", + "toothbrush": "Periuta de Dinti", + "bark": "Latrat", + "burping": "Ragaie", + "hiccup": "Sughite", + "bass_drum": "Toba Bass", + "fart": "Basina", + "bell": "Clopotel", + "reggae": "Reggae", + "accordion": "Acordeon", + "trombone": "Trombon", + "punk_rock": "Punk rock", + "church_bell": "Clopot de biserica", + "sanding": "Slefuire", + "whispering": "Soapte", + "laughter": "Raset", + "crying": "Planset", + "choir": "Cor", + "singing": "Canta", + "whoop": "Tusi", + "yodeling": "Vocalize", + "snicker": "Chicotit", + "sigh": "Suspin", + "mantra": "Mantră", + "child_singing": "Cantec de copil", + "snoring": "Sforaie", + "whistling": "Fluiera", + "breathing": "Respira", + "cough": "Tuseste", + "throat_clearing": "Curata gatul", + "wheeze": "Gafaie", + "gasp": "Suspina", + "snort": "Horcaie", + "humming": "Fredoneaza", + "groan": "Geamat", + "grunt": "Mormait", + "pant": "Gafaie", + "sneeze": "stranuta", + "sniff": "Adulmeca", + "run": "Fuge", + "footsteps": "Pasi", + "chewing": "Mesteca", + "hands": "Maini", + "clapping": "Aplauda", + "heartbeat": "Batai inima", + "cheering": "Incurajeaza", + "applause": "Aplauda", + "crowd": "Multime", + "pets": "Animal de companie", + "purr": "Toarce", + "meow": "Miau", + "duck": "Rata", + "quack": "Mac", + "goose": "Gasca", + "wild_animals": "Animal Salbatic", + "cattle": "Vita", + "moo": "Muu", + "cowbell": "Clopot", + "pig": "Porc", + "oink": "Guit", + "chicken": "Gaina", + "cock_a_doodle_doo": "Cucurigu", + "turkey": "Curcan", + "roar": "Raget", + "chirp": "Cipcirit", + "pigeon": "Porumbel", + "crow": "Cioara", + "owl": "Bufnita", + "dogs": "Caini", + "rats": "Sobolani", + "insect": "Insecta", + "cricket": "Greier", + "mosquito": "Tantar", + "fly": "Musca", + "frog": "Broasca", + "snake": "Sarpe", + "music": "Muzica", + "musical_instrument": "Instrument Muzical", + "electric_guitar": "Chitara Electronica", + "guitar": "Chitara", + "bass_guitar": "Chitara Bass", + "acoustic_guitar": "Chitara Acustica", + "tapping": "Bataie", + "banjo": "Banjo", + "mandolin": "Mandolina", + "piano": "Pian", + "electric_piano": "Pian Electronic", + "synthesizer": "Sintetizator", + "percussion": "Percutie", + "drum_kit": "Tobe", + "drum": "Toba", + "tambourine": "Tamburina", + "gong": "Gong", + "orchestra": "Orchestră", + "trumpet": "Trompeta", + "violin": "Vioară", + "cello": "Violoncel", + "flute": "Flaut", + "saxophone": "Saxofon", + "clarinet": "Clarinet", + "harp": "Harpa", + "bicycle_bell": "Sonerie de bicicletă", + "tuning_fork": "Diapazon", + "harmonica": "Muzicuta", + "bagpipes": "Cimpoi", + "pop_music": "Muzica Pop", + "hip_hop_music": "Muzica Hip-Hop", + "rock_music": "Muzica Rock", + "heavy_metal": "Heavy metal", + "progressive_rock": "Rock Progresiv", + "rock_and_roll": "Rock and Roll", + "soul_music": "Muzica Soul", + "funk": "Funk", + "folk_music": "Muzica Folk", + "country": "Muzica Country", + "jazz": "Jazz", + "disco": "Muzica Disco", + "classical_music": "Muzica Clasica", + "opera": "Opera", + "electronic_music": "Muzica Electronica", + "house_music": "Muzica House", + "techno": "Muzica Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Electronică", + "electronic_dance_music": "Muzica de dans electronica", + "ambient_music": "Muzica Ambientala", + "trance_music": "Muzica Trance", + "music_of_latin_america": "Muzica Latino", + "salsa_music": "Salsa", + "flamenco": "Flamenco", + "blues": "Blues", + "vocal_music": "Muzica Vocala", + "a_capella": "A Capella", + "music_of_africa": "Muzica Africana", + "christian_music": "Muzica Crestina", + "gospel_music": "Muzica Gaspel", + "music_of_asia": "Muzica Asiatica", + "music_of_bollywood": "Muzica Bollywood", + "traditional_music": "Muzica Traditionala", + "song": "Cantec", + "background_music": "Muzica de fundal", + "theme_music": "Muzica Tematica", + "jingle": "Colind", + "lullaby": "Muzica de adormit copiii", + "christmas_music": "Colind", + "dance_music": "Muzica Dance", + "wedding_music": "Muzica de nunta", + "happy_music": "Muzica Vesela", + "sad_music": "Muzica Trista", + "wind": "Vant", + "rustling_leaves": "Fosnet de frunze", + "wind_noise": "Zgomot de vant", + "thunderstorm": "Furtuna", + "thunder": "Tunet", + "water": "Apa", + "rain": "Ploaie", + "raindrop": "Picaturi", + "waterfall": "Cascada", + "ocean": "Ocean", + "waves": "Valuri", + "steam": "Abur", + "fire": "Foc", + "rowboat": "Barca cu vasle", + "sailboat": "Barca cu panza", + "motorboat": "Barca cu motor", + "ship": "Vapor", + "motor_vehicle": "Autovehicul", + "toot": "claxon", + "car_alarm": "Alarma de mașină", + "power_windows": "Geamuri electrice", + "skidding": "Derapaj", + "tire_squeal": "Scartait de roti", + "car_passing_by": "Mașină în trecere", + "race_car": "Mașină de curse", + "truck": "Camion", + "air_horn": "claxon", + "ice_cream_truck": "Mașină de înghețată", + "police_car": "Mașină de poliție", + "ambulance": "Ambulanta", + "fire_engine": "Mașină de pompieri", + "traffic_noise": "Zgomot de trafic", + "train_wheels_squealing": "Scartait de roti de tren", + "subway": "Metrou", + "aircraft": "Aeronava", + "aircraft_engine": "Motor de avion", + "jet_engine": "Motor cu reactie", + "propeller": "Elice", + "helicopter": "Elicopter", + "fixed-wing_aircraft": "Aeronava cu aripi fixe", + "engine": "Motor", + "lawn_mower": "Mașină de tuns iarba", + "chainsaw": "Drujba", + "engine_starting": "Pornire motor", + "idling": "Relanti", + "accelerating": "Accelerare", + "doorbell": "Sonerie", + "sliding_door": "Usa culisanta", + "slam": "Trantit", + "dishes": "Vase", + "cutlery": "Tacamuri", + "chopping": "Tocare", + "frying": "Prajire", + "microwave_oven": "Cuptor cu microunde", + "water_tap": "Robine", + "bathtub": "Cada", + "toilet_flush": "Tras apa", + "electric_toothbrush": "Periuta de dinti electronica", + "vacuum_cleaner": "Aspirator", + "keys_jangling": "Zornait de chei", + "coin": "Moneda", + "zipper": "Fermoar", + "electric_shaver": "Aparat de ras electric", + "shuffling_cards": "Amestecat de carti", + "typing": "Scrie", + "typewriter": "Mașină de scris", + "computer_keyboard": "Tastatura", + "writing": "Scrie", + "alarm": "Alarma", + "telephone": "Telefon", + "telephone_bell_ringing": "Sonerie de telefon", + "ringtone": "Ton de apel", + "dial_tone": "Ton", + "busy_signal": "Ocupat", + "alarm_clock": "Alarma de trezire", + "siren": "Sirena", + "smoke_detector": "Detector de fum", + "fire_alarm": "Alarma de incendiu", + "foghorn": "Sirena de ceata", + "whistle": "Fluierat", + "mechanisms": "Mecanism", + "gears": "Rotite", + "pulleys": "Scripeti", + "sewing_machine": "Mașină de cusut", + "mechanical_fan": "Ventilator mecanic", + "air_conditioning": "Aer Conditionat", + "printer": "Imprimanta", + "tools": "Unelte", + "hammer": "Ciocan", + "jackhammer": "Picamer", + "sawing": "Taiere", + "filing": "Umplere", + "power_tool": "Scule Electrice", + "drill": "Gaurire", + "explosion": "Explozie", + "artillery_fire": "Foc de artilerie", + "cap_gun": "Pistol cu capse", + "fireworks": "Foc de artificii", + "firecracker": "Pocnitoare", + "burst": "Izbucni", + "eruption": "Eruptie", + "boom": "Bum", + "wood": "Lemn", + "chop": "Reteza", + "splinter": "Aschie", + "crack": "Crapa", + "glass": "Geam", + "chink": "Fisura", + "shatter": "Sparge", + "silence": "Liniste", + "sound_effect": "Efect sonor", + "environmental_noise": "Zgomot de fundal", + "static": "Electrostatice", + "white_noise": "Zgomot alb", + "television": "Televizor", + "radio": "Radio", + "scream": "Tipa", + "chant": "Cântec", + "synthetic_singing": "Cântat sintetic", + "rapping": "Rap", + "shuffle": "Amestecă", + "biting": "Mușcare", + "gargling": "Gargară", + "stomach_rumble": "Gâdilitură stomacală", + "finger_snapping": "Pocnit din degete", + "heart_murmur": "Murmur inimă", + "chatter": "Conversații", + "children_playing": "Joacă copii", + "yip": "Lătrat", + "howl": "Urlet", + "bow_wow": "Ham-ham", + "growling": "Mârâit", + "whimper_dog": "Scheunat de câine", + "hiss": "Suflat", + "caterwaul": "Mieunat", + "livestock": "Animale de fermă", + "clip_clop": "Zgomot copite", + "neigh": "Nechezat", + "bleat": "Bâzâit", + "fowl": "Sunet păsări de curte", + "cluck": "Cotcodăcit", + "gobble": "Clocotit", + "honk": "Claxon", + "roaring_cats": "Răget", + "squawk": "Cârâit", + "coo": "Guguștiuc", + "caw": "Croncănit", + "hoot": "Huhuială", + "flapping_wings": "Fluturare aripi", + "patter": "Picurare", + "buzz": "Zumzăit", + "croak": "Orăcăit", + "rattle": "Zdrăngănit", + "whale_vocalization": "Vocalizare balenă", + "plucked_string_instrument": "Instrument cu corzi ciupite", + "steel_guitar": "Chitară cu bară metalică", + "strum": "Strângerea coardelor", + "sitar": "Sitar", + "zither": "Ziteră", + "ukulele": "Ukulele", + "organ": "Orgă", + "electronic_organ": "Orgă electronică", + "hammond_organ": "Orgă Hammond", + "sampler": "Eșantionator", + "harpsichord": "Clavecin", + "drum_machine": "Mașină de tobe", + "snare_drum": "Tobă mică", + "rimshot": "Bătaie pe tobă", + "drum_roll": "Rulou de tobe", + "timpani": "Timpane", + "tabla": "Tabla", + "cymbal": "Cinele", + "hi_hat": "Hi-Hat", + "wood_block": "Bloc de lemn", + "tubular_bells": "Clopote tubulare", + "mallet_percussion": "Percuție cu Baghete", + "marimba": "Marimbă", + "glockenspiel": "Glockenspiel", + "maraca": "Maracă", + "vibraphone": "Vibrafon", + "steelpan": "Steelpan", + "brass_instrument": "Instrument de alamă", + "french_horn": "Corn francez", + "bowed_string_instrument": "Instrument cu corzi cu arcuș", + "string_section": "Sectiunea corzi", + "pizzicato": "Pizzicato", + "double_bass": "Contrabas", + "wind_instrument": "Instrument de suflat", + "jingle_bell": "Zurgălău", + "chime": "Clopoțel", + "wind_chime": "Clopoțel de vânt", + "didgeridoo": "Didgeridoo", + "theremin": "Theremin", + "singing_bowl": "Bol cântător", + "scratching": "Zgâriere", + "beatboxing": "Beatboxing", + "grunge": "Grunge", + "psychedelic_rock": "Rock psihedelic", + "rhythm_and_blues": "R&B", + "swing_music": "Muzică swing", + "bluegrass": "Bluegrass", + "middle_eastern_music": "Muziă din Orientul mijlociu", + "music_for_children": "Muzică penru copii", + "new-age_music": "Muzică New Age", + "afrobeat": "Afrobeat", + "carnatic_music": "Muzică carnatică", + "ska": "Ska", + "independent_music": "Muzică independentă", + "soundtrack_music": "Muzica soundtrack", + "video_game_music": "Muzică de jocuri video", + "tender_music": "Muzică tandră", + "exciting_music": "Muzică antrenantă", + "angry_music": "Muzică furioasă", + "scary_music": "Muzică de speriat", + "rain_on_surface": "Ploaie pe suprafață", + "stream": "Stream", + "gurgling": "Gâlgâit", + "crackle": "Trosnet", + "air_brake": "Frână pneumatică", + "reversing_beeps": "Bipuri de mers înapoi", + "emergency_vehicle": "Vehicul de urgență", + "rail_transport": "Transportul feroviar", + "train_whistle": "Fluier tren", + "train_horn": "Goarnă tren", + "railroad_car": "Vagon", + "light_engine": "Motor ușor", + "dental_drill's_drill": "Burghiu dentar", + "medium_engine": "Motor mediu", + "heavy_engine": "Motor greu", + "engine_knocking": "Bătăi ale motorului", + "ding-dong": "Ding-Dong", + "knock": "Cioc-cioc", + "tap": "Apasă", + "squeak": "Screchet", + "cupboard_open_or_close": "Ușă dulap deschisă sau închisă", + "drawer_open_or_close": "Sertar deschis sau închis", + "telephone_dialing": "Formare apel telefonic", + "civil_defense_siren": "Sirena de apărare civilă", + "buzzer": "Buzzer", + "steam_whistle": "Fluier cu aburi", + "ratchet": "Clichet", + "tick": "Tic-tac", + "tick-tock": "Tic-tac", + "cash_register": "Casa de marcat", + "single-lens_reflex_camera": "Cameră reflex cu un singur obiectiv", + "fusillade": "descărcare de focuri", + "pink_noise": "Zgomot roz", + "field_recording": "Înregistrare pe teren", + "sodeling": "*Sodeling*", + "chird": "*Chird*", + "change_ringing": "Schimbă soneria", + "shofar": "Șofar", + "liquid": "Lichid", + "splash": "Stropire", + "slosh": "Sloș", + "squish": "Plescăit", + "drip": "Picur", + "pour": "Toarnă", + "trickle": "Picurare", + "gush": "Șuvoi", + "fill": "Umplere", + "spray": "Pulverizare", + "pump": "Pompă", + "stir": "Amestecare", + "boiling": "Fierbere", + "sonar": "Sonar", + "arrow": "Săgeată", + "whoosh": "Whoosh", + "thump": "Bufnitură", + "thunk": "Buft", + "electronic_tuner": "Tuner electronic", + "effects_unit": "Efect de unitate", + "chorus_effect": "Efect de cor", + "basketball_bounce": "Săritură minge basket", + "bang": "Bubuitură", + "slap": "Pălmuială", + "whack": "Lovitură", + "smash": "Zdrobitură", + "breaking": "Rupere", + "bouncing": "Saritură", + "whip": "Bici", + "flap": "fâlfâit", + "scratch": "Zgâriat", + "scrape": "Răzuire", + "rub": "Frecare", + "roll": "Rostogolire", + "crushing": "Spargere", + "crumpling": "Șifonare", + "tearing": "Sfâșiere", + "beep": "Bip", + "ping": "Ping", + "ding": "Ding", + "clang": "zăngănit", + "squeal": "Țipăt", + "creak": "Scârțâit", + "rustle": "Foșnet", + "whir": "Vuiet", + "clatter": "Zdrăngăneală", + "sizzle": "Sfârâit", + "clicking": "Clănțănit", + "clickety_clack": "Clănțăneală", + "rumble": "Bubuit", + "plop": "Plop", + "hum": "murmur", + "zing": "Zing", + "boing": "Boing", + "crunch": "ronţăire", + "sine_wave": "Unda Sinusoidală", + "harmonic": "Armonic", + "chirp_tone": "ton de ciripit", + "pulse": "Puls", + "inside": "În interior", + "outside": "Afară", + "reverberation": "Reverberație", + "echo": "Ecou", + "noise": "Gălăgie", + "mains_hum": "Zumzet principal", + "distortion": "Distorsionare", + "sidetone": "Ton lateral", + "cacophony": "Cacofonie", + "throbbing": "Trepidant", + "vibration": "Vibrație" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/common.json b/sam2-cpu/frigate-dev/web/public/locales/ro/common.json new file mode 100644 index 0000000..3707fc3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/common.json @@ -0,0 +1,304 @@ +{ + "time": { + "untilForTime": "Până la {{time}}", + "untilForRestart": "Pana la repornirea Frigate.", + "untilRestart": "Pana la repornire", + "ago": "{{timeAgo}} în urmă", + "justNow": "Acum", + "today": "Astăzi", + "yesterday": "Ieri", + "last7": "Ultimele 7 zile", + "last14": "Ultimele 14 zile", + "last30": "Ultimele 30 de zile", + "thisWeek": "Săptămâna aceasta", + "lastWeek": "Săptămâna trecută", + "thisMonth": "Luna aceasta", + "lastMonth": "Luna trecută", + "5minutes": "5 minute", + "10minutes": "10 minute", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyy" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "30minutes": "30 de minute", + "1hour": "1 oră", + "12hours": "12 ore", + "24hours": "24 de ore", + "pm": "PM", + "am": "AM", + "mo": "{{time}}lună", + "yr": "{{time}}an", + "year_one": "{{time}} an", + "year_few": "{{time}} ani", + "year_other": "{{time}} de ani", + "d": "{{time}}z", + "h": "{{time}}o", + "m": "{{time}}m", + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "month_one": "{{time}} lună", + "month_few": "{{time}} luni", + "month_other": "{{time}} de luni", + "day_one": "{{time}} zi", + "day_few": "{{time}} zile", + "day_other": "{{time}} de zile", + "hour_one": "{{time}} oră", + "hour_few": "{{time}} ore", + "hour_other": "{{time}} de ore", + "minute_one": "{{time}} minut", + "minute_few": "{{time}} minute", + "minute_other": "{{time}} de minute", + "second_one": "{{time}} secundă", + "second_few": "{{time}} secunde", + "second_other": "{{time}} de secunde", + "inProgress": "În desfășurare", + "invalidStartTime": "Oră de început invalidă", + "invalidEndTime": "Oră de sfârșit invalidă" + }, + "menu": { + "documentation": { + "title": "Documentație", + "label": "Documentație Frigate" + }, + "explore": "Căutare", + "uiPlayground": "UI Playground", + "faceLibrary": "Biblioteca de fețe", + "export": "Exportă", + "language": { + "ca": "Català (Catalană)", + "withSystem": { + "label": "Utilizează setările de limbă ale sistemului" + }, + "ja": "日本語 (Japoneză)", + "fa": "فارسی (Persiană)", + "pl": "Polski (Poloneză)", + "uk": "Українська (Ucrainiană)", + "he": "עברית (Ebraică)", + "yue": "粵語 (Cantoneză)", + "en": "English (Engleză)", + "de": "Deutsch (Germană)", + "es": "Español (Spaniolă)", + "zhCN": "简体中文 (Chineză simplificată)", + "hi": "हिन्दी (Hindi)", + "fr": "Français (Franceză)", + "ar": "العربية (Arabă)", + "pt": "Português (Portugheză)", + "ru": "Русский (Rusă)", + "tr": "Türkçe (Turcă)", + "it": "Italiano (Italiană)", + "nl": "Nederlands (Olandeză)", + "sv": "Svenska (Suedeză)", + "cs": "Čeština (Cehă)", + "nb": "Norsk Bokmål (Norvegiană Bokmål)", + "ko": "한국어 (Coreană)", + "vi": "Tiếng Việt (Vietnameză)", + "da": "Dansk (Daneză)", + "sk": "Slovenčina (Slovacă)", + "el": "Ελληνικά (Greacă)", + "ro": "Română (Română)", + "hu": "Magyar (Maghiară)", + "fi": "Suomi (Finlandeză)", + "th": "ไทย (Thailandeză)", + "ptBR": "Português brasileiro (Portugheză braziliană)", + "sr": "Српски (Sârbă)", + "sl": "Slovenščina (Slovenă)", + "lt": "Lietuvių (Lituaniană)", + "bg": "Български (Bulgară)", + "gl": "Galego (Galiciană)", + "id": "Bahasa Indonesia (Indoneziană)", + "ur": "اردو (Urdu)" + }, + "theme": { + "default": "Implicit", + "highcontrast": "Contrast ridicat", + "label": "Temă", + "blue": "Albastru", + "green": "Verde", + "nord": "Nord", + "red": "Roșu" + }, + "user": { + "title": "Utilizator", + "account": "Cont", + "current": "Utilizator actual: {{user}}", + "logout": "Deconectare", + "anonymous": "anonim", + "setPassword": "Schimă parola" + }, + "live": { + "cameras": { + "count_one": "{{count}} cameră", + "count_few": "{{count}} camere", + "count_other": "{{count}} de camere", + "title": "Camere" + }, + "title": "Live", + "allCameras": "Toate camerele" + }, + "help": "Ajutor", + "system": "Sistem", + "systemMetrics": "Metrici de sistem", + "configuration": "Configurație", + "systemLogs": "Jurnale de sistem", + "settings": "Setări", + "configurationEditor": "Editor de configurație", + "languages": "Limba", + "appearance": "Aspect", + "darkMode": { + "label": "Mod luminozitate", + "light": "Luminos", + "dark": "Întunecat", + "withSystem": { + "label": "Utilizează setările de sistem pentru modul luminos sau întunecat" + } + }, + "withSystem": "Modul sistemului", + "restart": "Repornește Frigate", + "review": "Revizuire", + "classification": "Clasificare" + }, + "button": { + "cameraAudio": "Sunet cameră", + "apply": "Aplică", + "reset": "Resetare", + "done": "Gata", + "enabled": "Activat", + "copyCoordinates": "Copiază coordonate", + "on": "PORNIT", + "off": "OPRIT", + "edit": "Editează", + "delete": "Șterge", + "yes": "Da", + "no": "Nu", + "download": "Descarcă", + "info": "Informații", + "enable": "Activează", + "twoWayTalk": "Conversație bidirecțională", + "disabled": "Dezactivat", + "disable": "Dezactivează", + "save": "Salvează", + "saving": "Se salvează…", + "cancel": "Renunță", + "close": "Închide", + "copy": "Copiază", + "back": "Înapoi", + "history": "Istorie", + "fullscreen": "Ecran complet", + "exitFullscreen": "ieși din ecran complet", + "pictureInPicture": "Imagine în imagine", + "suspended": "Suspendat", + "unsuspended": "Nesuspendat", + "play": "Redă", + "unselect": "Deselectează", + "export": "Exportă", + "deleteNow": "Șterge acum", + "next": "Următorul", + "continue": "Continuă" + }, + "unit": { + "speed": { + "mph": "mile/h", + "kph": "km/h" + }, + "length": { + "feet": "picioare", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/oră", + "mbph": "MB/oră", + "gbph": "GB/oră" + } + }, + "label": { + "back": "Mergi înapoi", + "hide": "Ascunde {{item}}", + "show": "Afișează {{item}}", + "ID": "ID", + "none": "Niciuna", + "all": "Toate" + }, + "selectItem": "Selectează {{item}}", + "pagination": { + "label": "paginare", + "next": { + "label": "Mergi la pagina următoare", + "title": "Următor" + }, + "previous": { + "title": "Anterior", + "label": "Meri la pagina anterioară" + }, + "more": "Mai multe pagini" + }, + "role": { + "viewer": "Vizualizator", + "desc": "Administratorii au acces complet la toate funcționalitățile din interfața Frigate. Vizualizatorii sunt limitați la vizualizarea camerelor, a elementelor de revizuire și a înregistrărilor istorice în interfață.", + "admin": "Administrator", + "title": "Rol" + }, + "toast": { + "copyUrlToClipboard": "URL-ul a fost copiat.", + "save": { + "title": "Salvează", + "error": { + "noMessage": "Nu s-au putut salva modificările de configurație", + "title": "Salvarea modificărilor de configurație a eșuat: {{errorMessage}}" + } + } + }, + "accessDenied": { + "title": "Acces refuzat", + "desc": "Nu ai permisiunea să vizualizezi această pagină.", + "documentTitle": "Acces refuzat - Frigate" + }, + "notFound": { + "documentTitle": "Nu a fost găsit - Frigate", + "title": "404", + "desc": "Pagină negăsită" + }, + "readTheDocumentation": "Citește documentația", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} și {{1}}", + "many": "{{items}}, și {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Opțional", + "internalID": "ID-ul Intern pe care Frigate îl folosește în configurație și în baza de date" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/auth.json new file mode 100644 index 0000000..cc3b592 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Nume utilizator", + "password": "Parola", + "login": "Logare", + "errors": { + "passwordRequired": "Parola este necesara", + "rateLimit": "Limita a fost depasita. Reincearca mai tarziu.", + "loginFailed": "Logare esuata", + "webUnknownError": "Eroare necunoscuta. Verifica logurile din consola.", + "usernameRequired": "Utilizatorul este necesar", + "unknownError": "Eroare necunoscuta. Verifica logurile." + }, + "firstTimeLogin": "Încercați să vă conectați pentru prima dată? Datele de autentificare sunt tipărite în jurnalele Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/camera.json new file mode 100644 index 0000000..5539636 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Grupuri de Camere", + "add": "Adaugă un grup de camere", + "edit": "Editează grupul de camere", + "delete": { + "label": "Șterge grupul de camere", + "confirm": { + "title": "Confirmă ștergerea", + "desc": "Ești sigur că dorești să ștergi grupul de camere {{name}}?" + } + }, + "name": { + "label": "Nume", + "placeholder": "Introdu un nume…", + "errorMessage": { + "mustLeastCharacters": "Numele grupului de cmere trebuie să conțină minim 2 caractere.", + "exists": "Numele grupului de camere există deja.", + "nameMustNotPeriod": "Numele grupului de camere nu trebuie să conțină punct.", + "invalid": "Nume invalid pentru grupul de camere." + } + }, + "cameras": { + "label": "Camere", + "desc": "Selectează camerele pentru acest grup." + }, + "icon": "Pictograma", + "success": "Grupul de camere ({{name}}) a fost salvat.", + "camera": { + "setting": { + "label": "Setările de streaming ale camerei", + "title": "{{cameraName}} Setări de streaming", + "stream": "Stream", + "placeholder": "Alege un stream", + "desc": "Schimbă opțiunile de streaming live pentru panoul acestui grup de camere. Aceste setări sunt specifice dispozitivului/browser-ului.", + "audioIsUnavailable": "Sunetul nu este disponibil pentru acest stream", + "audioIsAvailable": "Sunetul este disponibil pentru acest stream", + "audio": { + "tips": { + "title": "Sunetul trebuie să fie redat de camera ta și configurat în go2rtc pentru acest stream.", + "document": "Citește documentația " + } + }, + "streamMethod": { + "label": "Metoda de streaming", + "placeholder": "Alege o metodă de streaming", + "method": { + "noStreaming": { + "label": "Fără streaming", + "desc": "Imaginile camerelor se vor actualiza doar o dată pe minut și nu va exista streaming live." + }, + "smartStreaming": { + "label": "Streaming Inteligent (recomandat)", + "desc": "Streaming-ul inteligent va actualiza imaginea camerei o dată pe minut când nu există activitate detectabilă, pentru a economisi trafic de date și resurse. Când se detectează activitate, imaginea trece la streaming live." + }, + "continuousStreaming": { + "label": "Streaming continu", + "desc": { + "title": "Imaginea camerei va fi întotdeauna un stream live când este vizibilă pe panou, chiar dacă nu se detectează activitate.", + "warning": "Streaming-ul continuu poate provoca un consum mare de lățime de bandă și probleme de performanță. Folosește cu prudență." + } + } + } + }, + "compatibilityMode": { + "label": "Mod compatibilitate", + "desc": "Activează această opțiune doar dacă stream-ul live al camerei afișează artefacte de culoare și are o linie diagonală pe partea dreaptă a imaginii." + } + }, + "birdseye": "Vedere de ansamblu" + } + }, + "debug": { + "options": { + "label": "Setări", + "title": "Optiuni", + "showOptions": "Arata Optiuni", + "hideOptions": "Ascunde opțiunile" + }, + "boundingBox": "Casetă de delimitare", + "timestamp": "Marcaj temporal", + "zones": "Zone", + "mask": "Mască", + "motion": "Mișcare", + "regions": "Regiuni" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/dialog.json new file mode 100644 index 0000000..cbbbf71 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/dialog.json @@ -0,0 +1,136 @@ +{ + "restart": { + "title": "Ești sigur că dorești să repornești Frigate?", + "button": "Repornește", + "restarting": { + "title": "Frigate repornește", + "content": "Această pagină se va reâncărca automat în {{countdown}} secunde.", + "button": "Forțează acum reîncărcarea" + } + }, + "explore": { + "plus": { + "review": { + "true": { + "label": "Confirma aceasta eticheta pentru Frigate Plus", + "true_one": "Asta e o {{label}}", + "true_few": "Astea sunt {{label}}", + "true_other": "Astea sunt {{label}}" + }, + "false": { + "label": "Nu confirma aceasta eticheta pentru Frigate Plus", + "false_one": "Asta nu este {{label}}", + "false_few": "Astea nu sunt {{label}}", + "false_other": "Astea nu sunt {{label}}" + }, + "state": { + "submitted": "Trimis" + }, + "question": { + "label": "Confirmă această etichetă pentru Frigate Plus", + "ask_a": "Este acest obiect un {{label}}?", + "ask_an": "Este acest obiect un {{label}}?", + "ask_full": "Este acest obiect un {{untranslatedLabel}} ({{translatedLabel}})?" + } + }, + "submitToPlus": { + "label": "Trimite catre Frigate+", + "desc": "Obiectele din locațiile pe care dorești să le eviți nu sunt false-pozitive. Marcarea lor ca false-pozitive va induce confuzie modelul." + } + }, + "video": { + "viewInHistory": "Vezi în istoric" + } + }, + "recording": { + "button": { + "deleteNow": "Șterge acum", + "export": "Exportă", + "markAsReviewed": "Marchează ca revizuit", + "markAsUnreviewed": "Marchează ca nerevizuit" + }, + "confirmDelete": { + "toast": { + "success": "Înregistrările video asociate elementelor de revizuire selectate au fost șterse cu succes.", + "error": "Ștergerea a eșuat: {{error}}" + }, + "title": "Confirmă ștergerea", + "desc": { + "selected": "Ești sigur că vrei să ștergi toate videoclipurile înregistrate asociate acestui element de revizuire?

    Ține apăsată tasta Shift pentru a sări peste această confirmare pe viitor." + } + } + }, + "export": { + "time": { + "custom": "personalizat", + "fromTimeline": "Selectează din cronologie", + "lastHour_one": "Ultima oră", + "lastHour_few": "Ultimele {{count}} ore", + "lastHour_other": "Ultimele {{count}} ore", + "start": { + "title": "Ora de început", + "label": "Selectează ora de început" + }, + "end": { + "title": "Oră terminare", + "label": "Selectează ora de terminare" + } + }, + "name": { + "placeholder": "Denumește exportul" + }, + "select": "Selectează", + "export": "Exportă", + "selectOrExport": "Selectează sau exportă", + "toast": { + "success": "Exportul a început cu succes. Vizualizați fișierul pe pagina de exporturi.", + "error": { + "failed": "Eroare la pornirea exportului: {{error}}", + "endTimeMustAfterStartTime": "Ora de sfârșit trebuie să fie după ora de început", + "noVaildTimeSelected": "Nu a fost selectat un interval de timp valid" + }, + "view": "Vizualizează" + }, + "fromTimeline": { + "saveExport": "Salvează exportul", + "previewExport": "Previzualizează exportul" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Restreaming-ul nu este activat pentru această cameră.", + "desc": { + "title": "Configurează go2rtc pentru opțiuni suplimentare de vizualizare live și audio pentru această cameră.", + "readTheDocumentation": "Citește documentația" + } + }, + "showStats": { + "label": "Afișează statistici streaming", + "desc": "Activează această opțiune pentru a afișa statisticile de streaming ca un overlay peste stream-ul camerei." + }, + "debugView": "Vizualizator depanare" + }, + "search": { + "saveSearch": { + "label": "Salvează căutarea", + "desc": "Alege un nume pentru această căutare salvată.", + "placeholder": "Introdu un nume pentru căutarea ta", + "overwrite": "{{searchName}} există deja. Salvarea va suprascrie valoarea existentă.", + "success": "Căutarea ({{searchName}}) a fost salvată.", + "button": { + "save": { + "label": "Salvează această căutare" + } + } + } + }, + "imagePicker": { + "selectImage": "Selectează miniatura unui obiect urmărit", + "search": { + "placeholder": "Caută după etichetă sau subetichetă..." + }, + "noImages": "Nu s-au găsit miniaturi pentru această cameră", + "unknownLabel": "Imaginea declanșator salvată" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/filter.json new file mode 100644 index 0000000..9130c01 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filtru", + "labels": { + "label": "Etichete", + "all": { + "title": "Toate etichetele", + "short": "Etichete" + }, + "count_one": "Etichetă {{count}}", + "count_other": "{{count}} etichete" + }, + "dates": { + "selectPreset": "Selectează o presetare…", + "all": { + "title": "Toate datele", + "short": "Date" + } + }, + "zones": { + "label": "Zone", + "all": { + "title": "Toate zonele", + "short": "Zone" + } + }, + "reset": { + "label": "Resetează filtrele la valorile implicite" + }, + "timeRange": "Interval de timp", + "subLabels": { + "label": "Sub-etichete", + "all": "Toate sub-etichetele" + }, + "more": "Mai multe filtre", + "score": "Scor", + "estimatedSpeed": "Viteza estimată ({{unit}})", + "features": { + "label": "Caracteristici", + "hasSnapshot": "Are snapshot", + "hasVideoClip": "Are un videoclip", + "submittedToFrigatePlus": { + "label": "Trimis către Frigate+", + "tips": "Trebuie mai întâi să filtrezi obiectele urmărite care au un snapshot.

    Obiectele urmărite fără snapshot nu pot fi trimise către Frigate+." + } + }, + "sort": { + "label": "Sortează", + "dateAsc": "Dată (crescător)", + "dateDesc": "Dată (descrescător)", + "scoreAsc": "Scor obiect (crescător)", + "scoreDesc": "Scor obiect (descrescător)", + "speedAsc": "Viteză estimată (crescător)", + "speedDesc": "Viteză estimată (descrescător)", + "relevance": "Relevanță" + }, + "cameras": { + "all": { + "short": "Camere", + "title": "Toate camerele" + }, + "label": "Filtru camere" + }, + "review": { + "showReviewed": "Afișează cele revizuite" + }, + "motion": { + "showMotionOnly": "Afișează doar mișcarea" + }, + "explore": { + "settings": { + "title": "Setări", + "defaultView": { + "title": "Vizualizare implicită", + "unfilteredGrid": "Grilă nefiltrată", + "desc": "Când nu sunt selectate filtre, afișează un rezumat al celor mai recente obiecte urmărite pentru fiecare etichetă sau afișează o grilă nefiltrată.", + "summary": "Sumar" + }, + "gridColumns": { + "title": "Coloane grilă", + "desc": "Selectează numărul de coloane în vizualizarea grilă." + }, + "searchSource": { + "label": "Sursa căutării", + "desc": "Alege dacă dorești să cauți în miniaturi sau în descrierile obiectelor urmărite.", + "options": { + "thumbnailImage": "Imagine miniatură", + "description": "Descriere" + } + } + }, + "date": { + "selectDateBy": { + "label": "Selectează o dată pentru filtrare" + } + } + }, + "logSettings": { + "label": "Filtrează nivelul jurnalului", + "filterBySeverity": "Filtrează jurnalele după severitate", + "loading": { + "title": "Încărcare continuă", + "desc": "Când panoul de jurnale este derulat până jos, noile jurnale sunt afișate automat pe măsură ce sunt adăugate." + }, + "disableLogStreaming": "Dezactivează încărcarea continuă", + "allLogs": "Toate jurnalele" + }, + "trackedObjectDelete": { + "title": "Confirmă ștergerea", + "desc": "Ștergerea acestor {{objectLength}} obiecte urmărite elimină snapshot-ul, orice încorporări salvate și orice înregistrări asociate ciclului de viață al obiectului. Filmările înregistrate ale acestor obiecte urmărite în vizualizarea Istoric NU vor fi șterse.

    Ești sigur că dorești să continui?

    Țineți apăsată tasta Shift pentru a sări peste acest dialog în viitor.", + "toast": { + "success": "Obiectele urmărite au fost șterse cu succes.", + "error": "Ștergerea obiectelor urmărite a eșuat: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrează după masca de zonă" + }, + "recognizedLicensePlates": { + "title": "Numere de înmatriculare recunoscute", + "noLicensePlatesFound": "Nu s-au găsit plăcuțe de înmatriculare.", + "selectPlatesFromList": "Selectează una sau mai multe plăcuțe din listă.", + "loading": "Se încarcă numerele de înmatriculare recunoscute…", + "placeholder": "Caută plăcuțe de înmatriculare…", + "loadFailed": "Nu s-au putut încărca numerele de înmatriculare recunoscute.", + "selectAll": "Selectează tot", + "clearAll": "Elimină tot" + }, + "classes": { + "label": "Clase", + "all": { + "title": "Toate clasele" + }, + "count_one": "{{count}} Clasă", + "count_other": "{{count}} Clase" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/icons.json new file mode 100644 index 0000000..0d8ee62 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Selectează o pictogramă", + "search": { + "placeholder": "Caută o pictogramă…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/input.json new file mode 100644 index 0000000..8faa621 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Descarca Video", + "toast": { + "success": "A inceput descarcarea clipului ce contine articolul revizuit." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ro/components/player.json new file mode 100644 index 0000000..bbd8cea --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Nu au fost gasite inregistrari in perioada de timp mentionata", + "noPreviewFound": "Nu a fost gasita o Previzualizare", + "noPreviewFoundFor": "Nu există previzualizari pentru {{cameraName}}", + "submitFrigatePlus": { + "title": "Trimiteti acest cadru catre Frigate+?", + "submit": "Trimite" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 sau mai recent este necesar pentru acest tip de stream live.", + "streamOffline": { + "title": "Stream Offline", + "desc": "Nici un cadru nu a fost receptionat de la streamul {{cameraName}} detect, verifica logurile de eroare" + }, + "cameraDisabled": "Camera este dezactivata", + "stats": { + "streamType": { + "title": "Tip Stream:", + "short": "Tip" + }, + "bandwidth": { + "title": "Latime de Banda:", + "short": "Latime de Banda" + }, + "latency": { + "title": "Latenta:", + "value": "{{seconds}} secunde", + "short": { + "title": "Latenta", + "value": "{{seconds}} sec" + } + }, + "totalFrames": "Total Cadre:", + "droppedFrames": { + "title": "Cadre Pierdute:", + "short": { + "title": "Pierdut", + "value": "{{droppedFrames}} cadre" + } + }, + "decodedFrames": "Cadre Decodate:", + "droppedFrameRate": "Rata de Cadre Pierdute:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Eraoare trimitere Cadru catre Frigate+" + }, + "success": { + "submittedFrigatePlus": "Cadru trimis cu Succes catre Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ro/objects.json new file mode 100644 index 0000000..6c92d8b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Persoană", + "bicycle": "Bicicletă", + "car": "Mașină", + "airplane": "Avion", + "bus": "Autobuz", + "train": "Tren", + "boat": "Barcă", + "fire_hydrant": "Hidrant", + "street_sign": "Semn de Circulatie", + "stop_sign": "Semn de Stop", + "parking_meter": "Automat de Parcare", + "bench": "Bancheta", + "bird": "Pasare", + "cat": "Pisică", + "dog": "Câine", + "horse": "Cal", + "cow": "Vacă", + "elephant": "Elefant", + "bear": "Urs", + "giraffe": "Girafa", + "hat": "Palarie", + "backpack": "Rucsac", + "umbrella": "Umbrela", + "shoe": "Pantof", + "eye_glasses": "Ochelari", + "tie": "Cravata", + "suitcase": "Servieta", + "frisbee": "Frisbee", + "skis": "Schiuri", + "snowboard": "Placa de Snowboard", + "sports_ball": "Minge pentru Sport", + "kite": "Zmeu", + "baseball_bat": "Bata de Baseball", + "baseball_glove": "Manusa de Baseball", + "skateboard": "Skateboard", + "surfboard": "Placa de Surf", + "tennis_racket": "Racheta de Tenis", + "bottle": "Sticla", + "plate": "Placa", + "wine_glass": "Pahar de Vin", + "cup": "Ceasca", + "fork": "Furculita", + "knife": "Cutit", + "spoon": "Lingura", + "bowl": "Castron", + "banana": "Banana", + "apple": "Mar", + "motorcycle": "Motocicletă", + "traffic_light": "Semafor", + "sheep": "Oaie", + "zebra": "Zebra", + "handbag": "Geanta de mana", + "sandwich": "Sandwich", + "gls": "GLS", + "dpd": "DPD", + "sink": "Chiuveta", + "raccoon": "Raton", + "orange": "Portocala", + "laptop": "Laptop", + "fox": "Vulpe", + "animal": "Animal", + "package": "Pachet", + "remote": "Telecomanda", + "toilet": "Toaleta", + "amazon": "Amazon", + "broccoli": "Broccoli", + "carrot": "Morcov", + "hot_dog": "Hot Dog", + "dining_table": "Masa", + "hair_dryer": "Uscator de Par", + "pizza": "Pizza", + "donut": "Gogoasa", + "teddy_bear": "Ursulet de Plus", + "waste_bin": "Tomberon", + "cake": "Tort", + "window": "Fereastra", + "chair": "Scaun", + "door": "Usa", + "on_demand": "La Cerere", + "usps": "USPS", + "couch": "Canapea", + "blender": "Blender", + "scissors": "Foarfeca", + "cell_phone": "Telefon Mobil", + "potted_plant": "Ghiveci de Plante", + "bed": "Pat", + "refrigerator": "Frigider", + "mirror": "Oglinda", + "desk": "Birou", + "tv": "TV", + "ups": "UPS", + "fedex": "FedEx", + "mouse": "Soarece", + "keyboard": "Orga", + "microwave": "Microunde", + "oven": "Cuptor", + "rabbit": "Iepure", + "robot_lawnmower": "Robot de Tuns Iarba", + "toaster": "Prajitor de Paine", + "book": "Carte", + "clock": "Ceas", + "vase": "Vaza", + "toothbrush": "Periuta de Dinti", + "hair_brush": "Perie de Par", + "vehicle": "Vehicul", + "squirrel": "Veverita", + "deer": "Caprioara", + "bark": "Latrat", + "goat": "Capra", + "bbq_grill": "Gratar", + "face": "Fata", + "purolator": "Purolator", + "license_plate": "Numar de Inmatriculare", + "dhl": "DHL", + "an_post": "An Post", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/classificationModel.json new file mode 100644 index 0000000..47b2c13 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Modele de clasificare - Frigate", + "button": { + "deleteClassificationAttempts": "Șterge imaginile de clasificare", + "renameCategory": "Redenumește clasa", + "deleteCategory": "Șterge clasa", + "deleteImages": "Șterge imaginile", + "trainModel": "Antrenează modelul", + "addClassification": "Adaugă clasificare", + "deleteModels": "Șterge modelele", + "editModel": "Editează modelul" + }, + "toast": { + "success": { + "deletedCategory": "Clasă ștearsă", + "deletedImage": "Imagini șterse", + "categorizedImage": "Imagine clasificată cu succes", + "trainedModel": "Model antrenat cu succes.", + "trainingModel": "Antrenamentul modelului a fost pornit cu succes.", + "deletedModel_one": "{{count}} model șters cu succes", + "deletedModel_few": "{{count}} modele șterse cu succes", + "deletedModel_other": "{{count}} modele șterse cu succes", + "updatedModel": "Configurația modelului a fost actualizată cu succes", + "renamedCategory": "Clasa a fost redenumită cu succes în {{name}}" + }, + "error": { + "deleteImageFailed": "Ștergerea a eșuat: {{errorMessage}}", + "deleteCategoryFailed": "Ștergerea clasei a eșuat: {{errorMessage}}", + "categorizeFailed": "Categorisirea imaginii a eșuat: {{errorMessage}}", + "trainingFailed": "Antrenarea modelului a eșuat. Verifică jurnalele Frigate pentru detalii.", + "deleteModelFailed": "Ștergerea modelului a eșuat: {{errorMessage}}", + "updateModelFailed": "Actualizarea modelului a eșuat: {{errorMessage}}", + "renameCategoryFailed": "Redenumirea clasei a eșuat: {{errorMessage}}", + "trainingFailedToStart": "Nu s-a putut porni antrenarea modelului: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Șterge clasa", + "desc": "Sigur doriți să ștergeți clasa {{name}}? Aceasta va șterge permanent toate imaginile asociate și va necesita reantrenarea modelului.", + "minClassesTitle": "Nu se poate șterge clasa", + "minClassesDesc": "Un model de clasificare trebuie să aibă cel puțin 2 clase. Adaugă o altă clasă înainte de a o șterge pe aceasta." + }, + "deleteDatasetImages": { + "title": "Șterge imaginile setului de date", + "desc_one": "Sigur doriți să ștergeți {{count}} imagine din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului.", + "desc_few": "Sigur doriți să ștergeți {{count}} imagini din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului.", + "desc_other": "Sigur doriți să ștergeți {{count}} de imagini din {{dataset}}? Această acțiune nu poate fi anulată și va necesita reantrenarea modelului." + }, + "deleteTrainImages": { + "title": "Șterge imaginile de antrenament", + "desc_one": "Sigur doriți să ștergeți {{count}} imagine? Această acțiune nu poate fi anulată.", + "desc_few": "Sigur doriți să ștergeți {{count}} imagini? Această acțiune nu poate fi anulată.", + "desc_other": "Sigur doriți să ștergeți {{count}} de imagini? Această acțiune nu poate fi anulată." + }, + "renameCategory": { + "title": "Redenumește clasa", + "desc": "Introduceți un nume nou pentru {{name}}. Va trebui să reantrenați modelul pentru ca modificarea numelui să aibă efect." + }, + "description": { + "invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe." + }, + "train": { + "title": "Clasificări recente", + "titleShort": "Recent", + "aria": "Selectează clasificările recente" + }, + "categories": "Clase", + "createCategory": { + "new": "Creează clasă nouă" + }, + "categorizeImageAs": "Clasifică imaginea ca:", + "categorizeImage": "Clasifică imaginea", + "noModels": { + "object": { + "title": "Nu există modele de clasificare a obiectelor", + "description": "Creează un model personalizat pentru a clasifica obiectele detectate.", + "buttonText": "Creează model de obiect" + }, + "state": { + "title": "Nu există modele de clasificare a stării", + "description": "Creează un model personalizat pentru a monitoriza și clasifica schimbările de stare în anumite zone ale camerei.", + "buttonText": "Creează model de stare" + } + }, + "wizard": { + "title": "Creează clasificare nouă", + "steps": { + "nameAndDefine": "Numire și definire", + "stateArea": "Zona de stare", + "chooseExamples": "Alege exemple" + }, + "step1": { + "description": "Modelele de stare monitorizează zone fixe ale camerei pentru schimbări (de exemplu, ușă deschisă/închisă). Modelele de obiect adaugă clasificări obiectelor detectate (de exemplu, animale cunoscute, curieri etc.).", + "name": "Nume", + "namePlaceholder": "Introduceți numele modelului...", + "type": "Tip", + "typeState": "Stare", + "typeObject": "Obiect", + "objectLabel": "Etichetă obiect", + "objectLabelPlaceholder": "Selectează tipul obiectului...", + "classificationType": "Tip de clasificare", + "classificationTypeTip": "Află despre tipurile de clasificare", + "classificationTypeDesc": "Subetichetele adaugă text suplimentar la eticheta obiectului (de exemplu, 'Persoană: UPS'). Atributele sunt metadate căutabile, stocate separat în metadatele obiectului.", + "classificationSubLabel": "Subeticheta", + "classificationAttribute": "Atribut", + "classes": "Clase", + "classesTip": "Află despre clase", + "classesStateDesc": "Definește diferitele stări în care poate fi zona camerei tale. De exemplu: 'deschis' și 'închis' pentru o ușă de garaj.", + "classesObjectDesc": "Definește diferitele categorii în care să fie clasificate obiectele detectate. De exemplu: 'curier', 'rezident', 'necunoscut' pentru clasificarea persoanelor.", + "classPlaceholder": "Introduceți numele clasei...", + "errors": { + "nameRequired": "Numele modelului este obligatoriu", + "nameLength": "Numele modelului trebuie să aibă 64 de caractere sau mai puțin", + "nameOnlyNumbers": "Numele modelului nu poate conține doar cifre", + "classRequired": "Este necesară cel puțin 1 clasă", + "classesUnique": "Numele claselor trebuie să fie unice", + "stateRequiresTwoClasses": "Modelele de stare necesită cel puțin 2 clase", + "objectLabelRequired": "Vă rugăm să selectați o etichetă de obiect", + "objectTypeRequired": "Vă rugăm să selectați un tip de clasificare" + }, + "states": "Stări" + }, + "step2": { + "description": "Selectați camerele și definiți zona de monitorizat pentru fiecare cameră. Modelul va clasifica starea acestor zone.", + "cameras": "Camere", + "selectCamera": "Selectează camera", + "noCameras": "Apasă pe + pentru a adăuga camere", + "selectCameraPrompt": "Selectați o cameră din listă pentru a defini aria sa de monitorizare" + }, + "step3": { + "selectImagesPrompt": "Selectați toate imaginile cu: {{className}}", + "selectImagesDescription": "Apăsați pe imagini pentru a le selecta. Apăsați pe Continuare când ați terminat cu această clasă.", + "generating": { + "title": "Generare imagini de exemplu", + "description": "Frigate preia imagini reprezentative din înregistrările tale. Aceasta poate dura câteva momente..." + }, + "training": { + "title": "Antrenare model", + "description": "Modelul tău este antrenat în fundal. Închide această fereastră și modelul va începe să ruleze imediat ce antrenamentul este finalizat." + }, + "retryGenerate": "Reîncearcă generarea", + "noImages": "Nu s-au generat imagini de exemplu", + "classifying": "Clasificare și antrenare...", + "trainingStarted": "Antrenamentul a început cu succes", + "errors": { + "noCameras": "Nu există camere configurate", + "noObjectLabel": "Nu a fost selectată nicio etichetă de obiect", + "generateFailed": "Generarea exemplelor a eșuat: {{error}}", + "generationFailed": "Generarea a eșuat. Vă rugăm să încercați din nou.", + "classifyFailed": "Clasificarea imaginilor a eșuat: {{error}}" + }, + "generateSuccess": "Imaginile de exemplu au fost generate cu succes", + "allImagesRequired_one": "Te rog să clasifici toate imaginile. {{count}} imagine rămasă.", + "allImagesRequired_few": "Te rog să clasifici toate imaginile. {{count}} imagini rămase.", + "allImagesRequired_other": "Te rog să clasifici toate imaginile. {{count}} de imagini rămase.", + "modelCreated": "Modelul a fost creat cu succes. Folosește vizualizarea Clasificări recente pentru a adăuga imagini pentru stările lipsă, apoi antrenează modelul.", + "missingStatesWarning": { + "title": "Exemple de stări lipsă", + "description": "Este recomandat să alegi exemple pentru toate stările pentru rezultate optime. Poți continua fără a selecta toate stările, dar modelul nu va fi antrenat până când toate stările nu au imagini. După continuare, folosește vizualizarea Clasificări recente pentru a clasifica imagini pentru stările lipsă, apoi antrenează modelul." + } + } + }, + "deleteModel": { + "title": "Șterge modelul de clasificare", + "single": "Sigur doriți să ștergeți {{name}}? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_one": "Sigur doriți să ștergeți {{count}} model? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_few": "Sigur doriți să ștergeți {{count}} modele? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată.", + "desc_other": "Sigur doriți să ștergeți {{count}} de modele? Aceasta va șterge permanent toate datele asociate, inclusiv imaginile și datele de antrenament. Această acțiune nu poate fi anulată." + }, + "menu": { + "objects": "Obiecte", + "states": "Stări" + }, + "details": { + "scoreInfo": "Scorul reprezintă încrederea medie a clasificării pentru toate detecțiile acestui obiect." + }, + "edit": { + "title": "Editează modelul de clasificare", + "descriptionState": "Editează clasele pentru acest model de clasificare a stării. Modificările vor necesita reantrenarea modelului.", + "descriptionObject": "Editează tipul de obiect și tipul de clasificare pentru acest model de clasificare a obiectelor.", + "stateClassesInfo": "Notă: Modificarea claselor de stare necesită reantrenarea modelului cu clasele actualizate." + }, + "tooltip": { + "trainingInProgress": "Modelul este în curs de antrenare", + "noNewImages": "Nu există imagini noi pentru antrenare. Clasifică mai întâi mai multe imagini în setul de date.", + "modelNotReady": "Modelul nu este pregătit pentru antrenare", + "noChanges": "Nicio modificare a setului de date de la ultima antrenare." + }, + "none": "Niciuna" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/configEditor.json new file mode 100644 index 0000000..21f7d47 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor de configurație - Frigate", + "configEditor": "Editor de configurație", + "copyConfig": "Copiază setările", + "saveAndRestart": "Salvează și repornește", + "saveOnly": "Salvează", + "toast": { + "success": { + "copyToClipboard": "Setări copiate." + }, + "error": { + "savingError": "Eroare la salvarea setărilor" + } + }, + "confirm": "Ieși fără să salvezi?", + "safeConfigEditor": "Editor de configurație (mod de siguranță)", + "safeModeDescription": "Frigate este în modul de siguranță din cauza unei erori de validare a configurației." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/events.json new file mode 100644 index 0000000..6a045d1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/events.json @@ -0,0 +1,63 @@ +{ + "alerts": "Alerte", + "motion": { + "label": "Mișcare", + "only": "Doar mișcare" + }, + "allCameras": "Toate Camerele", + "empty": { + "alert": "Nu sunt alerte de revizuit", + "detection": "Nu sunt detecții de revizuit", + "motion": "Nu au fost găsite date despre mișcare" + }, + "timeline": "Cronologie", + "timeline.aria": "Selectează cronologia", + "events": { + "aria": "Selectează evenimente", + "noFoundForTimePeriod": "Niciun eveniment gasit pentru acest interval de timp.", + "label": "Evenimente" + }, + "documentTitle": "Revizuieste - Frigate", + "recordings": { + "documentTitle": "Inregistrari - frigate" + }, + "calendarFilter": { + "last24Hours": "Ultimele 24 de ore" + }, + "markAsReviewed": "Marchează ca revizuit", + "markTheseItemsAsReviewed": "Marchează aceste articole ca revizuite", + "newReviewItems": { + "label": "Vezi articole noi de revizuit", + "button": "Articole Noi de Revizuit" + }, + "camera": "Camera foto", + "detections": "Detecții", + "detected": "detectat", + "selected_one": "{{count}} selectate", + "selected_other": "{{count}} selectate", + "suspiciousActivity": "Activitate suspectă", + "threateningActivity": "Activitate amenințătoare", + "detail": { + "noDataFound": "Nicio dată detaliată de revizuit", + "aria": "Comută vizualizarea detaliată", + "trackedObject_one": "{{count}} obiect", + "trackedObject_other": "{{count}} obiecte", + "noObjectDetailData": "Nicio dată de detaliu obiect disponibilă.", + "label": "Detaliu", + "settings": "Setări vizualizare detaliată", + "alwaysExpandActive": { + "title": "Extinde întotdeauna activul", + "desc": "Extinde întotdeauna detaliile obiectului elementului activ de revizuire, atunci când sunt disponibile." + } + }, + "objectTrack": { + "trackedPoint": "Punct urmărit", + "clickToSeek": "Apasă pentru a naviga la acest moment" + }, + "zoomIn": "Mărește", + "zoomOut": "Micșorează", + "normalActivity": "Normal", + "needsReview": "Necesită revizuire", + "securityConcern": "Potențială problemă de securitate", + "select_all": "Toate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/explore.json new file mode 100644 index 0000000..08eb565 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/explore.json @@ -0,0 +1,295 @@ +{ + "documentTitle": "Căutare - Frigate", + "generativeAI": "AI Generativ", + "exploreIsUnavailable": { + "title": "Explorarea este Indisponibila", + "embeddingsReindexing": { + "startingUp": "Porneste…", + "estimatedTime": "Timp ramas estimat:", + "finishingShortly": "Termina curand", + "step": { + "descriptionsEmbedded": "Descrieri încorporate: ", + "trackedObjectsProcessed": "Obiecte urmărite procesate: ", + "thumbnailsEmbedded": "Miniaturi încorporate: " + }, + "context": "Funcția de căutare poate fi utilizată după ce reindexarea obiectelor urmărite este finalizată." + }, + "downloadingModels": { + "context": "Frigate descarcă modelele de încorporare necesare pentru a susține funcția de Căutare Semantică. Acest lucru poate dura câteva minute, în funcție de viteza conexiunii rețelei dvs.", + "setup": { + "visionModel": "Model viziune", + "visionModelFeatureExtractor": "Extractor de caracteristici pentru modelul de viziune", + "textModel": "Model de text", + "textTokenizer": "Tokenizer text" + }, + "tips": { + "context": "S-ar putea să dorești să reindexezi încorporările obiectelor urmărite odată ce modelele sunt descărcate.", + "documentation": "Citește documentația" + }, + "error": "A apărut o eroare. Verifică jurnalele Frigate." + } + }, + "type": { + "details": "detalii", + "snapshot": "snapshot", + "video": "video", + "object_lifecycle": "ciclul de viață al obiectului", + "thumbnail": "miniatură", + "tracking_details": "detalii de urmărire" + }, + "objectLifecycle": { + "lifecycleItemDesc": { + "visible": "S-a detectat {{label}}", + "active": "{{label}} a devenit activ", + "entered_zone": "{{label}} a intrat în {{zones}}", + "stationary": "{{label}} a devenit staționar", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat pentru {{label}}", + "other": "{{label}} recunoscut ca {{attribute}}" + }, + "header": { + "zones": "Zone", + "ratio": "Raport", + "area": "Suprafață" + }, + "gone": "{{label}} a părasit cadrul", + "heard": "{{label}} auzit(ă)", + "external": "{{label}} detectat(ă)" + }, + "title": "Ciclul de viață al obiectului", + "count": "{{first}} din {{second}}", + "trackedPoint": "Punct urmărit", + "noImageFound": "Nicio imagine găsită pentru această marcă temporală.", + "createObjectMask": "Creează mască de obiecte", + "adjustAnnotationSettings": "Ajustează setările de adnotare", + "scrollViewTips": "Derulează pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.", + "autoTrackingTips": "Pozițiile casetelor de delimitare vor fi inexacte pentru camerele cu urmărire automată.", + "annotationSettings": { + "showAllZones": { + "title": "Afișează toate zonele", + "desc": "Afișează întotdeauna zonele pe cadrele în care obiectele au intrat într-o zonă." + }, + "offset": { + "label": "Compensare adnotare", + "documentation": "Citește documentația ", + "desc": "Aceste date provin din stream-ul de detecție al camerei tale, dar sunt suprapuse pe imaginile din stream-ul de înregistrare. Este puțin probabil ca cele două stream-uri să fie perfect sincronizate. Ca urmare, caseta de delimitare și materialul video nu se vor potrivi perfect. Totuși, câmpul annotation_offset poate fi folosit pentru a ajusta acest lucru.", + "millisecondsToOffset": "Millisecondele cu care să compensezi adnotările de detecție. Implicit: 0", + "tips": "SFAT: Imaginează-ți că există un clip de eveniment cu o persoană care merge de la stânga la dreapta. Dacă caseta de delimitare de pe linia temporală a evenimentului este constant în partea stângă a persoanei, atunci valoarea ar trebui să fie scăzută. În mod similar, dacă persoana merge de la stânga la dreapta și caseta de delimitare este constant înaintea persoanei, atunci valoarea ar trebui să fie crescută.", + "toast": { + "success": "Compensarea adnotării pentru {{camera}} a fost salvată în fișierul de configurație. Repornește Frigate pentru a aplica modificările." + } + }, + "title": "Setări adnotare" + }, + "carousel": { + "previous": "Slide-ul anterior", + "next": "Slide-ul următor" + } + }, + "details": { + "timestamp": "Marcaj timp", + "item": { + "title": "Revizuiește detaliile articolului", + "desc": "Revizuiește detaliile articolului", + "button": { + "share": "Partajează acest articol de revizuire", + "viewInExplore": "Vezi în explorator" + }, + "tips": { + "mismatch_one": "{{count}} obiect indisponibil a fost detectat și inclus în acest element de revizuire. Acest obiect fie nu s-a calificat ca alertă sau detecție, fie a fost deja curățat/șters.", + "mismatch_few": "{{count}} obiecte indisponibile au fost detectate și incluse în acest element de revizuire. Aceste obiecte fie nu s-au calificat ca alertă sau detecție, fie au fost deja curățate/șterse.", + "mismatch_other": "{{count}} de obiecte indisponibile au fost detectate și incluse în acest element de revizuire. Aceste obiecte fie nu s-au calificat ca alertă sau detecție, fie au fost deja curățate/șterse.", + "hasMissingObjects": "Ajustează-ți configurația dacă vrei ca Frigate să salveze obiectele urmărite pentru următoarele etichete: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "O nouă descriere a fost solicitată de la {{provider}}. În funcție de viteza furnizorului tău, regenerarea noii descrieri poate dura ceva timp.", + "updatedSublabel": "Subeticheta a fost actualizată cu succes.", + "updatedLPR": "Plăcuța de înmatriculare a fost actualizată cu succes.", + "audioTranscription": "Transcrierea audio a fost solicitată cu succes. În funcție de viteza serverului dumneavoastră Frigate, transcrierea poate dura ceva timp până la finalizare." + }, + "error": { + "updatedSublabelFailed": "Nu s-a putut actualiza sub-etichetarea: {{errorMessage}}", + "updatedLPRFailed": "Plăcuța de înmatriculare nu a putut fi actualizată: {{errorMessage}}", + "regenerate": "Eroare la apelarea {{provider}} pentru o nouă descriere: {{errorMessage}}", + "audioTranscription": "Solicitarea transcrierii audio a eșuat: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "title": "Editează subeticheta", + "desc": "Introdu o sub-etichetă nouă pentru acest {{label}}", + "descNoLabel": "Introduceți o nouă subetichetă pentru acest obiect urmărit" + }, + "editLPR": { + "desc": "Introdu o nouă valoare pentru numărul de înmatriculare pentru acest {{label}}", + "descNoLabel": "Introduceți o nouă valoare a plăcuței de înmatriculare pentru acest obiect urmărit", + "title": "Editează plăcuța de înmatriculare" + }, + "topScore": { + "label": "Cel mai bun scor", + "info": "Scorul cel mai bun este scorul median cel mai ridicat pentru obiectul urmărit, prin urmare, acesta poate diferi de scorul afișat pe miniatura rezultatului căutării." + }, + "estimatedSpeed": "Viteză estimată", + "objects": "Obiecte", + "recognizedLicensePlate": "Plăcuță de înmatriculare recunoscută", + "snapshotScore": { + "label": "Scor snapshot" + }, + "camera": "Cameră", + "zones": "Zone", + "button": { + "findSimilar": "Găsește similare", + "regenerate": { + "title": "Regenerează", + "label": "Regenerează descrierea obiectului urmărit" + } + }, + "tips": { + "saveDescriptionFailed": "Actualizarea descrierii a eșuat: {{errorMessage}}", + "descriptionSaved": "Descrierea a fost salvată cu succes" + }, + "label": "Etichetă", + "description": { + "label": "Descriere", + "placeholder": "Descrierea obiectului urmărit", + "aiTips": "Frigate nu va solicita o descriere de la furnizorul tău de AI până când ciclul de viață al obiectului urmărit nu se încheie." + }, + "expandRegenerationMenu": "Extinde meniul de regenerare", + "regenerateFromSnapshot": "Regenerează din snapshot", + "regenerateFromThumbnails": "Regenerează din miniaturi", + "score": { + "label": "Scor" + } + }, + "exploreMore": "Explorează mai multe obiecte cu {{label}}", + "trackedObjectDetails": "Detalii despre obiectul urmărit", + "trackedObjectsCount_one": "{{count}} obiect urmărit ", + "trackedObjectsCount_few": "{{count}} obiecte urmărite ", + "trackedObjectsCount_other": "{{count}} de obiecte urmărite ", + "itemMenu": { + "downloadSnapshot": { + "aria": "Descarcă snapshot-ul", + "label": "Descarcă snapshot-ul" + }, + "viewObjectLifecycle": { + "label": "Afișează ciclul de viață al obiectului", + "aria": "Arată ciclul de viață al obiectului" + }, + "findSimilar": { + "label": "Găsește similare", + "aria": "Găsește obiecte urmărite similare" + }, + "viewInHistory": { + "label": "Vizualizează în Istoric", + "aria": "Vizualizează în Istoric" + }, + "deleteTrackedObject": { + "label": "Șterge acest obiect urmărit" + }, + "downloadVideo": { + "label": "Descarcă video-ul", + "aria": "Descarcă video-ul" + }, + "submitToPlus": { + "label": "Trimite către Frigate+", + "aria": "Trimite către Frigate Plus" + }, + "addTrigger": { + "label": "Adaugă declanșator", + "aria": "Adaugă un declanșator pentru acest obiect urmărit" + }, + "audioTranscription": { + "label": "Transcrie", + "aria": "Solicită transcrierea audio" + }, + "viewTrackingDetails": { + "label": "Vizualizați detaliile de urmărire", + "aria": "Vizualizați detaliile de urmărire" + }, + "showObjectDetails": { + "label": "Afișează traseul obiectului" + }, + "hideObjectDetails": { + "label": "Ascunde traseul obiectului" + }, + "downloadCleanSnapshot": { + "label": "Descarcă un snapshot curat", + "aria": "Descarcă snapshot curat" + } + }, + "dialog": { + "confirmDelete": { + "title": "Confirmă ștergerea", + "desc": "Ștergerea acestui obiect urmărit elimină snapshot-ul, orice încorporări salvate și orice intrări asociate detaliilor de urmărire. Materialul video înregistrat al acestui obiect urmărit în vizualizarea Istoric NU va fi șters.

    Ești sigur că vrei să continui?" + } + }, + "noTrackedObjects": "Nu au fost găsite obiecte urmărite", + "fetchingTrackedObjectsFailed": "Eroare la preluarea obiectelor urmărite: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Obiectul urmărit a fost șters cu succes.", + "error": "Ștergerea obiectului urmărit a eșuat: {{errorMessage}}" + } + }, + "tooltip": "Potrivire {{type}} cu {{confidence}}%", + "previousTrackedObject": "Obiectul urmărit anterior", + "nextTrackedObject": "Următorul obiect urmărit" + }, + "aiAnalysis": { + "title": "Analiză AI" + }, + "concerns": { + "label": "Îngrijorări" + }, + "trackingDetails": { + "title": "Detalii de Urmărire", + "noImageFound": "Nu s-a găsit nicio imagine pentru acest marcaj de timp.", + "createObjectMask": "Creează Masca Obiectului", + "adjustAnnotationSettings": "Ajustează Setările de anotare", + "scrollViewTips": "Apasă pentru a vizualiza momentele semnificative din ciclul de viață al acestui obiect.", + "autoTrackingTips": "Pozițiile casetelor de delimitare vor fi inexacte pentru camerele cu urmărire automată.", + "count": "{{first}} din {{second}}", + "trackedPoint": "Punct Urmărit", + "lifecycleItemDesc": { + "visible": "detectat {{label}}", + "entered_zone": "{{label}} a intrat în {{zones}}", + "active": "{{label}} a devenit activ", + "stationary": "{{label}} a devenit staționar", + "attribute": { + "faceOrLicense_plate": "{{attribute}} detectat pentru {{label}}", + "other": "{{label}} recunoscut ca {{attribute}}" + }, + "gone": "{{label}} a plecat", + "heard": "{{label}} auzit", + "external": "{{label}} detectat", + "header": { + "zones": "Zone", + "ratio": "Raport", + "area": "Aria", + "score": "Scor" + } + }, + "annotationSettings": { + "title": "Setări de adnotare", + "showAllZones": { + "title": "Afișează toate", + "desc": "Afișează întotdeauna zonele pe cadrele în care obiectele au intrat într-o zonă." + }, + "offset": { + "label": "Compensare adnotare", + "desc": "Aceste date provin din stream-ul de detectare al camerei tale, dar sunt suprapuse pe imaginile din stream-ul de înregistrare. Este puțin probabil ca cele două stream-uri să fie perfect sincronizate. Drept urmare, caseta delimitatoare și materialul video nu se vor alinia perfect. Poți folosi această setare pentru a decală adnotările înainte sau înapoi în timp, pentru a le alinia mai bine cu materialul înregistrat.", + "millisecondsToOffset": "Millisecunde pentru a decalca adnotările de detectare. Implicit: 0", + "tips": "Reduceți valoarea dacă redarea video este înaintea casetelor și punctelor de traseu și creșteți valoarea dacă redarea video este în urma acestora. Această valoare poate fi negativă.", + "toast": { + "success": "Decalajul de adnotare pentru {{camera}} a fost salvat în fișierul de configurare." + } + } + }, + "carousel": { + "previous": "Slide-ul anterior", + "next": "Slide-ul următor" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/exports.json new file mode 100644 index 0000000..fa90774 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Caută", + "documentTitle": "Export - Frigate", + "noExports": "Nu au fost gasite exporturi", + "deleteExport": "Șterge exportul", + "deleteExport.desc": "Ești sigur că vrei să ștergi {{exportName}}?", + "editExport": { + "title": "Redenumeste Exportul", + "saveExport": "Salveaza Export", + "desc": "Introdu un nume nou pentru acest Export." + }, + "toast": { + "error": { + "renameExportFailed": "Eroare redenumire export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Partajează exportul", + "downloadVideo": "Descarcă videoclipul", + "editName": "Editează numele", + "deleteExport": "Șterge exportul" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/faceLibrary.json new file mode 100644 index 0000000..360143d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "description": { + "addFace": "Adaugă o colecție nouă în Biblioteca de fețe încărcând prima ta imagine.", + "placeholder": "Introduceti un nume pentru aceasta colectie", + "invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe." + }, + "details": { + "person": "Persoană", + "subLabelScore": "Scor subetichetă", + "unknown": "Necunoscut", + "scoreInfo": "Scorul sub-etichetă este scorul ponderat pentru toate fețele recunoscute, așa că acesta poate diferi de scorul afișat în snapshot.", + "face": "Detalii față", + "faceDesc": "Detalii despre obiectul urmărit care a generat această față", + "timestamp": "Marcaj timp" + }, + "uploadFaceImage": { + "desc": "Încarcă o imagine pentru a scana fețele și a include pentru {{pageToggle}}", + "title": "Încarcă imaginea feței" + }, + "createFaceLibrary": { + "desc": "Creează o colecție nouă", + "title": "Creează colecție", + "nextSteps": "Pentru a construi o bază solidă:
  • Folosește fila „Recunoașteri Recente” pentru a selecta și antrena pe imagini pentru fiecare persoană detectată.
  • Concentrează-te pe imagini frontale pentru cele mai bune rezultate; evită imaginile de antrenament care surprind fețe din unghiuri laterale.
  • ", + "new": "Crează o față nouă" + }, + "collections": "Colecții", + "documentTitle": "Bibliotecă fețe - Frigate", + "train": { + "empty": "Nu există încercări recente de recunoaștere facială", + "title": "Recunoașteri Recente", + "aria": "Selectează Recunoașteri Recente", + "titleShort": "Recent" + }, + "steps": { + "description": { + "uploadFace": "Încarcă o imagine cu {{name}} care să arate fața dintr-un unghi frontal. Imaginea nu trebuie să fie decupată doar la nivelul feței." + }, + "faceName": "Introdu numele feței", + "uploadFace": "Încarcă imaginea feței", + "nextSteps": "Pașii următori" + }, + "selectFace": "Selectează fața", + "deleteFaceLibrary": { + "title": "Șterge numele", + "desc": "Ești sigur că vrei să ștergi colecția {{name}}? Aceasta va șterge definitiv toate fețele asociate." + }, + "renameFace": { + "title": "Redenumește fața", + "desc": "Introdu un nume nou pentru {{name}}" + }, + "deleteFaceAttempts": { + "title": "Șterge fețele", + "desc_one": "Ești sigur că vrei să ștergi {{count}} față? Această acțiune nu poate fi anulată.", + "desc_few": "Ești sigur că vrei să ștergi {{count}} fețe? Această acțiune nu poate fi anulată.", + "desc_other": "Ești sigur că vrei să ștergi {{count}} de fețe? Această acțiune nu poate fi anulată." + }, + "button": { + "addFace": "Adaugă față", + "deleteFaceAttempts": "Șterge fețele", + "renameFace": "Redenumește fața", + "uploadImage": "Încarcă imagine", + "deleteFace": "Șterge fața", + "reprocessFace": "Reprocesează fața" + }, + "selectItem": "Selectează {{item}}", + "toast": { + "success": { + "deletedName_one": "{{count}} față a fost ștearsă cu succes.", + "deletedName_few": "{{count}} fețe au fost șterse cu succes.", + "deletedName_other": "{{count}} de fețe au fost șterse cu succes.", + "trainedFace": "Față antrenată cu succes.", + "renamedFace": "Fața a fost redenumită cu succes ca {{name}}", + "updatedFaceScore": "Scorul feței a fost actualizat cu succes la {{name}} ({{score}}).", + "deletedFace_one": "{{count}} față a fost ștersă cu succes.", + "deletedFace_few": "{{count}} fețe au fost șterse cu succes.", + "deletedFace_other": "{{count}} de fețe au fost șterse cu succes.", + "uploadedImage": "Imagine încărcată cu succes.", + "addFaceLibrary": "{{name}} a fost adăugat(ă) cu succes la biblioteca de fețe!" + }, + "error": { + "addFaceLibraryFailed": "Setarea numelui feței a eșuat: {{errorMessage}}", + "deleteFaceFailed": "Ștergerea a eșuat: {{errorMessage}}", + "deleteNameFailed": "Ștergerea numelui a eșuat: {{errorMessage}}", + "renameFaceFailed": "Redenumirea feței a eșuat: {{errorMessage}}", + "trainFailed": "Antrenarea a eșuat: {{errorMessage}}", + "uploadingImageFailed": "Încărcarea imaginii a eșuat: {{errorMessage}}", + "updateFaceScoreFailed": "Nu s-a putut actualiza scorul feței: {{errorMessage}}" + } + }, + "imageEntry": { + "dropActive": "Trage imaginea aici…", + "dropInstructions": "Trage și plasează sau lipește o imagine aici sau fă clic pentru a selecta", + "maxSize": "Dimensiunea maximă: {{size}}MB", + "validation": { + "selectImage": "Te rog să selectezi un fișier imagine." + } + }, + "trainFaceAs": "Antrenează fața ca:", + "trainFace": "Antrenează fața", + "readTheDocs": "Citește documentația", + "nofaces": "Nu sunt fețe disponibile", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/live.json new file mode 100644 index 0000000..7ddaa53 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Frigate - Live", + "documentTitle.withCamera": "{{camera}} - Frigate - Live", + "lowBandwidthMode": "Mod Latime de Banda Limitata", + "twoWayTalk": { + "enable": "Activare dialog", + "disable": "Dezactivează conversația bidirecțională" + }, + "ptz": { + "zoom": { + "out": { + "label": "Îndepărtează camera PTZ" + }, + "in": { + "label": "Apropie camera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Apasă în cadrul imaginii pentru a centra camera", + "enable": "Activează mutarea prin clic", + "disable": "Dezactivează mutarea prin clic" + }, + "left": { + "label": "Mișcă camera PTZ spre stânga" + }, + "up": { + "label": "Mișcă camera PTZ în sus" + }, + "down": { + "label": "Mișcă camera PTZ în jos" + }, + "right": { + "label": "Mișcă camera PTZ spre dreapta" + } + }, + "frame": { + "center": { + "label": "Apasă în cadru pentru a centra camera PTZ" + } + }, + "presets": "Presetări cameră PTZ", + "focus": { + "in": { + "label": "Focalizează camera PTZ în interior" + }, + "out": { + "label": "Focalizează camera PTZ în exterior" + } + } + }, + "cameraAudio": { + "enable": "Activează sunetul camerei", + "disable": "Dezactivează sunetul camerei" + }, + "camera": { + "enable": "Activează camera", + "disable": "Dezactivează camera" + }, + "muteCameras": { + "enable": "Dezactivează sunetul pentru toate camerele", + "disable": "Activează sunetul pentru toate camerele" + }, + "detect": { + "enable": "Activează detectarea", + "disable": "Dezactivează detectarea" + }, + "recording": { + "enable": "Activează înregistrarea", + "disable": "Dezactivează înregistrarea" + }, + "snapshots": { + "disable": "Dezactivează snapshoturile", + "enable": "Activează snapshoturile" + }, + "audioDetect": { + "enable": "Activează detectarea audio", + "disable": "Dezactivează detectarea audio" + }, + "autotracking": { + "enable": "Activează urmărirea automată", + "disable": "Dezactivează urmărirea automată" + }, + "streamStats": { + "enable": "Afișează statistici streaming", + "disable": "Ascunde statisticile de streaming" + }, + "manualRecording": { + "title": "La-cerere", + "tips": "Descarcă un snapshot instant sau pornește un eveniment manual pe baza setărilor de reținere a înregistrărilor acestei camere.", + "playInBackground": { + "label": "Redă în fundal", + "desc": "Activează această opțiune pentru a continua redarea streaming-ului chiar și atunci când playerul este ascuns." + }, + "showStats": { + "label": "Afișează statistici", + "desc": "Activează această opțiune pentru a afișa statisticile de streaming suprapus peste imaginea camerei." + }, + "debugView": "Vizualizator depanare", + "start": "Pornește înregistrarea la cerere", + "started": "Înregistrare la cerere pornită manual.", + "failedToStart": "Nu s-a putut porni înregistrarea manuală la cerere.", + "recordDisabledTips": "Deoarece înregistrarea este dezactivată sau restricționată în configurația pentru această cameră, doar un snapshot va fi salvat.", + "end": "Oprește înregistrarea la cerere", + "ended": "Înregistrarea manuală la cerere s-a încheiat.", + "failedToEnd": "Nu s-a reușit încheierea înregistrării manuale la cerere." + }, + "streamingSettings": "Setări streaming", + "notifications": "Notificări", + "audio": "Audio", + "suspend": { + "forTime": "Suspendă pentru: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Sunetul trebuie să fie redat de camera dvs. și configurat în go2rtc pentru acest stream.", + "documentation": "Citește documentația " + }, + "available": "Sunetul este disponibil pentru acest stream", + "unavailable": "Sunetul nu este disponibil pentru acest stream" + }, + "twoWayTalk": { + "tips": "Dispozitivul dvs. trebuie să suporte această funcție, iar WebRTC trebuie configurat pentru comunicare bidirecțională.", + "tips.documentation": "Citește documentația ", + "available": "Comunicarea bidirecțională este disponibilă pentru acest stream", + "unavailable": "Comunicarea bidirecțională nu este disponibilă pentru acest stream" + }, + "lowBandwidth": { + "tips": "Vizualizarea live este în modul de lățime de bandă redusă din cauza întârzierilor sau a erorilor de streaming.", + "resetStream": "Resetează stream-ul" + }, + "playInBackground": { + "label": "Redare în fundal", + "tips": "Activează această opțiune pentru a continua streaming-ul când player-ul este ascuns." + }, + "debug": { + "picker": "Selectarea stream-ului nu este disponibilă în modul de depanare. Vizualizarea de depanare folosește întotdeauna stream-ul atribuit rolului de detectare." + } + }, + "cameraSettings": { + "title": "Setări pentru {{camera}}", + "cameraEnabled": "Cameră activată", + "objectDetection": "Detectare obiecte", + "recording": "Înregistrare", + "snapshots": "Snapshot-uri", + "audioDetection": "Detectare sunet", + "autotracking": "Urmărire automată", + "transcription": "Transcriere audio" + }, + "history": { + "label": "Afișează înregistrările istorice" + }, + "effectiveRetainMode": { + "modes": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + }, + "notAllTips": "Configurația ta de retenție pentru înregistrările {{source}} este setată la mode: {{effectiveRetainMode}}, astfel că această înregistrare la cerere va păstra doar segmentele cu {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Editează aspectul", + "group": { + "label": "Editează grupul de camere" + }, + "exitEdit": "Ieși din modul de editare" + }, + "transcription": { + "enable": "Activează transcrierea audio în timp real", + "disable": "Dezactivează transcrierea audio în timp real" + }, + "snapshot": { + "takeSnapshot": "Descarcă snapshot instant", + "noVideoSource": "Nicio sursă video disponibilă pentru snapshot.", + "captureFailed": "Eșec la capturarea snapshot-ului.", + "downloadStarted": "Descărcarea snapshot-ului a început." + }, + "noCameras": { + "title": "Nicio Cameră Configurată", + "description": "Începe prin a conecta o cameră la Frigate.", + "buttonText": "Adaugă cameră", + "restricted": { + "title": "Nicio Cameră Disponibilă", + "description": "Nu aveți permisiunea de a vizualiza camere în acest grup." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/recording.json new file mode 100644 index 0000000..1b96b7c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtru", + "export": "Exporta", + "calendar": "Calendar", + "filters": "Filtre", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Timpul de sfarsit trebuie sa fie dupa cel de start", + "noValidTimeSelected": "Niciun interval de timp valid nu a fost selectat" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/search.json new file mode 100644 index 0000000..94d035a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Caută", + "savedSearches": "Căutări salvate", + "searchFor": "Caută {{inputValue}}", + "button": { + "clear": "Șterge căutarea", + "save": "Salvează căutarea", + "filterInformation": "Filtrează informațiile", + "delete": "Șterge căutarea salvată", + "filterActive": "Filtre active" + }, + "trackedObjectId": "ID-ul obiectului urmărit", + "filter": { + "label": { + "cameras": "Camere", + "labels": "Etichete", + "zones": "Zone", + "sub_labels": "Sub-etichete", + "search_type": "Tip căutare", + "time_range": "Interval de timp", + "max_score": "Scor maxim", + "before": "Înainte", + "after": "După", + "min_score": "Scor minim", + "min_speed": "Viteza minimă", + "max_speed": "Viteza maximă", + "recognized_license_plate": "Număr de înmatriculare recunoscut", + "has_clip": "Are videoclip", + "has_snapshot": "Are snapshot" + }, + "tips": { + "desc": { + "step1": "Tastează un nume de filtru urmat de două puncte (ex. „camere:” ).", + "step3": "Folosește mai multe filtre adăugându-le unul după altul, separate prin spațiu.", + "step4": "Filtrele de dată (înainte: și după:) folosesc formatul {{DateFormat}}.", + "step6": "Elimină filtrele apăsând pe „X”-ul de lângă ele.", + "exampleLabel": "Exemplu:", + "step5": "Filtrul pentru intervalul de timp folosește formatul {{exampleTime}}.", + "step2": "Selectează o valoare din sugestii sau tastează propria valoare.", + "text": "Filtrele te ajută să restrângi rezultatele căutării. Iată cum să le folosești în câmpul de introducere:" + }, + "title": "Cum să folosești filtrele de text" + }, + "header": { + "noFilters": "Filtre", + "currentFilterType": "Valori filtru", + "activeFilters": "Filtre active" + }, + "searchType": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Data „înainte” trebuie să fie ulterioară datei „după”.", + "afterDatebeEarlierBefore": "Data „după” trebuie să fie mai recentă decât data „înainte”.", + "minScoreMustBeLessOrEqualMaxScore": "Valoarea „min_score” trebuie să fie mai mică sau egală cu „max_score”.", + "maxScoreMustBeGreaterOrEqualMinScore": "Valoarea „max_score” trebuie să fie mai mare sau egală cu „min_score”.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Valoarea „min_speed” trebuie să fie mai mică sau egală cu „max_speed”.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Valoarea „max_speed” trebuie să fie mai mare sau egală cu „min_speed”." + } + } + }, + "similaritySearch": { + "title": "Căutare după similaritate", + "active": "Căutarea după similaritate este activată", + "clear": "Șterge căutarea după similaritate" + }, + "placeholder": { + "search": "Căutare…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/settings.json new file mode 100644 index 0000000..81b7326 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/settings.json @@ -0,0 +1,1248 @@ +{ + "documentTitle": { + "authentication": "Setări de autentificare - Frigate", + "camera": "Setări cameră - Frigate", + "default": "Setări - Frigate", + "classification": "Setări de clasificare - Frigate", + "masksAndZones": "Editor Zonă si Mască - Frigate", + "notifications": "Setări notificări - Frigate", + "motionTuner": "Ajustare mișcare - Frigate", + "object": "Depanare - Frigate", + "general": "Setări interfață - Frigate", + "frigatePlus": "Setări Frigate+ - Frigate", + "enrichments": "Setări de Îmbogățiri - Frigate", + "cameraManagement": "Gestionează Camerele - Frigate", + "cameraReview": "Setări Revizuire Cameră - Frigate" + }, + "menu": { + "ui": "Interfață utilizator", + "cameras": "Setări cameră", + "masksAndZones": "Măști / Zone", + "motionTuner": "Reglaj mișcare", + "enrichments": "Îmbogățiri", + "debug": "Depanare", + "users": "Utilizatori", + "notifications": "Notificări", + "frigateplus": "Frigate+", + "triggers": "Declanșatoare", + "roles": "Roluri", + "cameraManagement": "Administrare", + "cameraReview": "Revizuire" + }, + "dialog": { + "unsavedChanges": { + "title": "Ai modificări nesalvate.", + "desc": "Vrei să salvezi modificările înainte de a continua?" + } + }, + "cameraSetting": { + "camera": "Cameră", + "noCamera": "Nicio cameră" + }, + "general": { + "title": "Setări interfață", + "liveDashboard": { + "title": "Tabloul de bord live", + "automaticLiveView": { + "desc": "Comută automat la vizualizarea live a unei camere când este detectată activitate. Dezactivarea acestei opțiuni face ca imaginile statice ale camerelor din panoul Live să se actualizeze doar o dată pe minut.", + "label": "Vizualizare Live Automată" + }, + "playAlertVideos": { + "label": "Redă videoclipurile de alertă", + "desc": "În mod implicit, alertele recente din panoul Live se redau ca videoclipuri mici, ce ruleaza repetat. Dezactivează această opțiune pentru a afișa doar o imagine statică a alertelor recente pe acest dispozitiv/browser." + }, + "displayCameraNames": { + "label": "Afișează întotdeauna numele camerelor", + "desc": "Afișează întotdeauna numele camerelor într-un indicator în tabloul de bord cu vizualizare live pe mai multe camere." + }, + "liveFallbackTimeout": { + "label": "Timp de expirare pentru redarea live", + "desc": "Când stream-ul live de înaltă calitate al unei camere nu este disponibil, revino la modul cu lățime de bandă scăzută după acest număr de secunde. Implicit: 3." + } + }, + "storedLayouts": { + "title": "Layout-uri salvate", + "desc": "Aranjamentul camerelor într-un grup de camere poate fi tras și redimensionat. Pozițiile sunt salvate în stocarea locală a browserului tău.", + "clearAll": "Șterge toate layout-urile" + }, + "cameraGroupStreaming": { + "title": "Setări de streaming pentru grupul de camere", + "desc": "Setările de streaming pentru fiecare grup de camere sunt stocate în memoria locală a browserului tău.", + "clearAll": "Șterge toate setările de streaming" + }, + "recordingsViewer": { + "title": "Vizualizator înregistrări", + "defaultPlaybackRate": { + "desc": "Viteza implicită de redare pentru înregistrări.", + "label": "Viteza implicită de redare" + } + }, + "calendar": { + "title": "Calendar", + "firstWeekday": { + "label": "Prima zi a săptămânii", + "desc": "Ziua cu care încep săptămânile calendarului de revizuire.", + "sunday": "Duminică", + "monday": "Luni" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Aspectul salvat pentru {{cameraName}} a fost șters", + "clearStreamingSettings": "Setările de streaming pentru toate grupurile de camere au fost resetate." + }, + "error": { + "clearStoredLayoutFailed": "Eroare la ștergerea layout-ului salvat: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nu s-au putut șterge setările de streaming: {{errorMessage}}" + } + } + }, + "enrichments": { + "faceRecognition": { + "modelSize": { + "large": { + "desc": "Utilizarea variantei mari folosește un model ArcFace pentru încorporarea fețelor și va rula automat pe GPU, dacă este disponibil.", + "title": "mare" + }, + "desc": "Dimensiunea modelului utilizat pentru recunoașterea facială.", + "small": { + "title": "mic", + "desc": "Utilizarea variantei mici folosește un model FaceNet pentru încorporarea fețelor, care rulează eficient pe majoritatea tipurilor de procesoare." + }, + "label": "Dimensiunea modelului" + }, + "title": "Recunoaștere facială", + "desc": "Recunoașterea facială permite atribuirea de nume persoanelor, iar când fața lor este recunoscută, Frigate va atribui numele persoanei ca sub-etichetă. Această informație este inclusă în interfața utilizatorului, filtre și în notificări.", + "readTheDocumentation": "Citește documentația" + }, + "semanticSearch": { + "reindexNow": { + "confirmDesc": "Ești sigur că vrei să reindexezi încorporările pentru toate obiectele urmărite? Acest proces va rula în fundal, dar poate folosi la maxim procesorul și poate dura ceva timp. Poți urmări progresul pe pagina de explorare.", + "label": "Reindexează acum", + "desc": "Reindexarea va regenera încorporările pentru toate obiectele urmărite. Acest proces rulează în fundal și poate utiliza la maxim procesorul, durând o perioadă considerabilă în funcție de numărul de obiecte urmărite pe care le ai.", + "confirmTitle": "Confirmă reindexarea", + "confirmButton": "Reindexează", + "success": "Reindexarea a început cu succes.", + "alreadyInProgress": "Reindexarea este deja în curs de desfășurare.", + "error": "Eroare la pornirea reindexării: {{errorMessage}}" + }, + "title": "Căutare semantică", + "desc": "Căutarea semantică în Frigate îți permite să găsești obiecte urmărite în elementele tale de revizuire folosind fie imaginea în sine, o descriere text definită de utilizator, sau una generată automat.", + "readTheDocumentation": "Citește documentația", + "modelSize": { + "label": "Dimensiunea modelului", + "desc": "Dimensiunea modelului utilizat pentru încorporările de căutare semantică.", + "small": { + "title": "mic", + "desc": "Utilizarea variantei mici folosește o versiune cuantificată a modelului care consumă mai puțină memorie RAM și rulează mai rapid pe CPU, cu o diferență foarte mică în calitatea încorporărilor." + }, + "large": { + "title": "mare", + "desc": "Utilizarea variantei mari folosește modelul complet Jina și va rula automat pe GPU, dacă este disponibil." + } + } + }, + "licensePlateRecognition": { + "desc": "Frigate poate recunoaște numerele de înmatriculare ale vehiculelor și poate adăuga automat caracterele detectate în câmpul recognized_license_plate sau un nume cunoscut ca sub_etichetă pentru obiectele de tip mașină. Un caz de utilizare comun poate fi citirea numerelor de înmatriculare ale mașinilor care intră într-o curte sau ale celor care trec pe stradă.", + "title": "Recunoaștere numere de înmatriculare", + "readTheDocumentation": "Citește documentația" + }, + "title": "Setări îmbogățiri", + "unsavedChanges": "Modificările nesalvate ale setărilor de îmbogățiri", + "birdClassification": { + "title": "Clasificarea păsărilor", + "desc": "Clasificarea păsărilor identifică păsările cunoscute folosind un model TensorFlow cuantificat. Când o pasăre recunoscută este identificată, numele său comun va fi adăugat ca sub_etichetă. Această informație este inclusă în interfața utilizator, filtre și în notificări." + }, + "restart_required": "Este necesară repornirea (setările de îmbogățiri au fost modificate)", + "toast": { + "success": "Setările de îmbogățiri au fost salvate. Repornește Frigate pentru a aplica modificările.", + "error": "Nu s-au putut salva modificările configurației: {{errorMessage}}" + } + }, + "camera": { + "title": "Setări cameră", + "streams": { + "title": "Stream-uri", + "desc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea completă a unei camere oprește procesarea streamului acesteia de către Frigate. Detecția, înregistrarea și depanarea nu vor fi disponibile.
    Notă: Aceasta nu dezactivează restream-urile go2rtc." + }, + "review": { + "title": "Revizuire", + "desc": "Activează/dezactivează temporar alertele și detecțiile pentru această cameră până la repornirea Frigate. Când este dezactivată, nu vor fi generate noi elemente pentru revizuire. ", + "alerts": "Alerte. ", + "detections": "Detecții. " + }, + "reviewClassification": { + "title": "Clasificare revizuiri", + "desc": "Frigate clasifică elementele de revizuire în \"alerte\" și \"detecții\". În mod implicit, toate obiectele de tip persoană și mașină sunt considerate alerte. Poți rafina clasificarea elementelor de revizuire configurând zonele necesare pentru acestea.", + "readTheDocumentation": "Citește documentația", + "unsavedChanges": "Setări de clasificare a revizuirilor nesalvate pentru {{camera}}", + "limitDetections": "Limitează detecțiile la zone specifice", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Toate obiectele {{detectionsLabels}} detectate în {{zone}} pe {{cameraName}} care nu sunt categorisite ca alerte vor fi afișate ca detecții, indiferent de zona în care se află.", + "regardlessOfZoneObjectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorisite pe {{cameraName}} vor fi afișate ca detecții, indiferent de zona în care se află.", + "text": "Toate obiectele {{detectionsLabels}} care nu sunt categorisite în {{zone}} pe {{cameraName}} vor fi afișate ca detecții." + }, + "selectDetectionsZones": "Selectează zone pentru detecții", + "zoneObjectAlertsTips": "Toate obiectele {{alertsLabels}} detectate în {{zone}} pe {{cameraName}} vor fi afișate ca alerte.", + "objectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorisite pe {{cameraName}} vor fi afișate ca detecții, indiferent de zona în care se află.", + "toast": { + "success": "Configurația clasificării pentru revizuire a fost salvată. Repornește Frigate pentru a aplica modificările." + }, + "selectAlertsZones": "Selectează zone pentru alerte", + "noDefinedZones": "Nu sunt definite zone pentru această cameră.", + "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor fi afișate ca alerte." + }, + "object_descriptions": { + "title": "Descrieri de obiecte generate de AI", + "desc": "Activează/dezactivează temporar descrierile de obiecte generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru obiectele urmărite pe această cameră." + }, + "review_descriptions": { + "title": "Descrieri de revizuiri generate de AI", + "desc": "Activează/dezactivează temporar descrierile recenziilor generate de AI pentru această cameră. Când această funcție este dezactivată, descrierile generate de AI nu vor fi solicitate pentru elementele de recenzie de pe această cameră." + }, + "addCamera": "Adaugă cameră nouă", + "editCamera": "Editează camera:", + "selectCamera": "Selectează camera", + "backToSettings": "Înapoi la setările camerei", + "cameraConfig": { + "add": "Adaugă cameră", + "edit": "Editează camera", + "description": "Configurează setările camerei, inclusiv intrările de flux și rolurile.", + "name": "Numele camerei", + "nameRequired": "Numele camerei este obligatoriu", + "nameInvalid": "Numele camerei trebuie să conțină doar litere, cifre, underscore-uri sau cratime", + "namePlaceholder": "de ex.: usa_principala", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale stream", + "pathRequired": "Calea stream-ului este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detectare, înregistrare) poate fi atribuit doar unui singur stream", + "addInput": "Adaugă stream de intrare", + "removeInput": "Elimină stream-ul de intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "toast": { + "success": "Camera {{cameraName}} a fost salvată cu succes" + }, + "nameLength": "Numele camerei trebuie să aibă mai puțin de 24 de caractere." + } + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "loiteringTime": { + "title": "Timp de ședere", + "desc": "Setează o durată minimă în secunde în care obiectul trebuie să fie în zonă pentru ca aceasta să se activeze. Implicit: 0" + }, + "speedEstimation": { + "desc": "Activează estimarea vitezei pentru obiectele din această zonă. Atenție: Pentru ca estimarea vitezei să funcționeze corect, zona trebuie să aibă exact 4 puncte.", + "title": "Estimare viteză", + "docs": "Citește documentația", + "lineADistance": "Distanța liniei A ({{unit}})", + "lineBDistance": "Distanța liniei B ({{unit}})", + "lineCDistance": "Distanța liniei C ({{unit}})", + "lineDDistance": "Distanța liniei D ({{unit}})" + }, + "add": "Adaugă zonă", + "desc": { + "title": "Zonele îți permit să definești o anumită zonă din cadrul vizual al camerei. Astfel, poți determina dacă un obiect se află sau nu într-o anumită arie de interes.", + "documentation": "Documentație" + }, + "edit": "Editează zona", + "name": { + "inputPlaceHolder": "Introdu un nume…", + "title": "Nume", + "tips": "Numele trebuie să aibă cel puțin 2 caractere, să conțină cel puțin o literă și să nu fie numele unei camere sau al unei alte zone din această cameră." + }, + "inertia": { + "title": "Inerție", + "desc": "Specifică câte cadre trebuie să fie un obiect într-o zonă înainte de a fi considerat prezent în zonă. Implicit: 3" + }, + "speedThreshold": { + "toast": { + "error": { + "pointLengthError": "Estimarea vitezei a fost dezactivată pentru această zonă. Zonele cu estimare a vitezei trebuie să aibă exact 4 puncte.", + "loiteringTimeError": "Zonele cu un timp de staționare mai mare de 0 nu ar trebui utilizate împreună cu estimarea vitezei." + } + }, + "title": "Prag de viteză ({{unit}})", + "desc": "Specifică o viteză minimă pe care trebuie să o aibă obiectele pentru a fi considerate în această zonă." + }, + "documentTitle": "Editează zone - Frigate", + "clickDrawPolygon": "Apasă pentru a desena un poligon pe imagine.", + "toast": { + "success": "Zona ({{zoneName}}) a fost salvată." + }, + "label": "Zone", + "objects": { + "title": "Obiecte", + "desc": "Lista de obiecte care se aplică acestei zone." + }, + "allObjects": "Toate obiectele" + }, + "motionMasks": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "clickDrawPolygon": "Fă clic pentru a desena un poligon pe imagine.", + "label": "Măști de mișcare", + "documentTitle": "Editează masca de mișcare - Frigate", + "desc": { + "documentation": "Documentație", + "title": "Măștile de mișcare sunt folosite pentru a preveni ca anumite tipuri de mișcare nedorită să declanșeze detecția. Mascare excesivă va îngreuna urmărirea obiectelor." + }, + "add": "Adaugă mască de mișcare", + "edit": "Editează masca de mișcare", + "context": { + "documentation": "Citește documentația", + "title": "Măștile de mișcare sunt folosite pentru a preveni declanșarea detecțiilor din cauza tipurilor nedorite de mișcare (de exemplu: ramuri de copaci, timestamp-uri ale camerei). Măștile de mișcare ar trebui folosite cu mare prudență, deoarece supramascarea va îngreuna urmărirea obiectelor." + }, + "toast": { + "success": { + "title": "{{polygonName}} a fost salvat.", + "noName": "Masca de mișcare a fost salvată." + } + }, + "polygonAreaTooLarge": { + "tips": "Măștile de mișcare nu împiedică detectarea obiectelor. Ele doar previn ca mișcarea nedorită să declanșeze o detecție.", + "title": "Masca de mișcare acoperă {{polygonArea}}% din cadrul camerei. Măștile mari de mișcare nu sunt recomandate.", + "documentation": "Citește documentația" + } + }, + "objectMasks": { + "point_one": "{{count}} punct", + "point_few": "{{count}} puncte", + "point_other": "{{count}} de puncte", + "documentTitle": "Editează masca de obiecte - Frigate", + "add": "Adaugă mască de obiecte", + "edit": "Editează masca de obiecte", + "desc": { + "documentation": "Documentație", + "title": "Măștile de filtrare a obiectelor sunt folosite pentru a filtra falsele pozitive pentru un anumit tip de obiect, în funcție de locație." + }, + "label": "Măști obiecte", + "objects": { + "desc": "Tipul de obiect căruia i se aplică această mască de obiecte.", + "allObjectTypes": "Toate tipurile de obiecte", + "title": "Obiecte" + }, + "toast": { + "success": { + "noName": "Masca de obiecte a fost salvată.", + "title": "{{polygonName}} a fost salvat." + } + }, + "clickDrawPolygon": "Fă clic pentru a desena un poligon pe imagine.", + "context": "Măștile de filtrare a obiectelor sunt folosite pentru a elimina falsele pozitive pentru un anumit tip de obiect, în funcție de locația acestuia." + }, + "restart_required": "Repornire necesară (măști/zone modificate)", + "toast": { + "success": { + "copyCoordinates": "Coordonatele pentru {{polyName}} au fost copiate." + }, + "error": { + "copyCoordinatesFailed": "Nu s-au putut copia coordonatele." + } + }, + "filter": { + "all": "Toate măștile și zonele" + }, + "motionMaskLabel": "Masca de mișcare {{number}}", + "objectMaskLabel": "Mască obiect {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Numele zonei trebuie să aibă cel puțin 2 caractere.", + "mustNotContainPeriod": "Numele zonei nu trebuie să conțină puncte.", + "hasIllegalCharacter": "Numele zonei conține caractere nepermise.", + "mustNotBeSameWithCamera": "Numele zonei nu trebuie să fie identic cu numele camerei.", + "alreadyExists": "O zonă cu acest nume există deja pentru această cameră.", + "mustHaveAtLeastOneLetter": "Numele zonei trebuie să aibă cel puțin o literă." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Ești sigur că vrei să ștergi {{type}} {{name}}?", + "success": "{{name}} a fost șters.", + "title": "Confirmă ștergerea" + }, + "removeLastPoint": "Elimină ultimul punct", + "reset": { + "label": "Șterge toate punctele" + }, + "snapPoints": { + "false": "Nu fixa punctele", + "true": "Fixează punctele" + }, + "error": { + "mustBeFinished": "Desenul poligonului trebuie finalizat înainte de salvare." + } + }, + "distance": { + "error": { + "mustBeFilled": "Toate câmpurile de distanță trebuie completate pentru a putea folosi estimarea vitezei.", + "text": "Distanța trebuie să fie mai mare sau egală cu 0.1." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Inerția trebuie să fie mai mare de 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Timpul de staționare trebuie să fie mai mare sau egal cu 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Pragul de viteză trebuie să fie mai mare sau egal cu 0.1." + } + } + } + }, + "debug": { + "motion": { + "tips": "

    Casete de mișcare


    Casetele roșii vor fi suprapuse pe zonele din cadru unde este detectată în prezent mișcare

    ", + "title": "Casete de mișcare", + "desc": "Arată chenarele în jurul zonelor unde este detectată mișcare" + }, + "regions": { + "tips": "

    Casete de regiune


    Casetele verde deschis vor fi suprapuse pe zonele de interes din cadru care sunt trimise către detectorul de obiecte.

    ", + "title": "Regiuni", + "desc": "Arată o casetă a regiunii de interes trimise detectorului de obiecte" + }, + "desc": "Vizualizarea de depanare îți arată o vizualizare în timp real a obiectelor urmărite și a statisticilor acestora. Lista de obiecte afișează un rezumat întârziat al obiectelor detectate.", + "objectShapeFilterDrawing": { + "document": "Citește documentația ", + "area": "Suprafață", + "title": "Desenare filtru formă obiect", + "desc": "Desenează un dreptunghi pe imagine pentru a vizualiza detaliile zonei și ale raportului", + "tips": "Activează această opțiune pentru a desena un dreptunghi pe imaginea camerei, pentru a-i arăta zona și raportul. Aceste valori pot fi apoi utilizate pentru a seta parametrii de filtrare a formei obiectelor în configurația ta.", + "score": "Scor", + "ratio": "Raport" + }, + "noObjects": "Nici un obiect", + "boundingBoxes": { + "title": "Casete de delimitare", + "desc": "Afișează casete de delimitare în jurul obiectelor urmărite", + "colors": { + "label": "Culori pentru casetele de delimitare ale obiectelor", + "info": "
  • La pornire, fiecărei etichete de obiect i se vor atribui culori diferite
  • O linie subțire albastru închis indică faptul că obiectul nu este detectat în acest moment
  • O linie subțire gri indică faptul că obiectul este detectat ca fiind staționar
  • O linie groasă indică faptul că obiectul este subiectul urmării automate (când este activată)
  • " + } + }, + "title": "Depanare", + "debugging": "Depanare", + "objectList": "Listă obiecte", + "mask": { + "desc": "Afișează poligoanele măștilor de mișcare", + "title": "Măști de mișcare" + }, + "detectorDesc": "Frigate folosește detectorii ({{detectors}}) pentru a detecta obiecte în stream-ul video al camerei tale.", + "timestamp": { + "title": "Marcaj temporal", + "desc": "Suprapune un marcaj temporal pe imagine" + }, + "zones": { + "title": "Zone", + "desc": "Afișează conturul oricăror zone definite" + }, + "paths": { + "title": "Căi", + "desc": "Afișează punctele semnificative ale traseului obiectului urmărit", + "tips": "

    Căi


    Liniile și cercurile vor indica punctele semnificative prin care obiectul urmărit s-a deplasat pe parcursul ciclului său de viață.

    " + }, + "audio": { + "title": "Audio", + "noAudioDetections": "Nicio detecție audio", + "score": "scor", + "currentRMS": "RMS curent", + "currentdbFS": "dbFS curent" + }, + "openCameraWebUI": "Deschide interfața web pentru {{camera}}" + }, + "users": { + "dialog": { + "deleteUser": { + "warn": "Ești sigur că vrei să ștergi utilizatorul {{username}}?", + "title": "Șterge utilizatorul", + "desc": "Această acțiune nu poate fi anulată. Aceasta va șterge definitiv contul de utilizator și va elimina toate datele asociate." + }, + "changeRole": { + "desc": "Actualizează permisiunile pentru {{username}}", + "roleInfo": { + "intro": "Selectează rolul potrivit pentru acest utilizator:", + "admin": "Administrator", + "adminDesc": "Acces complet la toate funcțiile.", + "viewer": "Vizualizator", + "viewerDesc": "Limitat doar la tablourile de bord Live, Revizuire, Explorare și Exporturi.", + "customDesc": "Rol personalizat cu acces specific la cameră." + }, + "select": "Selectează un rol", + "title": "Schimbă rolul utilizatorului" + }, + "form": { + "password": { + "strength": { + "weak": "Slabă", + "title": "Putere parolă: ", + "veryStrong": "Foarte puternică", + "medium": "Medie", + "strong": "Puternică" + }, + "placeholder": "Introdu parola", + "confirm": { + "title": "Confirmă parola", + "placeholder": "Confirmă parola" + }, + "title": "Parolă", + "match": "Parolele se potrivesc", + "notMatch": "Parolele nu se potrivesc", + "show": "Afișează parola", + "hide": "Ascunde parola", + "requirements": { + "title": "Cerințe parolă:", + "length": "Cel puțin 8 caracter", + "uppercase": "Cel puțin o literă majusculă", + "digit": "Cel puțin o cifră", + "special": "Cel puțin un caracter special (!@#$%^&*(),.?\":{}|<>)" + } + }, + "passwordIsRequired": "Este nevoie de parolă", + "user": { + "placeholder": "Introdu nume utilizator", + "title": "Nume utilizator", + "desc": "Sunt permise doar litere, cifre, puncte și subliniere." + }, + "newPassword": { + "title": "Parolă nouă", + "placeholder": "Introdu parola nouă", + "confirm": { + "placeholder": "Re-introdu parola nouă" + } + }, + "usernameIsRequired": "Este nevoie de numele de utilizator", + "currentPassword": { + "title": "Parola curentă", + "placeholder": "Introduceți parola curentă" + } + }, + "createUser": { + "confirmPassword": "Te rog să confirmi parola", + "title": "Crează un utilizator nou", + "desc": "Adaugă un cont de utilizator nou și specifică un rol pentru accesul la anumite zone ale interfeței Frigate.", + "usernameOnlyInclude": "Numele de utilizator poate conține doar litere, cifre, . sau _" + }, + "passwordSetting": { + "cannotBeEmpty": "Parola nu poate fi goală", + "doNotMatch": "Parolele nu se potrivesc", + "updatePassword": "Actualizează parola pentru {{username}}", + "setPassword": "Schimbă parola", + "desc": "Creează o parolă puternică pentru a securiza acest cont.", + "currentPasswordRequired": "Parola curentă este obligatorie", + "incorrectCurrentPassword": "Parola curentă incorectă", + "passwordVerificationFailed": "Nu s-a putut verifica parola", + "multiDeviceWarning": "Oricare alte dispozitive unde ești autentificat vor fi nevoite să se relogheze în termen de {{refresh_time}}. De asemenea, poți forța toți utilizatorii să se re-autentifice imediat prin rotirea secretului JWT." + } + }, + "addUser": "Adaugă utilizator", + "management": { + "desc": "Gestionează conturile de utilizator ale acestei instanțe Frigate.", + "title": "Gestionare utilizatori" + }, + "toast": { + "success": { + "roleUpdated": "Rolul a fost actualizat pentru {{user}}", + "updatePassword": "Parola a fost actualizată cu succes.", + "createUser": "Utilizatorul {{user}} a fost creat cu succes", + "deleteUser": "Utilizatorul {{user}} a fost șters cu succes" + }, + "error": { + "setPasswordFailed": "Salvarea parolei a eșuat: {{errorMessage}}", + "createUserFailed": "Crearea utilizatorului a eșuat: {{errorMessage}}", + "roleUpdateFailed": "Actualizarea rolului a eșuat: {{errorMessage}}", + "deleteUserFailed": "Ștergerea utilizatorului a eșuat: {{errorMessage}}" + } + }, + "updatePassword": "Actualizează parola", + "title": "Utilizatori", + "table": { + "username": "Nume utilizator", + "actions": "Acțiuni", + "role": "Rol", + "noUsers": "Nu a fost găsit niciun utilizator.", + "changeRole": "Schimbă rolul utilizatorului", + "deleteUser": "Șterge utilizatorul", + "password": "Parolă" + } + }, + "notification": { + "notificationSettings": { + "title": "Setări pentru notificări", + "desc": "Frigate poate trimite nativ notificări push către dispozitivul tău atunci când rulează în browser sau este instalat ca PWA.", + "documentation": "Citește documentația" + }, + "globalSettings": { + "desc": "Suspendă temporar notificările pentru camerele specifice pe toate dispozitivele înregistrate.", + "title": "Setări globale" + }, + "email": { + "placeholder": "ex. exemplu@email.com", + "desc": "Este necesar un email valid, care va fi folosit pentru a te notifica în cazul în care apar probleme cu serviciul de push.", + "title": "Email" + }, + "notificationUnavailable": { + "documentation": "Citește documentația", + "desc": "Notificările push web necesită un context securizat (https://…). Aceasta este o limitare a browserului. Accesează Frigate în mod securizat pentru a putea folosi notificările.", + "title": "Notificările nu sunt disponibile" + }, + "cameras": { + "title": "Camere", + "desc": "Selectează camerele pentru care dorești să activezi notificările.", + "noCameras": "Nu există camere disponibile" + }, + "deviceSpecific": "Setări specifice dispozitivului", + "registerDevice": "Înregistrează acest dispozitiv", + "unregisterDevice": "Deregistrează acest dispozitiv", + "sendTestNotification": "Trimite o notificare de test", + "suspendTime": { + "12hours": "Suspendă pentru 12 ore", + "suspend": "Suspendă", + "5minutes": "Suspendă pentru 5 minute", + "10minutes": "Suspendă pentru 10 minute", + "24hours": "Suspendă pentru 24 de ore", + "untilRestart": "Suspendă până la restart", + "1hour": "Suspendă pentru 1 oră", + "30minutes": "Suspendă pentru 30 minute" + }, + "toast": { + "success": { + "registered": "Înregistrarea pentru notificări a fost realizată cu succes. Este necesară repornirea Frigate înainte ca orice notificare (inclusiv o notificare de test) să poată fi trimisă.", + "settingSaved": "Setările notificărilor au fost salvate." + }, + "error": { + "registerFailed": "Eroare la salvarea înregistrării notificării." + } + }, + "suspended": "Notificări suspendate {{time}}", + "active": "Notificări active", + "unsavedRegistrations": "Înregistrări notificări nesalvate", + "unsavedChanges": "Modificări ale notificărilor nesalvate", + "title": "Notificări", + "cancelSuspension": "Anulează suspendarea" + }, + "frigatePlus": { + "apiKey": { + "plusLink": "Citește mai mult despre Frigate+", + "desc": "Cheia API Frigate+ permite integrarea cu serviciul Frigate+.", + "validated": "Frigate+ API key a fost detectată și validată", + "title": "Frigate+ API Key", + "notValidated": "Frigate+ API key nu a fost detectată sau nu a fost validată" + }, + "snapshotConfig": { + "title": "Configurație snapshot-uri", + "table": { + "snapshots": "Snapshot-uri", + "cleanCopySnapshots": "Snapshot-uri clean_copy", + "camera": "Cameră" + }, + "documentation": "Citește documentația", + "cleanCopyWarning": "Unele camere au snapshot-uri activate, dar copia curată (clean_copy) este dezactivată. Trebuie să activați clean_copy în configurația instantaneelor pentru a putea trimite imagini de la aceste camere către Frigate+.", + "desc": "Trimiterea către Frigate+ necesită ca atât snapshot-urile, cât și snapshot-urile clean_copy să fie activate în configurația ta." + }, + "modelInfo": { + "title": "Informații model", + "supportedDetectors": "Detectoare suportate", + "plusModelType": { + "baseModel": "Model de bază", + "userModel": "Reglat-fin" + }, + "loadingAvailableModels": "Se încarcă modelele disponibile…", + "modelSelect": "Modelele disponibile pe Frigate+ pot fi selectate aici. Rețineți că pot fi selectate doar modelele compatibile cu configurația actuală a detectorului dumneavoastră.", + "baseModel": "Model de bază", + "loading": "Se încarcă informațiile modelului…", + "error": "Încărcarea informațiilor modelului a eșuat", + "availableModels": "Modele disponibile", + "modelType": "Tip model", + "trainDate": "Dată antrenare", + "cameras": "Camere" + }, + "toast": { + "error": "Nu s-au putut salva modificările configurației: {{errorMessage}}", + "success": "Setările Frigate+ au fost salvate. Reporniti Frigate pentru a aplica modificările." + }, + "restart_required": "Repornire necesară (model Frigate+ schimbat)", + "unsavedChanges": "Modificări nesalvate ale setărilor Frigate+", + "title": "Setări Frigate+" + }, + "motionDetectionTuner": { + "unsavedChanges": "Modificări nesalvate la reglajul de mișcare pentru {{camera}}", + "Threshold": { + "title": "Prag", + "desc": "Valoarea pragului determină cât de mare trebuie să fie schimbarea luminozității unui pixel pentru a fi considerată mișcare. Implicit: 30" + }, + "contourArea": { + "desc": "Valoarea suprafeței conturului este folosită pentru a decide care grupuri de pixeli modificați se califică ca mișcare. Implicit: 10", + "title": "Suprafața conturului" + }, + "improveContrast": { + "title": "Îmbunătățire contrast", + "desc": "Îmbunătățește contrastul pentru scene întunecate. Implicit: ACTIVAT" + }, + "desc": { + "title": "Frigate utilizează detecția mișcării ca o primă verificare, pentru a vedea dacă există ceva semnificativ în cadru care merită verificat cu detecția de obiecte.", + "documentation": "Citește ghidul pentru reglajul mișcării" + }, + "toast": { + "success": "Setările de mișcare au fost salvate." + }, + "title": "Reglaj detecție mișcare" + }, + "triggers": { + "documentTitle": "Declanșatoare", + "management": { + "title": "Declanșatoare", + "desc": "Gestionează declanșatoarele pentru {{camera}}. Folosește tipul miniatură pentru a declanșa pe miniaturi similare cu obiectul urmărit selectat și tipul descriere pentru a declanșa pe descrieri similare textului pe care îl specifici." + }, + "addTrigger": "Adaugă declanșator", + "table": { + "name": "Nume", + "type": "Tip", + "content": "Conținut", + "threshold": "Prag", + "actions": "Acțiuni", + "noTriggers": "Nu sunt configurate declanșatoare pentru această cameră.", + "edit": "Editează", + "deleteTrigger": "Elimină declanșatorul", + "lastTriggered": "Ultima declanșare" + }, + "type": { + "thumbnail": "Miniatură", + "description": "Descriere" + }, + "actions": { + "alert": "Marchează ca alertă", + "notification": "Trimite notificare", + "sub_label": "Adaugă subeticheta", + "attribute": "Adaugă atribut" + }, + "dialog": { + "createTrigger": { + "title": "Crează declanșator", + "desc": "Creează un declanșator pentru camera {{camera}}" + }, + "editTrigger": { + "title": "Editează declanșatorul", + "desc": "Editează setările pentru declanșatorul de pe camera {{camera}}" + }, + "deleteTrigger": { + "title": "Elimină declanșatorul", + "desc": "Ești sigur că vrei să ștergi declanșatorul {{triggerName}}? Această acțiune nu poate fi anulată." + }, + "form": { + "name": { + "title": "Nume", + "placeholder": "Denumește acest declanșator", + "error": { + "minLength": "Câmpul trebuie să aibă cel puțin 2 caractere.", + "invalidCharacters": "Câmpul poate conține doar litere, cifre, underscore-uri și cratime.", + "alreadyExists": "Un declanșator cu acest nume există deja pentru această cameră." + }, + "description": "Introduceți un nume sau o descriere unică pentru a identifica acest declanșator" + }, + "enabled": { + "description": "Activează sau dezactivează acest declanșator" + }, + "type": { + "title": "Tip", + "placeholder": "Selectează tipul de declanșator", + "description": "Declanșează atunci când este detectată o descriere de obiect urmărit similară", + "thumbnail": "Declanșează atunci când este detectată o miniatură de obiect urmărit similară" + }, + "content": { + "title": "Conținut", + "imagePlaceholder": "Selectează o miniatură", + "textPlaceholder": "Introdu conținutul textului", + "imageDesc": "Sunt afișate doar ultimele 100 de miniaturi. Dacă nu găsiți miniatura dorită, vă rugăm să verificați obiectele anterioare în Explorator și să configurați un declanșator din meniul de acolo.", + "textDesc": "Introduceți textul pentru a declanșa această acțiune atunci când este detectată o descriere de obiect urmărit similară.", + "error": { + "required": "Conținutul este obligatoriu." + } + }, + "threshold": { + "title": "Prag", + "error": { + "min": "Pragul trebuie să fie cel puțin 0", + "max": "Pragul trebuie să fie cel mult 1" + }, + "desc": "Setați pragul de similitudine pentru acest declanșator. Un prag mai mare înseamnă că este necesară o potrivire mai apropiată pentru declanșarea acestuia." + }, + "actions": { + "title": "Acțiuni", + "desc": "În mod implicit, Frigate trimite un mesaj MQTT pentru toate declanșatoarele. Subetichetele adaugă numele declanșatorului la eticheta obiectului. Atributele sunt metadate căutabile, stocate separat în metadatele obiectului urmărit.", + "error": { + "min": "Trebuie selectată cel puțin o acțiune." + } + }, + "friendly_name": { + "title": "Nume prietenos", + "placeholder": "Denumește sau descrie acest declanșator", + "description": "Un nume prietenos opțional sau un text descriptiv pentru acest declanșator." + } + } + }, + "toast": { + "success": { + "createTrigger": "Declanșatorul {{name}} a fost creat cu succes.", + "updateTrigger": "Declanșatorul {{name}} a fost actualizat cu succes.", + "deleteTrigger": "Declanșatorul {{name}} a fost eliminat cu succes." + }, + "error": { + "createTriggerFailed": "Crearea declanșatorului a eșuat: {{errorMessage}}", + "updateTriggerFailed": "Actualizarea declanșatorului a eșuat: {{errorMessage}}", + "deleteTriggerFailed": "Eliminarea declanșatorului a eșuat: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Căutarea semantică este dezactivată", + "desc": "Căutarea semantică trebuie să fie activată pentru a utiliza declanșatoarele." + }, + "wizard": { + "title": "Creează declanșator", + "step1": { + "description": "Configurează setările de bază pentru declanșatorul tău." + }, + "step2": { + "description": "Configurează conținutul care va declanșa această acțiune." + }, + "step3": { + "description": "Configurează pragul și acțiunile pentru acest declanșator." + }, + "steps": { + "nameAndType": "Nume și Tip", + "configureData": "Configurează datele", + "thresholdAndActions": "Prag și Acțiuni" + } + } + }, + "roles": { + "management": { + "title": "Gestionare rol vizualizator", + "desc": "Gestionează rolurile personalizate de vizualizator și permisiunile lor de acces la cameră pentru această instanță Frigate." + }, + "addRole": "Adaugă rol", + "table": { + "role": "Rol", + "cameras": "Camere", + "actions": "Acțiuni", + "noRoles": "Nu au fost găsite roluri personalizate.", + "editCameras": "Editează camerele", + "deleteRole": "Șterge rol" + }, + "toast": { + "success": { + "createRole": "Rolul {{role}} a fost creat cu succes", + "updateCameras": "Camerele au fost actualizate pentru rolul {{role}}", + "deleteRole": "Rolul {{role}} a fost șters cu succes", + "userRolesUpdated_one": "{{count}} utilizator atribuit acestui rol a fost actualizat la „vizualizator”, care are acces la toate camerele.", + "userRolesUpdated_few": "{{count}} utilizatori atribuiți acestui rol au fost actualizați la „vizualizatori”, care are acces la toate camerele.", + "userRolesUpdated_other": "{{count}} de utilizatori atribuiți acestui rol au fost actualizați la „vizualizatori”, care are acces la toate camerele." + }, + "error": { + "createRoleFailed": "Crearea rolului a eșuat: {{errorMessage}}", + "updateCamerasFailed": "Actualizarea camerelor a eșuat: {{errorMessage}}", + "deleteRoleFailed": "Ștergerea rolului a eșuat: {{errorMessage}}", + "userUpdateFailed": "Actualizarea rolurilor utilizatorilor a eșuat: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Creează rol nou", + "desc": "Adaugă un rol nou și specifică permisiunile de acces la camere." + }, + "editCameras": { + "title": "Editează camerele rolului", + "desc": "Actualizează accesul la camere pentru rolul {{role}}." + }, + "deleteRole": { + "title": "Șterge rolul", + "desc": "Această acțiune nu poate fi anulată. Aceasta va șterge permanent rolul și va atribui orice utilizatori cu acest rol la rolul „vizualizator”, care va oferi acces vizualizator la toate camerele.", + "warn": "Ești sigur că vrei să ștergi {{role}}?", + "deleting": "Se șterge..." + }, + "form": { + "role": { + "title": "Nume rol", + "placeholder": "Introduceți numele rolului", + "desc": "Sunt permise doar litere, cifre, puncte și linii de subliniere.", + "roleIsRequired": "Numele rolului este obligatoriu", + "roleOnlyInclude": "Numele rolului poate include doar litere, cifre, . sau _", + "roleExists": "Un rol cu acest nume există deja." + }, + "cameras": { + "title": "Camere", + "desc": "Selectați camerele la care acest rol are acces. Este necesară cel puțin o cameră.", + "required": "Trebuie selectată cel puțin o cameră." + } + } + } + }, + "cameraWizard": { + "title": "Adaugă cameră", + "description": "Urmează pașii de mai jos pentru a adăuga o cameră nouă la sistemul tău Frigate.", + "steps": { + "nameAndConnection": "Nume și Conexiune", + "streamConfiguration": "Configurare streaming", + "validationAndTesting": "Validare și Testare", + "probeOrSnapshot": "Sondează sau fă snapshot" + }, + "save": { + "success": "Camera nouă {{cameraName}} a fost salvată cu succes.", + "failure": "Eroare la salvarea {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rezoluție", + "video": "Video", + "audio": "Audio", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Te rog să furnizezi un URL de streaming valid", + "testFailed": "Testul de streaming a eșuat: {{error}}" + }, + "step1": { + "description": "Introduceți detaliile camerei și alegeți să testați camera sau să selectați manual marca.", + "cameraName": "Nume cameră", + "cameraNamePlaceholder": "ex. usă_intrare sau Vedere Curte Spate", + "host": "Gazdă/Adresă IP", + "port": "Port", + "username": "Nume de utilizator", + "usernamePlaceholder": "Opțional", + "password": "Parolă", + "passwordPlaceholder": "Opțional", + "selectTransport": "Selectează protocolul de transport", + "cameraBrand": "Brand cameră", + "selectBrand": "Selectează marca camerei pentru șablonul de URL", + "customUrl": "URL Streaming Personalizat", + "brandInformation": "Informații despre brand", + "brandUrlFormat": "Pentru camere cu formatul URL RTSP ca: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://utilizator:parolă@gazdă:port/cale", + "testConnection": "Testează Conexiunea", + "testSuccess": "Testul de conexiune a reușit!", + "testFailed": "Testul de conexiune a eșuat. Te rog să verifici datele introduse și să încerci din nou.", + "streamDetails": "Detalii stream", + "warnings": { + "noSnapshot": "Nu se poate obține un snapshot de pe stream-ul configurat." + }, + "errors": { + "brandOrCustomUrlRequired": "Ori selectează un brand de cameră cu adresă gazdă/IP, ori alege „Alta” cu un URL personalizat", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să aibă 64 de caractere sau mai puțin", + "invalidCharacters": "Numele camerei conține caractere nevalide", + "nameExists": "Numele camerei există deja", + "brands": { + "reolink-rtsp": "RTSP Reolink nu este recomandat. Activează HTTP în setările firmware ale camerei și repornește asistentul." + }, + "customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu \"rtsp://\". Este necesară configurare manuală pentru stream-urile de cameră non-RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Sondare metadate cameră...", + "fetchingSnapshot": "Preluare snapshot cameră..." + }, + "connectionSettings": "Setări conexiune", + "detectionMethod": "Metoda de detecție stream", + "onvifPort": "Port ONVIF", + "probeMode": "Sondare cameră", + "manualMode": "Selecție manuală", + "detectionMethodDescription": "Sondează camera cu ONVIF (dacă este suportat) pentru a găsi URL-urile de stream ale camerei, sau selectează manual marca camerei pentru a utiliza URL-uri predefinite. Pentru a introduce un URL RTSP personalizat, alege metoda manuală și selectează \"Altele\".", + "onvifPortDescription": "Pentru camerele care suportă ONVIF, acesta este de obicei 80 sau 8080.", + "useDigestAuth": "Utilizați autentificarea digest", + "useDigestAuthDescription": "Utilizați autentificarea HTTP digest pentru ONVIF. Unele camere pot necesita un nume de utilizator/parolă ONVIF dedicat în locul utilizatorului standard de administrare." + }, + "step2": { + "description": "Testează camera pentru fluxurile disponibile sau configurează setările manuale pe baza metodei de detectare selectate.", + "streamsTitle": "Stream-uri cameră", + "addStream": "Adaugă stream", + "addAnotherStream": "Adaugă un alt stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "URL stream", + "streamUrlPlaceholder": "rtsp://utilizator:parolă@gazdă:port/cale", + "url": "URL", + "resolution": "Rezoluție", + "selectResolution": "Selectează rezoluția", + "quality": "Calitate", + "selectQuality": "Selectează calitatea", + "roles": "Roluri", + "roleLabels": { + "detect": "Detecție obiecte", + "record": "Înregistrare", + "audio": "Audio" + }, + "testStream": "Testează conexiunea", + "testSuccess": "Testul de conexiune a fost realizat cu succes!", + "testFailed": "Testul de conexiune a eșuat. Verifică datele introduse și încearcă din nou.", + "testFailedTitle": "Test eșuat", + "connected": "Conectat", + "notConnected": "Neconectat", + "featuresTitle": "Funcționalități", + "go2rtc": "Redu conexiunile la cameră", + "detectRoleWarning": "Cel puțin un stream trebuie să aibă rolul „detectare” pentru a continua.", + "rolesPopover": { + "title": "Roluri de streaming", + "detect": "Stream principal pentru detecția obiectelor.", + "record": "Salvează segmente ale stream-ului video pe baza setărilor de configurare.", + "audio": "Stream pentru detecția bazată pe sunet." + }, + "featuresPopover": { + "title": "Funcționalități streaming", + "description": "Folosește restreaming go2rtc pentru a reduce conexiunile la cameră." + }, + "streamDetails": "Detalii stream", + "probing": "Se sondează camera...", + "retry": "Reîncercare", + "testing": { + "probingMetadata": "Se sondează metadatele camerei...", + "fetchingSnapshot": "Se aduce snapshot cameră..." + }, + "probeFailed": "Sondarea camerei a eșuat: {{error}}", + "probingDevice": "Se sondează dispozitivul...", + "probeSuccessful": "Sondare reușită", + "probeError": "Eroare la sondare", + "probeNoSuccess": "Sondare nereușită", + "deviceInfo": "Informații dispozitiv", + "manufacturer": "Producător", + "model": "Model", + "firmware": "Firmware", + "profiles": "Profiluri", + "ptzSupport": "Suport PTZ", + "autotrackingSupport": "Suport autourmărire", + "presets": "Presetări", + "rtspCandidates": "Candidați RTSP", + "rtspCandidatesDescription": "Următoarele URL-uri RTSP au fost găsite în urma sondării camerei. Testați conexiunea pentru a vizualiza metadatele stream-ului.", + "noRtspCandidates": "Nu au fost găsite URL-uri RTSP de la cameră. Este posibil ca datele dumneavoastră de autentificare să fie incorecte, sau este posibil ca aparatul foto să nu suporte ONVIF sau metoda utilizată pentru a prelua URL-urile RTSP. Întoarceți-vă și introduceți URL-ul RTSP manual.", + "candidateStreamTitle": "Candidat {{number}}", + "useCandidate": "Folosește", + "uriCopy": "Copiază", + "uriCopied": "URI copiat în clipboard", + "testConnection": "Testează conexiunea", + "toggleUriView": "Click pentru a comuta vizualizarea URI completă", + "errors": { + "hostRequired": "Gazdă/adresaIP este necesară" + } + }, + "step3": { + "description": "Configurează rolurile stream-ului și adaugă stream-uri suplimentare pentru camera ta.", + "validationTitle": "Validare stream", + "connectAllStreams": "Conectează toate stream-urile", + "reconnectionSuccess": "Reconectare reușită.", + "reconnectionPartial": "Unele stram-uri nu s-au reconectat.", + "streamUnavailable": "Previzualizare streaming indisponibilă", + "reload": "Reîncarcă", + "connecting": "Conectare...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Eșuat", + "notTested": "Netestat", + "connectStream": "Conectare", + "connectingStream": "Se conectează", + "disconnectStream": "Deconectare", + "estimatedBandwidth": "Lățime de bandă estimată", + "roles": "Roluri", + "none": "Niciunul", + "error": "Eroare", + "streamValidated": "Stream {{number}} validat cu succes", + "streamValidationFailed": "Validarea pentru stream {{number}} a eșuat", + "saveAndApply": "Salvează Camera Nouă", + "saveError": "Configurație invalidă. Verifică setările.", + "issues": { + "title": "Validare stream", + "videoCodecGood": "Codecul video este {{codec}}.", + "audioCodecGood": "Codecul audio este {{codec}}.", + "noAudioWarning": "Nu s-a detectat audio pentru acest strem, înregistrările nu vor avea sunet.", + "audioCodecRecordError": "Codec-ul audio AAC este necesar pentru a suporta audio în înregistrări.", + "audioCodecRequired": "Un stream audio este necesar pentru a suporta detecția audio.", + "restreamingWarning": "Reducerea conexiunilor la cameră pentru stream-ul de înregistrare poate crește ușor utilizarea procesorului.", + "dahua": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Dahua / Amcrest / EmpireTech suportă substream-uri suplimentare care trebuie să fie activate în setările camerei. Este recomandat să verifici și să utilizezi acele stream-uri, dacă sunt disponibile." + }, + "hikvision": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Hikvision suportă substream-uri suplimentare care trebuie să fie activate în setările camerei. Este recomandat să verifici și să utilizezi acele strem-uri, dacă sunt disponibile." + }, + "resolutionHigh": "O rezoluție de {{resolution}} poate cauza o utilizare crescută a resurselor.", + "resolutionLow": "O rezoluție de {{resolution}} poate fi prea mică pentru detectarea fiabilă a obiectelor mici." + }, + "ffmpegModule": "Folosește modul de compatibilitate pentru stream-uri", + "ffmpegModuleDescription": "Dacă fluxul nu se încarcă după mai multe încercări, activați această opțiune. Când este activată, Frigate va folosi modulul ffmpeg împreună cu go2rtc. Aceasta poate oferi o compatibilitate mai bună cu unele fluxuri de camere.", + "streamsTitle": "Stream-uri cameră", + "addStream": "Adaugă stream", + "addAnotherStream": "Adaugă alt stream", + "streamUrl": "URL stream", + "streamUrlPlaceholder": "rtsp://utilizator:parolă@adresaIP:port/cale", + "selectStream": "Selectați un flux", + "searchCandidates": "Căutați candidați...", + "noStreamFound": "Niciun stream găsit", + "url": "URL", + "resolution": "Rezoluție", + "quality": "Calitate", + "selectResolution": "Selectează rezoluția", + "selectQuality": "Selectează calitatea", + "roleLabels": { + "detect": "Detecție Obiect", + "record": "Înregistrare", + "audio": "Audio" + }, + "testStream": "Testează conexiunea", + "testSuccess": "Testul stream-ului a avut succes!", + "testFailed": "Testul stream-ului a eșuat", + "testFailedTitle": "Testul a eșuat", + "connected": "Conectat", + "notConnected": "Neconectat", + "featuresTitle": "Funcționalități", + "go2rtc": "Reduceți conexiunile la cameră", + "detectRoleWarning": "Cel puțin un stream trebuie să aibă rolul \"detect\" pentru a continua.", + "rolesPopover": { + "title": "Roluri stream", + "detect": "Stream principal pentru detecția obiectelor.", + "record": "Salvează segmente ale stream-ului video pe baza setărilor de configurare.", + "audio": "Stream pentru detecția bazată pe audio." + }, + "featuresPopover": { + "title": "Funcționalități stream", + "description": "Utilizați go2rtc restreaming pentru a reduce conexiunile la cameră." + } + }, + "step4": { + "description": "Validare finală și analiză înainte de a salva noua cameră. Conectați fiecare stream înainte de a salva.", + "validationTitle": "Validare stream", + "connectAllStreams": "Conectează toate stream-urile", + "reconnectionSuccess": "Reconectare reușită.", + "reconnectionPartial": "Unele stream-uri nu au reușit să se reconecteze.", + "streamUnavailable": "Previzualizare flux indisponibilă", + "reload": "Reîncarcă", + "connecting": "Conectare...", + "streamTitle": "Stream {{number}}", + "valid": "Valid", + "failed": "Eșuat", + "notTested": "Netestat", + "connectStream": "Conectare", + "connectingStream": "Se conectează", + "disconnectStream": "Deconectare", + "estimatedBandwidth": "Lățime de bandă estimată", + "roles": "Roluri", + "ffmpegModule": "Utilizează modul de compatibilitate stream", + "ffmpegModuleDescription": "Dacă stream-ul nu se încarcă după câteva încercări, activați această opțiune. Când este activată, Frigate va utiliza modulul ffmpeg cu go2rtc. Acest lucru poate oferi o compatibilitate mai bună cu unele stream-uri de cameră.", + "none": "Niciuna", + "error": "Eroare", + "streamValidated": "Stream-ul {{number}} validat cu succes", + "streamValidationFailed": "Validarea stream-ului {{number}} a eșuat", + "saveAndApply": "Salvează camera nouă", + "saveError": "Configurație nevalidă. Vă rugăm să vă verificați setările.", + "issues": { + "title": "Validare stream", + "videoCodecGood": "Codecul video: {{codec}}.", + "audioCodecGood": "Codecul audio: {{codec}}.", + "resolutionHigh": "O rezoluție de {{resolution}} poate cauza o utilizare crescută a resurselor.", + "resolutionLow": "O rezoluție de {{resolution}} ar putea fi prea mică pentru detectarea fiabilă a obiectelor mici.", + "noAudioWarning": "Nu a fost detectat audio pentru acest stream, înregistrările nu vor avea audio.", + "audioCodecRecordError": "Codec-ul audio AAC este necesar pentru a suporta audio în înregistrări.", + "audioCodecRequired": "Este necesar un stream audio pentru a suporta detecția audio.", + "restreamingWarning": "Reducerea conexiunilor la cameră pentru stream-ul de înregistrare poate crește ușor utilizarea procesorului (CPU).", + "brands": { + "reolink-rtsp": "RTSP Reolink nu este recomandat. Activați HTTP în setările de firmware ale camerei și reporniți asistentul." + }, + "dahua": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Dahua / Amcrest / EmpireTech suportă stream-uri secundare suplimentare care trebuie activate în setările camerei. Se recomandă să verificați și să utilizați aceste stream-uri dacă sunt disponibile." + }, + "hikvision": { + "substreamWarning": "Substream-ul 1 este blocat la o rezoluție scăzută. Multe camere Hikvision suportă stream-uri secundare suplimentare care trebuie activate în setările camerei. Se recomandă să verificați și să utilizați aceste stream-uri dacă sunt disponibile." + } + } + } + }, + "cameraManagement": { + "title": "Administrează Camerele", + "addCamera": "Adaugă cameră nouă", + "editCamera": "Editează cameră:", + "selectCamera": "Selectează o cameră", + "backToSettings": "Înapoi la setările camerei", + "streams": { + "title": "Activează / dezactivează camere", + "desc": "Dezactivează temporar o cameră până la repornirea Frigate. Dezactivarea unei camere oprește complet procesarea streamingului acestei camere de către Frigate. Detecția, înregistrarea și depanarea vor fi indisponibile.
    Notă: Aceasta nu dezactivează restreamingul go2rtc." + }, + "cameraConfig": { + "add": "Adaugă cameră", + "edit": "Editează cameră", + "description": "Configurează setările camerei, inclusiv intrările și rolurile de streaming.", + "name": "Nume cameră", + "nameRequired": "Numele camerei este obligatoriu", + "nameLength": "Numele camerei trebuie să fie mai scurt de 64 de caractere.", + "namePlaceholder": "ex. ușă_intrare sau Vedere Curte Spate", + "enabled": "Activat", + "ffmpeg": { + "inputs": "Stream-uri de intrare", + "path": "Cale streaming", + "pathRequired": "Calea streaming este obligatorie", + "pathPlaceholder": "rtsp://...", + "roles": "Roluri", + "rolesRequired": "Este necesar cel puțin un rol", + "rolesUnique": "Fiecare rol (audio, detectare, înregistrare) poate fi atribuit unui singur stream", + "addInput": "Adaugă stream de intrare", + "removeInput": "Elimină stream-ul de intrare", + "inputsRequired": "Este necesar cel puțin un stream de intrare" + }, + "go2rtcStreams": "Streamuri go2rtc", + "streamUrls": "URL-uri streaming", + "addUrl": "Adaugă URL", + "addGo2rtcStream": "Adaugă stream go2rtc", + "toast": { + "success": "Camera {{cameraName}} salvată cu succes" + } + } + }, + "cameraReview": { + "title": "Setări de Revizuire a Camerei", + "object_descriptions": { + "title": "Descrieri de Obiecte cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile de obiecte cu AI Generativ pentru această cameră. Când este dezactivată, descrierile generate de AI nu vor fi solicitate pentru obiectele urmărite pe această cameră." + }, + "review_descriptions": { + "title": "Descrieri de revizuire cu AI Generativ", + "desc": "Activează/dezactivează temporar descrierile de revizuire cu AI Generativ pentru această cameră. Când este dezactivat, descrierile generate de AI nu vor fi solicitate pentru elementele de revizuire de pe această cameră." + }, + "review": { + "title": "Revizuire", + "desc": "Activează/dezactivează temporar alertele și detecțiile pentru această cameră până la repornirea Frigate. Când este dezactivat, nu vor fi generate elemente de revizuire noi. ", + "alerts": "Alerte ", + "detections": "Detecții " + }, + "reviewClassification": { + "title": "Clasificare revizuire", + "desc": "Frigate clasifică elementele de revizuire ca Alerte și Detecții. În mod implicit, toate obiectele de tip persoană și mașină sunt considerate Alerte. Poți rafina clasificarea elementelor tale de revizuire prin configurarea zonelor necesare pentru acestea.", + "noDefinedZones": "Nu sunt definite zone pentru această cameră.", + "objectAlertsTips": "Toate obiectele {{alertsLabels}} de pe {{cameraName}} vor fi afișate ca Alerte.", + "zoneObjectAlertsTips": "Toate obiectele {{alertsLabels}} detectate în {{zone}} pe {{cameraName}} vor fi afișate ca Alerte.", + "objectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorizate pe {{cameraName}} vor fi afișate ca Detecții indiferent de zona în care se află.", + "zoneObjectDetectionsTips": { + "text": "Toate obiectele {{detectionsLabels}} necategorizate în {{zone}} pe {{cameraName}} vor fi afișate ca Detecții.", + "notSelectDetections": "Toate obiectele {{detectionsLabels}} detectate în {{zone}} pe {{cameraName}} și necategorizate ca Alerte vor fi afișate ca Detecții indiferent de zona în care se află.", + "regardlessOfZoneObjectDetectionsTips": "Toate obiectele {{detectionsLabels}} necategorizate pe {{cameraName}} vor fi afișate ca Detecții indiferent de zona în care se află." + }, + "unsavedChanges": "Setări de Clasificare Revizuire nesalvate pentru {{camera}}", + "selectAlertsZones": "Selectați zonele pentru Alerte", + "selectDetectionsZones": "Selectați zonele pentru Detecții", + "limitDetections": "Limitați detecțiile la zone specifice", + "toast": { + "success": "Configurația Clasificare Revizuire a fost salvată. Reporniți Frigate pentru a aplica modificările." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ro/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ro/views/system.json new file mode 100644 index 0000000..2584d85 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ro/views/system.json @@ -0,0 +1,199 @@ +{ + "documentTitle": { + "storage": "Statistici Stocare - Frigate", + "cameras": "Statistici Camere - Frigate", + "general": "Statistici Generale - Frigate", + "logs": { + "go2rtc": "Jurnal Go2RTC - Frigate", + "nginx": "Jurnal Nginx - Frigate", + "frigate": "Jurnal Frigate - Frigate" + }, + "enrichments": "Statistici îmbogățiri - Frigate" + }, + "general": { + "hardwareInfo": { + "npuUsage": "Utilizare NPU", + "npuMemory": "Memorie NPU", + "gpuUsage": "Utilizare GPU", + "gpuMemory": "Utilizare Memorie", + "title": "Informații hardware", + "gpuEncoder": "Codificator GPU", + "gpuDecoder": "Decodificator GPU", + "gpuInfo": { + "vainfoOutput": { + "returnCode": "Cod de retur: {{code}}", + "processOutput": "Rezultatul procesului:", + "title": "Rezultat vainfo", + "processError": "Eroare de procesare:" + }, + "nvidiaSMIOutput": { + "title": "Rezultat Nvidia SMI", + "name": "Nume: {{name}}", + "driver": "Driver: {{driver}}", + "cudaComputerCapability": "Capacitate de calcul CUDA: {{cuda_compute}}", + "vbios": "Informații VBios: {{vbios}}" + }, + "copyInfo": { + "label": "Copiază informațiile GPU" + }, + "toast": { + "success": "Informațiile GPU au fost copiate" + }, + "closeInfo": { + "label": "Închide informațiile GPU" + } + }, + "intelGpuWarning": { + "title": "Avertisment statistici GPU Intel", + "message": "Statistici GPU indisponibile", + "description": "Aceasta este o eroare cunoscută în instrumentele Intel pentru raportarea statisticilor GPU (intel_gpu_top), unde acestea se blochează și returnează repetat o utilizare GPU de 0%, chiar și în cazurile în care accelerarea hardware și detectarea obiectelor rulează corect pe (i)GPU. Aceasta nu este o eroare Frigate. Poți reporni gazda pentru a remedia temporar problema și pentru a confirma că GPU-ul funcționează corect. Aceasta nu afectează performanța." + } + }, + "detector": { + "temperature": "Temperatura Detectorului", + "title": "Detectori", + "cpuUsage": "Utilizarea procesorului", + "inferenceSpeed": "Viteza de inferență", + "memoryUsage": "Utilizare memorie detector", + "cpuUsageInformation": "Procesorul utilizat pentru pregătirea datelor de intrare și ieșire către/dinspre modelele de detecție. Această valoare nu măsoară utilizarea în timpul inferenței, chiar dacă este folosit un GPU sau un accelerator." + }, + "otherProcesses": { + "title": "Alte Procese", + "processCpuUsage": "Utilizare CPU", + "processMemoryUsage": "Utilizare memorie" + }, + "title": "General" + }, + "storage": { + "recordings": { + "title": "Înregistrări", + "earliestRecording": "Prima înregistrare disponibilă:", + "tips": "Această valoare reprezintă spațiul total de stocare utilizat de înregistrările din baza de date a Frigate. Frigate nu urmărește utilizarea spațiului pentru toate fișierele de pe discul tău." + }, + "title": "Spațiu stocare", + "cameraStorage": { + "title": "Spațiu stocare camere", + "camera": "Camera", + "unusedStorageInformation": "Informații despre stocarea neutilizată", + "storageUsed": "Spațiu stocare", + "percentageOfTotalUsed": "Procent din total", + "unused": { + "title": "Nefolosit", + "tips": "Această valoare este posibil să nu reprezinte cu acuratețe spațiul liber disponibil pentru Frigate dacă ai și alte fișiere stocate pe disc, în afara înregistrărilor Frigate. Frigate nu monitorizează utilizarea spațiului pentru fișiere din afara propriilor sale înregistrări." + }, + "bandwidth": "Lățime de bandă" + }, + "overview": "Prezentare generală", + "shm": { + "title": "Alocare SHM (memorie partajată)", + "warning": "Dimensiunea curentă a SHM de {{total}}MB este prea mică. Măriți-o la cel puțin {{min_shm}}MB.", + "readTheDocumentation": "Citește documentația" + } + }, + "title": "Sistem", + "logs": { + "download": { + "label": "Jurnal Descărcări" + }, + "copy": { + "label": "Copiază", + "success": "Jurnalul a fost copiat", + "error": "Jurnalul nu s-a putut copia" + }, + "type": { + "label": "Tip", + "timestamp": "Data / ora", + "tag": "Etichetă", + "message": "Mesaj" + }, + "tips": "Jurnalele se transmit de pe server", + "toast": { + "error": { + "fetchingLogsFailed": "Eroare la preluarea jurnalelor: {{errorMessage}}", + "whileStreamingLogs": "Eroare la transmiterea jurnalelor: {{errorMessage}}" + } + } + }, + "metrics": "Metrici de sistem", + "enrichments": { + "title": "Îmbogățiri", + "embeddings": { + "image_embedding": "Încorporare imagini", + "text_embedding": "Încorporare text", + "plate_recognition": "Recunoaștere numere de înmatriculare", + "image_embedding_speed": "Viteză încorporare imagini", + "face_recognition": "Recunoaștere facială", + "face_recognition_speed": "Viteză recunoaștere facială", + "plate_recognition_speed": "Viteză recunoaștere numere de înmatriculare", + "face_embedding_speed": "Viteză încorporare fețe", + "yolov9_plate_detection_speed": "Viteza detecției numerelor de înmatriculare YOLOv9", + "text_embedding_speed": "Viteză încorporare text", + "yolov9_plate_detection": "Detectare numere de înmatriculare YOLOv9", + "review_description": "Descriere Revizuire", + "review_description_speed": "Viteză Descriere Revizuire", + "review_description_events_per_second": "Descriere Revizuire", + "object_description": "Descriere Obiect", + "object_description_speed": "Viteză Descriere Obiect", + "object_description_events_per_second": "Descriere Obiect" + }, + "infPerSecond": "Inferențe pe secundă", + "averageInf": "Timp Mediu de Inferență" + }, + "cameras": { + "info": { + "codec": "Codec:", + "resolution": "Rezoluție:", + "cameraProbeInfo": "Informații testare cameră {{camera}}", + "streamDataFromFFPROBE": "Datele stream-ului sunt obținute cu ffprobe.", + "aspectRatio": "raport aspect", + "fetching": "Se preiau datele camerei", + "stream": "Stream {{idx}}", + "video": "Video:", + "audio": "Sunet:", + "error": "Eroare:{{error}}", + "tips": { + "title": "Informații test cameră" + }, + "fps": "Cadre/s:", + "unknown": "Necunoscut" + }, + "label": { + "capture": "capturare", + "skipped": "sărite", + "overallSkippedDetectionsPerSecond": "Detecții totale sărite pe secundă", + "cameraCapture": "captură {{camName}}", + "cameraDetect": "detectare {{camName}}", + "cameraFramesPerSecond": "cadre pe secundă {{camName}}", + "cameraDetectionsPerSecond": "detecții pe secundă {{camName}}", + "cameraSkippedDetectionsPerSecond": "detecții sărite pe secundă {{camName}}", + "overallFramesPerSecond": "Cadre totale pe secundă", + "overallDetectionsPerSecond": "Detecții totale pe secundă", + "detect": "detectare", + "cameraFfmpeg": "{{camName}} FFmpeg", + "camera": "camere", + "ffmpeg": "FFmpeg" + }, + "title": "Camere", + "overview": "Prezentare generală", + "framesAndDetections": "Cadre / Detecții", + "toast": { + "success": { + "copyToClipboard": "Datele testului au fost copiate." + }, + "error": { + "unableToProbeCamera": "Testarea camerei nu a fost posibilă: {{errorMessage}}" + } + } + }, + "stats": { + "reindexingEmbeddings": "Reindexare încorporări ({{processed}}% completă)", + "detectIsVerySlow": "{{detect}} este foarte lent ({{speed}} ms)", + "detectIsSlow": "{{detect}} este lent ({{speed}} ms)", + "detectHighCpuUsage": "Camera {{camera}} are o utilizare ridicată a procesorului pentru detecție ({{detectAvg}}%)", + "ffmpegHighCpuUsage": "Camera {{camera}} are o utilizare ridicată a procesorului FFmpeg ({{ffmpegAvg}}%)", + "cameraIsOffline": "{{camera}} este offline", + "healthy": "Sistemul funcționează normal", + "shmTooLow": "Alocarea /dev/shm ({{total}} MB) ar trebui mărită la cel puțin {{min}} MB." + }, + "lastRefreshed": "Ultima reîmprospătare: " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ru/audio.json new file mode 100644 index 0000000..e9e6bfc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/audio.json @@ -0,0 +1,503 @@ +{ + "grunt": "Хрюк", + "babbling": "Бормотание", + "laughter": "Смех", + "choir": "Хор", + "mantra": "Мантра", + "speech": "Речь", + "child_singing": "Детское пение", + "whistling": "Свист", + "breathing": "Дыхание", + "synthetic_singing": "Синтетическое пение", + "rapping": "Рэп", + "yell": "Крик", + "humming": "Гудение", + "groan": "Стон", + "bellow": "Рёв", + "whispering": "Шёпот", + "whoop": "Возглас", + "crying": "Плач", + "yodeling": "Йодль (пение)", + "snicker": "Смешок", + "sigh": "Сигнал", + "singing": "Пение", + "wheeze": "Хрип", + "snoring": "Храп", + "gasp": "Вздох", + "pant": "Пыхтение", + "snort": "Фырканье", + "sniff": "Нюхание", + "burping": "Отрыжка", + "cough": "Кашель", + "run": "Бег", + "throat_clearing": "Прочистка горла", + "sneeze": "Чихание", + "shuffle": "Шаркание", + "chewing": "Жевание", + "biting": "Кусание", + "gargling": "Полоскание горла", + "stomach_rumble": "Урчание живота", + "hiccup": "Икание", + "fart": "Пукание", + "footsteps": "Шаги", + "chant": "Песнопение", + "hands": "Руки", + "finger_snapping": "Щелкать пальцами", + "clapping": "Хлопать", + "moo": "Мычание", + "cowbell": "Коровий колокольчик", + "heart_murmur": "Сердечный шум", + "cheering": "Ликование", + "applause": "Аплодисменты", + "chatter": "Болтовня", + "crowd": "Толпа", + "children_playing": "Игра детей", + "animal": "Животное", + "pets": "Домашние животные", + "dog": "Собака", + "bark": "Лай", + "yip": "Тявканье", + "howl": "Вой", + "whimper_dog": "Собачий скулеж", + "cat": "Кошка", + "purr": "Мурлыканье", + "meow": "Мяуканье", + "hiss": "Шипение", + "growling": "Рычание", + "bow_wow": "Гавканье", + "heartbeat": "Сердцебиение", + "caterwaul": "Кошачий вой", + "horse": "Лошадь", + "clip_clop": "Цоканье", + "neigh": "Ржание", + "livestock": "Скот", + "cattle": "Крупный рогатый скот", + "pig": "Свинья", + "oink": "Хрюканье", + "bleat": "Блеяние", + "sheep": "Овца", + "fowl": "Домашняя птица", + "goat": "Коза", + "chicken": "Курица", + "cluck": "Кудахтанье", + "cock_a_doodle_doo": "Кукареканье", + "turkey": "Индейка", + "gobble": "Бормотание индейки", + "duck": "Утка", + "quack": "Кряканье", + "goose": "Гусь", + "honk": "Гоготание", + "wild_animals": "Дикие животные", + "roaring_cats": "Рычащие кошки", + "roar": "Рык", + "chirp": "Чириканье", + "squawk": "Птичий крик", + "pigeon": "Голубь", + "coo": "Воркование", + "crow": "Ворона", + "caw": "Карканье", + "owl": "Сова", + "hoot": "Уханье", + "flapping_wings": "Хлопание крыльев", + "dogs": "Собаки", + "rats": "Крысы", + "mouse": "Мышь", + "insect": "Насекомое", + "cricket": "Сверчок", + "mosquito": "Комар", + "fly": "Муха", + "buzz": "Жужжание", + "frog": "Лягушка", + "croak": "Кваканье", + "snake": "Змея", + "rattle": "Треск", + "music": "Музыка", + "musical_instrument": "Музыкальный инструмент", + "whale_vocalization": "Пение кита", + "plucked_string_instrument": "Щипковый струнный инструмент", + "guitar": "Гитара", + "patter": "Шорох", + "bass_guitar": "Бас-гитара", + "steel_guitar": "Стальная гитара", + "tapping": "Постукивание", + "car": "Автомобиль", + "motorcycle": "Мотоцикл", + "bicycle": "Велосипед", + "bird": "Птица", + "electric_guitar": "Электрогитара", + "acoustic_guitar": "Акустическая гитара", + "scream": "Крик", + "strum": "Звук струн", + "banjo": "Банджо", + "zither": "Цитра", + "ukulele": "Укулеле", + "keyboard": "Клавиатура", + "electric_piano": "Электропианино", + "organ": "Орган", + "electronic_organ": "Электроорган", + "synthesizer": "Синтезатор", + "hammond_organ": "Орган Хаммонда", + "sampler": "Сэмплер", + "harpsichord": "Клавесин", + "percussion": "Ударные инструменты", + "drum_kit": "Ударная установка", + "drum_machine": "Драммашина", + "drum": "Барабан", + "snare_drum": "Малый барабан", + "rimshot": "Обод барабана", + "drum_roll": "Барабанная дробь", + "bass_drum": "Бас-барабан", + "timpani": "Литавры", + "tabla": "Табла", + "cymbal": "Тарелка", + "hi_hat": "Хай-хэт", + "wood_block": "Вуд-блок", + "tambourine": "Бубен", + "maraca": "Маракас", + "gong": "Гонг", + "tubular_bells": "Трубчатые колокола", + "mallet_percussion": "Маллет-перкуссия", + "marimba": "Маримба", + "glockenspiel": "Колокольчики", + "vibraphone": "Вибрафон", + "steelpan": "Стальной барабан", + "orchestra": "Оркестр", + "brass_instrument": "Медный духовой инструмент", + "french_horn": "Валторна", + "trumpet": "Труба", + "trombone": "Тромбон", + "bowed_string_instrument": "Смычковый струнный инструмент", + "string_section": "Струнная секция", + "mandolin": "Мандолина", + "piano": "Пианино", + "sitar": "Ситар", + "violin": "Скрипка", + "pizzicato": "Пиццикато", + "cello": "Виолончель", + "double_bass": "Контрабас", + "wind_instrument": "Духовой инструмент", + "flute": "Флейта", + "saxophone": "Саксофон", + "clarinet": "Кларнет", + "harp": "Арфа", + "bell": "Колокол", + "church_bell": "Церковный колокол", + "jingle_bell": "Бубенчик", + "bicycle_bell": "Велосипедный звонок", + "tuning_fork": "Камертон", + "chime": "Колокольчик", + "wind_chime": "Музыка ветра", + "harmonica": "Губная гармошка", + "accordion": "Аккордеон", + "bagpipes": "Волынка", + "didgeridoo": "Диджериду", + "theremin": "Терменвокс", + "singing_bowl": "Поющая чаша", + "scratching": "Скрэтчинг", + "pop_music": "Поп-музыка", + "hip_hop_music": "Хип-хоп", + "beatboxing": "Битбоксинг", + "rock_music": "Рок-музыка", + "heavy_metal": "Хеви-метал", + "punk_rock": "Панк-рок", + "grunge": "Гранж", + "progressive_rock": "Прогрессив-рок", + "rock_and_roll": "Рок-н-ролл", + "psychedelic_rock": "Психоделический рок", + "rhythm_and_blues": "Ритм-н-блюз", + "soul_music": "Соул", + "bluegrass": "Блюграсс", + "funk": "Фанк", + "middle_eastern_music": "Ближневосточная музыка", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Классическая музыка", + "opera": "Опера", + "house_music": "Хаус", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм-н-бейс", + "electronica": "Электроника", + "electronic_dance_music": "Электронная танцевальная музыка", + "ambient_music": "Эмбиент", + "music_of_latin_america": "Латиноамериканская музыка", + "salsa_music": "Сальса", + "flamenco": "Фламенко", + "blues": "Блюз", + "music_for_children": "Детская музыка", + "new-age_music": "Нью-эйдж", + "a_capella": "А капелла", + "music_of_africa": "Африканская музыка", + "afrobeat": "Афробит", + "christian_music": "Христианская музыка", + "gospel_music": "Госпел", + "music_of_asia": "Азиатская музыка", + "carnatic_music": "Карнатическая музыка", + "music_of_bollywood": "Музыка Болливуда", + "ska": "Ска", + "traditional_music": "Традиционная музыка", + "independent_music": "Инди", + "song": "Песня", + "background_music": "Фоновая музыка", + "theme_music": "Тематическая музыка", + "jingle": "Джингл", + "soundtrack_music": "Саундтрек", + "lullaby": "Колыбельная", + "video_game_music": "Музыка из видеоигр", + "christmas_music": "Рождественская музыка", + "dance_music": "Танцевальная музыка", + "wedding_music": "Свадебная музыка", + "happy_music": "Весёлая музыка", + "sad_music": "Грустная музыка", + "tender_music": "Нежная музыка", + "exciting_music": "Энергичная музыка", + "angry_music": "Агрессивная музыка", + "scary_music": "Жуткая музыка", + "wind": "Ветер", + "rustling_leaves": "Шуршание листьев", + "wind_noise": "Шум ветра", + "thunderstorm": "Гроза", + "thunder": "Гром", + "water": "Вода", + "rain": "Дождь", + "raindrop": "Капли дождя", + "rain_on_surface": "Дождь на поверхности", + "stream": "Поток", + "waterfall": "Водопад", + "gurgling": "Журчание", + "fire": "Огонь", + "crackle": "Потрескивание", + "vehicle": "Транспорт", + "boat": "Лодка", + "sailboat": "Парусник", + "rowboat": "Вёсельная лодка", + "motorboat": "Моторная лодка", + "ship": "Корабль", + "motor_vehicle": "Моторный транспорт", + "power_windows": "Электростеклоподъемники", + "skidding": "Занос", + "tire_squeal": "Визг шин", + "car_passing_by": "Проезжающая машина", + "race_car": "Гоночный автомобиль", + "truck": "Грузовик", + "air_brake": "Пневматический тормоз", + "air_horn": "Пневматический гудок", + "reversing_beeps": "Сигнал заднего хода", + "ice_cream_truck": "Грузовик с мороженым", + "bus": "Автобус", + "emergency_vehicle": "Транспорт экстренных служб", + "police_car": "Полицейский автомобиль", + "fire_engine": "Пожарная машина", + "rail_transport": "Рельсовый транспорт", + "train": "Поезд", + "train_whistle": "Свисток поезда", + "train_horn": "Гудок поезда", + "railroad_car": "Железнодорожный вагон", + "train_wheels_squealing": "Визг колес поезда", + "subway": "Метро", + "aircraft": "Воздушное судно", + "aircraft_engine": "Двигатель воздушного судна", + "jet_engine": "Реактивный двигатель", + "propeller": "Пропеллер", + "fixed-wing_aircraft": "Самолет с неподвижным крылом", + "skateboard": "Скейтборд", + "engine": "Двигатель", + "light_engine": "Легкий двигатель", + "dental_drill's_drill": "Стоматологическая бормашина", + "medium_engine": "Средний двигатель", + "heavy_engine": "Тяжёлый двигатель", + "engine_knocking": "Детонация двигателя", + "engine_starting": "Запуск двигателя", + "idling": "Холостой ход", + "accelerating": "Ускорение", + "ding-dong": "Дин-дон", + "sliding_door": "Раздвижная дверь", + "slam": "Хлопок", + "knock": "Стук", + "tap": "Небольшой стук", + "squeak": "Писк", + "cupboard_open_or_close": "Открытие или закрытие шкафа", + "drawer_open_or_close": "Открытие или закрытие ящика", + "dishes": "Тарелки", + "cutlery": "Столовые приборы", + "chopping": "Нарезание", + "frying": "Жарка", + "microwave_oven": "Микроволновка", + "blender": "Блендер", + "water_tap": "Водопроводный кран", + "sink": "Раковина", + "bathtub": "Ванна", + "hair_dryer": "Фен", + "toilet_flush": "Слив унитаза", + "toothbrush": "Зубная щетка", + "zipper": "Молния на одежде", + "keys_jangling": "Бряканье ключей", + "coin": "Монета", + "scissors": "Ножницы", + "electric_shaver": "Электробритва", + "shuffling_cards": "Тасование карт", + "typing": "Печатание", + "typewriter": "Печатная машинка", + "computer_keyboard": "Компьютерная клавиатура", + "writing": "Письмо", + "alarm": "Сигнализация", + "telephone": "Телефон", + "telephone_bell_ringing": "Звонок телефона", + "ringtone": "Рингтон", + "telephone_dialing": "Набор телефонного номера", + "dial_tone": "Телефонный гудок", + "busy_signal": "Сигнал занято", + "alarm_clock": "Будильник", + "siren": "Сирена", + "civil_defense_siren": "Сирена гражданской обороны", + "foghorn": "Туманный горн", + "whistle": "Свисток", + "steam_whistle": "Паровой свисток", + "mechanisms": "Механизмы", + "clock": "Часы", + "tick": "Тик", + "tick-tock": "Тик-так", + "gears": "Шестерни", + "pulleys": "Шкивы", + "sewing_machine": "Швейная машинка", + "mechanical_fan": "Механический вентилятор", + "printer": "Принтер", + "camera": "Камера", + "single-lens_reflex_camera": "Зеркальная камера", + "tools": "Инструменты", + "sawing": "Распиловка", + "filing": "Звук напильника", + "sanding": "Шлифовка", + "power_tool": "Электроинструмент", + "drill": "Дрель", + "explosion": "Взрыв", + "gunshot": "Выстрел", + "machine_gun": "Автомат", + "fusillade": "Оружейная очередь", + "artillery_fire": "Артиллерийский огонь", + "burst": "Очередь выстрелов", + "eruption": "Извержение", + "boom": "Бум", + "wood": "Дерево", + "chop": "Рубка", + "splinter": "Щепка", + "glass": "Стекло", + "crack": "Трещина", + "chink": "Звон", + "shatter": "Разбитие", + "silence": "Тишина", + "sound_effect": "Звуковой эффект", + "environmental_noise": "Шум окружающей среды", + "static": "Статический шум", + "field_recording": "Полевая запись", + "country": "Кантри", + "vocal_music": "Вокальная музыка", + "electronic_music": "Электронная музыка", + "folk_music": "Фолк-музыка", + "trance_music": "Транс", + "swing_music": "Свинг", + "reggae": "Регги", + "waves": "Волны", + "ambulance": "Скорая помощь", + "helicopter": "Вертолет", + "radio": "Радио", + "lawn_mower": "Газонокосилка", + "electric_toothbrush": "Электрическая зубная щетка", + "air_conditioning": "Кондиционер", + "toot": "Гудок", + "traffic_noise": "Дорожный шум", + "ocean": "Океан", + "steam": "Пар", + "car_alarm": "Автомобильная сигнализация", + "buzzer": "Зуммер", + "chainsaw": "Цепная пила", + "door": "Дверь", + "doorbell": "Дверной звонок", + "smoke_detector": "Датчик дыма", + "white_noise": "Белый шум", + "cash_register": "Касса", + "vacuum_cleaner": "Пылесос", + "fire_alarm": "Пожарная сигнализация", + "ratchet": "Трещотка", + "cap_gun": "Игрушечный пистолет", + "fireworks": "Фейерверк", + "jackhammer": "Отбойный молоток", + "pink_noise": "Розовый шум", + "hammer": "Молоток", + "firecracker": "Петарда", + "television": "Телевидение", + "echo": "Эхо", + "noise": "Шум", + "mains_hum": "Гул сети", + "cacophony": "Какофония", + "throbbing": "Пульсирующий", + "vibration": "Вибрация", + "sodeling": "Соделинг", + "chird": "Чирд", + "change_ringing": "Перезвон", + "shofar": "Шофар", + "liquid": "Жидкость", + "splash": "Брызги", + "slosh": "Плеск", + "squish": "Хлюпанье", + "drip": "Капля", + "pour": "Литьё", + "trickle": "Струйка", + "gush": "Бурный поток", + "fill": "Наполнение", + "spray": "Распыление", + "pump": "Насос", + "stir": "Перемешивание", + "boiling": "Кипение", + "sonar": "Сонар", + "arrow": "Стрела", + "whoosh": "Вжух", + "thump": "Глухой удар", + "thunk": "Тупой удар", + "electronic_tuner": "Электронный тюнер", + "effects_unit": "Блок эффектов", + "chorus_effect": "Эффект хоруса", + "basketball_bounce": "Отскок баскетбольного мяча", + "bang": "Бах", + "slap": "Шлепок", + "whack": "Удар", + "smash": "Разбивание", + "breaking": "Разрушение", + "bouncing": "Отскок", + "whip": "Хлыст", + "flap": "Хлопание", + "scratch": "Царапанье", + "scrape": "Скребок", + "rub": "Трение", + "roll": "Качение", + "crushing": "Дробление", + "crumpling": "Сминание", + "tearing": "Разрывание", + "beep": "Бип", + "ping": "Пинг", + "ding": "Динь", + "clang": "Лязг", + "squeal": "Визг", + "creak": "Скрипение", + "rustle": "Шуршание", + "whir": "Жужжание", + "clatter": "Грохот", + "sizzle": "Шипение", + "clicking": "Щелканье", + "clickety_clack": "Щелчок-Клак", + "rumble": "Грохотать", + "plop": "Плюх", + "hum": "Гул", + "zing": "Зинг", + "boing": "Боинг", + "crunch": "Хруст", + "sine_wave": "Синусоида", + "harmonic": "Гармоника", + "chirp_tone": "Тон чириканья", + "pulse": "Импульс", + "inside": "Внутри", + "outside": "Снаружи", + "reverberation": "Реверберация", + "distortion": "Искажение", + "sidetone": "Боковой тон" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/common.json b/sam2-cpu/frigate-dev/web/public/locales/ru/common.json new file mode 100644 index 0000000..341cad2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/common.json @@ -0,0 +1,311 @@ +{ + "time": { + "untilForTime": "До {{time}}", + "untilForRestart": "До перезапуска Frigate.", + "untilRestart": "До перезапуска", + "ago": "{{timeAgo}} назад", + "justNow": "Только что", + "today": "Сегодня", + "yesterday": "Вчера", + "thisWeek": "На этой неделе", + "last14": "Последние 14 дней", + "last30": "Последние 30 дней", + "last7": "Последние 7 дней", + "thisMonth": "В этом месяце", + "5minutes": "5 минут", + "30minutes": "30 минут", + "1hour": "1 час", + "12hours": "12 часов", + "24hours": "24 часа", + "pm": "pm", + "am": "am", + "yr": "{{time}} л", + "year_one": "{{time}} год", + "year_few": "{{time}} года", + "year_many": "{{time}} лет", + "mo": "{{time}} мес", + "month_one": "{{time}} месяц", + "month_few": "{{time}} месяца", + "month_many": "{{time}} месяцев", + "d": "{{time}} д", + "h": "{{time}} ч", + "hour_one": "{{time}} час", + "hour_few": "{{time}} часа", + "hour_many": "{{time}} часов", + "m": "{{time}} мин", + "minute_one": "{{time}} минута", + "minute_few": "{{time}} минуты", + "minute_many": "{{time}} минут", + "day_one": "{{time}} день", + "day_few": "{{time}} дня", + "day_many": "{{time}} дней", + "lastWeek": "На прошлой неделе", + "lastMonth": "В прошлом месяце", + "10minutes": "10 минут", + "s": "{{time}} с", + "second_one": "{{time}} секунда", + "second_few": "{{time}} секунды", + "second_many": "{{time}} секунд", + "formattedTimestampExcludeSeconds": { + "24hour": "%b %-d, %H:%M", + "12hour": "%b %-d, %I:%M %p" + }, + "formattedTimestampWithYear": { + "24hour": "%b %-d %Y, %H:%M", + "12hour": "%b %-d %Y, %I:%M %p" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "dd/MM h:mm:ssa" + }, + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestampOnlyMonthAndDay": "%b %-d", + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampFilename": { + "24hour": "dd-MM-yy-HH-mm-ss", + "12hour": "dd-MM-yy-h-mm-ss-a" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "В процессе" + }, + "selectItem": "Выбрать {{item}}", + "button": { + "apply": "Применить", + "done": "Готово", + "enabled": "Включено", + "enable": "Включить", + "save": "Сохранить", + "saving": "Сохранение…", + "fullscreen": "Полноэкранный режим", + "pictureInPicture": "Картинка в картинке", + "twoWayTalk": "Двусторонняя связь", + "cameraAudio": "Аудио с камеры", + "on": "Вкл", + "edit": "Редактировать", + "copyCoordinates": "Скопировать координаты", + "delete": "Удалить", + "yes": "Да", + "no": "Нет", + "download": "Загрузить", + "info": "Информация", + "suspended": "Приостановлено", + "cancel": "Отменить", + "disable": "Отключить", + "reset": "Сбросить", + "disabled": "Отключено", + "close": "Закрыть", + "copy": "Скопировать", + "back": "Назад", + "history": "История", + "off": "Выкл", + "exitFullscreen": "Выйти из полноэкранного режима", + "unsuspended": "Возобновить", + "play": "Воспроизвести", + "unselect": "Снять выбор", + "export": "Экспортировать", + "deleteNow": "Удалить сейчас", + "next": "Следующий" + }, + "label": { + "back": "Вернуться", + "hide": "Скрыть {{item}}", + "show": "Показать {{item}}", + "ID": "ID", + "all": "Все", + "none": "Ничего" + }, + "unit": { + "speed": { + "kph": "км/ч", + "mph": "миль/ч" + }, + "length": { + "meters": "метры", + "feet": "футы" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/час", + "mbph": "МБ/час", + "gbph": "ГБ/час" + } + }, + "menu": { + "configuration": "Конфигурация", + "systemLogs": "Логи системы", + "settings": "Настройки", + "configurationEditor": "Редактор конфигурации", + "system": "Система", + "systemMetrics": "Метрики системы", + "languages": "Языки", + "language": { + "en": "English (Английский)", + "zhCN": "简体中文 (Упрощённый китайский)", + "es": "Español (Испанский)", + "hi": "हिन्दी (Хинди)", + "fr": "Français (Французский)", + "ar": "العربية (Арабский)", + "pt": "Português (Португальский)", + "ru": "Русский", + "tr": "Türkçe (Турецкий)", + "nl": "Nederlands (Нидерландский)", + "cs": "Čeština (Чешский)", + "nb": "Norsk Bokmål (Норвежский (букмол))", + "vi": "Tiếng Việt (Вьетнамский)", + "fa": "فارسی (Фарси)", + "pl": "Polski (Польский)", + "uk": "Українська (Украинский)", + "el": "Ελληνικά (Греческий)", + "da": "Dansk (Датский)", + "sk": "Slovenčina (Словацкий)", + "sv": "Svenska (Шведский)", + "hu": "Magyar (Венгерский)", + "fi": "Suomi (Финский)", + "ro": "Română (Румынский)", + "ja": "日本語 (Японский)", + "it": "Italiano (Итальянский)", + "de": "Deutsch (Немецкий)", + "ko": "한국어 (Корейский)", + "he": "עברית (Иврит)", + "withSystem": { + "label": "Использовать системные настройки языка" + }, + "yue": "粵語 (Кантонский)", + "th": "ไทย (Тайский)", + "ca": "Català (Каталонский)", + "ptBR": "Português brasileiro (Бразильский португальский)", + "sr": "Српски (Сербский)", + "sl": "Slovenščina (Словенский)", + "lt": "Lietuvių (Литовский)", + "bg": "Български (Болгарский)", + "gl": "Galego (Галисийский)", + "id": "Bahasa Indonesia (Индонезийский)", + "ur": "اردو (Урду)" + }, + "darkMode": { + "withSystem": { + "label": "Использовать системные настройки светлой/тёмной темы" + }, + "label": "Тёмный режим", + "light": "Светлый", + "dark": "Тёмный" + }, + "withSystem": "Системный", + "theme": { + "label": "Тема", + "blue": "Синяя", + "default": "По умолчанию", + "green": "Зелёная", + "nord": "Северная", + "red": "Красная", + "contrast": "Высокий контраст", + "highcontrast": "Контрастная" + }, + "help": "Помощь", + "documentation": { + "title": "Документация", + "label": "Документация по Frigate" + }, + "explore": "Поиск событий", + "restart": "Перезапуск Frigate", + "live": { + "title": "Прямой эфир", + "allCameras": "Все камеры", + "cameras": { + "count_one": "{{count}} камера", + "count_few": "{{count}} камеры", + "count_many": "{{count}} камер", + "title": "Камеры" + } + }, + "review": "Обзор событий", + "export": "Экспортировать", + "uiPlayground": "Среда тестирования интерфейсов", + "faceLibrary": "Библиотека лиц", + "user": { + "title": "Пользователь", + "account": "Аккаунт", + "current": "Текущий пользователь: {{user}}", + "anonymous": "anonymous", + "logout": "Выход", + "setPassword": "Установить пароль" + }, + "appearance": "Внешний вид", + "classification": "Распознование" + }, + "pagination": { + "label": "пагинация", + "previous": { + "title": "Предыдущая", + "label": "Переход на предыдущую страницу" + }, + "next": { + "title": "Следующая", + "label": "Переход на следующую страницу" + }, + "more": "Больше страниц" + }, + "accessDenied": { + "desc": "У вас нет разрешения на просмотр этой страницы.", + "documentTitle": "Доступ запрещён - Frigate", + "title": "Доступ запрещён" + }, + "notFound": { + "desc": "Страница не найдена", + "documentTitle": "Не найдена - Frigate", + "title": "404" + }, + "toast": { + "copyUrlToClipboard": "URL скопирован в буфер обмена.", + "save": { + "error": { + "noMessage": "Не удалось сохранить изменения конфигурации", + "title": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "title": "Сохранить" + } + }, + "role": { + "title": "Роль", + "admin": "Администратор", + "viewer": "Наблюдатель", + "desc": "Администраторы имеют полный доступ ко всем функциям в интерфейсе Frigate. Наблюдатели ограничены просмотром камер, элементов просмотра и архивных записей." + }, + "readTheDocumentation": "Читать документацию", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} и {{1}}", + "many": "{{items}}, и {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необязательный", + "internalID": "Внутренний идентификатор Frigate, используемый в конфигурации и базе данных" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/auth.json new file mode 100644 index 0000000..17b9839 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Имя пользователя", + "password": "Пароль", + "login": "Логин", + "errors": { + "usernameRequired": "Необходимо ввести имя пользователя", + "passwordRequired": "Необходимо ввести пароль", + "rateLimit": "Превышение числа попыток. Попробуй еще раз позже.", + "loginFailed": "Ошибка входа", + "unknownError": "Неизвестная ошибка. Проверьте логи.", + "webUnknownError": "Неизвестная ошибка. Проверьте логи консоли." + }, + "firstTimeLogin": "Пытаетесь войти в систему впервые? Учетные данные указаны в логах Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/camera.json new file mode 100644 index 0000000..8a8c1a4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Группы камер", + "add": "Добавить группу камер", + "edit": "Редактирование группы камер", + "delete": { + "label": "Удалить группу камер", + "confirm": { + "title": "Подтвердить удаление", + "desc": "Вы уверены, что хотите удалить группу камер {{name}}?" + } + }, + "name": { + "label": "Название", + "placeholder": "Введите название…", + "errorMessage": { + "exists": "Такое название группы камер уже существует.", + "nameMustNotPeriod": "Название группы камер не должно содержать точки.", + "invalid": "Неверное название группы камер.", + "mustLeastCharacters": "Название группы камер должно содержать не менее 2 символов." + } + }, + "cameras": { + "label": "Камеры", + "desc": "Выберите камеры для этой группы." + }, + "icon": "Иконка", + "success": "Группа камер {{name}} сохранена.", + "camera": { + "setting": { + "label": "Настройки видеопотока", + "desc": "Изменение параметров прямой трансляции для панели этой группы камер. Эти настройки зависят от устройства/браузера.", + "audioIsAvailable": "Для этого потока доступен звук", + "audioIsUnavailable": "Для этого потока звук недоступен", + "audio": { + "tips": { + "title": "Аудио должно выводиться с вашей камеры и быть настроено в go2rtc для этого потока.", + "document": "Читать документацию " + } + }, + "streamMethod": { + "label": "Метод стриминга", + "method": { + "noStreaming": { + "label": "Нет потока", + "desc": "Кадры с камеры обновляются раз в минуту, без прямой трансляции." + }, + "smartStreaming": { + "label": "Умный поток (рекомендуется)", + "desc": "Для экономии ресурсов поток обновляется раз в минуту. При обнаружении активности автоматически активируется прямая трансляция." + }, + "continuousStreaming": { + "label": "Непрерывный поток", + "desc": { + "warning": "Непрерывная потоковая передача может привести к высокому потреблению трафика и проблемам с производительностью. Используйте с осторожностью.", + "title": "Когда изображение выводится на панель, оно всегда обновляется в режиме реального времени, вне зависимости от обнаружения активности." + } + } + }, + "placeholder": "Выберите способ потоковой передачи" + }, + "compatibilityMode": { + "label": "Режим совместимости", + "desc": "Активируйте эту настройку только при появлении цветовых искажений или диагональной полосы с правого края в прямой трансляции." + }, + "title": "Настройки видеопотока {{cameraName}}", + "stream": "Поток", + "placeholder": "Выбрать поток" + }, + "birdseye": "Birdseye" + } + }, + "debug": { + "options": { + "label": "Настройки", + "title": "Опции", + "hideOptions": "Скрыть опции", + "showOptions": "Показать опции" + }, + "boundingBox": "Ограничивающая рамка", + "timestamp": "Метка времени", + "zones": "Зоны", + "mask": "Маска", + "motion": "Движение", + "regions": "Регионы" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/dialog.json new file mode 100644 index 0000000..a1dc88e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/dialog.json @@ -0,0 +1,135 @@ +{ + "restart": { + "title": "Вы уверены, что хотите перезапустить Frigate?", + "button": "Перезапуск", + "restarting": { + "title": "Frigate перезапускается", + "content": "Эта страница перезагрузится через {{countdown}} сек.", + "button": "Принудительная перезагрузка" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Отправить в Frigate+", + "desc": "Объекты в местах, которых вы хотите избежать, не являются ложными срабатываниями. Отправка их как ложных срабатываний запутает модель." + }, + "review": { + "true": { + "label": "Подтвердите метку для Frigate Plus", + "true_one": "Это {{label}}", + "true_few": "Это {{label}}", + "true_many": "Это {{label}}" + }, + "false": { + "label": "Не подтверждать эту метку для Frigate Plus", + "false_one": "Это не {{label}}", + "false_few": "Это не {{label}}", + "false_many": "Это не {{label}}" + }, + "state": { + "submitted": "Отправлено" + }, + "question": { + "ask_an": "Это объект — {{label}} ?", + "label": "Подтвердить эту метку для Frigate Plus", + "ask_a": "Это объект — {{label}}?", + "ask_full": "Это объект — {{untranslatedLabel}} ({{translatedLabel}})?" + } + } + }, + "video": { + "viewInHistory": "Посмотреть в истории" + } + }, + "export": { + "time": { + "fromTimeline": "Выбрать на таймлайне", + "custom": "Пользовательский", + "start": { + "title": "Время начала", + "label": "Выберите время начала" + }, + "end": { + "title": "Время окончания", + "label": "Выберите время окончания" + }, + "lastHour_one": "Последний час", + "lastHour_few": "Последние {{count}} часа", + "lastHour_many": "Последние {{count}} часов" + }, + "name": { + "placeholder": "Введите название для экспорта" + }, + "select": "Выбрать", + "export": "Экспорт", + "selectOrExport": "Выбрать или экспортировать", + "toast": { + "success": "Экспорт успешно запущен. Файл доступен на странице экспорта.", + "error": { + "failed": "Не удалось запустить экспорт: {{error}}", + "noVaildTimeSelected": "Не выбран допустимый временной диапазон", + "endTimeMustAfterStartTime": "Время окончания должно быть после времени начала" + } + }, + "fromTimeline": { + "saveExport": "Сохранить экспорт", + "previewExport": "Предпросмотр экспорта" + } + }, + "streaming": { + "label": "Поток", + "restreaming": { + "disabled": "Рестриминг не включён для этой камеры.", + "desc": { + "title": "Настройте go2rtc для дополнительных вариантов просмотра в реальном времени и аудио для этой камеры.", + "readTheDocumentation": "Читать документацию" + } + }, + "debugView": "Режим отладки", + "showStats": { + "label": "Отображение статистики потока", + "desc": "Включите эту опцию, чтобы отображать статистику потока в виде наложения на изображение с камеры." + } + }, + "search": { + "saveSearch": { + "label": "Сохранить поиск", + "placeholder": "Введите название для вашего поиска", + "overwrite": "{{searchName}} уже существует. Сохранение перезапишет существующее значение.", + "success": "Поиск {{searchName}} был сохранен.", + "button": { + "save": { + "label": "Сохранить этот поиск" + } + }, + "desc": "Укажите название этого сохранённого поиска." + } + }, + "recording": { + "confirmDelete": { + "title": "Подтвердить удаление", + "desc": { + "selected": "Вы уверены, что хотите удалить все записанное видео, связанное с этим элементом просмотра?

    Удерживайте клавишу Shift, чтобы пропустить это окно в будущем." + }, + "toast": { + "error": "Не удалось удалить: {{error}}", + "success": "Видеоматериалы, связанные с выбранными предметами просмотра, были успешно удалены." + } + }, + "button": { + "export": "Экспорт", + "markAsReviewed": "Пометить как просмотренное", + "deleteNow": "Удалить сейчас", + "markAsUnreviewed": "Отметить как непросмотренное" + } + }, + "imagePicker": { + "search": { + "placeholder": "Искать по метке..." + }, + "selectImage": "Выбор миниатюры отслеживаемого объекта", + "noImages": "Не обнаружено миниатюр для этой камеры", + "unknownLabel": "Сохраненное изображение триггера" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/filter.json new file mode 100644 index 0000000..0b75d23 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/filter.json @@ -0,0 +1,137 @@ +{ + "filter": "Фильтр", + "labels": { + "label": "Метки", + "all": { + "title": "Все метки", + "short": "Метки" + }, + "count": "{{count}} меток", + "count_one": "{{count}} Метка", + "count_other": "{{count}} меток" + }, + "zones": { + "all": { + "title": "Все зоны", + "short": "Зоны" + }, + "label": "Зоны" + }, + "dates": { + "all": { + "title": "Все даты", + "short": "Даты" + }, + "selectPreset": "Период…" + }, + "timeRange": "Временной диапазон", + "subLabels": { + "label": "Дополнительные метки", + "all": "Все дополнительные метки" + }, + "score": "Оценка", + "estimatedSpeed": "Расчетная скорость ({{unit}})", + "more": "Больше фильтров", + "reset": { + "label": "Сброс фильтров к значениям по умолчанию" + }, + "features": { + "hasSnapshot": "Есть снимок", + "hasVideoClip": "Есть видеоклип", + "submittedToFrigatePlus": { + "label": "Отправлено в Frigate+", + "tips": "Сначала необходимо отфильтровать отслеживаемые объекты, у которых есть снимок.

    Отслеживаемые объекты без снимка нельзя отправить в Frigate+." + }, + "label": "Функции" + }, + "sort": { + "speedAsc": "Расчетная скорость (по возрастанию)", + "speedDesc": "Расчетная скорость (по убыванию)", + "label": "Сортировка", + "dateAsc": "Дата (по возрастанию)", + "dateDesc": "Дата (по убыванию)", + "scoreAsc": "Оценка объекта (по возрастанию)", + "scoreDesc": "Оценка объекта (по убыванию)", + "relevance": "Релевантность" + }, + "cameras": { + "label": "Фильтр камер", + "all": { + "title": "Все камеры", + "short": "Камеры" + } + }, + "explore": { + "settings": { + "defaultView": { + "unfilteredGrid": "Нефильтрованная сетка", + "summary": "Сводка", + "title": "Вид по умолчанию", + "desc": "При отсутствии выбранных фильтров отображать сводку последних отслеживаемых объектов для каждой метки или показывать нефильтрованную сетку." + }, + "gridColumns": { + "title": "Столбцы сетки", + "desc": "Выберите количество столбцов сетки." + }, + "searchSource": { + "label": "Источник поиска", + "desc": "Выберите, выполнять поиск по миниатюрам или описаниям отслеживаемых объектов.", + "options": { + "thumbnailImage": "Изображение миниатюры", + "description": "Описание" + } + }, + "title": "Настройки" + }, + "date": { + "selectDateBy": { + "label": "Выберите дату для фильтрации" + } + } + }, + "logSettings": { + "filterBySeverity": "Фильтровать логи по уровню важности", + "loading": { + "title": "Загрузка", + "desc": "При прокрутке панели логов в самый низ новые записи автоматически отображаются по мере их добавления." + }, + "label": "Уровень детализации логов", + "allLogs": "Все логи", + "disableLogStreaming": "Отключить потоковую передачу логов" + }, + "trackedObjectDelete": { + "title": "Подтвердить удаление", + "toast": { + "error": "Не удалось удалить отслеживаемые объекты: {{errorMessage}}", + "success": "Отслеживаемые объекты успешно удалены." + }, + "desc": "Удаление этих {{objectLength}} отслеживаемых объектов приведёт к удалению их снимков, сохранённых эмбеддингов и записей жизненного цикла. НО сами записи в разделе «История» останутся.

    Вы уверены, что хотите продолжить?

    Удерживайте Shift, чтобы пропустить это окно в будущем." + }, + "zoneMask": { + "filterBy": "Фильтр по маске зоны" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Номерных знаков не найдено.", + "placeholder": "Введите номер для поиска знака…", + "title": "Распознанные номерные знаки", + "loadFailed": "Не удалось загрузить распознанные номерные знаки.", + "loading": "Загрузка распознанных номерных знаков…", + "selectPlatesFromList": "Выберите один или более знаков из списка.", + "selectAll": "Выбрать все", + "clearAll": "Очистить все" + }, + "review": { + "showReviewed": "Показать просмотренные" + }, + "motion": { + "showMotionOnly": "Показывать только движение" + }, + "classes": { + "label": "Классы", + "all": { + "title": "Все классы" + }, + "count_one": "{{count}} класс", + "count_other": "{{count}} классы" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/icons.json new file mode 100644 index 0000000..2d0f3cc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Выберите иконку", + "search": { + "placeholder": "Поиск иконки…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/input.json new file mode 100644 index 0000000..149b56d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Скачать видео", + "toast": { + "success": "Загрузка видео начата." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ru/components/player.json new file mode 100644 index 0000000..f0a44ef --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Не найдено ни одной записи", + "noPreviewFound": "Предпросмотр не найден", + "submitFrigatePlus": { + "title": "Отправить этот кадр в Frigate+?", + "submit": "Отправить" + }, + "noPreviewFoundFor": "Не найдено предпросмотра для {{cameraName}}", + "livePlayerRequiredIOSVersion": "iOS 17.1 или выше требуется для этого типа стрима.", + "streamOffline": { + "title": "Поток оффлайн", + "desc": "С потока detect камеры {{cameraName}} не получено кадров, проверьте логи ошибок" + }, + "cameraDisabled": "Камера отключена", + "stats": { + "streamType": { + "title": "Тип потока:", + "short": "Тип" + }, + "bandwidth": { + "title": "Пропускная способность:", + "short": "Пропускная способность" + }, + "latency": { + "title": "Задержка:", + "value": "{{seconds}} сек", + "short": { + "title": "Задержка", + "value": "{{seconds}} сек" + } + }, + "totalFrames": "Всего кадров:", + "droppedFrames": { + "title": "Пропущено кадров:", + "short": { + "title": "Пропущено", + "value": "{{droppedFrames}} кадров" + } + }, + "decodedFrames": "Декодированные кадры:", + "droppedFrameRate": "Частота пропущенных кадров:" + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "Не удалось отправить кадр в Frigate+" + }, + "success": { + "submittedFrigatePlus": "Кадр успешно загружен в Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ru/objects.json new file mode 100644 index 0000000..c8cdac4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "Собака", + "cat": "Кошка", + "animal": "Животное", + "bark": "Лай", + "person": "Человек", + "bicycle": "Велосипед", + "car": "Автомобиль", + "motorcycle": "Мотоцикл", + "bird": "Птица", + "horse": "Лошадь", + "sheep": "Овца", + "mouse": "Мышь", + "goat": "Коза", + "airplane": "Самолет", + "keyboard": "Клавиатура", + "boat": "Лодка", + "bus": "Автобус", + "train": "Поезд", + "skateboard": "Скейтборд", + "door": "Дверь", + "blender": "Блендер", + "sink": "Раковина", + "clock": "Часы", + "vehicle": "Транспорт", + "hair_dryer": "Фен", + "toothbrush": "Зубная щетка", + "scissors": "Ножницы", + "traffic_light": "Светофор", + "fire_hydrant": "Пожарный гидрант", + "street_sign": "Дорожный знак", + "stop_sign": "Знак Стоп", + "parking_meter": "Парковочный счётчик", + "bench": "Скамейка", + "cow": "Корова", + "elephant": "Слон", + "bear": "Медведь", + "zebra": "Зебра", + "giraffe": "Жираф", + "hat": "Шляпа", + "backpack": "Рюкзак", + "umbrella": "Зонтик", + "shoe": "Обувь", + "eye_glasses": "Очки", + "tie": "Галстук", + "suitcase": "Чемодан", + "handbag": "Сумочка", + "frisbee": "Фрисби", + "skis": "Лыжи", + "snowboard": "Сноуборд", + "kite": "Воздушный змей", + "baseball_bat": "Бейсбольная бита", + "baseball_glove": "Бейсбольная перчатка", + "sports_ball": "Спортивный мяч", + "surfboard": "Доска для серфинга", + "tennis_racket": "Теннисная ракетка", + "bottle": "Бутылка", + "plate": "Тарелка", + "wine_glass": "Винный бокал", + "cup": "Чашка", + "fork": "Вилка", + "spoon": "Ложка", + "bowl": "Миска", + "banana": "Банан", + "apple": "Яблоко", + "orange": "Апельсин", + "broccoli": "Брокколи", + "sandwich": "Сэндвич", + "carrot": "Морковь", + "hot_dog": "Хот-дог", + "pizza": "Пицца", + "donut": "Пончик", + "cake": "Торт", + "chair": "Стул", + "couch": "Диван", + "potted_plant": "Комнатное растение", + "bed": "Кровать", + "mirror": "Зеркало", + "dining_table": "Обеденный стол", + "window": "Окно", + "desk": "Стол", + "toilet": "Туалет", + "tv": "Телевизор", + "laptop": "Ноутбук", + "remote": "Пульт дистанционного управления", + "cell_phone": "Мобильный телефон", + "microwave": "Микроволновка", + "oven": "Духовка", + "toaster": "Тостер", + "refrigerator": "Холодильник", + "book": "Книга", + "vase": "Ваза", + "teddy_bear": "Плюшевый мишка", + "hair_brush": "Расчёска", + "squirrel": "Белка", + "deer": "Олень", + "fox": "Лиса", + "rabbit": "Кролик", + "raccoon": "Енот", + "robot_lawnmower": "Роботизированная газонокосилка", + "waste_bin": "Мусорное ведро", + "on_demand": "По требованию", + "face": "Лицо", + "license_plate": "Номерной знак", + "package": "Посылка", + "bbq_grill": "Гриль и барбекю", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "knife": "Нож", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/classificationModel.json new file mode 100644 index 0000000..1017128 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/classificationModel.json @@ -0,0 +1,173 @@ +{ + "documentTitle": "Модели классификации", + "details": { + "scoreInfo": "Оценка представляет собой среднюю степень достоверности классификации по всем обнаружениям данного объекта." + }, + "button": { + "deleteClassificationAttempts": "Удалить изображения классификации", + "renameCategory": "Переименовать класс", + "deleteCategory": "Удалить класс", + "deleteImages": "Удалить изображения", + "trainModel": "Тренировать модель", + "addClassification": "Добавить классификацию", + "deleteModels": "Удалить модели", + "editModel": "Редактировать модель" + }, + "toast": { + "success": { + "deletedCategory": "Класс удалён", + "deletedImage": "Изображения удалены", + "deletedModel_one": "Успешно удалена {{count}} модель", + "deletedModel_few": "Успешно удалены {{count}} модели", + "deletedModel_many": "Успешно удалены {{count}} моделей", + "categorizedImage": "Изображение успешно классифицировано", + "trainedModel": "Модель успешно обучена.", + "trainingModel": "Обучение модели успешно запущено.", + "updatedModel": "Конфигурация модели успешно обновлена", + "renamedCategory": "Класс успешно переименован в {{name}}" + }, + "error": { + "deleteImageFailed": "Не удалось удалить: {{errorMessage}}", + "deleteCategoryFailed": "Не удалось удалить класс: {{errorMessage}}", + "deleteModelFailed": "Не удалось удалить модель: {{errorMessage}}", + "categorizeFailed": "Не удалось классифицировать изображение: {{errorMessage}}", + "trainingFailed": "Не удалось начать обучение модели: {{errorMessage}}", + "updateModelFailed": "Не удалось обновить модель: {{errorMessage}}", + "renameCategoryFailed": "Не удалось переименовать класс: {{errorMessage}}", + "trainingFailedToStart": "Не удалось начать обучение модели: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Удалить класс", + "desc": "Вы уверены, что хотите удалить класс {{name}}? Это приведёт к безвозвратному удалению всех связанных с ним изображений и потребует повторного обучения модели." + }, + "deleteModel": { + "title": "Удалить модель классификации", + "single": "Вы уверены, что хотите удалить {{name}}? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_one": "Вы уверены, что хотите удалить {{count}} модель? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} модели? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} моделей? Это приведёт к безвозвратному удалению всех связанных данных, включая изображения и данные обучения. Это действие нельзя отменить." + }, + "edit": { + "title": "Редактировать модель классификации", + "descriptionState": "Редактировать классы для этой модели классификации состояний. Изменения потребуют повторного обучения модели.", + "descriptionObject": "Редактировать тип объекта и тип классификации для этой модели классификации объектов.", + "stateClassesInfo": "Примечание: изменение классов состояний требует повторного обучения модели с обновлёнными классами." + }, + "deleteDatasetImages": { + "title": "Удалить изображения набора данных", + "desc_one": "Вы уверены, что хотите удалить {{count}} изображение из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели.", + "desc_few": "Вы уверены, что хотите удалить {{count}} изображения из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели.", + "desc_many": "Вы уверены, что хотите удалить {{count}} изображений из {{dataset}}? Это действие нельзя отменить и потребует повторного обучения модели." + }, + "deleteTrainImages": { + "title": "Удалить обучающие изображения", + "desc_one": "Вы уверены, что хотите удалить {{count}} изображение? Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} изображения? Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} изображений? Это действие нельзя отменить." + }, + "renameCategory": { + "title": "Переименовать класс", + "desc": "Введите новое имя для {{name}}. Вам потребуется повторно обучить модель, чтобы изменение имени вступило в силу." + }, + "description": { + "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы." + }, + "train": { + "title": "Недавние классификации", + "titleShort": "Недавние", + "aria": "Выбрать недавние классификации" + }, + "categories": "Классы", + "createCategory": { + "new": "Создать новый класс" + }, + "categorizeImageAs": "Классифицировать изображение как:", + "categorizeImage": "Классифицировать изображение", + "menu": { + "objects": "Объекты", + "states": "Состояния" + }, + "noModels": { + "object": { + "title": "Нет моделей классификации объектов", + "description": "Создайте пользовательскую модель для классификации обнаруженных объектов.", + "buttonText": "Создать модель объекта" + }, + "state": { + "title": "Нет моделей классификации состояний", + "description": "Создайте пользовательскую модель для мониторинга и классификации изменений состояний в определённых областях камеры.", + "buttonText": "Создать модель состояния" + } + }, + "wizard": { + "title": "Создать новую классификацию", + "steps": { + "nameAndDefine": "Имя и определение", + "stateArea": "Область состояния", + "chooseExamples": "Выбрать примеры" + }, + "step1": { + "description": "Модели состояний отслеживают фиксированные области камеры на предмет изменений (например, дверь открыта/закрыта). Модели объектов добавляют классификации к обнаруженным объектам (например, известные животные, курьеры и т.д.).", + "name": "Имя", + "namePlaceholder": "Введите имя модели…", + "type": "Тип", + "typeState": "Состояние", + "typeObject": "Объект", + "objectLabel": "Метка объекта", + "objectLabelPlaceholder": "Выберите тип объекта…", + "classificationType": "Тип классификации", + "classificationTypeTip": "Узнать о типах классификации", + "classificationTypeDesc": "Подметки добавляют дополнительный текст к метке объекта (например, 'Человек: UPS'). Атрибуты — это доступные для поиска метаданные, хранящиеся отдельно в метаданных объекта.", + "classificationSubLabel": "Подметка", + "classificationAttribute": "Атрибут", + "classes": "Классы", + "states": "Состояния", + "classesTip": "Узнать о классах", + "classesStateDesc": "Определите различные состояния, в которых может находиться область вашей камеры. Например: 'открыто' и 'закрыто' для гаражных ворот.", + "classesObjectDesc": "Определите различные категории для классификации обнаруженных объектов. Например: 'курьер', 'житель', 'незнакомец' для классификации людей.", + "classPlaceholder": "Введите имя класса…", + "errors": { + "nameRequired": "Имя модели обязательно", + "nameLength": "Имя модели должно содержать не более 64 символов", + "nameOnlyNumbers": "Имя модели не может состоять только из цифр", + "classRequired": "Требуется хотя бы 1 класс", + "classesUnique": "Имена классов должны быть уникальными", + "stateRequiresTwoClasses": "Модели состояний требуют не менее 2 классов", + "objectLabelRequired": "Пожалуйста, выберите метку объекта", + "objectTypeRequired": "Пожалуйста, выберите тип классификации" + } + }, + "step2": { + "description": "Выберите камеры и определите область для мониторинга для каждой камеры. Модель будет классифицировать состояние этих областей.", + "cameras": "Камеры", + "selectCamera": "Выбрать камеру", + "noCameras": "Нажмите +, чтобы добавить камеры", + "selectCameraPrompt": "Выберите камеру из списка, чтобы определить область её мониторинга" + }, + "step3": { + "selectImagesPrompt": "Выберите все изображения с {{className}}", + "selectImagesDescription": "Нажмите на изображения, чтобы выбрать их. Нажмите Продолжить, когда закончите с этим классом.", + "generating": { + "title": "Генерация примеров изображений", + "description": "Frigate извлекает репрезентативные изображения из ваших записей. Это может занять некоторое время…" + }, + "training": { + "title": "Обучение модели", + "description": "Ваша модель обучается в фоновом режиме. Закройте это диалоговое окно, и ваша модель начнёт работать, как только обучение будет завершено." + }, + "retryGenerate": "Повторить генерацию", + "noImages": "Примеры изображений не сгенерированы", + "classifying": "Классификация и обучение…", + "trainingStarted": "Обучение успешно запущено", + "errors": { + "noCameras": "Камеры не настроены", + "noObjectLabel": "Метка объекта не выбрана", + "generateFailed": "Не удалось сгенерировать примеры: {{error}}", + "generationFailed": "Генерация не удалась. Пожалуйста, попробуйте снова.", + "classifyFailed": "Не удалось классифицировать изображения: {{error}}" + }, + "generateSuccess": "Примеры изображений успешно сгенерированы" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/configEditor.json new file mode 100644 index 0000000..0dd775b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "configEditor": "Редактор конфигурации", + "copyConfig": "Скопировать конфигурацию", + "saveAndRestart": "Сохранить и перезапустить", + "saveOnly": "Только сохранить", + "documentTitle": "Редактор конфигурации - Frigate", + "toast": { + "success": { + "copyToClipboard": "Конфигурация скопирована в буфер обмена." + }, + "error": { + "savingError": "Ошибка сохранения конфигурации" + } + }, + "confirm": "Выйти без сохранения?", + "safeConfigEditor": "Редактор конфигурации (безопасный режим)", + "safeModeDescription": "Frigate находится в безопасном режиме из-за ошибки проверки конфигурации." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/events.json new file mode 100644 index 0000000..c54e542 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/events.json @@ -0,0 +1,60 @@ +{ + "alerts": "Тревоги", + "detections": "Обнаружения", + "motion": { + "label": "Движение", + "only": "Только движение" + }, + "allCameras": "Все камеры", + "camera": "Камера", + "empty": { + "alert": "Отсутствуют тревоги для просмотра", + "detection": "Отсутствуют обнаружения для просмотра", + "motion": "Не найдено данных о движении" + }, + "timeline": "Таймлайн", + "timeline.aria": "Выбор таймлайна", + "events": { + "label": "События", + "aria": "Выбор событий", + "noFoundForTimePeriod": "Для этого периода времени не найдено ни одного события." + }, + "documentTitle": "Обзор событий - Frigate", + "recordings": { + "documentTitle": "Записи - Frigate" + }, + "calendarFilter": { + "last24Hours": "Последние 24 часа" + }, + "markAsReviewed": "Пометить как просмотренное", + "newReviewItems": { + "label": "Посмотреть новые элементы для просмотра", + "button": "Новые элементы для просмотра" + }, + "markTheseItemsAsReviewed": "Пометить эти элементы как просмотренные", + "selected": "{{count}} выбрано", + "selected_one": "{{count}} выбрано", + "selected_other": "{{count}} выбрано", + "detected": "обнаружен", + "suspiciousActivity": "Подозрительная активность", + "threateningActivity": "Угрожающая активность", + "detail": { + "noDataFound": "Нет данных для просмотра", + "aria": "Переключить подробный режим просмотра", + "trackedObject_one": "объект", + "trackedObject_other": "объекты", + "noObjectDetailData": "Данные о деталях объекта недоступны.", + "label": "Деталь", + "settings": "Настройки подробного просмотра", + "alwaysExpandActive": { + "title": "Всегда раскрывать активный", + "desc": "Всегда раскрывать сведения об объекте активного элемента обзора, если они доступны." + } + }, + "objectTrack": { + "trackedPoint": "Отслеживаемая точка", + "clickToSeek": "Перейти к этому моменту" + }, + "zoomIn": "Увеличить", + "zoomOut": "Отдалить" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/explore.json new file mode 100644 index 0000000..18c211a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/explore.json @@ -0,0 +1,289 @@ +{ + "exploreIsUnavailable": { + "embeddingsReindexing": { + "context": "Поиск станет доступен после завершения переиндексации эмбеддингов отслеживаемых объектов.", + "startingUp": "Запуск…", + "estimatedTime": "Оставшееся время:", + "finishingShortly": "Скоро завершится", + "step": { + "descriptionsEmbedded": "Встроенные описания: ", + "trackedObjectsProcessed": "Обработанные отслеживаемые объекты: ", + "thumbnailsEmbedded": "Встроенные миниатюры: " + } + }, + "title": "Поиск событий недоступен", + "downloadingModels": { + "setup": { + "visionModel": "Модель компьютерного зрения", + "visionModelFeatureExtractor": "Экстрактор признаков модели компьютерного зрения", + "textModel": "Текстовая модель", + "textTokenizer": "Текстовый токенизатор" + }, + "tips": { + "context": "Возможно, вы захотите переиндексировать эмбеддинги отслеживаемых объектов после загрузки моделей.", + "documentation": "Читать документацию" + }, + "context": "Frigate загружает необходимые модели эмбеддингов для поддержки функции семантического поиска. Это может занять несколько минут в зависимости от скорости вашего интернет-соединения.", + "error": "Произошла ошибка. Проверьте логи Frigate." + } + }, + "generativeAI": "Генеративный ИИ", + "documentTitle": "Поиск событий - Frigate", + "details": { + "timestamp": "Метка времени", + "item": { + "title": "Детали элемента просмотра", + "desc": "Детали элемента просмотра", + "button": { + "share": "Поделиться этим элементом просмотра", + "viewInExplore": "Смотреть в Поиске событий" + }, + "tips": { + "hasMissingObjects": "Настройте конфигурацию, если хотите, чтобы Frigate сохранял отслеживаемые объекты для следующих меток: {{objects}}", + "mismatch_one": "{{count}} недоступный объект обнаружен и включен в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены.", + "mismatch_few": "{{count}} недоступных объекта обнаружено и включено в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены.", + "mismatch_many": "{{count}} недоступных объектов обнаружено и включено в этот элемент просмотра. Эти объекты либо не соответствовали критериям тревоги/детекции, либо уже были удалены." + }, + "toast": { + "success": { + "updatedSublabel": "Успешно обновлена дополнительная метка.", + "updatedLPR": "Номерной знак успешно обновлён.", + "regenerate": "Новое описание запрошено у {{provider}}. В зависимости от скорости работы вашего провайдера, генерация нового описания может занять некоторое время.", + "audioTranscription": "Запрос на транскрипцию звука успешно выполнен." + }, + "error": { + "updatedSublabelFailed": "Не удалось обновить дополнительную метку: {{errorMessage}}", + "updatedLPRFailed": "Не удалось обновить номерной знак: {{errorMessage}}", + "regenerate": "Не удалось запросить новое описание у {{provider}}: {{errorMessage}}", + "audioTranscription": "Не удалось запросить транскрипцию аудио: {{errorMessage}}" + } + } + }, + "editSubLabel": { + "descNoLabel": "Введите новую дополнительную метку для этого отслеживаемого объекта", + "title": "Редактирование дополнительной метки", + "desc": "Введите новую дополнительную метку для {{label}}" + }, + "topScore": { + "label": "Лучшая оценка", + "info": "Лучшая оценка — это наивысшая медианная оценка для отслеживаемого объекта, поэтому она может отличаться от оценки, показанной на превью в результатах поиска." + }, + "estimatedSpeed": "Расчётная скорость", + "tips": { + "saveDescriptionFailed": "Не удалось обновить описание: {{errorMessage}}", + "descriptionSaved": "Описание успешно сохранено" + }, + "label": "Метка", + "editLPR": { + "title": "Редактирование номерного знака", + "descNoLabel": "Введите новое значение номерного знака для этого отслеживаемого объекта", + "desc": "Введите новое значение номерного знака для {{label}}" + }, + "recognizedLicensePlate": "Распознанный номерной знак", + "objects": "Объекты", + "camera": "Камера", + "zones": "Зоны", + "button": { + "findSimilar": "Найти похожее", + "regenerate": { + "title": "Перегенерировать", + "label": "Перегенерировать описание отслеживаемого объекта" + } + }, + "description": { + "label": "Описание", + "aiTips": "Frigate не будет запрашивать описание у вашего генеративного ИИ-провайдера, пока жизненный цикл отслеживаемого объекта не завершится.", + "placeholder": "Описание отслеживаемого объекта" + }, + "expandRegenerationMenu": "Развернуть меню перегенерации", + "regenerateFromSnapshot": "Перегенерировать из снимка", + "regenerateFromThumbnails": "Перегенерировать из миниатюры", + "snapshotScore": { + "label": "Оценка снимка" + }, + "score": { + "label": "Оценка" + } + }, + "trackedObjectDetails": "Детали объекта", + "type": { + "details": "детали", + "snapshot": "снимок", + "video": "видео", + "object_lifecycle": "жизненный цикл объекта", + "thumbnail": "миниатюра" + }, + "objectLifecycle": { + "title": "Жизненный цикл объекта", + "noImageFound": "Для этой метки времени изображение не найдено.", + "createObjectMask": "Создать маску объекта", + "adjustAnnotationSettings": "Изменить настройки аннотаций", + "scrollViewTips": "Прокрутите, чтобы просмотреть ключевые моменты жизненного цикла этого объекта.", + "autoTrackingTips": "Позиции ограничивающих рамок будут неточными для камер с автотрекингом.", + "lifecycleItemDesc": { + "visible": "Обнаружен(а) {{label}}", + "entered_zone": "{{label}} зафиксирован(а) в {{zones}}", + "active": "{{label}} активировался(ась)", + "stationary": "{{label}} перестал(а) двигаться", + "attribute": { + "faceOrLicense_plate": "{{attribute}} обнаружен для {{label}}", + "other": "{{label}} распознан(а) как {{attribute}}" + }, + "gone": "{{label}} покинул(а) зону", + "heard": "Обнаружен звук {{label}}", + "external": "Обнаружен(а) {{label}}", + "header": { + "zones": "Зоны", + "ratio": "Соотношение", + "area": "Область" + } + }, + "annotationSettings": { + "title": "Настройки аннотаций", + "showAllZones": { + "title": "Показать все зоны", + "desc": "Всегда показывать зоны на кадрах, где объекты вошли в зону." + }, + "offset": { + "label": "Сдвиг аннотаций", + "desc": "Эти данные поступают из потока детекции вашей камеры, но накладываются на изображения из потока записи. Потоки вряд ли идеально синхронизированы, поэтому ограничивающая рамка и видео могут не совпадать. Для корректировки используйте поле Сдвиг аннотаций.", + "millisecondsToOffset": "Смещение аннотаций детекции в миллисекундах. По умолчанию: 0", + "documentation": "Читать документацию ", + "tips": "СОВЕТ: Представьте, у вас клип события, где человек идёт слева направо. Если рамка на таймлайне постоянно смещена влево от человека — уменьшите значение. Если рамка опережает движение — увеличьте значение.", + "toast": { + "success": "В конфигурационном файле сохранено значение смещения для {{camera}}. Перезапустите Frigate, чтобы применить изменения." + } + } + }, + "carousel": { + "previous": "Предыдущий слайд", + "next": "Следующий слайд" + }, + "count": "{{first}} из {{second}}", + "trackedPoint": "Отслеживаемая точка" + }, + "itemMenu": { + "downloadVideo": { + "label": "Скачать видео", + "aria": "Скачать видео" + }, + "downloadSnapshot": { + "label": "Скачать снимок", + "aria": "Скачать снимок" + }, + "viewObjectLifecycle": { + "label": "Просмотр жизненного цикла объекта", + "aria": "Показать жизненный цикл объекта" + }, + "findSimilar": { + "label": "Найти похожее", + "aria": "Найти похожие отслеживаемые объекты" + }, + "submitToPlus": { + "label": "Отправить в Frigate+", + "aria": "Отправить в Frigate Plus" + }, + "viewInHistory": { + "label": "Посмотреть в Истории", + "aria": "Посмотреть в Истории" + }, + "deleteTrackedObject": { + "label": "Удалить этот отслеживаемый объект" + }, + "addTrigger": { + "label": "Добавить триггер", + "aria": "Добавить триггер для этого отслеживаемого объекта" + }, + "audioTranscription": { + "label": "Транскрибировать", + "aria": "Запросить аудиотранскрипцию" + }, + "viewTrackingDetails": { + "label": "Просмотреть детали отслеживания", + "aria": "Показать детали отслеживания" + }, + "showObjectDetails": { + "label": "Показать путь объекта" + }, + "hideObjectDetails": { + "label": "Скрыть путь объекта" + } + }, + "dialog": { + "confirmDelete": { + "title": "Подтвердить удаление", + "desc": "Удаление этого отслеживаемого объекта приведёт к удалению снимка, всех сохранённых эмбеддингов и всех связанных записей деталей отслеживания. Записанное видео этого отслеживаемого объекта в представлении Истории НЕ будет удалено.

    Вы уверены, что хотите продолжить?" + } + }, + "noTrackedObjects": "Отслеживаемые объекты не найдены", + "fetchingTrackedObjectsFailed": "Ошибка при получении отслеживаемых объектов: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} отслеживаемый объект ", + "trackedObjectsCount_few": "{{count}} отслеживаемых объекта ", + "trackedObjectsCount_many": "{{count}} отслеживаемых объектов ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Отслеживаемый объект успешно удалён.", + "error": "Не удалось удалить отслеживаемый объект: {{errorMessage}}" + } + }, + "tooltip": "Соответствие с {{type}} на {{confidence}}%", + "previousTrackedObject": "Предыдущий отслеживаемый объект", + "nextTrackedObject": "Следующий отслеживаемый объект" + }, + "exploreMore": "Просмотреть больше объектов {{label}}", + "aiAnalysis": { + "title": "Анализ при помощи ИИ" + }, + "concerns": { + "label": "Требуют внимания" + }, + "trackingDetails": { + "count": "{{first}} из {{second}}", + "title": "Детали отслеживания", + "noImageFound": "Для этой метки времени изображение не найдено.", + "createObjectMask": "Создать маску объекта", + "adjustAnnotationSettings": "Изменить настройки аннотаций", + "scrollViewTips": "Нажмите, чтобы просмотреть ключевые моменты жизненного цикла этого объекта.", + "autoTrackingTips": "Позиции ограничивающих рамок будут неточными для камер с автотрекингом.", + "trackedPoint": "Отслеживаемая точка", + "lifecycleItemDesc": { + "visible": "Обнаружен(а) {{label}}", + "entered_zone": "{{label}} зафиксирован(а) в {{zones}}", + "active": "{{label}} активировался(ась)", + "stationary": "{{label}} перестал(а) двигаться", + "attribute": { + "faceOrLicense_plate": "{{attribute}} обнаружен для {{label}}", + "other": "{{label}} распознан(а) как {{attribute}}" + }, + "gone": "{{label}} покинул(а) зону", + "heard": "Обнаружен звук {{label}}", + "external": "Обнаружен(а) {{label}}", + "header": { + "zones": "Зоны", + "ratio": "Соотношение", + "area": "Область" + } + }, + "annotationSettings": { + "title": "Настройки аннотаций", + "showAllZones": { + "title": "Показать все зоны", + "desc": "Всегда показывать зоны на кадрах, где объекты вошли в зону." + }, + "offset": { + "label": "Сдвиг аннотаций", + "desc": "Эти данные поступают из потока детекции вашей камеры, но накладываются на изображения из потока записи. Потоки вряд ли идеально синхронизированы, поэтому ограничивающая рамка и видео могут не совпадать. Вы можете использовать эту настройку для смещения аннотаций вперед или назад во времени, чтобы лучше выровнять их с записанным видео.", + "millisecondsToOffset": "Смещение аннотаций детекции в миллисекундах. По умолчанию: 0", + "tips": "Уменьшите значение, если воспроизведение видео опережает рамки и точки пути, и увеличьте значение, если воспроизведение видео отстаёт от них. Это значение может быть отрицательным.", + "toast": { + "success": "Смещение аннотаций для {{camera}} сохранено в конфигурационном файле. Перезапустите Frigate, чтобы применить изменения." + } + } + }, + "carousel": { + "previous": "Предыдущий слайд", + "next": "Следующий слайд" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/exports.json new file mode 100644 index 0000000..c14a578 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Экспорт - Frigate", + "search": "Поиск", + "noExports": "Не найдено файлов экспорта", + "deleteExport": "Удалить экспорт", + "deleteExport.desc": "Вы уверены, что хотите удалить {{exportName}}?", + "editExport": { + "title": "Переименовать экспорт", + "desc": "Введите новое имя для этого экспорта.", + "saveExport": "Сохранить экспорт" + }, + "toast": { + "error": { + "renameExportFailed": "Не удалось переименовать экспорт: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Поделиться экспортом", + "downloadVideo": "Скачать видео", + "editName": "Изменить название", + "deleteExport": "Удалить экспорт" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/faceLibrary.json new file mode 100644 index 0000000..ee8d702 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "details": { + "person": "Человек", + "timestamp": "Метка времени", + "face": "Подробности о лице", + "faceDesc": "Информация об отслеживаемом объекте, который сгенерировал это лицо", + "confidence": "Достоверность", + "scoreInfo": "Оценка доп. метки — это взвешенная оценка всех распознанных лиц, поэтому она может отличаться от оценки на снимке.", + "subLabelScore": "Оценка доп. метки", + "unknown": "Неизвестно" + }, + "documentTitle": "Библиотека лиц - Frigate", + "description": { + "placeholder": "Введите название коллекции", + "addFace": "Добавьте новую коллекцию в библиотеку лиц, загрузив свое первое изображение.", + "invalidName": "Недопустимое имя. Имена могут содержать только буквы, цифры, пробелы, апострофы, подчёркивания и дефисы." + }, + "createFaceLibrary": { + "desc": "Создание новой коллекции", + "nextSteps": "Для создания надежной базы:
  • Используйте вкладку \"Недавние распознавания\", чтобы выбрать изображения каждого обнаруженного человека и обучить систему
  • Используйте фронтальные изображения для лучшего результата; избегайте изображений с лицами, снятыми под углом.
  • ", + "title": "Создать коллекцию", + "new": "Создать новое лицо" + }, + "selectFace": "Выбор лица", + "uploadFaceImage": { + "desc": "Загрузите изображение для поиска лиц и связывания с {{pageToggle}}", + "title": "Загрузка изображения с лицом" + }, + "selectItem": "Выбор {{item}}", + "train": { + "aria": "Выберите последние распознавания", + "title": "Последние распознавания", + "empty": "Нет недавних попыток распознавания лиц" + }, + "toast": { + "success": { + "deletedFace_one": "Успешно удалено {{count}} лицо.", + "deletedFace_few": "Успешно удалено {{count}} лица.", + "deletedFace_many": "Успешно удалено {{count}} лиц.", + "deletedName_one": "{{count}} лицо успешно удалено.", + "deletedName_few": "{{count}} лица успешно удалено.", + "deletedName_many": "{{count}} лиц успешно удалено.", + "uploadedImage": "Изображение успешно загружено.", + "trainedFace": "Лицо успешно запомнено.", + "addFaceLibrary": "{{name}} успешно добавлен(а) в Библиотеку лиц!", + "updatedFaceScore": "Оценка лица успешно обновлена для {{name}} {{score}}.", + "renamedFace": "Лицо успешно переименовано в {{name}}" + }, + "error": { + "deleteFaceFailed": "Не удалось удалить: {{errorMessage}}", + "uploadingImageFailed": "Не удалось загрузить изображение: {{errorMessage}}", + "trainFailed": "Не удалось запомнить: {{errorMessage}}", + "updateFaceScoreFailed": "Не удалось обновить оценку лица: {{errorMessage}}", + "addFaceLibraryFailed": "Не удалось установить имя для лица: {{errorMessage}}", + "deleteNameFailed": "Не удалось удалить имя: {{errorMessage}}", + "renameFaceFailed": "Не удалось переименовать лицо: {{errorMessage}}" + } + }, + "deleteFaceLibrary": { + "title": "Удалить имя", + "desc": "Вы уверены, что хотите удалить коллекцию «{{name}}»? Это действие безвозвратно удалит все лица в коллекции." + }, + "imageEntry": { + "dropActive": "Перетащите изображение сюда…", + "dropInstructions": "Перетащите или вставьте изображение сюда или щелкните, чтобы выбрать", + "maxSize": "Макс. размер: {{size}}Мб", + "validation": { + "selectImage": "Пожалуйста, выберите файл изображения." + } + }, + "readTheDocs": "Читать документацию", + "trainFaceAs": "Запомнить лицо как:", + "button": { + "uploadImage": "Загрузить изображение", + "deleteFaceAttempts": "Удалить лица", + "addFace": "Добавить лицо", + "reprocessFace": "Обработать лицо повторно", + "renameFace": "Переименовать лицо", + "deleteFace": "Удалить лицо" + }, + "trainFace": "Запомнить лицо", + "steps": { + "faceName": "Введите имя лица", + "nextSteps": "Следующие шаги", + "uploadFace": "Загрузить изображение лица", + "description": { + "uploadFace": "Загрузите изображение {{name}}, на котором лицо показано спереди. Не нужно обрезать фотографию только до лица." + } + }, + "renameFace": { + "desc": "Введите новое имя для {{name}}", + "title": "Переименовать лицо" + }, + "collections": "Коллекции", + "deleteFaceAttempts": { + "title": "Удалить лица", + "desc_one": "Вы уверены, что хотите удалить {{count}} лицо? Это действие нельзя отменить.", + "desc_few": "Вы уверены, что хотите удалить {{count}} лица? Это действие нельзя отменить.", + "desc_many": "Вы уверены, что хотите удалить {{count}} лиц? Это действие нельзя отменить." + }, + "nofaces": "Лица отсутствуют", + "pixels": "{{area}} пикс" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/live.json new file mode 100644 index 0000000..8a189bf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/live.json @@ -0,0 +1,183 @@ +{ + "documentTitle": "Прямой эфир - Frigate", + "documentTitle.withCamera": "{{camera}} - Прямой эфир - Frigate", + "lowBandwidthMode": "Экономичный режим", + "twoWayTalk": { + "enable": "Включить двустороннюю связь", + "disable": "Отключить двустороннюю связь" + }, + "cameraAudio": { + "enable": "Включить звук с камеры", + "disable": "Отключить звук с камеры" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Кликните в кадре для центрирования камеры", + "enable": "Включить перемещение по клику", + "disable": "Отключить перемещение по клику" + }, + "left": { + "label": "Переместить PTZ-камеру влево" + }, + "down": { + "label": "Переместить PTZ-камеру вниз" + }, + "up": { + "label": "Переместить PTZ-камеру вверх" + }, + "right": { + "label": "Переместить PTZ-камеру вправо" + } + }, + "zoom": { + "in": { + "label": "Приблизить PTZ-камеру" + }, + "out": { + "label": "Отдалить PTZ-камеру" + } + }, + "frame": { + "center": { + "label": "Кликните в кадре для центрирования PTZ-камеры" + } + }, + "presets": "Предустановки PTZ-камеры", + "focus": { + "in": { + "label": "Сфокусировать PTZ камеру на" + }, + "out": { + "label": "Отдалить фокус PTZ камеры" + } + } + }, + "camera": { + "enable": "Включить камеру", + "disable": "Отключить камеру" + }, + "muteCameras": { + "enable": "Отключить звук на всех камерах", + "disable": "Включить звук на всех камерах" + }, + "detect": { + "enable": "Включить детекцию", + "disable": "Отключить детекцию" + }, + "recording": { + "enable": "Включить запись", + "disable": "Отключить запись" + }, + "snapshots": { + "enable": "Включить снимки", + "disable": "Отключить снимки" + }, + "audioDetect": { + "enable": "Включить детекцию аудио", + "disable": "Отключить детекцию аудио" + }, + "autotracking": { + "enable": "Включить автотрекинг", + "disable": "Отключить автотрекинг" + }, + "streamStats": { + "enable": "Показать статистику потока", + "disable": "Скрыть статистику потока" + }, + "manualRecording": { + "title": "По требованию", + "tips": "Создать ручное событие на основе настроек хранения записей этой камеры.", + "playInBackground": { + "label": "Воспроизведение в фоне", + "desc": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "showStats": { + "label": "Показать статистику", + "desc": "Включите эту опцию, чтобы отображать статистику потока в виде наложения на изображение с камеры." + }, + "debugView": "Режим отладки", + "start": "Запустить запись по запросу", + "started": "Запущена запись по запросу.", + "failedToStart": "Не удалось запустить запись по требованию.", + "recordDisabledTips": "Поскольку запись отключена или ограничена в конфигурации для этой камеры, будет сохранён только снимок.", + "end": "Завершить запись по требованию", + "ended": "Запись по требованию остановлена.", + "failedToEnd": "Не удалось остановить запись по требованию." + }, + "streamingSettings": "Настройки потока", + "suspend": { + "forTime": "Приостановить на: " + }, + "stream": { + "audio": { + "tips": { + "documentation": "Читать документацию ", + "title": "Аудио должно выводиться с вашей камеры и быть настроено в go2rtc для этого потока." + }, + "available": "Для этого потока доступен звук", + "unavailable": "Аудио недоступно для этого потока" + }, + "title": "Поток", + "twoWayTalk": { + "tips": "Ваше устройство должно поддерживать эту функцию, а WebRTC должен быть настроен для двусторонней связи.", + "tips.documentation": "Читать документацию ", + "available": "Двусторонняя связь доступна для этого потока", + "unavailable": "Двусторонняя связь недоступна для этого потока" + }, + "lowBandwidth": { + "tips": "Режим просмотра в реальном времени переведён в экономичный режим из-за буферизации или ошибок потока.", + "resetStream": "Сброс потока" + }, + "playInBackground": { + "label": "Воспроизвести в фоне", + "tips": "Включите эту опцию, чтобы продолжать трансляцию при скрытом плеере." + }, + "debug": { + "picker": "В режиме отладки выбор потока камеры недоступен. Вид отладчика всегда использует поток настроенный для режима обнаружения." + } + }, + "cameraSettings": { + "title": "Настройки {{camera}}", + "objectDetection": "Обнаружение объектов", + "recording": "Запись", + "audioDetection": "Детекция аудио", + "snapshots": "Снимки", + "autotracking": "Автотрекинг", + "cameraEnabled": "Камера активирована", + "transcription": "Транскрипция аудио" + }, + "history": { + "label": "Отобразить архивные записи" + }, + "effectiveRetainMode": { + "modes": { + "all": "Все", + "motion": "Движение", + "active_objects": "Активные объекты" + }, + "notAllTips": "Ваша конфигурация хранения записей {{source}} установлена в mode: {{effectiveRetainMode}}, поэтому эта запись по запросу будет сохранять только сегменты с {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Редактировать макет", + "group": { + "label": "Редактирование группы камер" + }, + "exitEdit": "Выход из редактирования" + }, + "audio": "Аудио", + "notifications": "Уведомления", + "transcription": { + "enable": "Включить транскрипцию звука в реальном времени", + "disable": "Выключить транскрипцию звука" + }, + "snapshot": { + "noVideoSource": "Нет видеоисточника для снимка", + "captureFailed": "Не удалось сделать снимок." + }, + "noCameras": { + "title": "Камеры не настроены", + "description": "Начните с подключения камеры к Frigate.", + "buttonText": "Добавить камеру" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/recording.json new file mode 100644 index 0000000..24d34f5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Фильтр", + "export": "Экспорт", + "calendar": "Календарь", + "filters": "Фильтры", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Конечное время должно быть позже начального", + "noValidTimeSelected": "Выбран недопустимый временной диапазон" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/search.json new file mode 100644 index 0000000..0c7f847 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/search.json @@ -0,0 +1,74 @@ +{ + "savedSearches": "Сохраненные поиски", + "button": { + "clear": "Очистить поиск", + "save": "Сохранить поиск", + "delete": "Удалить сохранённый поиск", + "filterActive": "Активные фильтры", + "filterInformation": "Информация о фильтре" + }, + "search": "Поиск", + "searchFor": "Поиск {{inputValue}}", + "trackedObjectId": "ID отслеживаемого объекта", + "filter": { + "label": { + "cameras": "Камеры", + "zones": "Зоны", + "sub_labels": "Дополнительные метки", + "search_type": "Тип поиска", + "time_range": "Временной диапазон", + "before": "До", + "after": "После", + "min_score": "Мин. оценка", + "max_score": "Макс. оценка", + "min_speed": "Мин. скорость", + "recognized_license_plate": "Распознанный номерной знак", + "max_speed": "Макс. скорость", + "has_clip": "Есть клип", + "has_snapshot": "Есть снимок", + "labels": "Метки" + }, + "searchType": { + "thumbnail": "Миниатюра", + "description": "Описание" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Дата 'до' должна быть позже, чем дата 'после'.", + "afterDatebeEarlierBefore": "Дата 'после' должна быть раньше, чем дата 'до'.", + "minScoreMustBeLessOrEqualMaxScore": "Значение 'min_score' должно быть меньше или равно значению 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Значение 'max_score' должно быть больше или равно значению 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Значение 'min_speed' должно быть меньше или равно значению 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Значение 'max_speed' должно быть больше или равно значению 'min_speed'." + } + }, + "tips": { + "title": "Как использовать текстовые фильтры", + "desc": { + "text": "Фильтры помогают уточнить результаты поиска. Вот как их использовать в поле ввода:", + "step": "
    • Введите название фильтра, затем двоеточие (например, \"камеры:\").
    • Выберите значение из подсказок или введите своё.
    • Используйте несколько фильтров, добавляя их через пробел.
    • Фильтры даты (before:/after:) используют формат {{DateFormat}}.
    • Временной диапазон — в формате {{exampleTime}}.
    • Удаляйте фильтры нажатием на «×» рядом с ними.
    ", + "example": "Пример: cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "step1": "Введите имя ключа фильтра с двоеточием (например, \"камеры:\").", + "step5": "Фильтр временного диапазона использует формат {{exampleTime}}.", + "exampleLabel": "Пример:", + "step2": "Выберите значение из предложенных или введите свое собственное.", + "step3": "Вы можете применять несколько фильтров, указывая их подряд через пробел.", + "step6": "Удаляйте фильтры, нажав на значок \"x\" рядом с ними.", + "step4": "Фильтры по дате (до: и после:) используют формат {{DateFormat}}." + } + }, + "header": { + "currentFilterType": "Значения фильтров", + "noFilters": "Фильтры", + "activeFilters": "Активные фильтры" + } + }, + "similaritySearch": { + "title": "Поиск похожего", + "active": "Активен поиск похожего", + "clear": "Очистить поиск похожего" + }, + "placeholder": { + "search": "Поиск…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/settings.json new file mode 100644 index 0000000..7044dc7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/settings.json @@ -0,0 +1,1216 @@ +{ + "documentTitle": { + "default": "Настройки - Frigate", + "camera": "Настройки камеры - Frigate", + "masksAndZones": "Маски и Зоны - Frigate", + "motionTuner": "Детекции движения - Frigate", + "general": "Настройки интерфейса - Frigate", + "frigatePlus": "Настройки Frigate+ - Frigate", + "authentication": "Настройки аутентификации - Frigate", + "classification": "Настройки распознавания - Frigate", + "object": "Отладка - Frigate", + "notifications": "Настройки уведомлений - Frigate", + "enrichments": "Настройки обогащения - Frigate", + "cameraManagement": "Управление камерами - Frigate", + "cameraReview": "Настройки просмотра камеры - Frigate" + }, + "menu": { + "cameras": "Настройки камеры", + "masksAndZones": "Маски / Зоны", + "motionTuner": "Детекции движения", + "debug": "Отладка", + "users": "Пользователи", + "notifications": "Уведомления", + "frigateplus": "Frigate+", + "ui": "Интерфейс", + "classification": "Распознавание", + "enrichments": "Обогащения", + "triggers": "Триггеры", + "cameraManagement": "Управление", + "cameraReview": "Обзор", + "roles": "Роли" + }, + "dialog": { + "unsavedChanges": { + "title": "У вас есть несохраненные изменения.", + "desc": "Хотите сохранить изменения перед продолжением?" + } + }, + "cameraSetting": { + "camera": "Камера", + "noCamera": "Нет камеры" + }, + "general": { + "title": "Настройки интерфейса", + "liveDashboard": { + "title": "Панель мониторинга", + "automaticLiveView": { + "desc": "Автоматически переключаться на просмотр камеры в реальном времени при обнаружении активности. Если отключить эту опцию, статичные изображения камер на панели мониторинга будут обновляться только раз в минуту.", + "label": "Автоматический просмотр в реальном времени" + }, + "playAlertVideos": { + "label": "Воспроизводить видео с тревогами", + "desc": "По умолчанию последние тревоги на панели мониторинга воспроизводятся как короткие зацикленные видео. Отключите эту опцию, чтобы показывать только статичное изображение последних оповещений на этом устройстве/браузере." + }, + "displayCameraNames": { + "label": "Всегда показывать названия камер", + "desc": "Всегда показывать названия камер в виде метки на панели мониторинга с несколькими камерами." + }, + "liveFallbackTimeout": { + "label": "Таймаут переключения на низкое качество", + "desc": "Когда высококачественный поток камеры недоступен, переключиться на режим низкой пропускной способности через указанное количество секунд. По умолчанию: 3." + } + }, + "calendar": { + "title": "Календарь", + "firstWeekday": { + "sunday": "Воскресенье", + "monday": "Понедельник", + "label": "Первый день недели", + "desc": "День, с которого начинаются недели в календаре обзора событий." + } + }, + "recordingsViewer": { + "title": "Просмотр записей", + "defaultPlaybackRate": { + "label": "Скорость воспроизведения по умолчанию", + "desc": "Скорость воспроизведения записей по умолчанию." + } + }, + "storedLayouts": { + "clearAll": "Сбросить все макеты", + "desc": "Расположение камер в группе можно настраивать перетаскиванием и изменением размера. Позиции сохраняются в локальном хранилище браузера.", + "title": "Сохранённые макеты" + }, + "cameraGroupStreaming": { + "title": "Настройки трансляции группы камер", + "desc": "Настройки трансляции для каждой группы камер хранятся локально в вашем браузере.", + "clearAll": "Очистить все настройки трансляции" + }, + "toast": { + "success": { + "clearStoredLayout": "Сохранённый макет для {{cameraName}} удалён", + "clearStreamingSettings": "Настройки потоков для всех групп камер сброшены." + }, + "error": { + "clearStoredLayoutFailed": "Не удалось удалить макет: {{errorMessage}}", + "clearStreamingSettingsFailed": "Не удалось очистить настройки потока: {{errorMessage}}" + } + } + }, + "classification": { + "semanticSearch": { + "title": "Семантический поиск", + "readTheDocumentation": "Читать документацию", + "reindexNow": { + "label": "Переиндексировать сейчас", + "confirmButton": "Переиндексировать", + "alreadyInProgress": "Переиндексация уже выполняется.", + "desc": "Переиндексация заново сгенерирует векторные представления для всех отслеживаемых объектов. Этот процесс выполняется в фоновом режиме и может максимально загрузить ваш процессор, а также занять значительное время в зависимости от количества отслеживаемых объектов.", + "confirmTitle": "Подтвердить переиндексацию", + "success": "Реиндексация запущена успешно.", + "error": "Не удалось начать реиндексацию: {{errorMessage}}", + "confirmDesc": "Вы уверены, что хотите переиндексировать все векторные представления отслеживаемых объектов? Этот процесс будет выполняться в фоновом режиме, но может максимально загрузить ваш процессор и занять довольно много времени. Вы можете следить за ходом выполнения на странице «Поиск событий»." + }, + "desc": "Семантический поиск во Frigate позволяет находить отслеживаемые объекты в записях с помощью самого изображения, пользовательского текстового описания или автоматически сгенерированного описания.", + "modelSize": { + "label": "Размер модели", + "desc": "Размер модели, используемой для создания векторных представлений для семантического поиска.", + "small": { + "title": "малый", + "desc": "Использование малой модели задействует квантованную версию модели, которая потребляет меньше оперативной памяти и работает быстрее на CPU с очень незначительной разницей в качестве эмбеддингов." + }, + "large": { + "title": "большой", + "desc": "Использование большой модели задействует полную модель Jina и автоматически запускается на GPU, если это возможно." + } + } + }, + "faceRecognition": { + "desc": "Функция распознавания лиц позволяет присваивать людям имена, и когда их лицо будет распознано, Frigate присвоит имя человека в качестве дополнительной метки. Эта информация содержится в пользовательском интерфейсе, фильтрах, а также в уведомлениях.", + "title": "Распознавание лиц", + "readTheDocumentation": "Читать документацию", + "modelSize": { + "label": "Размер модели", + "desc": "Размер модели, используемой для распознавания лиц.", + "small": { + "title": "малый", + "desc": "Использование малой модели задействует модель FaceNet для векторного представления лиц, которая эффективно работает на большинстве CPU." + }, + "large": { + "title": "большой", + "desc": "При выборе большой модели используется модель векторизации лиц ArcFace, которая автоматически задействует GPU (если он доступен)." + } + } + }, + "licensePlateRecognition": { + "title": "Распознавание номерных знаков", + "readTheDocumentation": "Читать документацию", + "desc": "Frigate может распознавать автомобильные номера и автоматически добавлять для объектов типа «автомобиль» обнаруженные символы в поле «распознанный номерной знак» или известное имя в качестве дополнительной метки. Типичный пример использования — чтение номеров автомобилей, заезжающих на подъездную дорожку или проезжающих по улице." + }, + "toast": { + "success": "Настройки классификации сохранены. Перезапустите Frigate, чтобы применить внесенные изменения.", + "error": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "title": "Настройки классификации", + "birdClassification": { + "title": "Классификация птиц", + "desc": "Классификация птиц определяет известные виды с помощью квантованной модели TensorFlow. Когда птица распознана, её обиходное название добавляется в качестве дополнительной метки. Эти информация используется в интерфейсе, фильтрах и уведомлениях." + }, + "restart_required": "Требуется перезапуск (изменены настройки классификации)", + "unsavedChanges": "Настройки классификации не сохранены" + }, + "users": { + "dialog": { + "passwordSetting": { + "updatePassword": "Обновить пароль для {{username}}", + "setPassword": "Установить пароль", + "desc": "Создайте надежный пароль для защиты аккаунта.", + "cannotBeEmpty": "Пароль не может быть пустым", + "doNotMatch": "Пароли не совпадают" + }, + "deleteUser": { + "warn": "Вы уверены, что хотите удалить пользователя {{username}}?", + "title": "Удалить пользователя", + "desc": "Это действие необратимо. Учётная запись пользователя и все связанные с ней данные будут удалены без возможности восстановления." + }, + "changeRole": { + "title": "Изменить роль пользователя", + "desc": "Обновить права доступа для {{username}}", + "roleInfo": { + "intro": "Выберите подходящую роль для этого пользователя:", + "viewer": "Наблюдатель", + "viewerDesc": "Доступны только панель мониторинга, обзор событий, поиск и экспорт данных.", + "admin": "Администратор", + "adminDesc": "Полный доступ ко всем функциям." + }, + "select": "Выбрать роль" + }, + "form": { + "user": { + "placeholder": "Введите имя пользователя", + "desc": "Допустимо использовать только буквы, цифры, точки и подчёркивания.", + "title": "Имя пользователя" + }, + "password": { + "title": "Пароль", + "placeholder": "Введите пароль", + "confirm": { + "title": "Подтвердите пароль", + "placeholder": "Подтвердите пароль" + }, + "strength": { + "title": "Сложность пароля: ", + "weak": "Слабый", + "medium": "Средний", + "strong": "Сложный", + "veryStrong": "Очень сложный" + }, + "match": "Пароли совпадают", + "notMatch": "Пароли не совпадают" + }, + "newPassword": { + "title": "Новый пароль", + "confirm": { + "placeholder": "Повторно введите новый пароль" + }, + "placeholder": "Введите новый пароль" + }, + "usernameIsRequired": "Необходимо ввести имя пользователя", + "passwordIsRequired": "Требуется пароль" + }, + "createUser": { + "title": "Создать нового пользователя", + "usernameOnlyInclude": "Имя пользователя может включать только буквы, цифры, . или _", + "desc": "Добавить новую учетную запись пользователя и определить роль для доступа к разделам интерфейса Frigate.", + "confirmPassword": "Пожалуйста, подтвердите пароль" + } + }, + "title": "Пользователи", + "toast": { + "success": { + "roleUpdated": "Обновлена роль для {{user}}", + "createUser": "Пользователь {{user}} успешно создан", + "deleteUser": "Пользователь {{user}} успешно удалён", + "updatePassword": "Пароль успешно обновлён." + }, + "error": { + "setPasswordFailed": "Не удалось сохранить пароль: {{errorMessage}}", + "createUserFailed": "Не удалось создать пользователя: {{errorMessage}}", + "deleteUserFailed": "Не удалось удалить пользователя: {{errorMessage}}", + "roleUpdateFailed": "Не удалось обновить роль: {{errorMessage}}" + } + }, + "table": { + "username": "Имя пользователя", + "actions": "Действия", + "password": "Пароль", + "noUsers": "Пользователей не найдено.", + "changeRole": "Изменить роль пользователя", + "role": "Роль", + "deleteUser": "Удалить пользователя" + }, + "management": { + "title": "Управление пользователями", + "desc": "Управление учетными записями пользователей Frigate." + }, + "updatePassword": "Обновить пароль", + "addUser": "Добавить пользователя" + }, + "notification": { + "title": "Уведомления", + "notificationSettings": { + "documentation": "Читать документацию", + "title": "Настройки уведомлений", + "desc": "Frigate может отправлять push-уведомления на ваше устройство, когда приложение открыто в браузере или установлено как PWA." + }, + "notificationUnavailable": { + "documentation": "Читать документацию", + "title": "Уведомления недоступны", + "desc": "Веб-уведомления требуют защищённого контекста (https://…). Это ограничение браузера. Получите безопасный доступ к Frigate, чтобы использовать уведомления." + }, + "email": { + "title": "Email", + "desc": "Для уведомлений о проблемах с push-сервисом требуется указать действующий адрес электронной почты.", + "placeholder": "например, example@email.com" + }, + "globalSettings": { + "title": "Глобальные настройки", + "desc": "Временно приостановить уведомления для определённых камер на всех зарегистрированных устройствах." + }, + "cameras": { + "title": "Камеры", + "noCameras": "Нет доступных камер", + "desc": "Выберите камеры для активации уведомлений." + }, + "deviceSpecific": "Настройки для конкретного устройства", + "registerDevice": "Зарегистрировать это устройство", + "unregisterDevice": "Отменить регистрацию этого устройства", + "suspended": "Уведомления приостановлены {{time}}", + "sendTestNotification": "Отправить тестовое уведомление", + "active": "Уведомления активны", + "suspendTime": { + "30minutes": "Приостановить на 30 минут", + "1hour": "Приостановить на 1 час", + "12hours": "Приостановить на 12 часов", + "24hours": "Приостановить на 24 часа", + "untilRestart": "Приостановить до перезапуска", + "5minutes": "Приостановить на 5 минут", + "10minutes": "Приостановить на 10 минут", + "suspend": "Приостановить" + }, + "toast": { + "success": { + "settingSaved": "Настройки уведомлений сохранены.", + "registered": "Регистрация для уведомлений успешно завершена. Перезапуск Frigate необходим перед отправкой любых уведомлений (включая тестовое уведомление)." + }, + "error": { + "registerFailed": "Не удалось сохранить регистрацию уведомлений." + } + }, + "cancelSuspension": "Отменить приостановку", + "unsavedChanges": "Изменения уведомлений не сохранены", + "unsavedRegistrations": "Регистрации уведомлений не сохранены" + }, + "camera": { + "review": { + "alerts": "Тревоги ", + "desc": "Временно включить/отключить тревоги и обнаружения для этой камеры до перезапуска Frigate. В отключенном состоянии новые события не будут записываться. ", + "detections": "Обнаружения ", + "title": "Обзор событий" + }, + "reviewClassification": { + "objectAlertsTips": "Все объекты {{alertsLabels}} на камере {{cameraName}} будут отображаться как тревоги.", + "desc": "Frigate разделяет записи для проверки на два типа как «Тревоги» и «Обнаружения». По умолчанию все объекты person и car считаются тревогами. Вы можете уточнить эту классификацию, настроив для них требуемые зоны.", + "selectAlertsZones": "Выберите зоны для тревог", + "zoneObjectDetectionsTips": { + "notSelectDetections": "Все объекты {{detectionsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, которые не отнесены к тревогам, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся.", + "text": "Все объекты {{detectionsLabels}}, не отнесённые к категории в {{zone}} на камере {{cameraName}}, будут отображаться как обнаружения.", + "regardlessOfZoneObjectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся." + }, + "zoneObjectAlertsTips": "Все объекты {{alertsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, будут отображаться как тревоги.", + "selectDetectionsZones": "Выберите зоны для обнаружения", + "noDefinedZones": "Для этой камеры не определено ни одной зоны.", + "objectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как обнаружения, независимо от того, в какой зоне они находятся.", + "title": "Классификация событий", + "readTheDocumentation": "Читать документацию", + "limitDetections": "Ограничение обнаружения отдельными зонами", + "toast": { + "success": "Конфигурация классификации событий была сохранена. Перезапустите Frigate для применения изменений." + }, + "unsavedChanges": "Настройки классификации событий для {{camera}} не сохранены" + }, + "title": "Настройки камеры", + "streams": { + "title": "Потоки", + "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
    Примечание: Это не отключает рестриминг go2rtc." + }, + "object_descriptions": { + "title": "Сгенерировать описания объектов при помощи ИИ", + "desc": "Временно включить/отключить описание объектов при помощи генеративного ИИ для этой камеры. При отключении описания, описание объектов при помощи генеративного ИИ не будут запрашиваться для отслеживаемых объектов на этой камере." + }, + "review_descriptions": { + "title": "Описания обзоров генеративного ИИ", + "desc": "Временно включить/отключить описания обзоров с помощью генеративного ИИ для этой камеры. Если отключено, описания, описания обзоров с помощью генеративного ИИ, не будут запрашиваться для элементов обзора для этой камеры." + }, + "addCamera": "Добавить новую камеру", + "editCamera": "Редактировать камеру:", + "selectCamera": "Выбрать камеру", + "backToSettings": "Вернуться к настройкам камеры", + "cameraConfig": { + "add": "Добавить камеру", + "edit": "Редактировать камеру", + "description": "Настройте параметры камеры, включая входные трансляции и роли.", + "name": "Название камеры", + "nameRequired": "Требуется имя камеры", + "nameInvalid": "Имя камеры должно содержать только буквы, цифры, подчеркивания или дефисы", + "namePlaceholder": "например, front_door", + "enabled": "Включено", + "ffmpeg": { + "inputs": "Входные трансляции", + "path": "Путь трансляции", + "pathRequired": "Требуется путь трансляции", + "pathPlaceholder": "rtsp://...", + "roles": "Роли", + "rolesRequired": "Требуется хотя бы одна роль", + "rolesUnique": "Каждая роль (аудио, обнаружение, запись) может быть назначена только одной трансляции", + "addInput": "Добавить входной поток", + "removeInput": "Удалить входной поток", + "inputsRequired": "Требуется хотя бы 1 входной поток" + }, + "toast": { + "success": "Камера {{cameraName}} успешно сохранена" + }, + "nameLength": "Название камеры должно содержать не более 24 символов." + } + }, + "masksAndZones": { + "zones": { + "objects": { + "title": "Объекты", + "desc": "Список объектов, применяемых к этой зоне." + }, + "speedEstimation": { + "desc": "Включить оценку скорости объектов в этой зоне. Зона должна состоять ровно из 4 точек.", + "title": "Расчёт скорости", + "docs": "Читать документацию", + "lineBDistance": "Длина линии B ({{unit}})", + "lineADistance": "Длина линии A ({{unit}})", + "lineCDistance": "Длина линии C ({{unit}})", + "lineDDistance": "Длина линии D ({{unit}})" + }, + "label": "Зоны", + "documentTitle": "Редактирование зоны - Frigate", + "desc": { + "title": "Зоны позволяют определить конкретную область кадра, чтобы можно было определить, находится ли объект в заданной области.", + "documentation": "Документация" + }, + "add": "Добавить зону", + "edit": "Редактировать зону", + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "clickDrawPolygon": "Кликните, чтобы нарисовать полигон на изображении.", + "name": { + "title": "Название", + "inputPlaceHolder": "Введите название…", + "tips": "Имя должно содержать не менее 2 символов, включать хотя бы одну букву и не совпадать с названием камеры или другой зоны." + }, + "inertia": { + "title": "Инерция", + "desc": "Указывает, сколько кадров объект должен находиться в зоне, прежде чем он будет считаться находящимся в ней. Значение по умолчанию: 3" + }, + "loiteringTime": { + "title": "Время присутствия", + "desc": "Устанавливает минимальное время в секундах, которое объект должен находиться в зоне для её активации. Значение по умолчанию: 0" + }, + "allObjects": "Все объекты", + "speedThreshold": { + "title": "Предел скорости ({{unit}})", + "toast": { + "error": { + "loiteringTimeError": "Зоны с установленным временем присутствия более 0 не должны использоваться для вычисления скорости.", + "pointLengthError": "Расчёт скорости отключён для этой зоны. Зоны с расчётом скорости должны содержать ровно 4 точки." + } + }, + "desc": "Задаёт минимальную скорость объектов для учёта в этой зоне." + }, + "toast": { + "success": "Зона ({{zoneName}}) сохранена. Перезапустите Frigate для применения изменений." + } + }, + "motionMasks": { + "desc": { + "documentation": "Документация", + "title": "Маски движения используются, чтобы предотвратить срабатывание обнаружений на нежелательные типы движения. Чрезмерная маскировка усложняет отслеживание объектов." + }, + "add": "Новая маска движения", + "edit": "Редактировать маску движения", + "context": { + "documentation": "Читать документацию", + "title": "Маски движения используются, чтобы предотвратить срабатывание обнаружений на нежелательные типы движения (например, ветки деревьев, метки времени на камере). При этом маски движения нужно использовать очень умеренно: чрезмерное применение масок затруднит отслеживание объектов." + }, + "clickDrawPolygon": "Нажмите, чтобы нарисовать полигон на изображении.", + "polygonAreaTooLarge": { + "documentation": "Читать документацию", + "title": "Маска движения покрывает {{polygonArea}}% кадра. Большие маски движения не рекомендуются.", + "tips": "Маски движения не предотвращают обнаружение объектов. Вместо этого следует использовать обязательную зону." + }, + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "label": "Маска движения", + "documentTitle": "Редактирование маски движения - Frigate", + "toast": { + "success": { + "title": "{{polygonName}} сохранена. Перезапустите Frigate для применения изменений.", + "noName": "Маска движения сохранена. Перезапустите Frigate для применения изменений." + } + } + }, + "filter": { + "all": "Все маски и зоны" + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Имя зоны должно содержать не менее 2 символов.", + "mustNotBeSameWithCamera": "Имя зоны не должно совпадать с именем камеры.", + "hasIllegalCharacter": "Имя зоны содержит недопустимые символы.", + "alreadyExists": "Зона с таким именем уже существует для этой камеры.", + "mustNotContainPeriod": "Имя зоны не должно содержать точки.", + "mustHaveAtLeastOneLetter": "Название зоны должно содержать хотя бы одну букву." + } + }, + "distance": { + "error": { + "text": "Расстояние должно быть больше или равно 0.1.", + "mustBeFilled": "Все поля расстояния должны быть заполнены для расчёта скорости." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Инерция должна быть больше 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Время присутствия должно быть больше или равно 0." + } + }, + "polygonDrawing": { + "removeLastPoint": "Удалить последнюю точку", + "error": { + "mustBeFinished": "Рисование полигона должно быть завершено перед сохранением." + }, + "delete": { + "success": "{{name}} удалён.", + "title": "Подтвердить удаление", + "desc": "Вы уверены, что хотите удалить {{type}} {{name}}?" + }, + "snapPoints": { + "false": "Не привязывать к точкам", + "true": "Привязать точки" + }, + "reset": { + "label": "Удалить все точки" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Порог скорости должен быть не меньше 0,1." + } + } + }, + "toast": { + "error": { + "copyCoordinatesFailed": "Не удалось скопировать координаты в буфер обмена." + }, + "success": { + "copyCoordinates": "Координаты {{polyName}} скопированы в буфер обмена." + } + }, + "objectMasks": { + "label": "Маски объектов", + "desc": { + "documentation": "Документация", + "title": "Маски фильтра объектов используются для исключения ложных срабатываний определённого типа объектов в зависимости от местоположения." + }, + "documentTitle": "Редактирование маски объектов - Frigate", + "add": "Добавить маску объектов", + "clickDrawPolygon": "Кликните, чтобы нарисовать полигон на изображении.", + "edit": "Редактирование маски объектов", + "context": "Маски фильтра объектов используются для исключения ложных срабатываний определённого типа объектов в зависимости от местоположения.", + "point_one": "{{count}} точка", + "point_few": "{{count}} точки", + "point_many": "{{count}} точек", + "objects": { + "allObjectTypes": "Все типы объектов", + "title": "Объекты", + "desc": "Тип объекта, который применяется к этой маске объекта." + }, + "toast": { + "success": { + "title": "{{polygonName}} сохранена. Перезапустите Frigate для применения изменений.", + "noName": "Маска объектов сохранена. Перезапустите Frigate для применения изменений." + } + } + }, + "restart_required": "Требуется перезапуск (изменены маски/зоны)", + "objectMaskLabel": "Маска объекта {{number}} {{label}}", + "motionMaskLabel": "Маска движения {{number}}" + }, + "motionDetectionTuner": { + "desc": { + "documentation": "Читать руководство по настройке детекции движения", + "title": "Frigate использует детекцию движения как первичную проверку, чтобы определить, есть ли в кадре что-то, что стоит анализировать с помощью детекции объектов." + }, + "title": "Настройка детекции движения", + "contourArea": { + "title": "Площадь контура", + "desc": "Параметр площади контура определяет, какие группы изменённых пикселей считаются движением. По умолчанию: 10" + }, + "improveContrast": { + "title": "Улучшить контрастность", + "desc": "Улучшение контрастности в тёмных сценах. По умолчанию: ВКЛ" + }, + "Threshold": { + "title": "Порог", + "desc": "Пороговое значение определяет, насколько должна измениться яркость пикселя, чтобы считаться движением. По умолчанию: 30" + }, + "toast": { + "success": "Настройки движения сохранены." + }, + "unsavedChanges": "Настройки детекции движения для ({{camera}}) не сохранены" + }, + "debug": { + "objectShapeFilterDrawing": { + "document": "Читать документацию ", + "title": "Отрисовка фильтра формы объекта", + "desc": "Отображает прямоугольник на изображении, чтобы видеть данные о площади и соотношении сторон", + "tips": "Включите эту опцию, чтобы нарисовать прямоугольник на изображении с камеры для отображения его площади и соотношения сторон. Эти значения можно затем использовать для настройки параметров фильтра формы объектов в вашем конфигурационном файле.", + "area": "Площадь", + "ratio": "Соотношение", + "score": "Оценка" + }, + "detectorDesc": "Frigate использует ваши детекторы ({{detectors}}) для обнаружения объектов в видеопотоке с камер.", + "desc": "Режим отладки отображает отслеживаемые объекты и их статистику в реальном времени. Список объектов показывает отложенную по времени сводку обнаруженных объектов.", + "debugging": "Отладка", + "title": "Отладка", + "boundingBoxes": { + "colors": { + "label": "Цвета ограничивающих рамок объектов", + "info": "
  • При запуске каждой метке объекта назначается уникальный цвет
  • Тонкая синяя линия: объект в данный момент не обнаружен
  • Тонкая серая линия: объект помечен как статичный
  • Толстая линия: объект под автотрекингом (если включено)
  • " + }, + "title": "Ограничивающие рамки", + "desc": "Показывать ограничивающие рамки вокруг отслеживаемых объектов" + }, + "objectList": "Список объектов", + "noObjects": "Нет объектов", + "timestamp": { + "title": "Метка времени", + "desc": "Наложить временную метку на изображение" + }, + "zones": { + "title": "Зоны", + "desc": "Показать контур всех определённых зон" + }, + "mask": { + "title": "Маски движения", + "desc": "Показать полигоны маски движения" + }, + "motion": { + "title": "Области движения", + "desc": "Показать рамки вокруг областей, в которых определяется движение", + "tips": "

    Области движения


    Красные рамки будут наложены на участки кадра, где в данный момент обнаружено движение

    " + }, + "regions": { + "title": "Регионы", + "desc": "Показать рамку области интереса, отправленной детектору объектов", + "tips": "

    Рамки областей интереса


    Ярко-зелёные рамки будут наложены на области интереса в кадре, которые отправляются детектору объектов.

    " + }, + "paths": { + "title": "Пути", + "desc": "Показывать значимые точки пути отслеживаемого объекта", + "tips": "

    Пути


    Линии и круги будут обозначать важные точки, которые отслеживаемый объект посетил в течение своего жизненного цикла.

    " + }, + "openCameraWebUI": "Открыть веб-интерфейс {{camera}}", + "audio": { + "title": "Аудио", + "noAudioDetections": "Аудиообнаружений нет", + "score": "оценка", + "currentRMS": "Текущий RMS", + "currentdbFS": "Текущий dbFS" + } + }, + "frigatePlus": { + "snapshotConfig": { + "documentation": "Читать документацию", + "title": "Настройки снимков", + "cleanCopyWarning": "У некоторых камер включены снимки (snapshots), но отключена опция чистой копии (clean copy). Чтобы иметь возможность отправлять изображения с этих камер в Frigate+, необходимо включить параметр clean_copy в конфигурации снимков.", + "table": { + "cleanCopySnapshots": "Снимки clean_copy", + "camera": "Камера", + "snapshots": "Снимки" + }, + "desc": "Отправка в Frigate+ требует, чтобы в вашей конфигурации были включены как снимки (snapshots), так и снимки clean_copy." + }, + "title": "Настройки Frigate+", + "apiKey": { + "title": "Ключ API Frigate+", + "validated": "Ключ API Frigate+ найден и проверен", + "notValidated": "Ключ API Frigate+ не найден или не проверен", + "desc": "Ключ API Frigate+ включает интеграцию с сервисом Frigate+.", + "plusLink": "Подробнее про Frigate+" + }, + "modelInfo": { + "title": "Информация о модели", + "modelType": "Тип модели", + "trainDate": "Дата обучения", + "error": "Не удалось загрузить информацию о модели", + "availableModels": "Доступные модели", + "loadingAvailableModels": "Загрузка доступных моделей…", + "modelSelect": "Здесь можно выбрать ваши доступные модели на Frigate+. Обратите внимание, что могут быть выбраны только модели, совместимые с текущей конфигурацией детектора.", + "baseModel": "Базовая модель", + "supportedDetectors": "Поддерживаемые детекторы", + "dimensions": "Размеры", + "loading": "Загрузка информации о модели…", + "cameras": "Камеры", + "plusModelType": { + "baseModel": "Базовая модель", + "userModel": "Дообученная" + } + }, + "toast": { + "success": "Настройки Frigate+ были сохранены. Перезапустите Frigate, чтобы применить изменения.", + "error": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" + }, + "restart_required": "Требуется перезапуск (изменена модель Frigate+)", + "unsavedChanges": "Настройки Frigate+ не сохранены" + }, + "enrichments": { + "title": "Настройки обогащения", + "semanticSearch": { + "readTheDocumentation": "Читать документацию", + "desc": "Семантический поиск во Frigate позволяет находить отслеживаемые объекты в записях с помощью самого изображения, пользовательского текстового описания или автоматически сгенерированного описания.", + "reindexNow": { + "desc": "Переиндексация заново сгенерирует векторные представления для всех отслеживаемых объектов. Этот процесс выполняется в фоновом режиме и может максимально загрузить ваш процессор, а также занять значительное время в зависимости от количества отслеживаемых объектов.", + "label": "Переиндексировать сейчас", + "confirmTitle": "Подтвердить переиндексацию", + "confirmDesc": "Вы уверены, что хотите переиндексировать все векторные представления отслеживаемых объектов? Этот процесс будет выполняться в фоновом режиме, но может максимально загрузить ваш процессор и занять довольно много времени. Вы можете следить за ходом выполнения на странице «Поиск событий».", + "confirmButton": "Переиндексировать", + "success": "Переиндексация успешно запущена.", + "alreadyInProgress": "Переиндексация уже выполняется.", + "error": "Не удалось запустить переиндексацию: {{errorMessage}}" + }, + "modelSize": { + "desc": "Размер модели, используемой для создания векторных представлений для семантического поиска.", + "small": { + "desc": "Использование малой модели задействует квантованную версию модели, которая потребляет меньше оперативной памяти и работает быстрее на CPU с очень незначительной разницей в качестве эмбеддингов.", + "title": "малый" + }, + "label": "Размер модели", + "large": { + "title": "большой", + "desc": "Использование большой модели задействует полную модель Jina и автоматически запускается на GPU, если это возможно." + } + }, + "title": "Семантический поиск" + }, + "birdClassification": { + "desc": "Классификация птиц определяет известные виды с помощью квантованной модели TensorFlow. Когда птица распознана, её обиходное название добавляется в качестве дополнительной метки. Эти информация используется в интерфейсе, фильтрах и уведомлениях.", + "title": "Классификация птиц" + }, + "faceRecognition": { + "modelSize": { + "large": { + "desc": "При выборе большой модели используется модель векторизации лиц ArcFace, которая автоматически задействует GPU (если он доступен).", + "title": "большой" + }, + "small": { + "title": "малый", + "desc": "Использование малой модели задействует модель FaceNet для векторного представления лиц, которая эффективно работает на большинстве CPU." + }, + "label": "Размер модели", + "desc": "Размер модели, используемой для распознавания лиц." + }, + "desc": "Функция распознавания лиц позволяет присваивать людям имена, и когда их лицо будет распознано, Frigate присвоит имя человека в качестве дополнительной метки. Эта информация содержится в пользовательском интерфейсе, фильтрах, а также в уведомлениях.", + "title": "Распознавание лиц", + "readTheDocumentation": "Читать документацию" + }, + "licensePlateRecognition": { + "desc": "Frigate может распознавать автомобильные номера и автоматически добавлять для объектов типа «автомобиль» обнаруженные символы в поле «распознанный номерной знак» или известное имя в качестве дополнительной метки. Типичный пример использования — чтение номеров автомобилей, заезжающих на подъездную дорожку или проезжающих по улице.", + "title": "Распознавание номерных знаков", + "readTheDocumentation": "Читать документацию" + }, + "unsavedChanges": "Несохранённые изменения настроек обогащений", + "restart_required": "Требуется перезапуск (изменены настройки обогащений)", + "toast": { + "success": "Настройки обогащений сохранены. Перезапустите Frigate, чтобы применить изменения.", + "error": "Не удалось сохранить изменения: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Триггеры", + "management": { + "title": "Управление триггерами", + "desc": "Управление триггерами для камеры {{camera}}. Используйте тип миниатюры для срабатывания по миниатюрам, похожим на выбранный отслеживаемый объект, и тип описания для срабатывания по описаниям, похожим на указанный вами текст." + }, + "addTrigger": "Добавить Триггер", + "table": { + "name": "Имя", + "type": "Тип", + "content": "Содержимое", + "threshold": "Порог", + "actions": "Действия", + "noTriggers": "Для этой камеры не настроены триггеры.", + "edit": "Редактировать", + "deleteTrigger": "Удалить триггер", + "lastTriggered": "Последний сработавший" + }, + "type": { + "thumbnail": "Миниатюра", + "description": "Описание" + }, + "actions": { + "alert": "Отметить как предупреждение", + "notification": "Отправить оповещение" + }, + "dialog": { + "createTrigger": { + "title": "Создать триггер", + "desc": "Создать триггер для камеры {{camera}}" + }, + "editTrigger": { + "title": "Изменить триггер", + "desc": "Изменить настройки триггера для камеры {{camera}}" + }, + "deleteTrigger": { + "title": "Удалить триггер", + "desc": "Вы уверены, что хотите удалить триггер {{triggerName}}? Это действие не может быть отменено." + }, + "form": { + "name": { + "title": "Имя", + "placeholder": "Введите имя триггера", + "error": { + "minLength": "Имя должно быть длиной не менее 2 символов.", + "invalidCharacters": "Имя может содержать только буквы, цифры, символы подчеркивания и дефисы.", + "alreadyExists": "Триггер с таким именем уже существует для этой камеры." + } + }, + "enabled": { + "description": "Включить или отключить этот триггер" + }, + "type": { + "title": "Тип", + "placeholder": "Выберите тип триггера" + }, + "content": { + "title": "Содержимое", + "imagePlaceholder": "Выберите изображение", + "textPlaceholder": "Введите текстовое содержимое", + "imageDesc": "Выберите изображение, чтобы активировать это действие при обнаружении похожего изображения.", + "textDesc": "Введите текст, чтобы активировать это действие при обнаружении похожего описания отслеживаемого объекта.", + "error": { + "required": "Требуется содержимое." + } + }, + "threshold": { + "title": "Порог", + "error": { + "min": "Порог должен быть не менее 0", + "max": "Порог должен быть не более 1" + } + }, + "actions": { + "title": "Действия", + "desc": "По умолчанию Frigate отправляет MQTT-сообщение для всех триггеров. Выберите дополнительное действие, которое будет выполняться при срабатывании этого триггера.", + "error": { + "min": "Необходимо выбрать хотя бы одно действие." + } + }, + "friendly_name": { + "description": "Необязательное название или описание к этому триггеру", + "placeholder": "Название или описание триггера", + "title": "Понятное название" + } + } + }, + "toast": { + "success": { + "createTrigger": "Триггер {{name}} успешно создан.", + "updateTrigger": "Триггер {{name}} успешно обновлен.", + "deleteTrigger": "Триггер {{name}} успешно удален." + }, + "error": { + "createTriggerFailed": "Не удалось создать триггер: {{errorMessage}}", + "updateTriggerFailed": "Не удалось обновить триггер: {{errorMessage}}", + "deleteTriggerFailed": "Не удалось удалить триггер: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Семантический поиск выключен", + "desc": "Для использования триггеров необходимо включить семантический поиск." + } + }, + "cameraWizard": { + "title": "Добавить камеру", + "description": "Следуйте инструкциям ниже, чтобы добавить новую камеру в вашу установку Frigate.", + "steps": { + "nameAndConnection": "Имя и подключение", + "streamConfiguration": "Конфигурация потока", + "validationAndTesting": "Проверка и тестирование", + "probeOrSnapshot": "Проверка или снимок" + }, + "save": { + "success": "Новая камера {{cameraName}} успешно сохранена.", + "failure": "Ошибка при сохранении {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Разрешение", + "video": "Видео", + "audio": "Аудио", + "fps": "Кадры в секунду (FPS)" + }, + "commonErrors": { + "noUrl": "Пожалуйста, укажите корректный URL потока", + "testFailed": "Тест потока не удался: {{error}}" + }, + "step1": { + "description": "Введите данные камеры и проверьте подключение.", + "cameraName": "Имя камеры", + "cameraNamePlaceholder": "Например, front_door или Обзор заднего двора", + "host": "Хост/IP-адрес", + "port": "Порт", + "username": "Имя пользователя", + "usernamePlaceholder": "Необязательно", + "password": "Пароль", + "passwordPlaceholder": "Необязательно", + "selectTransport": "Выберите транспортный протокол", + "cameraBrand": "Бренд камеры", + "selectBrand": "Выберите бренд камеры для шаблона URL", + "customUrl": "Пользовательский URL потока", + "brandInformation": "Информация о бренде", + "brandUrlFormat": "Для камер с форматом RTSP-URL вида: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "testConnection": "Проверить соединение", + "testSuccess": "Соединение успешно установлено!", + "testFailed": "Проверка соединения не удалась. Проверьте введённые данные и попробуйте снова.", + "streamDetails": "Детали потока", + "warnings": { + "noSnapshot": "Не удалось получить снимок из настроенного потока." + }, + "errors": { + "brandOrCustomUrlRequired": "Выберите бренд камеры с указанием хоста/IP или выберите \"Другое\" и укажите пользовательский URL", + "nameRequired": "Необходимо указать имя камеры", + "nameLength": "Имя камеры должно содержать не более 64 символов", + "invalidCharacters": "Имя камеры содержит недопустимые символы", + "nameExists": "Имя камеры уже используется", + "brands": { + "reolink-rtsp": "RTSP от Reolink не рекомендуется. Включите HTTP в настройках камеры и перезапустите мастер настройки камеры." + }, + "customUrlRtspRequired": "Пользовательские URL должны начинаться с \"rtsp://\". Для потоков камер, не использующих RTSP, требуется ручная настройка." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Проверка метаданных камеры…", + "fetchingSnapshot": "Получение снимка с камеры…" + }, + "connectionSettings": "Настройки подключения", + "detectionMethod": "Метод обнаружения потока", + "onvifPort": "Порт ONVIF", + "probeMode": "Проверить камеру", + "manualMode": "Ручной выбор", + "detectionMethodDescription": "Проверьте камеру с помощью ONVIF (если поддерживается) для поиска URL потоков камеры или вручную выберите бренд камеры для использования предопределённых URL. Чтобы ввести пользовательский RTSP URL, выберите ручной метод и выберите \"Другое\".", + "onvifPortDescription": "Для камер, поддерживающих ONVIF, это обычно 80 или 8080.", + "useDigestAuth": "Использовать digest-аутентификацию", + "useDigestAuthDescription": "Использовать HTTP digest-аутентификацию для ONVIF. Некоторые камеры могут требовать отдельное имя пользователя/пароль ONVIF вместо стандартного пользователя администратора." + }, + "step2": { + "description": "Проверьте камеру на наличие доступных потоков или настройте параметры вручную в зависимости от выбранного метода обнаружения.", + "streamsTitle": "Потоки камеры", + "addStream": "Добавить поток", + "addAnotherStream": "Добавить ещё один поток", + "streamTitle": "Поток {{number}}", + "streamUrl": "URL потока", + "streamUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "url": "URL", + "resolution": "Разрешение", + "selectResolution": "Выберите разрешение", + "quality": "Качество", + "selectQuality": "Выберите качество", + "roles": "Роли", + "roleLabels": { + "detect": "Обнаружение объектов", + "record": "Запись", + "audio": "Аудио" + }, + "testStream": "Проверить соединение", + "testSuccess": "Проверка соединения успешна!", + "testFailed": "Проверка соединения не удалась. Проверьте введённые данные и попробуйте снова.", + "testFailedTitle": "Проверка не удалась", + "streamDetails": "Детали потока", + "probing": "Проверка камеры…", + "retry": "Повторить", + "testing": { + "probingMetadata": "Проверка метаданных камеры…", + "fetchingSnapshot": "Получение снимка с камеры…" + }, + "probeFailed": "Не удалось проверить камеру: {{error}}", + "probingDevice": "Проверка устройства…", + "probeSuccessful": "Проверка успешна", + "probeError": "Ошибка проверки", + "probeNoSuccess": "Проверка не удалась", + "deviceInfo": "Информация об устройстве", + "manufacturer": "Производитель", + "model": "Модель", + "firmware": "Прошивка", + "profiles": "Профили", + "ptzSupport": "Поддержка PTZ", + "autotrackingSupport": "Поддержка автотрекинга", + "presets": "Предустановки", + "rtspCandidates": "Кандидаты RTSP", + "rtspCandidatesDescription": "Следующие RTSP URL были найдены при проверке камеры. Проверьте соединение, чтобы просмотреть метаданные потока.", + "noRtspCandidates": "RTSP URL не найдены для камеры. Ваши учётные данные могут быть неверными, или камера может не поддерживать ONVIF или метод, используемый для получения RTSP URL. Вернитесь назад и введите RTSP URL вручную.", + "candidateStreamTitle": "Кандидат {{number}}", + "useCandidate": "Использовать", + "uriCopy": "Копировать", + "uriCopied": "URI скопирован в буфер обмена", + "testConnection": "Проверить соединение", + "toggleUriView": "Нажмите, чтобы переключить полный вид URI", + "connected": "Подключено", + "notConnected": "Не подключено", + "errors": { + "hostRequired": "Требуется хост/IP-адрес" + } + }, + "step3": { + "description": "Настройте роли потоков и добавьте дополнительные потоки для вашей камеры.", + "streamsTitle": "Потоки камеры", + "addStream": "Добавить поток", + "addAnotherStream": "Добавить ещё поток", + "streamTitle": "Поток {{number}}", + "streamUrl": "URL потока", + "streamUrlPlaceholder": "rtsp://имя_пользователя:пароль@хост:порт/путь", + "selectStream": "Выбрать поток", + "searchCandidates": "Поиск кандидатов…", + "noStreamFound": "Поток не найден", + "url": "URL", + "resolution": "Разрешение", + "selectResolution": "Выберите разрешение", + "quality": "Качество", + "selectQuality": "Выберите качество", + "roles": "Роли", + "roleLabels": { + "detect": "Обнаружение объектов", + "record": "Запись", + "audio": "Аудио" + }, + "testStream": "Проверить соединение", + "testSuccess": "Тест потока выполнен успешно!", + "testFailed": "Тест потока не пройден", + "testFailedTitle": "Тест не пройден", + "connected": "Подключено", + "notConnected": "Не подключено", + "featuresTitle": "Функции", + "go2rtc": "Уменьшить количество подключений к камере", + "detectRoleWarning": "Хотя бы один поток должен иметь роль \"detect\" для продолжения.", + "rolesPopover": { + "title": "Роли потоков", + "detect": "Основной поток для обнаружения объектов.", + "record": "Сохраняет сегменты видеопотока на основе настроек конфигурации.", + "audio": "Поток для обнаружения на основе аудио." + }, + "featuresPopover": { + "title": "Функции потоков", + "description": "Использовать рестриминг go2rtc для уменьшения количества подключений к камере." + } + }, + "step4": { + "description": "Финальная проверка и анализ перед сохранением новой камеры. Подключите каждый поток перед сохранением.", + "validationTitle": "Проверка потоков", + "connectAllStreams": "Подключить все потоки", + "reconnectionSuccess": "Переподключение успешно.", + "reconnectionPartial": "Некоторые потоки не удалось переподключить.", + "streamUnavailable": "Предпросмотр потока недоступен", + "reload": "Перезагрузить", + "connecting": "Подключение…", + "streamTitle": "Поток {{number}}", + "valid": "Действителен", + "failed": "Не удалось", + "notTested": "Не проверен", + "connectStream": "Подключить", + "connectingStream": "Подключение", + "disconnectStream": "Отключить", + "estimatedBandwidth": "Расчётная пропускная способность", + "roles": "Роли", + "ffmpegModule": "Использовать режим совместимости потоков", + "ffmpegModuleDescription": "Если поток не загружается после нескольких попыток, попробуйте включить это. При включении Frigate будет использовать модуль ffmpeg с go2rtc. Это может обеспечить лучшую совместимость с некоторыми потоками камер.", + "none": "Нет", + "error": "Ошибка", + "streamValidated": "Поток {{number}} успешно проверен", + "streamValidationFailed": "Проверка потока {{number}} не удалась", + "saveAndApply": "Сохранить новую камеру", + "saveError": "Неверная конфигурация. Пожалуйста, проверьте настройки.", + "issues": { + "title": "Проверка потоков", + "videoCodecGood": "Видеокодек: {{codec}}.", + "audioCodecGood": "Аудиокодек: {{codec}}.", + "resolutionHigh": "Разрешение {{resolution}} может привести к увеличению использования ресурсов.", + "resolutionLow": "Разрешение {{resolution}} может быть слишком низким для надёжного обнаружения мелких объектов.", + "noAudioWarning": "Аудио не обнаружено для этого потока, записи не будут содержать аудио.", + "audioCodecRecordError": "Для поддержки аудио в записях требуется аудиокодек AAC.", + "audioCodecRequired": "Для поддержки обнаружения аудио требуется аудиопоток.", + "restreamingWarning": "Уменьшение количества подключений к камере для потока записи может немного увеличить использование CPU.", + "brands": { + "reolink-rtsp": "RTSP от Reolink не рекомендуется. Включите HTTP в настройках прошивки камеры и перезапустите мастер.", + "reolink-http": "HTTP потоки Reolink должны использовать FFmpeg для лучшей совместимости. Включите 'Использовать режим совместимости потоков' для этого потока." + }, + "dahua": { + "substreamWarning": "Подпоток 1 заблокирован на низком разрешении. Многие камеры Dahua / Amcrest / EmpireTech поддерживают дополнительные подпотоки, которые необходимо включить в настройках камеры. Рекомендуется проверить и использовать эти потоки, если они доступны." + }, + "hikvision": { + "substreamWarning": "Подпоток 1 заблокирован на низком разрешении. Многие камеры Hikvision поддерживают дополнительные подпотоки, которые необходимо включить в настройках камеры. Рекомендуется проверить и использовать эти потоки, если они доступны." + } + } + } + }, + "roles": { + "addRole": "Добавить роль", + "table": { + "role": "Роль", + "cameras": "Камеры", + "actions": "Действия", + "editCameras": "Редактировать камеры", + "deleteRole": "Удалить роль", + "noRoles": "Пользовательских ролей не найдено." + }, + "toast": { + "success": { + "createRole": "Роль {{role}} успешно создана", + "updateCameras": "Камеры обновлены для роли {{role}}", + "deleteRole": "Роль {{role}} успешно удалена", + "userRolesUpdated_one": "{{count}} пользователь, назначенный на эту роль, был обновлён до роли 'наблюдатель', которая имеет доступ ко всем камерам.", + "userRolesUpdated_few": "{{count}} пользователя, назначенных на эту роль, были обновлены до роли 'наблюдатель', которая имеет доступ ко всем камерам.", + "userRolesUpdated_many": "{{count}} пользователей, назначенных на эту роль, были обновлены до роли 'наблюдатель', которая имеет доступ ко всем камерам." + }, + "error": { + "createRoleFailed": "Не удалось создать роль: {{errorMessage}}", + "updateCamerasFailed": "Не удалось обновить камеры: {{errorMessage}}", + "deleteRoleFailed": "Не удалось удалить роль: {{errorMessage}}", + "userUpdateFailed": "Не удалось обновить роли пользователей: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Создать новую роль", + "desc": "Добавьте новую роль и укажите права доступа к камерам." + }, + "editCameras": { + "title": "Редактировать камеры роли", + "desc": "Обновите доступ к камерам для роли {{role}}." + }, + "deleteRole": { + "title": "Удалить роль", + "desc": "Это действие нельзя отменить. Это действие навсегда удалит роль и назначит всех пользователей с этой ролью на роль 'наблюдатель', что даст наблюдателю доступ ко всем камерам.", + "warn": "Вы уверены, что хотите удалить {{role}}?", + "deleting": "Удаление…" + }, + "form": { + "role": { + "title": "Название роли", + "placeholder": "Введите название роли", + "desc": "Разрешены только буквы, цифры, точки и подчёркивания.", + "roleIsRequired": "Требуется название роли", + "roleOnlyInclude": "Название роли может содержать только буквы, цифры, . или _", + "roleExists": "Роль с таким названием уже существует." + }, + "cameras": { + "title": "Камеры", + "desc": "Выберите камеры, к которым эта роль имеет доступ. Необходимо выбрать хотя бы одну камеру.", + "required": "Необходимо выбрать хотя бы одну камеру." + } + } + } + }, + "cameraManagement": { + "title": "Управление камерами", + "addCamera": "Добавить новую камеру", + "editCamera": "Редактировать камеру:", + "selectCamera": "Выбрать камеру", + "backToSettings": "Вернуться к настройкам камеры", + "streams": { + "title": "Включить / Отключить камеры", + "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
    Примечание: Это не отключает рестриминг go2rtc." + }, + "cameraConfig": { + "add": "Добавить камеру", + "edit": "Редактировать камеру", + "description": "Настройте параметры камеры, включая входные трансляции и роли.", + "name": "Название камеры", + "nameRequired": "Требуется имя камеры", + "nameLength": "Название камеры должно содержать менее 64 символов.", + "namePlaceholder": "например, front_door или Обзор заднего двора", + "enabled": "Включено", + "ffmpeg": { + "inputs": "Входные потоки", + "path": "Путь потока", + "pathRequired": "Требуется путь потока", + "pathPlaceholder": "rtsp://…", + "roles": "Роли", + "rolesRequired": "Требуется хотя бы одна роль", + "rolesUnique": "Каждая роль (аудио, обнаружение, запись) может быть назначена только одной трансляции", + "addInput": "Добавить входной поток", + "removeInput": "Удалить входной поток", + "inputsRequired": "Требуется хотя бы один входной поток" + }, + "go2rtcStreams": "Потоки go2rtc", + "streamUrls": "URL потоков", + "addUrl": "Добавить URL", + "addGo2rtcStream": "Добавить поток go2rtc", + "toast": { + "success": "Камера {{cameraName}} успешно сохранена" + } + } + }, + "cameraReview": { + "title": "Настройки просмотра камеры", + "object_descriptions": { + "title": "Генеративные описания объектов ИИ", + "desc": "Временно включить/отключить генеративные описания объектов ИИ для этой камеры. При отключении описания объектов, сгенерированные ИИ, не будут запрашиваться для отслеживаемых объектов на этой камере." + }, + "review_descriptions": { + "title": "Генеративные описания обзоров ИИ", + "desc": "Временно включить/отключить генеративные описания обзоров ИИ для этой камеры. При отключении описания обзоров, сгенерированные ИИ, не будут запрашиваться для элементов обзора на этой камере." + }, + "review": { + "title": "Обзор", + "desc": "Временно включить/отключить тревоги и обнаружения для этой камеры до перезапуска Frigate. При отключении новые элементы обзора не будут создаваться. ", + "alerts": "Тревоги ", + "detections": "Обнаружения " + }, + "reviewClassification": { + "title": "Классификация обзора", + "desc": "Frigate классифицирует элементы обзора как Тревоги и Обнаружения. По умолчанию все объекты person и car считаются Тревогами. Вы можете уточнить классификацию элементов обзора, настроив для них требуемые зоны.", + "noDefinedZones": "Для этой камеры не определено ни одной зоны.", + "objectAlertsTips": "Все объекты {{alertsLabels}} на камере {{cameraName}} будут отображаться как Тревоги.", + "zoneObjectAlertsTips": "Все объекты {{alertsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, будут отображаться как Тревоги.", + "objectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся.", + "zoneObjectDetectionsTips": { + "text": "Все объекты {{detectionsLabels}}, не отнесённые к категории в {{zone}} на камере {{cameraName}}, будут отображаться как Обнаружения.", + "notSelectDetections": "Все объекты {{detectionsLabels}}, обнаруженные в {{zone}} на камере {{cameraName}}, которые не отнесены к Тревогам, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся.", + "regardlessOfZoneObjectDetectionsTips": "Все объекты {{detectionsLabels}}, не отнесённые к категории на камере {{cameraName}}, будут отображаться как Обнаружения, независимо от того, в какой зоне они находятся." + }, + "unsavedChanges": "Несохранённые настройки классификации обзора для {{camera}}", + "selectAlertsZones": "Выберите зоны для Тревог", + "selectDetectionsZones": "Выберите зоны для обнаружений", + "limitDetections": "Ограничить обнаружения определёнными зонами", + "toast": { + "success": "Конфигурация классификации обзора была сохранена. Перезапустите Frigate для применения изменений." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ru/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ru/views/system.json new file mode 100644 index 0000000..ad2b914 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ru/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "Статистика камер - Frigate", + "storage": "Статистика хранилища - Frigate", + "general": "Общая статистика - Frigate", + "enrichments": "Статистика обогащений - Frigate", + "logs": { + "frigate": "Логи Frigate - Frigate", + "go2rtc": "Логи Go2RTC - Frigate", + "nginx": "Логи Nginx - Frigate" + } + }, + "title": "Система", + "metrics": "Показатели системы", + "logs": { + "download": { + "label": "Загрузить логи" + }, + "copy": { + "label": "Копировать в буфер", + "success": "Логи скопированы в буфер", + "error": "Не удалось скопировать логи в буфер обмена" + }, + "type": { + "label": "Тип", + "timestamp": "Метка времени", + "tag": "Тег", + "message": "Сообщение" + }, + "tips": "Логи передаются с сервера в потоковом режиме", + "toast": { + "error": { + "fetchingLogsFailed": "Ошибка получения логов: {{errorMessage}}", + "whileStreamingLogs": "Ошибка при потоковой передаче логов: {{errorMessage}}" + } + } + }, + "general": { + "title": "Общие", + "detector": { + "title": "Детекторы", + "inferenceSpeed": "Скорость вывода детектора", + "cpuUsage": "Использование CPU детектором", + "memoryUsage": "Использование памяти детектором", + "temperature": "Температура детектора", + "cpuUsageInformation": "CPU используется при подготовке входных и выходных данных к/от моделей обнаружения. Это значение не измеряет использование вывода, даже если использовать GPU или ускоритель." + }, + "hardwareInfo": { + "title": "Информация об оборудовании", + "gpuUsage": "Использование GPU", + "gpuMemory": "Память GPU", + "gpuEncoder": "GPU-кодировщик", + "gpuDecoder": "GPU-декодер", + "gpuInfo": { + "vainfoOutput": { + "title": "Вывод Vainfo", + "returnCode": "Код возврата: {{code}}", + "processOutput": "Вывод процесса:", + "processError": "Ошибка процесса:" + }, + "nvidiaSMIOutput": { + "title": "Вывод Nvidia SMI", + "name": "Название: {{name}}", + "driver": "Драйвер: {{driver}}", + "cudaComputerCapability": "Вычислительная способность CUDA: {{cuda_compute}}", + "vbios": "Информация VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Закрыть информацию GPU" + }, + "copyInfo": { + "label": "Скопировать информацию о GPU" + }, + "toast": { + "success": "Информация о GPU скопирована в буфер обмена" + } + }, + "npuMemory": "Память NPU", + "npuUsage": "Использование NPU" + }, + "otherProcesses": { + "title": "Другие процессы", + "processCpuUsage": "Использование CPU процессом", + "processMemoryUsage": "Использование памяти процессом" + } + }, + "storage": { + "title": "Хранилище", + "overview": "Обзор", + "recordings": { + "title": "Записи", + "tips": "Это значение показывает, сколько места в хранилище занимают записи из базы данных Frigate. Frigate не учитывает другие файлы на диске.", + "earliestRecording": "Первая запись:" + }, + "cameraStorage": { + "title": "Хранилище камеры", + "camera": "Камера", + "unusedStorageInformation": "Информация о неиспользованном хранилище", + "storageUsed": "Хранилище", + "percentageOfTotalUsed": "Доля (%)", + "bandwidth": "Пропускная способность", + "unused": { + "title": "Не используется", + "tips": "Это значение может неточно отражать свободное место, доступное Frigate, если на вашем диске есть другие файлы помимо записей Frigate. Frigate не отслеживает использование хранилища за пределами своих записей." + } + }, + "shm": { + "title": "Выделение разделяемой памяти", + "warning": "Текущеее значение разделяемой памяти в {{total}}MB слишком мало. Увеличьте его хотя бы до {{min_shm}}MB." + } + }, + "cameras": { + "title": "Камеры", + "overview": "Обзор", + "info": { + "cameraProbeInfo": "Информация о проверке камеры {{camera}}", + "streamDataFromFFPROBE": "Данные о потоке получены от ffprobe.", + "fetching": "Получение данных камеры", + "stream": "Поток {{idx}}", + "video": "Видео:", + "codec": "Кодек:", + "resolution": "Разрешение:", + "fps": "FPS:", + "unknown": "Неизвестно", + "audio": "Аудио:", + "error": "Ошибка: {{error}}", + "tips": { + "title": "Информация о тестировании камеры" + }, + "aspectRatio": "соотношение сторон" + }, + "framesAndDetections": "Кадры/детекции", + "label": { + "ffmpeg": "FFmpeg", + "camera": "камера", + "capture": "захват", + "skipped": "пропущено", + "detect": "детекция", + "cameraDetectionsPerSecond": "{{camName}} обнаружений в секунду", + "cameraSkippedDetectionsPerSecond": "{{camName}} пропущенных обнаружений в секунду", + "cameraFramesPerSecond": "{{camName}} кадров в секунду", + "overallFramesPerSecond": "общее количество кадров в секунду", + "overallDetectionsPerSecond": "общее количество обнаружений в секунду", + "overallSkippedDetectionsPerSecond": "общее количество пропущенных обнаружений в секунду", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} захват", + "cameraDetect": "{{camName}} обнаружения" + }, + "toast": { + "success": { + "copyToClipboard": "Данные тестирования скопированы в буфер обмена." + }, + "error": { + "unableToProbeCamera": "Не удалось протестировать камеру: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Обновлено: ", + "stats": { + "ffmpegHighCpuUsage": "Камера {{camera}} использует чрезмерно много ресурсов CPU в FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "Камера {{camera}} использует слишком много ресурсов CPU для детекции ({{detectAvg}}%)", + "healthy": "Система в порядке", + "reindexingEmbeddings": "Переиндексация эмбеддингов (выполнено {{processed}} %)", + "cameraIsOffline": "{{camera}} отключена", + "detectIsVerySlow": "{{detect}} идёт очень медленно ({{speed}} мс)", + "detectIsSlow": "{{detect}} идёт медленно ({{speed}} мс)", + "shmTooLow": "Объем выделенной памяти /dev/shm ({{total}} МБ) должен быть увеличен как минимум до {{min}} МБ." + }, + "enrichments": { + "title": "Обогащение данных", + "infPerSecond": "Выводов в секунду", + "embeddings": { + "image_embedding_speed": "Скорость векторизации изображений", + "plate_recognition_speed": "Скорость распознавания номеров", + "text_embedding_speed": "Скорость векторизации текста", + "face_embedding_speed": "Скорость векторизации лиц", + "face_recognition_speed": "Скорость распознавания лиц", + "text_embedding": "Векторизация текста", + "yolov9_plate_detection_speed": "Скорость обнаружения номеров YOLOv9", + "yolov9_plate_detection": "Обнаружение номеров YOLOv9", + "face_recognition": "Распознавание лиц", + "plate_recognition": "Распознавание номеров", + "image_embedding": "Векторизация изображений" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/audio.json b/sam2-cpu/frigate-dev/web/public/locales/sk/audio.json new file mode 100644 index 0000000..5612935 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "Reč", + "babbling": "Bľabotanie", + "yell": "Krik", + "bellow": "Pod", + "whispering": "Šepkanie", + "whoop": "Výskanie", + "laughter": "Smiech", + "crying": "Plač", + "sigh": "Vzdych", + "singing": "Spev", + "snicker": "Chichotanie", + "choir": "Zbor", + "yodeling": "Jódlovanie", + "chant": "Štrajkovanie", + "mantra": "Mantra", + "child_singing": "Detský spev", + "synthetic_singing": "Syntetický spev", + "rapping": "Repovanie", + "humming": "Bzučanie", + "groan": "Stenanie", + "grunt": "Zabručanie", + "whistling": "Pískot", + "breathing": "Dych", + "wheeze": "Sipenie", + "snoring": "Chrápanie", + "gasp": "Zalapanie po dychu", + "snort": "Funenie", + "cough": "Kašel", + "throat_clearing": "Odkašlanie", + "sneeze": "Kýchnutie", + "sniff": "Čuchanie", + "run": "Beh", + "shuffle": "Miešenie", + "footsteps": "Kroky", + "chewing": "Žuvanie", + "biting": "Hrizenie", + "bicycle": "Bicykel", + "car": "Auto", + "motorcycle": "Motocykel", + "bus": "Autobus", + "train": "Vlak", + "boat": "Čln", + "bird": "Vták", + "cat": "Mačka", + "dog": "Pes", + "horse": "Kôň", + "sheep": "Ovce", + "camera": "Kamera", + "pant": "Oddychávanie", + "gargling": "Grganie", + "stomach_rumble": "Škvŕkanie v žalúdku", + "burping": "Grganie", + "skateboard": "Skateboard", + "hiccup": "Škytavka", + "fart": "Prd", + "hands": "Ruky", + "finger_snapping": "Lusknutie prstom", + "clapping": "Tlieskanie", + "heartbeat": "Tlkot srdca", + "heart_murmur": "Srdcový šelest", + "cheering": "Fandenie", + "applause": "Potlesk", + "chatter": "Chatárčenie", + "crowd": "Dav", + "children_playing": "Deti hrajúce sa", + "animal": "Zviera", + "pets": "Domáce zvieratá", + "bark": "Kôra", + "yip": "Áno", + "howl": "Zavýjať", + "bow_wow": "Hlasitého protestu", + "growling": "Vrčanie", + "whimper_dog": "Psie kňučanie", + "purr": "Pradenie", + "meow": "Mňau", + "hiss": "Syčanie", + "caterwaul": "Kričať", + "livestock": "Hospodárske zvieratá", + "clip_clop": "Klepanie kopyt", + "neigh": "Eržanie", + "door": "Dvere", + "cattle": "Hovädzí dobytok", + "moo": "Búčanie", + "cowbell": "Kravský zvonec", + "mouse": "Myška", + "pig": "Prasa", + "oink": "Chrčanie", + "keyboard": "Klávesnica", + "goat": "Koza", + "bleat": "nariekať", + "fowl": "Sliepky", + "chicken": "Slepica", + "sink": "Umývadlo", + "cluck": "Kvákanie", + "cock_a_doodle_doo": "Kykyryký", + "blender": "Mixér", + "turkey": "Morka", + "gobble": "Hltať", + "clock": "Hodiny", + "duck": "Kačica", + "wild_animals": "Divoké zvieratá", + "toothbrush": "Zubná kefka", + "roaring_cats": "Revúce mačky", + "roar": "Revať", + "vehicle": "Vozidlo", + "quack": "Quack", + "scissors": "Nožnice", + "goose": "Hus", + "honk": "Truba", + "hair_dryer": "Sušič vlasov", + "chirp": "Cvrlikanie", + "squawk": "Škriekanie", + "pigeon": "Holub", + "coo": "Vrkanie", + "crow": "Vrana", + "caw": "Krákanie", + "owl": "Sova", + "hoot": "Húkanie", + "flapping_wings": "Mávanie krídel", + "dogs": "Psi", + "rats": "Potkany", + "patter": "Plácanie", + "insect": "Hmyz", + "cricket": "Cvrček", + "mosquito": "Komár", + "fly": "Mucha", + "buzz": "Bzučanie", + "frog": "Žaba", + "croak": "Kvákanie žaby", + "snake": "Had", + "rattle": "Hrkanie", + "whale_vocalization": "Veľrybí spev", + "music": "Hudba", + "musical_instrument": "Hudobný nástroj", + "plucked_string_instrument": "Drnkací strunový nástroj", + "guitar": "Gitara", + "electric_guitar": "Elektrická gitara", + "bass_guitar": "Basová gitara", + "acoustic_guitar": "Akustická gitara", + "steel_guitar": "Oceľová gitara", + "tapping": "Ťukanie", + "strum": "Brnkanie", + "banjo": "Banjo", + "sitar": "Sitár", + "mandolin": "Mandolína", + "zither": "Citera", + "ukulele": "Ukulele", + "piano": "Klavír", + "electric_piano": "Elektrický klavír", + "organ": "Organ", + "electronic_organ": "Elektronické organ", + "gong": "Gong", + "tubular_bells": "Trubicové zvony", + "mallet_percussion": "Palička perkusie", + "marimba": "Marimba", + "orchestra": "Orchester", + "brass_instrument": "Žesťový nástroj", + "french_horn": "Lesný roh", + "trumpet": "Rúrka", + "trombone": "Trombón", + "bowed_string_instrument": "Sláčikový nástroj", + "string_section": "Sláčiková sekcia", + "violin": "Husle", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabas", + "wind_instrument": "Dychový nástroj", + "flute": "Flauta", + "saxophone": "Saxofón", + "clarinet": "Klarinet", + "harp": "Harfa", + "bell": "Zvon", + "church_bell": "Kostolný zvon", + "jingle_bell": "Rolnička", + "bicycle_bell": "Cyklistický zvonček", + "tuning_fork": "Ladička", + "chime": "Zvonenie", + "wind_chime": "Zvonkohra", + "harmonica": "Harmonika", + "accordion": "Akordeón", + "bagpipes": "Dudy", + "didgeridoo": "Didžeridu", + "theremin": "Theremin", + "singing_bowl": "Singing Bowl", + "scratching": "Škrabanie", + "pop_music": "Popová hudba", + "hip_hop_music": "Hip-hopová muzika", + "beatboxing": "Beatboxing", + "rock_music": "Rocková muzika", + "heavy_metal": "Heavy metal", + "punk_rock": "Punk Rock", + "grunge": "Grunge", + "progressive_rock": "Progressive Rock", + "rock_and_roll": "Rock and Roll", + "psychedelic_rock": "Psychadelický Rock", + "rhythm_and_blues": "Rythm & Blues", + "soul_music": "Soulová hudba", + "reggae": "Reggae", + "country": "Krajina", + "swing_music": "Swingová hudba", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folková hudba", + "middle_eastern_music": "Stredo-východná hudba", + "jazz": "Jazz", + "disco": "Disco", + "classical_music": "Klasická hudba", + "opera": "Opera", + "electronic_music": "Elektronická hudba", + "house_music": "House hudba", + "techno": "Techno", + "dubstep": "Dubstep", + "drum_and_bass": "Drum and Bass", + "electronica": "Elektronická hudba", + "electronic_dance_music": "Elektronická tanečná hudba", + "ambient_music": "Ambientná hudba", + "trance_music": "Trance hudba", + "music_of_latin_america": "Latinsko-americká hudba", + "salsa_music": "Salsa Music", + "flamenco": "Flamengo", + "blues": "Blues", + "music_for_children": "Hudba pre deti", + "new-age_music": "Novodobá hudba", + "vocal_music": "Vokálna hudba", + "a_capella": "A Capella", + "music_of_africa": "Africká hudba", + "afrobeat": "Afrobeat", + "christian_music": "Kresťanská hudba", + "gospel_music": "Gospelová hudba", + "music_of_asia": "Ázijská hudba", + "carnatic_music": "Karnatická hudba", + "music_of_bollywood": "Hudba z Bollywoodu", + "ska": "SKA", + "traditional_music": "Tradičná hudba", + "independent_music": "Nezávislá hudba", + "song": "Pieseň", + "background_music": "Hudba na pozadí", + "theme_music": "Tematická hudba", + "jingle": "Jingle", + "soundtrack_music": "Soundtracková hudba", + "lullaby": "Uspávanka", + "video_game_music": "Herná hudba", + "shuffling_cards": "Miešanie kariet", + "hammond_organ": "Hammondovy organ", + "synthesizer": "Syntezátor", + "sampler": "Sampler", + "harpsichord": "Cembalo", + "percussion": "Perkusia", + "drum_kit": "Bubny", + "drum_machine": "Bicí automat", + "drum": "Bubon", + "snare_drum": "Malý bubon", + "rimshot": "Rana na obruč", + "drum_roll": "Vírenie", + "bass_drum": "Basový bubon", + "timpani": "Tympány", + "tabla": "Tabla", + "cymbal": "Činel", + "hi_hat": "Hi-hat", + "wood_block": "Drevený blok", + "tambourine": "Tamburína", + "maraca": "Maraka", + "glockenspiel": "Zvonkohra", + "vibraphone": "Vibrafón", + "steelpan": "Ocelový bubon", + "christmas_music": "Vianočná hudba", + "dance_music": "Tanečná hudba", + "wedding_music": "Svadobná hudba", + "happy_music": "Šťastná hudba", + "sad_music": "Smutná hudba", + "tender_music": "Nežná hudba", + "exciting_music": "Vzrušujúca hudba", + "angry_music": "Naštvaná hudba", + "scary_music": "Strašidelná hudba", + "wind": "Vietor", + "rustling_leaves": "Šuštiace Listy", + "wind_noise": "Hluk Vetra", + "thunderstorm": "Búrka", + "thunder": "Hrom", + "water": "Voda", + "rain": "Dážď", + "raindrop": "Dažďové kvapky", + "rain_on_surface": "Dážď na povrchu", + "stream": "Prúd", + "waterfall": "Vodopád", + "ocean": "Oceán", + "waves": "Vlny", + "steam": "Para", + "gurgling": "Grganie", + "fire": "Oheň", + "crackle": "Praskať", + "sailboat": "Plachtenie", + "rowboat": "Veslica", + "motorboat": "Motorový čln", + "ship": "Loď", + "motor_vehicle": "Motorové vozidlo", + "toot": "Trúbenie", + "car_alarm": "Autoalarm", + "power_windows": "Elektrické okná", + "skidding": "Šmykom", + "tire_squeal": "Pískanie pneumatík", + "car_passing_by": "Prechádzajúce auto", + "race_car": "Závodné auto", + "truck": "Kamión", + "air_brake": "Vzduchová brzda", + "air_horn": "Vzduchový klaksón", + "reversing_beeps": "Pípanie pri cúvaní", + "ice_cream_truck": "Auto so zmrzlinou", + "emergency_vehicle": "Pohotovostné vozidlo", + "police_car": "Policajné auto", + "ambulance": "Ambulancia", + "fire_engine": "Hasiči", + "traffic_noise": "Hluk z dopravy", + "rail_transport": "Železničná preprava", + "train_whistle": "Húkanie vlaku", + "train_horn": "Rúrenie vlaku", + "railroad_car": "Železničný vagón", + "train_wheels_squealing": "Škrípanie kolies vlaku", + "subway": "Metro", + "aircraft": "Lietadlo", + "aircraft_engine": "Motor lietadla", + "jet_engine": "Tryskový motor", + "propeller": "Vrtuľa", + "helicopter": "Helikoptéra", + "fixed-wing_aircraft": "Lietadlo s pevnými krídlami", + "engine": "Motor", + "light_engine": "Ľahký motor", + "dental_drill's_drill": "Zubná vŕtačka", + "lawn_mower": "Kosačka", + "chainsaw": "Motorová píla", + "medium_engine": "Stredný motor", + "heavy_engine": "Ťažký motor", + "engine_knocking": "Klepanie motora", + "engine_starting": "Štartovanie motora", + "idling": "Bežiaci motor", + "accelerating": "Pridávanie plynu", + "doorbell": "Zvonček", + "ding-dong": "Cink", + "sliding_door": "Posuvné dvere", + "slam": "Búchnutie", + "knock": "Klepanie", + "tap": "Poklepanie", + "squeak": "Škrípanie", + "cupboard_open_or_close": "Otváranie alebo zatváranie skrine", + "drawer_open_or_close": "Otváranie alebo zatváranie šuplíka", + "dishes": "Riad", + "cutlery": "Príbory", + "chopping": "Krájanie", + "frying": "Vyprážanie", + "microwave_oven": "Mikrovnka", + "water_tap": "Vodovodný kohútik", + "bathtub": "Vaňa", + "toilet_flush": "Splachovanie toalety", + "electric_toothbrush": "Elektrická zubná kefka", + "vacuum_cleaner": "Vysávač", + "zipper": "Zips", + "keys_jangling": "Klepanie kľúčov", + "coin": "Mince", + "electric_shaver": "Elektrický holiaci strojček", + "typing": "Písanie", + "typewriter": "Písací stroj", + "computer_keyboard": "Počítačový kľúč", + "writing": "Písanie", + "alarm": "Alarm", + "telephone": "Telefón", + "telephone_bell_ringing": "Zvonenie telefónu", + "ringtone": "Vyzváňací tón", + "telephone_dialing": "Telefonické vytáčanie", + "dial_tone": "Vytáčací tón", + "busy_signal": "Zaneprázdnený signál", + "alarm_clock": "Budík", + "siren": "Siréna", + "civil_defense_siren": "Siréna civilnej obrany", + "buzzer": "Bzučiak", + "smoke_detector": "Detektor dymu", + "fire_alarm": "Požiarny Alarm", + "foghorn": "Hmlovka", + "whistle": "Zapískať", + "steam_whistle": "Parná píšťalka", + "mechanisms": "Mechanizmy", + "ratchet": "Račňa", + "tick": "Ťik", + "tick-tock": "Tik-tok", + "gears": "Ozubené kolesá", + "pulleys": "Kladky", + "sewing_machine": "Šijací stroj", + "mechanical_fan": "Mechanický ventilátor", + "air_conditioning": "Klimatizácia", + "cash_register": "Registračná pokladňa", + "printer": "Tlačiareň", + "single-lens_reflex_camera": "Jednooká zrkadlovka", + "tools": "Nástroje", + "hammer": "Kladivo", + "jackhammer": "Zbíjačka", + "sawing": "Pílenie", + "filing": "Podanie", + "sanding": "Brúsenie", + "power_tool": "Elektrické náradie", + "drill": "Vŕtačka", + "explosion": "Explózia", + "gunshot": "Výstrel", + "machine_gun": "Guľomet", + "fusillade": "Streľba", + "artillery_fire": "Delostrelecká paľba", + "cap_gun": "Kapslíková pištoľ", + "fireworks": "Ohňostroj", + "firecracker": "Petarda", + "burst": "Prasknutie", + "eruption": "Erupcia", + "boom": "Bum", + "wood": "Drevo", + "chop": "Nasekať", + "splinter": "Trieska", + "crack": "Prasknutie", + "glass": "Sklo", + "chink": "Cinknutie", + "shatter": "Rozbiť", + "silence": "Ticho", + "sound_effect": "Zvukový efekt", + "environmental_noise": "Okolitý hluk", + "static": "Statické", + "white_noise": "Biely šum", + "pink_noise": "Ružový šum", + "television": "Televízia", + "radio": "Rádio", + "field_recording": "Záznam v teréne", + "scream": "Kričať", + "sodeling": "Sodeling", + "chird": "Chord", + "change_ringing": "Zmeniť zvonenie", + "shofar": "Šofar", + "liquid": "Kvapalina", + "splash": "Šplechnutie", + "slosh": "Slosh", + "squish": "Vytlačiť", + "drip": "Kvapkať", + "pour": "Nalej", + "trickle": "Pokvapkať", + "gush": "Striekať", + "fill": "Vyplňte", + "spray": "Striekajte", + "pump": "Pumpa", + "stir": "Miešajte", + "boiling": "Varenie", + "sonar": "Sonar", + "arrow": "Šípka", + "whoosh": "Ktoosh", + "thump": "Palec", + "thunk": "Thunk", + "electronic_tuner": "Elektronický tuner", + "effects_unit": "Efektuje jednotky", + "chorus_effect": "Zborový efekt", + "basketball_bounce": "Odrážanie basketbalovej lopty", + "bang": "Bang", + "slap": "Buchnutie", + "whack": "Odpáliť", + "smash": "Rozbiť", + "breaking": "Prelomenie", + "bouncing": "Odskakovanie", + "whip": "Bič", + "flap": "Klapka", + "scratch": "Poškriabanie", + "scrape": "Škrabať", + "rub": "Potrieť", + "roll": "Rolovať", + "crushing": "Rozdrvovanie", + "crumpling": "Mačkanie", + "tearing": "Trhanie", + "beep": "Pípnutie", + "ping": "Ping", + "ding": "Ding", + "clang": "Zvonenie", + "squeal": "Kňučať", + "creak": "Vŕzganie", + "rustle": "Šuchot", + "whir": "Vrčanie", + "clatter": "Cvakať", + "sizzle": "Syčať", + "clicking": "Klikanie", + "clickety_clack": "Klikanie kľak", + "rumble": "Rachot", + "plop": "Prasknutie", + "hum": "Hmkanie", + "zing": "Zing", + "boing": "Boing", + "crunch": "Chrumnutie", + "sine_wave": "Sínusoida", + "harmonic": "Harmonický", + "chirp_tone": "Cvrlikací tón", + "pulse": "Pulz", + "inside": "Vnútri", + "outside": "Vonku", + "reverberation": "Dozvuk", + "echo": "Ozvena", + "noise": "Zvuk", + "mains_hum": "Hlavné Hum", + "distortion": "Skreslenie", + "sidetone": "Vedľajší tón", + "cacophony": "Kakofónia", + "throbbing": "Pulzujúci", + "vibration": "Vibrácia" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/common.json b/sam2-cpu/frigate-dev/web/public/locales/sk/common.json new file mode 100644 index 0000000..199493f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/common.json @@ -0,0 +1,304 @@ +{ + "time": { + "untilForTime": "Do{{time}}", + "untilForRestart": "Do reštartu Frigate.", + "untilRestart": "Do reštartu", + "ago": "{{timeAgo}} pred časom", + "justNow": "Práve teraz", + "today": "Dnes", + "yesterday": "Včera", + "last7": "Posledných 7 dní", + "last14": "Posledných 14 dní", + "last30": "Posledných 30 dní", + "thisWeek": "Tento týždeň", + "lastWeek": "Minulý týždeň", + "thisMonth": "Tento mesiac", + "lastMonth": "Minulý mesiac", + "5minutes": "5 minút", + "10minutes": "10 minút", + "30minutes": "30 minút", + "1hour": "1 hodina", + "12hours": "12 hodín", + "24hours": "24 hodín", + "am": "ráno", + "yr": "{{time}}r", + "pm": "popoludní", + "year_one": "{{time}}rok", + "year_few": "{{time}}rokov", + "year_other": "{{time}}rokov", + "mo": "{{time}}mes", + "month_one": "{{time}}mesiac", + "month_few": "{{time}} mesiace", + "month_other": "{{time}} mesiaca", + "d": "{{time}}d", + "day_one": "{{time}}deň", + "day_few": "{{time}}dni", + "day_other": "{{time}}dni", + "h": "{{time}}h", + "hour_one": "{{time}}hodina", + "hour_few": "{{time}}hodiny", + "hour_other": "{{time}}hodin", + "m": "{{time}} min", + "s": "{{time}}s", + "minute_one": "{{time}}minuta", + "minute_few": "{{time}}minuty", + "minute_other": "{{time}}minut", + "second_one": "{{time}}sekunda", + "second_few": "{{time}}sekundy", + "second_other": "{{time}}sekund", + "formattedTimestamp": { + "12hour": "Deň MMM, h:mm:ss aaa", + "24hour": "Deň MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "MMM d, yyyy", + "24hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "inProgress": "Spracováva sa", + "invalidStartTime": "Neplatný čas štartu", + "invalidEndTime": "Neplatný čas ukončenia" + }, + "selectItem": "Vyberte {{item}}", + "unit": { + "speed": { + "mph": "mph", + "kph": "Km/h" + }, + "length": { + "feet": "nohy", + "meters": "metrov" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kb/hour", + "mbph": "MB/hour", + "gbph": "GB/hour" + } + }, + "readTheDocumentation": "Prečítajte si dokumentáciu", + "label": { + "back": "Choď späť", + "hide": "Skryť {{item}}", + "show": "Zobraziť {{item}}", + "ID": "ID", + "none": "None", + "all": "Všetko" + }, + "button": { + "apply": "Použiť", + "reset": "Resetovať", + "done": "Hotovo", + "enabled": "Povolené", + "enable": "Povoliť", + "disabled": "Zakázané", + "disable": "Zakázať", + "save": "Uložiť", + "saving": "Ukladá sa…", + "cancel": "Zrušiť", + "close": "Zavrieť", + "copy": "Kopírovať", + "back": "Späť", + "history": "História", + "fullscreen": "Celá obrazovka", + "exitFullscreen": "Opustiť režim celú obrazovku", + "pictureInPicture": "Obraz v obraze", + "twoWayTalk": "Obojsmerná komunikácia", + "cameraAudio": "Zvuk kamery", + "on": "ON", + "off": "OFF", + "edit": "Upraviť", + "copyCoordinates": "Kopírovať súradnice", + "delete": "Odstrániť", + "yes": "Ano", + "no": "Nie", + "download": "Stiahnuť", + "info": "Informacie", + "suspended": "Pozastavené", + "export": "Exportovať", + "deleteNow": "Odstrániť teraz", + "next": "Ďalej", + "unsuspended": "Zrušte pozastavenie", + "play": "Hrať", + "unselect": "Zrušte výber", + "continue": "Pokračovať" + }, + "menu": { + "system": "Systém", + "systemMetrics": "Systémové metriky", + "configuration": "Konfigurácia", + "systemLogs": "Systémový záznam", + "settings": "Nastavenia", + "configurationEditor": "Editor konfigurácie", + "languages": "Jazyky", + "language": { + "en": "English (Angličtina)", + "es": "Español (Španielčina)", + "zhCN": "简体中文 (Zjednodušená čínština)", + "hi": "हिन्दी (Hindčina)", + "fr": "Français (Francúzština)", + "ar": "العربية (Arabčina)", + "pt": "Portugalčina (Portugalčina)", + "ptBR": "Português brasileiro (Brazílska Portugalčina)", + "ru": "Русский (Ruština)", + "de": "nemčina (Nemčina)", + "ja": "日本語 (Japončina)", + "tr": "Türkçe (Turečtina)", + "it": "Italiano (Taliančina)", + "nl": "Nederlands (Holandčina)", + "sv": "Svenska (Švédčina)", + "cs": "Czech (Čeština)", + "nb": "Norsk Bokmål (Norský Bokmål)", + "ko": "한국어 (Korejština)", + "vi": "Tiếng Việt (Vietnamština)", + "fa": "فارسی (Perština)", + "pl": "Polski (Polština)", + "uk": "Українська (Ukrainština)", + "he": "עברית (Hebrejština)", + "el": "Ελληνικά (Gréčtina)", + "ro": "Română (Rumunčina)", + "hu": "Magyar (Maďarština)", + "fi": "Suomi (Fínčina)", + "da": "Dansk (Dánština)", + "sk": "Slovenčina (Slovenčina)", + "yue": "粵語 (Kantónčina)", + "th": "ไทย (Thajčina)", + "ca": "Català (Katalánčina)", + "sr": "Српски (Serbsky)", + "sl": "Slovinština (Slovinsko)", + "lt": "Lietuvių (Lithuanian)", + "bg": "Български (Bulgarian)", + "gl": "Galego (Galician)", + "id": "Bahasa Indonesia (Indonesian)", + "ur": "اردو (Urdu)", + "withSystem": { + "label": "Použiť systémové nastavenia pre jazyk" + } + }, + "restart": "Reštartovať Frigate", + "live": { + "title": "Naživo", + "allCameras": "Všetky kamery", + "cameras": { + "title": "Kamery", + "count_one": "{{count}}kamera", + "count_few": "{{count}}kamery", + "count_other": "{{count}}kamier" + } + }, + "export": "Exportovať", + "uiPlayground": "UI ihrisko", + "faceLibrary": "Knižnica Tvárov", + "user": { + "title": "Užívateľ", + "account": "Účet", + "current": "Aktuálny používateľ: {{user}}", + "anonymous": "anonymný", + "logout": "Odhlásiť", + "setPassword": "Nastaviť heslo" + }, + "appearance": "Vzhľad", + "darkMode": { + "label": "Tmavý režim", + "light": "Svetlý", + "dark": "Tma", + "withSystem": { + "label": "Použiť systémové nastavenia pre svetlý a tmavý režim" + } + }, + "withSystem": "Systém", + "theme": { + "label": "Téma", + "blue": "Modrá", + "green": "Zelená", + "nord": "Polárna", + "red": "Červená", + "highcontrast": "Vysoký kontrast", + "default": "Predvolené" + }, + "help": "Pomocník", + "documentation": { + "title": "Dokumentácia", + "label": "Dokumentácia Frigate" + }, + "review": "Recenzia", + "explore": "Preskúmať", + "classification": "Klasifikácia" + }, + "toast": { + "copyUrlToClipboard": "Adresa URL bola skopírovaná do schránky.", + "save": { + "title": "Uložiť", + "error": { + "title": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}", + "noMessage": "Chyba pri ukladaní zmien konfigurácie" + } + } + }, + "role": { + "title": "Rola", + "admin": "Správca", + "viewer": "Divák", + "desc": "Správcovia majú plný prístup ku všetkým funkciám v užívateľskom rozhraní Frigate. Diváci sú obmedzení na sledovanie kamier, položiek prehľadu a historických záznamov v UI." + }, + "pagination": { + "label": "stránkovanie", + "previous": { + "title": "Predchádzajúci", + "label": "Ísť na predchádzajúcu stranu" + }, + "next": { + "title": "Ďalšia", + "label": "Ísť na ďalšiu stranu" + }, + "more": "Viac strán" + }, + "accessDenied": { + "documentTitle": "Prístup odmietnutý - Frigate", + "title": "Prístup odmietnutý", + "desc": "Nemáte oprávnenie zobraziť túto stránku." + }, + "notFound": { + "documentTitle": "Nenájdené - Frigate", + "title": "404", + "desc": "Stránka nenájdená" + }, + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} a {{1}}", + "many": "{{items}}, a {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Voliteľné", + "internalID": "Interné ID Frigate používa v konfigurácii a databáze" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/auth.json new file mode 100644 index 0000000..5d44c93 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Používateľské meno", + "password": "Heslo", + "login": "Prihlásenie", + "errors": { + "usernameRequired": "Vyžaduje sa používateľské meno", + "passwordRequired": "Heslo je povinné", + "rateLimit": "Prekročený limit. Skúste to znova neskôr.", + "loginFailed": "Prihlásenie zlyhalo", + "unknownError": "Neznáma chyba. Skontrolujte protokoly.", + "webUnknownError": "Neznáma chyba. Skontrolujte protokoly konzoly." + }, + "firstTimeLogin": "Snažíte sa prihlásiť prvýkrát? Prihlasovacie údaje sú vytlačené v protokoloch Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/camera.json new file mode 100644 index 0000000..e2245bd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Skupiny Kamier", + "add": "Pridať skupinu kamier", + "edit": "Upraviť skupinu kamier", + "delete": { + "label": "Odstrániť skupinu kamier", + "confirm": { + "title": "Potvrďte Odstránenie", + "desc": "Ste si istý že chcete vymazať skupinu kamier {{name}}?" + } + }, + "name": { + "label": "Meno", + "placeholder": "Zadajte meno…", + "errorMessage": { + "mustLeastCharacters": "Názov skupiny kamier musí mať aspoň 2 znaky.", + "exists": "Názov skupiny kamier už existuje.", + "nameMustNotPeriod": "Názov skupiny kamier nesmie obsahovať bodku.", + "invalid": "Neplatný názov skupiny kamier." + } + }, + "cameras": { + "label": "Kamery", + "desc": "Vyberte kamery pre túto skupinu." + }, + "icon": "Ikona", + "success": "Skupina kamier ({{name}}) bola uložená.", + "camera": { + "setting": { + "label": "Nastavenia streamovania z kamery", + "title": "Nastavenia streamovania {{cameraName}}", + "desc": "Zmeňte možnosti živého vysielania pre ovládací panel tejto skupiny kamier. Tieto nastavenia sú špecifické pre zariadenie/prehliadač.", + "audioIsAvailable": "Pre tento stream je k dispozícii zvuk", + "audioIsUnavailable": "Zvuk nie je pre tento stream k dispozícii", + "audio": { + "tips": { + "title": "Zvuk musí byť vyvedený z vašej kamery a nakonfigurovaný v go2rtc pre tento stream.", + "document": "Prečítajte si dokumentáciu " + } + }, + "stream": "Prúd", + "placeholder": "Vyberte prúd", + "streamMethod": { + "label": "Metóda streamovania", + "placeholder": "Vyberte metódu vysielania", + "method": { + "noStreaming": { + "label": "Žiadny stream", + "desc": "Snímky z kamery sa budú aktualizovať iba raz za minútu a nebude prebiehať žiadne živé vysielanie." + }, + "smartStreaming": { + "label": "Inteligentné streamovanie (odporúčané)", + "desc": "Inteligentné streamovanie aktualizuje obraz z kamery raz za minútu, keď sa neprejavuje žiadna detekovateľná aktivita, aby sa šetrila šírka pásma a zdroje. Keď sa zistí aktivita, obraz sa plynule prepne na živý stream." + }, + "continuousStreaming": { + "label": "Nepretržité streamovanie", + "desc": { + "title": "Obraz z kamery bude vždy vysielaný naživo, keď bude viditeľný na palubnej doske, aj keď nebude detekovaná žiadna aktivita.", + "warning": "Nepretržité streamovanie môže spôsobiť vysoké využitie šírky pásma a problémy s výkonom. Používajte opatrne." + } + } + } + }, + "compatibilityMode": { + "label": "Režim kompatibility", + "desc": "Túto možnosť povoľte iba v prípade, že živý prenos z vašej kamery zobrazuje farebné artefakty a na pravej strane obrazu sa nachádza diagonálna čiara." + } + }, + "birdseye": "Vtáčie oko" + } + }, + "debug": { + "options": { + "label": "Nastavenia", + "title": "Možnosti", + "showOptions": "Zobraziť možnosti", + "hideOptions": "Skryť možnosti" + }, + "boundingBox": "Hranica", + "timestamp": "Časová pečiatka", + "zones": "Zóny", + "mask": "Maska", + "motion": "Pohyb", + "regions": "Kraje" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/dialog.json new file mode 100644 index 0000000..6904bc0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/dialog.json @@ -0,0 +1,124 @@ +{ + "restart": { + "title": "Ste si istý, že chcete reštartovať Frigate ?", + "button": "Reštart", + "restarting": { + "title": "Frigate sa reštartuje", + "content": "Táto stránka bude obnovená o {{countdown}} sekúnd.", + "button": "Vynútiť opätovné načítanie teraz" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Odoslať do Frigate+", + "desc": "Objekty na miestach, ktorým sa chcete vyhnúť, nie sú falošne pozitívne. Ich odoslanie ako falošne pozitívnych výsledkov spôsobí zmätok v modeli." + }, + "review": { + "question": { + "label": "Potvrďte tento štítok pre Frigate Plus", + "ask_a": "Je tento objekt typom {{label}}?", + "ask_an": "Tento objekt je {{label}}?", + "ask_full": "Je tento objekt typom {{untranslatedLabel}}{{translatedLabel}}?" + }, + "state": { + "submitted": "Predložené" + } + } + }, + "video": { + "viewInHistory": "Zobraziť v histórii" + } + }, + "export": { + "time": { + "fromTimeline": "Vyberte z časovej osi", + "custom": "Vlastné", + "start": { + "title": "Čas začiatku", + "label": "Vyberte Čas začiatku" + }, + "end": { + "title": "Čas ukončenia", + "label": "Vybrat čas ukončenia" + }, + "lastHour_one": "Minulu hodinu", + "lastHour_few": "Minule{{count}}hodiny", + "lastHour_other": "Minulych{{count}}hodin" + }, + "name": { + "placeholder": "Pomenujte Export" + }, + "select": "Vybrat", + "export": "Exportovať", + "selectOrExport": "Vybrať pre Export", + "toast": { + "success": "Export bol úspešne spustený. Súbor si pozrite na stránke exportov.", + "error": { + "failed": "Chyba spustenia exportu: {{error}}", + "endTimeMustAfterStartTime": "Čas konca musí byť po čase začiatku", + "noVaildTimeSelected": "Nie je vybrané žiadne platné časové obdobie" + }, + "view": "Zobraziť" + }, + "fromTimeline": { + "saveExport": "Uložiť Export", + "previewExport": "Export ukážky" + } + }, + "streaming": { + "label": "Stream", + "restreaming": { + "disabled": "Opätovné streamovanie nie je pre túto kameru povolené.", + "desc": { + "title": "Pre ďalšie možnosti živého náhľadu a zvuku pre túto kameru nastavte go2rtc.", + "readTheDocumentation": "Prečítajte si dokumentáciu" + } + }, + "showStats": { + "label": "Zobraziť štatistiky streamu", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia" + }, + "search": { + "saveSearch": { + "label": "Uložiť vyhľadávanie", + "desc": "Zadajte názov pre toto uložené vyhľadávanie.", + "placeholder": "Zadajte názov pre vyhľadávanie", + "overwrite": "{{searchName}} už existuje. Uložením sa prepíše existujúca hodnota.", + "success": "Hľadanie ({{searchName}}) bolo uložené.", + "button": { + "save": { + "label": "Uložte toto vyhľadávanie" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potvrďte Odstrániť", + "desc": { + "selected": "Naozaj chcete odstrániť všetky nahrané videá spojené s touto položkou recenzie?

    Podržte kláves Shift, aby ste v budúcnosti toto dialógové okno obišli." + }, + "toast": { + "success": "Videozáznam spojený s vybranými položkami recenzie bol úspešne odstránený.", + "error": "Nepodarilo sa odstrániť: {{error}}" + } + }, + "button": { + "export": "Exportovať", + "markAsReviewed": "Označiť ako skontrolované", + "deleteNow": "Odstrániť teraz", + "markAsUnreviewed": "Označiť ako neskontrolované" + } + }, + "imagePicker": { + "selectImage": "Výber miniatúry sledovaného objektu", + "search": { + "placeholder": "Hľadať podľa štítku alebo podštítku..." + }, + "noImages": "Pre tuto kameru sa nenašli žiadne miniatúry", + "unknownLabel": "Uložený obrázok spúšťača" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/filter.json new file mode 100644 index 0000000..83305f9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filter", + "labels": { + "label": "Označenia", + "all": { + "title": "Všetky popisky", + "short": "Štítky" + }, + "count_one": "{{count}} Štítok", + "count_other": "{{count}} Štítkov" + }, + "zones": { + "label": "Zóny", + "all": { + "title": "Všetky zóny", + "short": "Zóny" + } + }, + "dates": { + "selectPreset": "Vyberte predvoľbu…", + "all": { + "title": "Všetky dátumy", + "short": "Dátumy" + } + }, + "more": "Viac filtrov", + "reset": { + "label": "Obnoviť filtre na predvolené hodnoty" + }, + "timeRange": "Časový rozsah", + "subLabels": { + "label": "Podštítky", + "all": "Všetky vedľajšie štítky" + }, + "score": "Skóre", + "estimatedSpeed": "Odhadovaná rýchlosť ({{unit}})", + "features": { + "label": "Vlastnosti", + "hasSnapshot": "Má snímku", + "hasVideoClip": "Má videoklip", + "submittedToFrigatePlus": { + "label": "Odoslané do Frigate+", + "tips": "Najprv musíte filtrovať sledované objekty, ktoré majú snímku.

    Sledované objekty bez snímky nie je možné odoslať do Frigate+." + } + }, + "sort": { + "label": "Zoradiť", + "dateAsc": "Dátum (Vzostupne)", + "dateDesc": "Dátum (Zostupne)", + "scoreAsc": "Skóre objektu (Vzostupne)", + "scoreDesc": "Skóre objektu (zostupne)", + "speedAsc": "Odhadovaná rýchlosť (vzostupne)", + "speedDesc": "Odhadovaná rýchlosť (zostupne)", + "relevance": "Relevantnosť" + }, + "cameras": { + "label": "Filter kamier", + "all": { + "title": "Všetky kamery", + "short": "Kamery" + } + }, + "classes": { + "label": "Triedy", + "all": { + "title": "Všetky triedy" + }, + "count_one": "Trieda {{count}}", + "count_other": "Triedy {{count}}" + }, + "review": { + "showReviewed": "Zobraziť skontrolované" + }, + "motion": { + "showMotionOnly": "Zobraziť len pohyb" + }, + "explore": { + "settings": { + "title": "Nastavenia", + "defaultView": { + "title": "Predvolené zobrazenie", + "desc": "Ak nie sú vybraté žiadne filtre, zobrazte súhrn naposledy sledovaných objektov pre každý štítok alebo zobrazte nefiltrovanú mriežku.", + "summary": "Zhrnutie", + "unfilteredGrid": "Nefiltrovaná mriežka" + }, + "gridColumns": { + "title": "Stĺpce mriežky", + "desc": "Vyberte počet stĺpcov v mriežkovom zobrazení." + }, + "searchSource": { + "label": "Vyhľadať zdroj", + "desc": "Vyberte, či chcete vyhľadávať v miniatúrach alebo v popisoch sledovaných objektov.", + "options": { + "thumbnailImage": "Obrázok miniatúry", + "description": "Popis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Vyberte dátum, podľa ktorého chcete filtrovať" + } + } + }, + "logSettings": { + "label": "Úroveň denníka filtra", + "filterBySeverity": "Filtrujte protokoly podľa závažnosti", + "loading": { + "title": "Načítava sa", + "desc": "Keď sa panel protokolov posunie nadol, nové protokoly sa automaticky streamujú hneď po ich pridaní." + }, + "disableLogStreaming": "Zakázať streamovanie denníka", + "allLogs": "Všetky denníky" + }, + "trackedObjectDelete": { + "title": "Potvrďte Odstrániť", + "desc": "Odstránením týchto sledovaných objektov ({{objectLength}}) sa odstráni snímka, všetky uložené vnorenia a všetky súvisiace položky životného cyklu objektu. Zaznamenané zábery týchto sledovaných objektov v zobrazení História NEBUDÚ odstránené.

    Naozaj chcete pokračovať?

    Podržte kláves Shift, aby ste v budúcnosti toto dialógové okno obišli.", + "toast": { + "success": "Sledované objekty boli úspešne odstránené.", + "error": "Nepodarilo sa odstrániť sledované objekty: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrujte podľa masky zóny" + }, + "recognizedLicensePlates": { + "title": "Rozpoznané evidenčné čísla vozidiel", + "loadFailed": "Nepodarilo sa načítať rozpoznané evidenčné čísla vozidiel.", + "loading": "Načítavajú sa rozpoznané evidenčné čísla…", + "placeholder": "Zadajte text pre vyhľadávanie evidenčných čísel…", + "noLicensePlatesFound": "Neboli nájdené SPZ.", + "selectPlatesFromList": "Vyberte jeden alebo viacero tanierov zo zoznamu.", + "selectAll": "Vybrať všetko", + "clearAll": "Vymazať všetko" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/icons.json new file mode 100644 index 0000000..dc780fb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Zvoľte ikonu", + "search": { + "placeholder": "Hľadať ikonu…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/input.json new file mode 100644 index 0000000..e13e5ea --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Stiahnuť Video", + "toast": { + "success": "Video pre náhľad sa začalo sťahovanie." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/sk/components/player.json new file mode 100644 index 0000000..fed9c0b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "V danom čase nenájdená žiadna nahrávka", + "noPreviewFound": "Náhľad nenájdený", + "noPreviewFoundFor": "Žiadný náhľad nájdený pre {{cameraName}}", + "submitFrigatePlus": { + "title": "Odoslať tento rám na Frigate+?", + "submit": "Odoslať" + }, + "livePlayerRequiredIOSVersion": "Pre tento typ živého vysielania je potrebný systém iOS 17.1 alebo novší.", + "streamOffline": { + "title": "Streamujte Offline", + "desc": "V streame detect {{cameraName}} neboli prijaté žiadne snímky, skontrolujte protokoly chýb" + }, + "cameraDisabled": "Kamera je zakázaná", + "stats": { + "streamType": { + "title": "Typ streamu:", + "short": "Typ" + }, + "bandwidth": { + "title": "Šírka pásma:", + "short": "Šírka pásma" + }, + "latency": { + "title": "Latencia:", + "value": "{{seconds}} sekund", + "short": { + "title": "Latencia", + "value": "{{seconds}} sek" + } + }, + "totalFrames": "Celkový počet snímok:", + "droppedFrames": { + "title": "Znížené snímky:", + "short": { + "title": "Spadol", + "value": "{{droppedFrames}} snimku" + } + }, + "decodedFrames": "Dekódované snímky:", + "droppedFrameRate": "Frekvencia stratených snímok:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "Snímok bol úspešne odoslaný službe Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Nepodarilo sa odoslať snímku službe Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/objects.json b/sam2-cpu/frigate-dev/web/public/locales/sk/objects.json new file mode 100644 index 0000000..42ec664 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/objects.json @@ -0,0 +1,120 @@ +{ + "person": "Osoba", + "bicycle": "Bicykel", + "car": "Auto", + "motorcycle": "Motocykel", + "airplane": "Lietadlo", + "bus": "Autobus", + "train": "Vlak", + "boat": "Čln", + "traffic_light": "Semafor", + "fire_hydrant": "Požiarny hydrant", + "street_sign": "Značka ulice", + "stop_sign": "Značka Stop", + "parking_meter": "Parkovací automat", + "bench": "Lavička", + "bird": "Vták", + "cat": "Mačka", + "dog": "Pes", + "horse": "Kôň", + "sheep": "Ovce", + "cow": "Krava", + "elephant": "Slon", + "bear": "Medveď", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Čiapka", + "backpack": "Batoh", + "umbrella": "Dáždnik", + "shoe": "Topánka", + "eye_glasses": "Okuliare", + "handbag": "Kabelka", + "tie": "Kravata", + "suitcase": "Kufor", + "frisbee": "Frisbee", + "skis": "Lyže", + "snowboard": "Snowboard", + "sports_ball": "Športová lopta", + "kite": "Drak", + "baseball_bat": "Bejzbalová pálka", + "baseball_glove": "Baseballová rukavica", + "skateboard": "Skateboard", + "surfboard": "Surfová doska", + "tennis_racket": "Tenisová raketa", + "bottle": "Fľaša", + "plate": "Doska", + "wine_glass": "Pohár na víno", + "cup": "Pohár", + "fork": "Vidlička", + "knife": "Nôž", + "spoon": "Lyžica", + "bowl": "Misa", + "banana": "Banán", + "apple": "Jablko", + "animal": "Zviera", + "sandwich": "Sendvič", + "orange": "Pomaranč", + "broccoli": "Brokolica", + "bark": "Kôra", + "carrot": "Mrkva", + "hot_dog": "Hot Dog", + "pizza": "Pizza", + "donut": "Donut", + "cake": "Koláč", + "chair": "Stolička", + "couch": "Gauč", + "potted_plant": "Rastlina v kvetináči", + "bed": "Posteľ", + "mirror": "Zrkadlo", + "dining_table": "Jedálenský stôl", + "window": "okno", + "desk": "Stôl", + "toilet": "Toaleta", + "door": "Dvere", + "tv": "TV", + "laptop": "Laptop", + "mouse": "Myška", + "remote": "Diaľkové ovládanie", + "keyboard": "Klávesnica", + "goat": "Koza", + "cell_phone": "Mobilný telefón", + "microwave": "Mikrovlnná rúra", + "oven": "Rúra", + "toaster": "Hriankovač", + "sink": "Umývadlo", + "refrigerator": "Chladnička", + "blender": "Mixér", + "book": "Kniha", + "clock": "Hodiny", + "vase": "Váza", + "toothbrush": "Zubná kefka", + "hair_brush": "Kefa na vlasy", + "vehicle": "Vozidlo", + "squirrel": "Veverička", + "scissors": "Nožnice", + "teddy_bear": "Medvedík", + "hair_dryer": "Sušič vlasov", + "deer": "Jeleň", + "fox": "Líška", + "rabbit": "Zajac", + "raccoon": "Mýval", + "robot_lawnmower": "Robotická kosačka", + "waste_bin": "Odpadkový kôš", + "on_demand": "Na požiadanie", + "face": "Tvár", + "license_plate": "ŠPZ", + "package": "Balíček", + "bbq_grill": "Gril", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Čistič", + "postnl": "PostNL", + "nzpost": "NZPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/classificationModel.json new file mode 100644 index 0000000..f8529ea --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/classificationModel.json @@ -0,0 +1,182 @@ +{ + "documentTitle": "Klasifikačné modely", + "button": { + "deleteClassificationAttempts": "Odstrániť obrázky klasifikácie", + "renameCategory": "Premenovať triedu", + "deleteCategory": "Odstrániť triedu", + "deleteImages": "Odstrániť obrázky", + "trainModel": "Model vlaku", + "addClassification": "Pridať klasifikáciu", + "deleteModels": "Odstrániť modely", + "editModel": "Editovať model" + }, + "toast": { + "success": { + "deletedCategory": "Vymazaná trieda", + "deletedImage": "Vymazané obrázky", + "categorizedImage": "Obrázok bol úspešne klasifikovaný", + "trainedModel": "Úspešne vyškolený model.", + "trainingModel": "Úspešne spustený modelový tréning.", + "deletedModel_one": "Úspešne zmazané {{count}} model (y)", + "deletedModel_few": "", + "deletedModel_other": "", + "updatedModel": "Úspešne zmenená konfigurácia modelu", + "renamedCategory": "Úspešne premenovaná trieda na" + }, + "error": { + "deleteImageFailed": "Nepodarilo sa odstrániť: {{errorMessage}}", + "deleteCategoryFailed": "Nepodarilo sa odstrániť triedu: {{errorMessage}}", + "categorizeFailed": "Nepodarilo sa kategorizovať obrázok: {{errorMessage}}", + "trainingFailed": "Nepodarilo sa spustiť trénovanie modelu: {{errorMessage}}", + "deleteModelFailed": "Nepodarilo sa odstrániť model: {{errorMessage}}", + "trainingFailedToStart": "Neuspešny štart trenovania modelu:", + "updateModelFailed": "Chyba pri úprave modelu:", + "renameCategoryFailed": "Chyba pri premenovani triedy:" + } + }, + "deleteCategory": { + "title": "Odstrániť triedu", + "desc": "Naozaj chcete odstrániť triedu {{name}}? Týmto sa natrvalo odstránia všetky súvisiace obrázky a bude potrebné pretrénovať model.", + "minClassesTitle": "Nemožete zmazať triedu", + "minClassesDesc": "Klasifikačný model musí mať aspoň 2 triedy. Pred odstránením tejto triedy pridajte ďalšiu triedu." + }, + "deleteDatasetImages": { + "title": "Odstrániť obrázky množiny údajov", + "desc": "Naozaj chcete odstrániť {{count}} obrázkov z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu." + }, + "deleteTrainImages": { + "title": "Odstrániť obrázky vlakov", + "desc": "Naozaj chcete odstrániť {{count}} obrázkov? Túto akciu nie je možné vrátiť späť." + }, + "renameCategory": { + "title": "Premenovať triedu", + "desc": "Zadajte nový názov pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila." + }, + "description": { + "invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky." + }, + "train": { + "title": "Posledné klasifikácie", + "aria": "Vyberte Nedávne Klasifikácie", + "titleShort": "Nedávne" + }, + "categories": "Triedy", + "createCategory": { + "new": "Vytvorenie novej triedy" + }, + "categorizeImageAs": "Klasifikovať obrázok ako:", + "categorizeImage": "Klasifikovať obrázok", + "noModels": { + "object": { + "title": "Žiadne modely klasifikácie objektov", + "description": "Vytvorte si vlastný model na klasifikáciu detekovaných objektov.", + "buttonText": "Vytvorte objektový model" + }, + "state": { + "title": "Žiadne modely klasifikácie štátov", + "description": "Vytvorte si vlastný model na monitorovanie a klasifikáciu zmien stavu v špecifických oblastiach kamery.", + "buttonText": "Vytvorte model stavu" + } + }, + "wizard": { + "title": "Vytvorte novú klasifikáciu", + "steps": { + "nameAndDefine": "Názov a definícia", + "stateArea": "Štátna oblasť", + "chooseExamples": "Vyberte Príklady" + }, + "step1": { + "description": "Stavové modely monitorujú oblasti pevných kamier a sledujú zmeny (napr. otvorenie/zatvorenie dverí). Objektové modely pridávajú klasifikácie k detekovaným objektom (napr. známe zvieratá, doručovatelia atď.).", + "name": "Meno", + "namePlaceholder": "Zadajte názov modelu...", + "type": "Typ", + "typeState": "štátu", + "typeObject": "Objekt", + "objectLabel": "Označenie objektu", + "objectLabelPlaceholder": "Vyberte typ objektu...", + "classificationType": "Typ klasifikácie", + "classificationTypeTip": "Získajte informácie o typoch klasifikácie", + "classificationTypeDesc": "Podznačky pridávajú k označeniu objektu ďalší text (napr. „Osoba: UPS“). Atribúty sú vyhľadávateľné metadáta uložené samostatne v metadátach objektu.", + "classificationSubLabel": "Podštítky", + "classificationAttribute": "Atribút", + "classes": "Triedy", + "classesTip": "Naučte sa o triedach", + "classesStateDesc": "Definujte rôzne stavy, v ktorých sa môže nachádzať oblasť kamery. Napríklad: „otvorené“ a „zatvorené“ pre garážovú bránu.", + "classesObjectDesc": "Definujte rôzne kategórie, do ktorých sa majú detekované objekty klasifikovať. Napríklad: „doručovateľ/doručovateľka“, „obyvateľ/obyvateľka“, „cudzinec/cudzinec“ pre klasifikáciu osôb.", + "classPlaceholder": "Zadajte názov triedy...", + "errors": { + "nameRequired": "Vyžaduje sa názov modelu", + "nameLength": "Názov modelu musí mať 64 znakov alebo menej", + "nameOnlyNumbers": "Názov modelu nemôže obsahovať iba čísla", + "classRequired": "Vyžaduje sa aspoň 1 kurz", + "classesUnique": "Názvy tried musia byť jedinečné", + "stateRequiresTwoClasses": "Modely štátov vyžadujú aspoň 2 triedy", + "objectLabelRequired": "Vyberte označenie objektu", + "objectTypeRequired": "Vyberte typ klasifikácie" + }, + "states": "Štátov" + }, + "step2": { + "description": "Vyberte kamery a definujte oblasť, ktorú chcete pre každú kameru monitorovať. Model klasifikuje stav týchto oblastí.", + "cameras": "Kamery", + "selectCamera": "Vyberte kameru", + "noCameras": "Kliknite + na pridanie kamier", + "selectCameraPrompt": "Vyberte kameru zo zoznamu a definujte jej oblasť monitorovania" + }, + "step3": { + "selectImagesPrompt": "Vybrať všetky obrázky s: {{className}}", + "selectImagesDescription": "Kliknite na obrázky a vyberte ich. Po dokončení tejto hodiny kliknite na tlačidlo Pokračovať.", + "generating": { + "title": "Generovanie vzorových obrázkov", + "description": "Frigate načítava reprezentatívne obrázky z vašich nahrávok. Môže to chvíľu trvať..." + }, + "training": { + "title": "Tréningový model", + "description": "Váš model sa trénuje na pozadí. Zatvorte toto dialógové okno a váš model sa spustí hneď po dokončení trénovania." + }, + "retryGenerate": "Opakovať generovanie", + "noImages": "Nevygenerovali sa žiadne vzorové obrázky", + "classifying": "Klasifikácia a tréning...", + "trainingStarted": "Školenie začalo úspešne", + "errors": { + "noCameras": "Nie sú nakonfigurované žiadne kamery", + "noObjectLabel": "Nie je vybratý žiadny štítok objektu", + "generateFailed": "Nepodarilo sa vygenerovať príklady: {{error}}", + "generationFailed": "Generovanie zlyhalo. Skúste to znova.", + "classifyFailed": "Nepodarilo sa klasifikovať obrázky: {{error}}" + }, + "generateSuccess": "Vzorové obrázky boli úspešne vygenerované", + "allImagesRequired_one": "Uveďte všetky obrázky. {{count}} obrázok zostáva.", + "allImagesRequired_few": "Uveďte všetky obrázky. {{count}} obrázky zostávajú.", + "allImagesRequired_other": "Uveďte všetky obrázky. {{count}} obrázkov zostávajú.", + "modelCreated": "Model vytvorený úspešne. Použite aktuálne klasifikácie na pridanie obrázkov pre chýbajúce stavy a nasledne dajte trénovať model.", + "missingStatesWarning": { + "title": "Chýbajúce príklady stavov" + } + } + }, + "deleteModel": { + "title": "Odstrániť klasifikačný model", + "single": "Ste si istí, že chcete odstrániť {{name}}? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená.", + "desc": "Ste si istí, že chcete odstrániť {{count}} model (y)? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená." + }, + "menu": { + "objects": "Objekty", + "states": "Štátov" + }, + "details": { + "scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu." + }, + "tooltip": { + "trainingInProgress": "Model sa aktuálne trénuje", + "noNewImages": "Žiadne nové obrázky na trénovanie. Najskor klasifikuj nové obrazky do datasetu.", + "noChanges": "Žiadne zmeny v datasete od posledného tréningu.", + "modelNotReady": "Model nie je pripravený na trénovanie." + }, + "edit": { + "title": "Nastavenie modelu", + "descriptionState": "Upravte triedy pre tento model klasifikácie. Zmeny budú vyžadovať pretrénovanie modelu.", + "descriptionObject": "Upravte typ objektu a typ klasifikácie pre tento model klasifikácie.", + "stateClassesInfo": "Poznámka: Zmena tried stavov vyžaduje pretrénovanie modelu s aktualizovanými triedami." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/configEditor.json new file mode 100644 index 0000000..c10f789 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Editor nastavení - Frigate", + "configEditor": "Editor nastavení", + "copyConfig": "Kopírovať konfiguráciu", + "saveAndRestart": "Uložiť a reštartovať", + "saveOnly": "Len uložit", + "confirm": "Opustiť bez uloženia?", + "toast": { + "success": { + "copyToClipboard": "Konfigurácia bola skopírovaná do schránky." + }, + "error": { + "savingError": "Chyba ukladaní konfigurácie" + } + }, + "safeConfigEditor": "Editor konfigurácie (núdzový režim)", + "safeModeDescription": "Frigate je v núdzovom režime kvôli chybe overenia konfigurácie." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/events.json new file mode 100644 index 0000000..fe86d41 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/events.json @@ -0,0 +1,62 @@ +{ + "alerts": "Upozornenia", + "detections": "Detekcie", + "motion": { + "label": "Pohyb", + "only": "Iba pohyb" + }, + "allCameras": "Všetky Kamery", + "empty": { + "alert": "Nie sú žiadne upozornenia na kontrolu", + "detection": "Nie sú žiadne detekcie na kontrolu", + "motion": "Nenašli sa žiadne údaje o pohybe" + }, + "timeline": "Časová os", + "timeline.aria": "Vyberte časovú os", + "events": { + "label": "Udalosti", + "aria": "Vyberte udalosti", + "noFoundForTimePeriod": "Pre toto časové obdobie sa nenašli žiadne udalosti." + }, + "documentTitle": "Recenzia - Frikgate", + "recordings": { + "documentTitle": "Nahrávky - Frigate" + }, + "calendarFilter": { + "last24Hours": "Posledných 24 hodín" + }, + "markAsReviewed": "Označiť ako skontrolované", + "markTheseItemsAsReviewed": "Označiť tieto položky ako skontrolované", + "newReviewItems": { + "label": "Zobraziť nové položky recenzie", + "button": "Nové položky na kontrolu" + }, + "selected_one": "{{count}} vybraných", + "selected_other": "{{count}} vybraných", + "camera": "Kamera", + "detected": "Detekované", + "suspiciousActivity": "Podozrivá aktivita", + "threateningActivity": "Ohrozujúca činnosť", + "detail": { + "noDataFound": "Žiadne podrobné údaje na kontrolu", + "aria": "Prepnúť zobrazenie detailov", + "trackedObject_one": "objekt", + "trackedObject_other": "objekty", + "noObjectDetailData": "Nie sú k dispozícii žiadne podrobné údaje o objekte.", + "label": "Detail", + "settings": "Nastavenia podrobného zobrazenia", + "alwaysExpandActive": { + "title": "Rozbaľte vždy aktívne", + "desc": "Vždy rozbaľte podrobnosti objektu aktívnej položky recenzie, ak sú k dispozícii." + } + }, + "objectTrack": { + "trackedPoint": "Sledovaný bod", + "clickToSeek": "Kliknutím prejdete na tento čas" + }, + "zoomIn": "Priblížiť", + "zoomOut": "Oddialiť", + "normalActivity": "Narmalne", + "needsReview": "Potrebuje preakúmať", + "securityConcern": "Záujem o bezpečnosť" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/explore.json new file mode 100644 index 0000000..223eb80 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/explore.json @@ -0,0 +1,295 @@ +{ + "documentTitle": "Preskúmať - Frigate", + "generativeAI": "Generatívna AI", + "details": { + "timestamp": "Časová pečiatka", + "item": { + "title": "Skontrolujte podrobnosti položky", + "desc": "Skontrolujte podrobnosti položky", + "button": { + "share": "Zdieľajte túto recenziu", + "viewInExplore": "Zobraziť v Preskúmať" + }, + "tips": { + "mismatch_one": "Bol zistený a zahrnutý do tejto položky kontroly nedostupný objekt ({{count}}). Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "mismatch_few": "Bolo zistených a zahrnutých do tejto položky kontroly {{count}} nedostupných objektov. Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "mismatch_other": "Bolo zistených a zahrnutých do tejto položky kontroly {{count}} nedostupných objektov. Tieto objekty buď neboli kvalifikované ako upozornenie alebo detekcia, alebo už boli vyčistené/odstránené.", + "hasMissingObjects": "Upravte si konfiguráciu, ak chcete, aby Frigate ukladal sledované objekty pre nasledujúce označenia: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Od poskytovateľa {{provider}} bol vyžiadaný nový popis. V závislosti od rýchlosti vášho poskytovateľa môže jeho obnovenie chvíľu trvať.", + "updatedSublabel": "Podštítok bol úspešne aktualizovaný.", + "updatedLPR": "ŠPZ bola úspešne aktualizovaná.", + "audioTranscription": "Úspešne požiadané o prepis zvuku. V závislosti od rýchlosti vášho servera Frigate môže dokončenie prepisu trvať určitý čas." + }, + "error": { + "regenerate": "Nepodarilo sa zavolať od {{provider}} pre nový popis: {{errorMessage}}", + "updatedSublabelFailed": "Nepodarilo sa aktualizovať podštítok: {{errorMessage}}", + "updatedLPRFailed": "Nepodarilo sa aktualizovať evidenčné číslo vozidla: {{errorMessage}}", + "audioTranscription": "Nepodarilo sa vyžiadať prepis zvuku: {{errorMessage}}" + } + } + }, + "label": "Označenie", + "editSubLabel": { + "title": "Upraviť vedľajší štítok", + "desc": "Zadajte nový podštítok pre tento {{label}}", + "descNoLabel": "Zadajte nový podštítok pre tento sledovaný objekt" + }, + "editLPR": { + "title": "Upraviť ŠPZ", + "desc": "Zadajte novú hodnotu evidenčného čísla vozidla pre toto {{label}}", + "descNoLabel": "Zadajte novú hodnotu evidenčného čísla vozidla pre tento sledovaný objekt" + }, + "snapshotScore": { + "label": "Snímka skóre" + }, + "topScore": { + "label": "Najlepšie skóre", + "info": "Najvyššie skóre je najvyššie mediánové skóre sledovaného objektu, takže sa môže líšiť od skóre zobrazeného na miniatúre výsledkov vyhľadávania." + }, + "score": { + "label": "Skóre" + }, + "recognizedLicensePlate": "Uznaná SPZ", + "estimatedSpeed": "Odhadovaná rýchlosť", + "objects": "Objekty", + "camera": "Kamera", + "zones": "Zóny", + "button": { + "findSimilar": "Nájsť podobné", + "regenerate": { + "title": "Regenerovať", + "label": "Obnoviť popis sledovaného objektu" + } + }, + "description": { + "placeholder": "Popis sledovaného objektu", + "aiTips": "Frigate si od vášho poskytovateľa generatívnej umelej inteligencie nevyžiada popis, kým sa neukončí životný cyklus sledovaného objektu.", + "label": "Popis" + }, + "expandRegenerationMenu": "Rozbaľte ponuku regenerácie", + "regenerateFromSnapshot": "Obnoviť zo snímky", + "regenerateFromThumbnails": "Obnoviť z miniatúr", + "tips": { + "descriptionSaved": "Úspešne uložený popis", + "saveDescriptionFailed": "Nepodarilo sa aktualizovať popis: {{errorMessage}}" + } + }, + "exploreMore": "Preskumať viac {{label}} objektov", + "exploreIsUnavailable": { + "title": "Preskúmanie nie je k dispozícii", + "embeddingsReindexing": { + "context": "Preskúmanie je možné použiť po dokončení opätovného indexovania vložených sledovaných objektov.", + "startingUp": "Spúšťanie…", + "estimatedTime": "Odhadovaný zostávajúci čas:", + "finishingShortly": "Čoskoro končí", + "step": { + "thumbnailsEmbedded": "Vložené miniatúry: ", + "descriptionsEmbedded": "Vložené popisy: ", + "trackedObjectsProcessed": "Spracované sledované objekty: " + } + }, + "downloadingModels": { + "context": "Frigate sťahuje potrebné modely vkladania na podporu funkcie sémantického vyhľadávania. V závislosti od rýchlosti vášho sieťového pripojenia to môže trvať niekoľko minút.", + "setup": { + "visionModel": "Model vízie", + "visionModelFeatureExtractor": "Extraktor prvkov modelu videnia", + "textModel": "Textový model", + "textTokenizer": "Textový tokenizér" + }, + "tips": { + "context": "Po stiahnutí modelov možno budete chcieť znova indexovať vloženia sledovaných objektov.", + "documentation": "Prečítajte si dokumentáciu" + }, + "error": "Vyskytla sa chyba. Skontrolujte protokoly Fregaty." + } + }, + "trackedObjectDetails": "Podrobnosti sledovaného objektu", + "type": { + "details": "detaily", + "snapshot": "snímka", + "video": "video", + "object_lifecycle": "životný cyklus objektu", + "thumbnail": "Náhľad", + "tracking_details": "Pohybové detaili" + }, + "objectLifecycle": { + "title": "Životný cyklus Objektu", + "noImageFound": "Žiadny obrázok pre túto časovú pečiatku.", + "createObjectMask": "Vytvoriť masku objektu", + "adjustAnnotationSettings": "Upravte nastavenia anotácií", + "scrollViewTips": "Posúvaním zobrazíte významné momenty životného cyklu tohto objektu.", + "autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.", + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "visible": "Zistený {{label}}", + "entered_zone": "{{label}} vstúpil do {{zones}}", + "active": "{{label}} sa stal aktívnym", + "stationary": "{{label}} sa zastavil", + "attribute": { + "faceOrLicense_plate": "Pre {{label}} bol zistený {{attribute}}", + "other": "{{label}} rozpoznané ako {{attribute}}" + }, + "gone": "{{label}} zostalo", + "heard": "{{label}} počul", + "external": "Zistený {{label}}", + "header": { + "zones": "Zóny", + "ratio": "Pomer", + "area": "Oblasť" + } + }, + "annotationSettings": { + "title": "Nastavenia anotácií", + "showAllZones": { + "title": "Zobraziť všetky zóny", + "desc": "Vždy zobrazovať zóny na rámoch, do ktorých objekty vstúpili." + }, + "offset": { + "label": "Odsadenie anotácie", + "desc": "Tieto údaje pochádzajú z detekčného kanála vašej kamery, ale prekrývajú sa s obrázkami zo záznamového kanála. Je nepravdepodobné, že tieto dva streamy sú dokonale synchronizované. V dôsledku toho sa ohraničujúci rámček a zábery nebudú dokonale zarovnané. Na úpravu tohto posunu je však možné použiť pole annotation_offset.", + "documentation": "Prečítajte si dokumentáciu ", + "millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. Predvolené: 0", + "tips": "TIP: Predstavte si klip udalosti, v ktorom osoba kráča zľava doprava. Ak je ohraničujúci rámček časovej osi udalosti stále naľavo od osoby, hodnota by sa mala znížiť. Podobne, ak osoba kráča zľava doprava a ohraničujúci rámček je stále pred ňou, hodnota by sa mala zvýšiť.", + "toast": { + "success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "carousel": { + "previous": "Predchádzajúca snímka", + "next": "Ďalšia snímka" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Stiahnut video", + "aria": "Stiahnite si video" + }, + "downloadSnapshot": { + "label": "Stiahnite si snímok", + "aria": "Stiahnite si snímok" + }, + "viewObjectLifecycle": { + "label": "Pozrieť životný cyklus objektu", + "aria": "Životný cyklus objektu" + }, + "findSimilar": { + "label": "Nájsť podobné", + "aria": "Nájdite podobné sledované objekty" + }, + "addTrigger": { + "label": "Pridať spúšťač", + "aria": "Pridať spúšťač pre tento sledovaný objekt" + }, + "audioTranscription": { + "label": "Prepisovať", + "aria": "Požiadajte o prepis zvuku" + }, + "submitToPlus": { + "label": "Odoslať na Frigate+", + "aria": "Odoslať na Frigate Plus" + }, + "viewInHistory": { + "label": "Zobraziť v histórii", + "aria": "Zobraziť v histórii" + }, + "deleteTrackedObject": { + "label": "Odstrániť tento sledovaný objekt" + }, + "showObjectDetails": { + "label": "Zobraziť cestu objektu" + }, + "hideObjectDetails": { + "label": "Skryť cestu objektu" + }, + "viewTrackingDetails": { + "label": "Zobraziť podrobnosti sledovania", + "aria": "Zobraziť podrobnosti o sledovaní" + }, + "downloadCleanSnapshot": { + "label": "Stiahnuť čistý snapshot", + "aria": "Stiahnuť čistý snapshot" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potvrdiť zmazanie", + "desc": "Odstránením tohto sledovaného objektu sa odstráni snímka, všetky uložené vložené prvky a všetky súvisiace položky s podrobnosťami o sledovaní. Zaznamenané zábery tohto sledovaného objektu v zobrazení História NEBUDÚ odstránené.

    Naozaj chcete pokračovať?" + } + }, + "noTrackedObjects": "Žiadne sledované objekty neboli nájdené", + "fetchingTrackedObjectsFailed": "Chyba pri načítaní sledovaných objektov: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} sledovaný objekt ", + "trackedObjectsCount_few": "{{count}} sledované objekty ", + "trackedObjectsCount_other": "{{count}} sledovaných objektov ", + "searchResult": { + "tooltip": "Zhoda s {{type}} na {{confidence}} %", + "deleteTrackedObject": { + "toast": { + "success": "Sledovaný objekt úspešne zmazaný.", + "error": "Sledovaný objekt sa nepodarilo zmazať: {{errorMessage}}" + } + }, + "previousTrackedObject": "Predchádzajúci trackovaný objekt", + "nextTrackedObject": "Ďalší trackovaný objekt" + }, + "aiAnalysis": { + "title": "Analýza AI" + }, + "concerns": { + "label": "Obavy" + }, + "trackingDetails": { + "title": "Podrobnosti sledovania", + "noImageFound": "Pre túto časovú pečiatku sa nenašiel žiadny obrázok.", + "createObjectMask": "Vytvoriť masku objektu", + "adjustAnnotationSettings": "Upravte nastavenia anotácií", + "scrollViewTips": "Kliknite pre zobrazenie významných momentov životného cyklu tohto objektu.", + "autoTrackingTips": "Pozície ohraničujúcich rámčekov budú pre kamery s automatickým sledovaním nepresné.", + "count": "{{first}} z {{second}}", + "trackedPoint": "Sledovaný bod", + "lifecycleItemDesc": { + "visible": "Zistený {{label}}", + "entered_zone": "{{label}} vstúpil do {{zones}}", + "active": "{{label}} sa stal aktívnym", + "stationary": "{{label}} sa zastavil", + "attribute": { + "faceOrLicense_plate": "Pre {{label}} bol zistený {{attribute}}", + "other": "{{label}} rozpoznané ako {{attribute}}" + }, + "gone": "{{label}} zostalo", + "heard": "{{label}} počul", + "external": "Zistený {{label}}", + "header": { + "zones": "Zóny", + "ratio": "Pomer", + "area": "Oblasť", + "score": "Skóre" + } + }, + "annotationSettings": { + "title": "Nastavenia anotácií", + "showAllZones": { + "title": "Zobraziť všetky zóny", + "desc": "Vždy zobrazovať zóny na rámoch, do ktorých objekty vstúpili." + }, + "offset": { + "label": "Odsadenie anotácie", + "desc": "Tieto údaje pochádzajú z detektoru kamery, ale sú prepustené na obrázky z rekordného krmiva. Je nepravdepodobné, že dva prúdy sú perfektne synchronizované. V dôsledku toho, skreslenie box a zábery nebudú dokonale zaradiť. Toto nastavenie môžete použiť na ofsetovanie annotácií dopredu alebo dozadu, aby ste ich lepšie zladili s zaznamenanými zábermi.", + "millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. Predvolené: 0", + "tips": "TIP: Predstavte si klip udalosti, v ktorom osoba kráča zľava doprava. Ak je ohraničujúci rámček časovej osi udalosti stále naľavo od osoby, hodnota by sa mala znížiť. Podobne, ak osoba kráča zľava doprava a ohraničujúci rámček je stále pred ňou, hodnota by sa mala zvýšiť.", + "toast": { + "success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "carousel": { + "previous": "Predchádzajúca snímka", + "next": "Ďalšia snímka" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/exports.json new file mode 100644 index 0000000..d9df685 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Exportovať - Frigate", + "search": "Nájsť", + "noExports": "Nenašli sa žiadne exporty", + "deleteExport": "Vymazať export", + "deleteExport.desc": "Ste si isty že chcete vymazať {{exportName}}?", + "editExport": { + "title": "Premenovať Export", + "desc": "Zadajte nové meno pre tento export.", + "saveExport": "Uložiť Export" + }, + "toast": { + "error": { + "renameExportFailed": "Nepodarilo sa premenovať export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Zdieľať export", + "downloadVideo": "Stiahnite si video", + "editName": "Upraviť meno", + "deleteExport": "Odstrániť export" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/faceLibrary.json new file mode 100644 index 0000000..ba46fda --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/faceLibrary.json @@ -0,0 +1,102 @@ +{ + "description": { + "addFace": "Pridajte novú kolekciu do Face Library nahrať svoj prvý obrázok.", + "invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky.", + "placeholder": "Zadajte názov pre túto kolekciu" + }, + "details": { + "person": "Osoba", + "subLabelScore": "Skóre podúrovne", + "scoreInfo": "Skóre podúrovne je vážené skóre všetkých rozpoznaných dôverností tváre, to ale môže byť odlišné od skóre zobrazeného na snímke.", + "face": "Detail tváre", + "faceDesc": "Podrobnosti o sledovanom objekte, ktorý vytvoril túto tvár", + "timestamp": "Časová pečiatka", + "unknown": "Neznáme" + }, + "documentTitle": "Knižnica tvárí", + "uploadFaceImage": { + "title": "Nahrať obrázok tváre", + "desc": "Nahrajte obrázok na skenovanie tvárí a zahrňte ho do {{pageToggle}}" + }, + "collections": "Zbierky", + "createFaceLibrary": { + "title": "Vytvoriť Zbierku", + "desc": "Vytvoriť novú zbierku", + "new": "Vytvoriť novú tvár", + "nextSteps": "Vybudovanie silného základu:
  • Použite kartu Nedávne rozpoznania na výber a trénovanie obrázkov pre každú rozpoznanú osobu.
  • Pre dosiahnutie najlepších výsledkov sa zamerajte na priame obrázky; vyhnite sa trénovaniu obrázkov, ktoré zachytávajú tváre pod uhlom.
  • " + }, + "steps": { + "faceName": "Zadajte Meno tváre", + "uploadFace": "Nahrať obrázok tváre", + "nextSteps": "Ďalšie kroky", + "description": { + "uploadFace": "Nahrajte obrázok {{name}}, ktorý zobrazuje tvár osoby z čelného uhla. Obrázok nemusí byť orezaný len na jej tvár." + } + }, + "train": { + "title": "Nedávne uznania", + "aria": "Vyberte posledné rozpoznania", + "empty": "Neexistujú žiadne predchádzajúce pokusy o rozpoznávanie tváre" + }, + "selectItem": "Vyberte {{item}}", + "selectFace": "Vyberte tvár", + "deleteFaceLibrary": { + "title": "Odstrániť Meno", + "desc": "Ste si istí, že chcete odstrániť kolekciu {{name}}? Tým sa natrvalo odstránia všetky pridružené tváre." + }, + "deleteFaceAttempts": { + "title": "Odstrániť tváre", + "desc_one": "Ste si istí, že chcete odstrániť {{count}} tvár? Túto akciu nemožno vrátiť späť.", + "desc_few": "Ste si istí, že chcete odstrániť {{count}} tvárí? Túto akciu nemožno vrátiť späť.", + "desc_other": "Ste si istí, že chcete odstrániť {{count}} tvárí? Túto akciu nemožno vrátiť späť." + }, + "renameFace": { + "title": "Premonovať tvár", + "desc": "Zadajte nový názov pre {{name}}" + }, + "button": { + "deleteFaceAttempts": "Odstraniť tváre", + "addFace": "Pridať tvár", + "renameFace": "Premenovať tvár", + "deleteFace": "Zmazať tvár", + "uploadImage": "Nahrať obrázok", + "reprocessFace": "Nanovo spracovať tvár" + }, + "imageEntry": { + "validation": { + "selectImage": "Vyberte súbor s obrázkom." + }, + "dropActive": "Presunte obrázok sem…", + "dropInstructions": "Pretiahnite obrázok tu, alebo kliknite na výber", + "maxSize": "Max velkosť: {{size}} MB" + }, + "nofaces": "Žiadne tváre", + "pixels": "{{area}}px", + "readTheDocs": "Prečitajte si návod", + "trainFaceAs": "Trénovať tvár ako:", + "trainFace": "Trénovať tvár", + "toast": { + "success": { + "uploadedImage": "Obrázok bol úspešne nahraný.", + "addFaceLibrary": "{{name}} bol(a) úspešne pridaný(á) do knižnice tvárí!", + "deletedFace_one": "Úspešne odstránená {{count}} tvár.", + "deletedFace_few": "Úspešne odstránené {{count}} tváre.", + "deletedFace_other": "Úspešne odstránených {{count}} tvárí.", + "deletedName_one": "{{count}} tvár bola úspešne odstránená.", + "deletedName_few": "{{count}} tváre boli úspešne odstránené.", + "deletedName_other": "{{count}} tvárí bolo úspešne odstránených.", + "renamedFace": "Úspešne premenovaná tvár na {{name}}", + "trainedFace": "Úspešne natrénovaná tvár.", + "updatedFaceScore": "Úspešne aktualizované skóre tváre." + }, + "error": { + "uploadingImageFailed": "Nepodarilo sa nahrať obrázok: {{errorMessage}}", + "addFaceLibraryFailed": "Nepodarilo sa nastaviť meno tváre: {{errorMessage}}", + "deleteFaceFailed": "Nepodarilo sa odstrániť: {{errorMessage}}", + "deleteNameFailed": "Nepodarilo sa odstrániť meno: {{errorMessage}}", + "renameFaceFailed": "Nepodarilo sa premenovať tvár: {{errorMessage}}", + "trainFailed": "Nepodarilo sa trénovať: {{errorMessage}}", + "updateFaceScoreFailed": "Nepodarilo sa aktualizovať skóre tváre: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/live.json new file mode 100644 index 0000000..546a603 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/live.json @@ -0,0 +1,187 @@ +{ + "documentTitle": "Naživo - Frigate", + "documentTitle.withCamera": "{{camera}} - Naživo - Frigate", + "lowBandwidthMode": "Režim nízkej šírky pásma", + "twoWayTalk": { + "enable": "Povoliť obojsmernú komunikáciu", + "disable": "Zakázať obojsmernú komunikáciu" + }, + "cameraAudio": { + "enable": "Povoliť zvuk kamery", + "disable": "Zakázať zvuk kamerám" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite do rámčeka pre vycentrovanie kamery", + "enable": "Povoliť pohyb kliknutím", + "disable": "Zakázať pohyb kliknutím" + }, + "left": { + "label": "Posuňte PTZ kameru doľava" + }, + "up": { + "label": "Posuňte PTZ kameru nahor" + }, + "down": { + "label": "Posuňte PTZ kameru nadol" + }, + "right": { + "label": "Posuňte PTZ kameru doprava" + } + }, + "zoom": { + "in": { + "label": "Priblíženie PTZ kamery" + }, + "out": { + "label": "Oddialenie PTZ kamery" + } + }, + "frame": { + "center": { + "label": "Kliknite do rámčeka pre vycentrovanie PTZ kamery" + } + }, + "presets": "Predvoľby PTZ kamery", + "focus": { + "in": { + "label": "Zaostrenie PTZ kamery v" + }, + "out": { + "label": "Výstup zaostrenia PTZ kamery" + } + } + }, + "camera": { + "enable": "Povoliť kameru", + "disable": "Zakázať kameru" + }, + "muteCameras": { + "enable": "Stlmiť všetky kamery", + "disable": "Zapnúť zvuk všetkých kamier" + }, + "detect": { + "enable": "Povoliť detekciu", + "disable": "Zakázať detekciu" + }, + "recording": { + "enable": "Povoliť nahrávanie", + "disable": "Zakázať nahrávanie" + }, + "snapshots": { + "enable": "Povoliť vytváranie snímok", + "disable": "Zakázať snímky" + }, + "audioDetect": { + "enable": "Povoliť detekciu zvuku", + "disable": "Zakázať detekciu zvuku" + }, + "autotracking": { + "enable": "Povoliť automatické sledovanie", + "disable": "Zakázať automatické sledovanie" + }, + "transcription": { + "enable": "Povoliť živý prepis zvuku", + "disable": "Zakázať živý prepis zvuku" + }, + "streamStats": { + "enable": "Zobraziť štatistiky streamu", + "disable": "Skryť štatistiky streamu" + }, + "manualRecording": { + "title": "Na požiadanie", + "tips": "Stiahnite si okamžité snímky alebo začnite manuálnu akciu založenú na nastavení nahrávania tejto kamery.", + "playInBackground": { + "label": "Hrať na pozadí", + "desc": "Povoľte túto možnosť, ak chcete pokračovať v streamovaní, aj keď je prehrávač skrytý." + }, + "showStats": { + "label": "Zobraziť štatistiky", + "desc": "Povoľte túto možnosť, ak chcete zobraziť štatistiky streamu ako prekrytie na obraze z kamery." + }, + "debugView": "Zobrazenie ladenia", + "start": "Spustiť nahrávanie na požiadanie", + "started": "Spustené manuálne nahrávanie na požiadanie.", + "failedToStart": "Nepodarilo sa spustiť manuálne nahrávanie na požiadanie.", + "recordDisabledTips": "Keďže nahrávanie je v konfigurácii tejto kamery zakázané alebo obmedzené, uloží sa iba snímka.", + "end": "Ukončiť nahrávanie na požiadanie", + "ended": "Manuálne nahrávanie na požiadanie bolo ukončené.", + "failedToEnd": "Nepodarilo sa ukončiť manuálne nahrávanie na požiadanie." + }, + "streamingSettings": "Nastavenia streamovania", + "notifications": "Notifikacie", + "audio": "Zvuk", + "suspend": { + "forTime": "Pozastaviť na: " + }, + "stream": { + "title": "Stream", + "audio": { + "tips": { + "title": "Zvuk musí byť vyvedený z vašej kamery a nakonfigurovaný v go2rtc pre tento stream." + }, + "available": "Pre tento stream je k dispozícii zvuk", + "unavailable": "Zvuk nie je pre tento stream k dispozícii" + }, + "twoWayTalk": { + "tips": "Vaše zariadenie musí túto funkciu podporovať a WebRTC musí byť nakonfigurované na obojsmernú komunikáciu.", + "available": "Pre tento stream je k dispozícii obojsmerná komunikácia", + "unavailable": "Obojsmerná komunikácia nie je pre tento stream k dispozícii" + }, + "lowBandwidth": { + "tips": "Živý náhľad je v režime nízkej šírky pásma z dôvodu chýb načítavania do vyrovnávacej pamäte alebo streamu.", + "resetStream": "Obnoviť stream" + }, + "playInBackground": { + "label": "Hrať na pozadí", + "tips": "Povoľte túto možnosť, ak chcete pokračovať v streamovaní, aj keď je prehrávač skrytý." + }, + "debug": { + "picker": "Výber streamu nie je k dispozícii v režime ladenia. Zobrazenie ladenia vždy používa stream, ktorému je priradená rola detekcie." + } + }, + "cameraSettings": { + "title": "Nastavenia {{camera}}", + "cameraEnabled": "Kamera povolená", + "objectDetection": "Detekcia objektov", + "recording": "Nahrávanie", + "snapshots": "Snímky", + "audioDetection": "Detekcia zvuku", + "transcription": "Zvukový prepis", + "autotracking": "Automatické sledovanie" + }, + "history": { + "label": "Zobraziť historické zábery" + }, + "effectiveRetainMode": { + "modes": { + "all": "Všetko", + "motion": "Pohyb", + "active_objects": "Aktívne objekty" + }, + "notAllTips": "Vaša konfigurácia uchovávania nahrávok {{source}} je nastavená na režim : {{effectiveRetainMode}}, takže táto nahrávka na požiadanie uchová iba segmenty s nastavením {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Upraviť rozloženie", + "group": { + "label": "Upraviť skupinu kamier" + }, + "exitEdit": "Ukončiť úpravy" + }, + "noCameras": { + "title": "Nie sú konfigurované žiadne kamery", + "description": "Začnite tým, že pripojíte kameru do Frigate.", + "buttonText": "Pridať kameru", + "restricted": { + "title": "Žiadne kamery k dispozícii", + "description": "Nemáte povolenie na zobrazenie akýchkoľvek kamier v tejto skupine." + } + }, + "snapshot": { + "takeSnapshot": "Stiahnite si okamžité snímky", + "noVideoSource": "Žiadny zdroj videa k dispozícii pre snapshot.", + "captureFailed": "Nepodarilo sa zachytiť snímku.", + "downloadStarted": "Sťahovanie snímky sa začalo." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/recording.json new file mode 100644 index 0000000..d14b865 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Exportovať", + "calendar": "Kalendár", + "filters": "Filtre", + "toast": { + "error": { + "noValidTimeSelected": "Nie je vybratý žiadny platný časový rozsah", + "endTimeMustAfterStartTime": "Čas konca musí byť po čase začiatku" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/search.json new file mode 100644 index 0000000..a368ca1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Nájsť", + "savedSearches": "Uložené vyhľadávania", + "searchFor": "Hľadať pre {{inputValue}}", + "button": { + "clear": "Prehľadné vyhľadávanie", + "save": "Uložiť vyhladávanie", + "delete": "Vymazať uložené vyhladávania", + "filterInformation": "Filtrovanie informacii", + "filterActive": "Aktívne filtre" + }, + "trackedObjectId": "ID sledovaného objektu", + "filter": { + "label": { + "cameras": "Kamery", + "labels": "Štítky", + "zones": "Zóny", + "sub_labels": "Podštítky", + "search_type": "Typ vyhľadávania", + "time_range": "Časový rozsah", + "before": "Predtým", + "after": "Po", + "min_score": "Min. Skóre", + "max_score": "Maximálne skóre", + "min_speed": "Min rýchlosť", + "max_speed": "Max rýchlosť", + "recognized_license_plate": "Uznaná poznávacia značka", + "has_clip": "Má Klip", + "has_snapshot": "Má Snímok" + }, + "searchType": { + "thumbnail": "Náhľad", + "description": "Popis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Dátum 'pred' musí byť neskorší ako dátum 'po'.", + "afterDatebeEarlierBefore": "Dátum „po“ musí byť skorší ako dátum „pred“.", + "minScoreMustBeLessOrEqualMaxScore": "Hodnota „min_score“ musí byť menšia alebo rovná hodnote „max_score“.", + "maxScoreMustBeGreaterOrEqualMinScore": "Hodnota „max_score“ musí byť väčšia alebo rovná hodnote „min_score“.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Hodnota „min_speed“ musí byť menšia alebo rovná hodnote „max_speed“.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Hodnota „max_speed“ musí byť väčšia alebo rovná hodnote „min_speed“." + } + }, + "tips": { + "title": "Ako používať textové filtre", + "desc": { + "text": "Filtre vám pomôžu zúžiť výsledky vyhľadávania. Tu je postup, ako ich použiť vo vstupnom poli:", + "step1": "Zadajte názov kľúča filtra, za ktorým nasleduje dvojbodka (napr. „kamery:“).", + "step2": "Vyberte hodnotu z návrhov alebo zadajte vlastnú.", + "step3": "Použite viacero filtrov tak, že ich pridáte jeden po druhom s medzerou medzi nimi.", + "step4": "Filtre dátumu (pred: a po:) používajú formát {{DateFormat}}.", + "step5": "Filter časového rozsahu používa formát {{exampleTime}}.", + "step6": "Filtre odstránite kliknutím na „x“ vedľa nich.", + "exampleLabel": "Príklad:" + } + }, + "header": { + "currentFilterType": "Hodnoty filtra", + "noFilters": "Filtre", + "activeFilters": "Aktívne filtre" + } + }, + "similaritySearch": { + "title": "Vyhľadávanie podobností", + "active": "Vyhľadávanie podobnosti je aktívne", + "clear": "Jasné vyhľadávanie podobnosti" + }, + "placeholder": { + "search": "Hľadať…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/settings.json new file mode 100644 index 0000000..9002366 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/settings.json @@ -0,0 +1,1210 @@ +{ + "documentTitle": { + "default": "Nastavenia - Frigate", + "authentication": "Nastavenie autentifikácie- Frigate", + "camera": "Nastavenia Kamier– Frigate", + "enrichments": "Nastavenia obohatenia – Frigate", + "masksAndZones": "Editor masky a zón - Frigate", + "motionTuner": "Ladič detekcie pohybu - Frigate", + "object": "Ladenie - Frigate", + "general": "UI nastavenia – Frigate", + "frigatePlus": "Nastavenia Frigate+ – Frigate", + "notifications": "Nastavenia upozornení – Frigate", + "cameraManagement": "Manažment kamier - Frigate", + "cameraReview": "Nastavenie kamier - Frigate" + }, + "menu": { + "ui": "Uživaťelské rozohranie", + "enrichments": "Obohatenia", + "cameras": "Nastavenia kamier", + "masksAndZones": "Masky / Zóny", + "motionTuner": "Ladič detekcie pohybu", + "debug": "Ladenie", + "users": "Uživatelia", + "notifications": "Notifikacie", + "frigateplus": "Frigate+", + "triggers": "Spúšťače", + "roles": "Roly", + "cameraManagement": "Manažment", + "cameraReview": "Recenzia" + }, + "dialog": { + "unsavedChanges": { + "title": "Máte neuložené zmeny.", + "desc": "Chcete uložiť zmeny pred pokračovaním?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Žiadna Kamera" + }, + "general": { + "title": "UI nastavenia", + "liveDashboard": { + "title": "Živý Dashboard", + "automaticLiveView": { + "label": "Automatický živý náhľad", + "desc": "Pri detekcii aktivity sa automaticky prepne na živý náhľad kamery. Vypnutie tejto možnosti spôsobí, že sa statické snímky z kamery na ovládacom paneli Live aktualizujú iba raz za minútu." + }, + "playAlertVideos": { + "label": "Prehrať videá s upozornením", + "desc": "Predvolene sa nedávne upozornenia na paneli Živé vysielanie prehrávajú ako krátke cyklické videá. Túto možnosť vypnite, ak chcete zobrazovať iba statický obrázok nedávnych upozornení na tomto zariadení/prehliadači." + }, + "displayCameraNames": { + "label": "Vždy Zobraziť názvy kamier", + "desc": "Vždy zobrazujte názvy kamier v čipe na ovládacom paneli živého náhľadu z viacerých kamier." + }, + "liveFallbackTimeout": { + "label": "Časový limit", + "desc": "Keď je kamerový vysoko kvalitný živý stream nedostupný, prejdite späť na režim nízkej kvality. Predvolené: 3." + } + }, + "storedLayouts": { + "title": "Uložené rozloženia", + "desc": "Rozloženie kamier v skupine kamier je možné presúvať/zmeniť jeho veľkosť. Pozície sú uložené v lokálnom úložisku vášho prehliadača.", + "clearAll": "Vymazať všetky rozloženia" + }, + "cameraGroupStreaming": { + "title": "Nastavenia streamovania skupiny kamier", + "desc": "Nastavenia streamovania pre každú skupinu kamier sú uložené v lokálnom úložisku vášho prehliadača.", + "clearAll": "Vymazať všetky nastavenia streamovania" + }, + "recordingsViewer": { + "title": "Prehliadač nahrávok", + "defaultPlaybackRate": { + "label": "Predvolená rýchlosť prehrávania", + "desc": "Predvolená rýchlosť prehrávania nahrávok." + } + }, + "calendar": { + "title": "Kalendár", + "firstWeekday": { + "label": "Prvý pracovný deň", + "desc": "Deň, kedy začínajú týždne v kalendári kontroly.", + "sunday": "Nedeľa", + "monday": "Pondelok" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Uložené rozloženie pre {{cameraName}} bolo vymazané", + "clearStreamingSettings": "Nastavenia streamovania pre všetky skupiny kamier boli vymazané." + }, + "error": { + "clearStoredLayoutFailed": "Nepodarilo sa vymazať uložené rozloženie: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nepodarilo sa vymazať nastavenia streamovania: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavenia obohatení", + "unsavedChanges": "Zmeny nastavení neuložených obohatení", + "birdClassification": { + "title": "Klasifikácia vtákov", + "desc": "Klasifikácia vtákov identifikuje známe vtáky pomocou kvantizovaného modelu Tensorflow. Keď je známy vták rozpoznaný, jeho bežný názov sa pridá ako podoznačenie (sub_label). Tieto informácie sú zahrnuté v používateľskom rozhraní, filtroch, ako aj v oznámeniach." + }, + "semanticSearch": { + "title": "Sémantické vyhľadávanie", + "desc": "Sémantické vyhľadávanie vo Frigate vám umožňuje nájsť sledované objekty v rámci vašich recenzovaných položiek pomocou samotného obrázka, textového popisu definovaného používateľom alebo automaticky vygenerovaného popisu.", + "reindexNow": { + "label": "Preindexovať teraz", + "desc": "Reindexovanie obnoví vložené súbory pre všetky sledované objekty. Tento proces beží na pozadí a môže maximálne zaťažiť váš procesor a trvať pomerne dlho v závislosti od počtu sledovaných objektov, ktoré máte.", + "confirmTitle": "Potvrďte opätovné indexovanie", + "confirmDesc": "Naozaj chcete preindexovať všetky sledované vložené objekty? Tento proces bude bežať na pozadí, ale môže maximálne zaťažiť váš procesor a trvať pomerne dlho. Priebeh si môžete pozrieť na stránke Preskúmať.", + "confirmButton": "Preindexovať", + "success": "Reindexovanie sa úspešne spustilo.", + "alreadyInProgress": "Reindexovanie už prebieha.", + "error": "Nepodarilo sa spustiť reindexáciu: {{errorMessage}}" + }, + "modelSize": { + "label": "Veľkosť modelu", + "desc": "Veľkosť modelu použitého pre vkladanie sémantického vyhľadávania.", + "small": { + "title": "malý", + "desc": "Použitie funkcie small využíva kvantizovanú verziu modelu, ktorá spotrebuje menej pamäte RAM a beží rýchlejšie na CPU s veľmi zanedbateľným rozdielom v kvalite vkladania." + }, + "large": { + "title": "veľký", + "desc": "Použitie parametra large využíva celý model Jina a v prípade potreby sa automaticky spustí na GPU." + } + } + }, + "faceRecognition": { + "title": "Rozpoznávanie tváre", + "desc": "Rozpoznávanie tváre umožňuje priradiť ľuďom mená a po rozpoznaní ich tváre Frigate priradí meno osoby ako podštítok. Tieto informácie sú zahrnuté v používateľskom rozhraní, filtroch, ako aj v upozorneniach.", + "modelSize": { + "label": "Veľkosť modelu", + "desc": "Veľkosť modelu použitého na rozpoznávanie tváre.", + "small": { + "title": "malý", + "desc": "Použitie funkcie small využíva model vkladania tvárí FaceNet, ktorý efektívne beží na väčšine procesorov." + }, + "large": { + "title": "veľký", + "desc": "Použitie funkcie large využíva model vkladania tvárí ArcFace a v prípade potreby sa automaticky spustí na grafickom procesore." + } + } + }, + "licensePlateRecognition": { + "title": "Rozpoznávanie ŠPZ", + "desc": "Frigate dokáže rozpoznávať evidenčné čísla vozidiel a automaticky pridávať detekované znaky do poľa recognized_license_plate alebo známy názov ako podradený štítok k objektom typu car. Bežným prípadom použitia môže byť čítanie evidenčných čísel áut vchádzajúcich na príjazdovú cestu alebo áut prechádzajúcich po ulici." + }, + "restart_required": "Vyžaduje sa reštart (zmenené nastavenia obohatenia)", + "toast": { + "success": "Nastavenia obohatenia boli uložené. Reštartujte Frigate, aby sa zmeny prejavili.", + "error": "Nepodarilo sa uložiť zmeny konfigurácie: {{errorMessage}}" + } + }, + "camera": { + "title": "Nastavenie kamier", + "streams": { + "title": "Streamy", + "desc": "Dočasne deaktivujte kameru, kým sa Frigate nereštartuje. Deaktivácia kamery úplne zastaví spracovanie streamov z tejto kamery aplikáciou Frigate. Detekcia, nahrávanie a ladenie nebudú k dispozícii.
    Poznámka: Toto nezakáže restreamy go2rtc." + }, + "review": { + "title": "Recenzia", + "desc": "Dočasne povoliť/zakázať upozornenia a detekcie pre túto kameru, kým sa Frigate nereštartuje. Po zakázaní sa nebudú generovať žiadne nové položky kontroly. ", + "alerts": "Upozornenia ", + "detections": "Detekcie " + }, + "object_descriptions": { + "title": "Generatívne popisy objektov umelej inteligencie", + "desc": "Dočasne povoliť/zakázať generatívne popisy objektov AI pre túto kameru. Ak je táto funkcia zakázaná, pre sledované objekty na tejto kamere sa nebudú vyžadovať popisy generované AI." + }, + "review_descriptions": { + "title": "Popisy generatívnej umelej inteligencie", + "desc": "Dočasne povoliť/zakázať generatívne popisy kontroly pomocou umelej inteligencie pre túto kameru. Ak je táto funkcia zakázaná, popisy generované umelou inteligenciou sa nebudú vyžadovať pre položky kontroly v tejto kamere." + }, + "reviewClassification": { + "title": "Preskúmať klasifikáciu", + "desc": "Frigate kategorizuje položky recenzií ako Upozornenia a Detekcie. Predvolene sa všetky objekty typu osoba a auto považujú za Upozornenia. Kategorizáciu položiek recenzií môžete spresniť konfiguráciou požadovaných zón pre ne.", + "noDefinedZones": "Pre túto kameru nie sú definované žiadne zóny.", + "objectAlertsTips": "Všetky objekty {{alertsLabels}} na {{cameraName}} sa zobrazia ako Upozornenia.", + "zoneObjectAlertsTips": "Všetky objekty {{alertsLabels}} detekované v {{zone}} na {{cameraName}} budú zobrazené ako Upozornenia.", + "objectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "zoneObjectDetectionsTips": { + "text": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{zone}} na kamere {{cameraName}}, budú zobrazené ako detekcie.", + "notSelectDetections": "Všetky objekty typu {{detectionsLabels}} detekované v zóne {{zone}} na kamere {{cameraName}}, ktoré nie sú zaradené do kategórie Upozornenia, sa zobrazia ako Detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "regardlessOfZoneObjectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú." + }, + "unsavedChanges": "Neuložené nastavenia klasifikácie recenzií pre {{camera}}", + "selectAlertsZones": "Vyberte podobné sledované objekty", + "selectDetectionsZones": "Vyberte zóny pre detekcie", + "limitDetections": "Obmedziť detekciu na konkrétne zóny", + "toast": { + "success": "Konfigurácia klasifikácie bola uložená. Reštartujte Frigate, aby sa zmeny prejavili." + } + }, + "addCamera": "Pridať novu kameru", + "editCamera": "Upraviť kameru:", + "selectCamera": "Vyberte kameru", + "backToSettings": "Späť na nastavenia kamery", + "cameraConfig": { + "add": "Pridať kameru", + "edit": "Upraviť kameru", + "description": "Konfigurovať nastavenia kamery, vrátane vstupov streamu a rolí.", + "name": "Názov kamery", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí mať menej ako 24 znakov.", + "namePlaceholder": "napr. predné dvere", + "enabled": "Povoliť", + "ffmpeg": { + "inputs": "Vstupné streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta k streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Roly", + "rolesRequired": "Je vyžadovaná aspoň jedna rola", + "rolesUnique": "Každá rola (audio, detekcia, záznam) môže byť priradená iba k jednému streamu", + "addInput": "Pridať vstupný stream", + "removeInput": "Odobrať vstupný stream", + "inputsRequired": "Je vyžadovaný aspoň jeden vstupný stream" + }, + "toast": { + "success": "Kamera {{cameraName}} bola úspešne uložená" + } + } + }, + "masksAndZones": { + "filter": { + "all": "Všetky Masky a Zóny" + }, + "restart_required": "Vyžadovaný reštart (masky/zóny boli zmenené)", + "toast": { + "success": { + "copyCoordinates": "Súradnice pre {{polyName}} skopírované do schránky." + }, + "error": { + "copyCoordinatesFailed": "Nemohol kopírovať súradnice na klipboard." + } + }, + "form": { + "polygonDrawing": { + "error": { + "mustBeFinished": "Kreslenie polygónu musí byť pred uložením dokončené." + }, + "removeLastPoint": "Odobrať posledný bod", + "reset": { + "label": "Vymazať všetky body" + }, + "snapPoints": { + "true": "Prichytávať body", + "false": "Neprichytávať body" + }, + "delete": { + "title": "Potvrdiť Zmazanie", + "desc": "Naozaj chcete zmazať {{type}}{{name}}?", + "success": "{{name}} bolo zmazané." + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Názov Zóny musia mať minimálne 2 znaky.", + "mustNotBeSameWithCamera": "Názov Zóny nesmie byť rovnaký ako názov kamery.", + "alreadyExists": "Zóna s rovnakým názvom pri tejto kamere už existuje.", + "mustNotContainPeriod": "Názov zóny nesmie obsahovať bodky.", + "hasIllegalCharacter": "Názov zóny obsahuje zakázané znaky.", + "mustHaveAtLeastOneLetter": "Názov zóny musí mať aspoň jedno písmeno." + } + }, + "distance": { + "error": { + "text": "Vzdialenosť musí byť väčšia alebo rovná 0.1.", + "mustBeFilled": "Na použitie odhadu rýchlosti musia byť vyplnené všetky polia pre vzdialenosť." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Zotrvačnosť musí byť väčšia ako 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Doba zotrvania musí byť väčšia alebo rovná nule." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Prahová hodnota rýchlosti musí byť väčšia alebo rovná 0,1." + } + } + }, + "zones": { + "label": "Zóny", + "documentTitle": "Upraviť Zónu - Frigate", + "desc": { + "title": "Zóny umožňujú definovať konkrétnu oblasť v zábere, vďaka čomu je možné určiť, či sa objekt nachádza v danej oblasti alebo nie.", + "documentation": "Dokumentácia" + }, + "clickDrawPolygon": "Kliknite pre kreslenie polygónu na obrázku.", + "name": { + "title": "Meno", + "inputPlaceHolder": "Zadajte meno…", + "tips": "Názov musí mať aspoň 2 znaky, musí mať aspoň jedno písmeno a nesmie byť názvom kamery alebo inej zóny." + }, + "inertia": { + "title": "Zotrvačnosť", + "desc": "Určuje, po koľkých snímkach strávených v zóne je objekt považovaný za prítomný v tejto zóne.Predvolená hodnota: 3" + }, + "loiteringTime": { + "title": "Doba zotrvania", + "desc": "Nastavuje minimálnu dobu v sekundách, počas ktorej musí byť objekt v zóne, aby došlo k aktivácii.Predvolená hodnota: 0" + }, + "objects": { + "title": "Objekty", + "desc": "Zoznam objektov, na ktoré sa táto zóna vzťahuje." + }, + "allObjects": "Všetky Objekty", + "speedEstimation": { + "title": "Odhad rýchlosti", + "desc": "Povoliť odhad rýchlosti pre objekty v tejto zóne. Zóna musí mať presne 4 body.", + "lineADistance": "Vzdialenosť linky A ({{unit}})", + "lineBDistance": "Vzdialenosť linky B ({{unit}})", + "lineCDistance": "Vzdialenosť linky C ({{unit}})", + "lineDDistance": "Vzdialenosť linky D ({{unit}})" + }, + "speedThreshold": { + "title": "Prah rýchlosti ({{unit}})", + "desc": "Určuje minimálnu rýchlosť, pri ktorej sú objekty v tejto zóne zohľadnené.", + "toast": { + "error": { + "pointLengthError": "Odhad rýchlosti bol pre túto zónu deaktivovaný. Zóny s odhadom rýchlosti musia mať presne 4 body.", + "loiteringTimeError": "Pokiaľ má zóna nastavenú dobu zotrvania väčšiu ako 0, neodporúča sa používať odhad rýchlosti." + } + } + }, + "toast": { + "success": "Zóna {{zoneName}} bola uložená. Reštartujte Frigate pre aplikovanie zmien." + }, + "add": "Pridať zónu", + "edit": "Upraviť zónu", + "point_one": "{{count}}bod", + "point_few": "{{count}}body", + "point_other": "{{count}}bodov" + }, + "motionMasks": { + "label": "Maska Detekcia pohybu", + "documentTitle": "Editovať Masku Detekcia pohybu - Frigate", + "desc": { + "title": "Masky detekcie pohybu slúžia na zabránenie nežiaducim typom pohybu v spustení detekcie. Príliš rozsiahle maskovanie však môže sťažiť sledovanie objektov.", + "documentation": "Dokumentácia" + }, + "add": "Nová Maska Detekcia pohybu", + "edit": "Upraviť Masku Detekcia pohybu", + "context": { + "title": "Masky detekcie pohybu slúžia na zabránenie tomu, aby nežiaduce typy pohybu spúšťali detekciu (napríklad vetvy stromov alebo časové značky kamery). Masky detekcie pohybu by sa mali používať veľmi striedmo – príliš rozsiahle maskovanie môže sťažiť sledovanie objektov." + }, + "point_one": "{{count}} bod", + "point_few": "{{count}} body", + "point_other": "{{count}} bodov", + "clickDrawPolygon": "Kliknutím nakreslíte polygón do obrázku.", + "polygonAreaTooLarge": { + "title": "Maska detekcie pohybu pokrýva {{polygonArea}}% záberu kamery. Príliš veľké masky detekcie pohybu nie sú odporúčané.", + "tips": "Masky detekcie pohybu nebránia detekcii objektov. Namiesto toho by ste mali použiť požadovanú zónu." + }, + "toast": { + "success": { + "title": "{{polygonName}} bol uložený. Reštartujte Frigate pre aplikovanie zmien.", + "noName": "Maska Detekcia pohybu bola uložená. Reštartujte Frigate pre aplikovanie zmien." + } + } + }, + "objectMasks": { + "label": "Masky Objektu", + "documentTitle": "Upraviť Masku Objektu - Frigate", + "desc": { + "title": "Masky filtrovania objektov slúžia na odfiltrovanie falošných detekcií daného typu objektu na základe jeho umiestnenia.", + "documentation": "Dokumentácia" + }, + "add": "Pridať Masku Objektu", + "edit": "Upraviť Masku Objektu", + "context": "Masky filtrovania objektov slúžia na odfiltrovanie falošných poplachov konkrétneho typu objektu na základe jeho umiestnenia.", + "point_one": "{{count}}bod", + "point_few": "{{count}}body", + "point_other": "{{count}} bodov", + "clickDrawPolygon": "Kliknutím nakreslite polygón do obrázku.", + "objects": { + "title": "Objekty", + "desc": "Typ objektu, na ktorý sa táto maska objektu vzťahuje.", + "allObjectTypes": "Všetky typy objektov" + }, + "toast": { + "success": { + "title": "{{polygonName}} bol uložený. Reštartujte Frigate pre aplikovanie zmien.", + "noName": "Maska Objektu bola uložená. Reštartujte Frigate pre aplikovanie zmien." + } + } + }, + "motionMaskLabel": "Maska Detekcia pohybu {{number}}", + "objectMaskLabel": "Maska Objektu {{number}} {{label}}" + }, + "motionDetectionTuner": { + "title": "Ladenie detekcie pohybu", + "unsavedChanges": "Neuložené zmeny ladenia detekcie pohybu {{camera}}", + "desc": { + "title": "Frigate používa detekciu pohybu ako prvú kontrolu na overenie, či sa v snímke deje niečo, čo stojí za ďalšiu analýzu pomocou detekcie objektov.", + "documentation": "Prečítajte si príručku Ladenie detekcie pohybu" + }, + "Threshold": { + "title": "Prah", + "desc": "Prahová hodnota určuje, aká veľká zmena jasu pixelu je nutná, aby bol považovaný za pohyb. Predvolené: 30" + }, + "contourArea": { + "title": "Obrysová Oblasť", + "desc": "Hodnota plochy obrysu sa používa na rozhodnutie, ktoré skupiny zmenených pixelov sa kvalifikujú ako pohyb. Predvolené: 10" + }, + "improveContrast": { + "title": "Zlepšiť Kontrast", + "desc": "Zlepšiť kontrast pre tmavé scény Predvolené: ON" + }, + "toast": { + "success": "Nastavenie detekcie pohybu bolo uložené." + } + }, + "debug": { + "title": "Ladenie", + "detectorDesc": "Frigate používa vaše detektory {{detectors}} na detekciu objektov v streame vašich kamier.", + "desc": "Ladiace zobrazenie ukazuje sledované objekty a ich štatistiky v reálnom čase. Zoznam objektov zobrazuje časovo oneskorený prehľad detekovaných objektov.", + "openCameraWebUI": "Otvoriť webové rozhranie {{camera}}", + "debugging": "Ladenie", + "objectList": "Zoznam objektov", + "noObjects": "Žiadne objekty", + "audio": { + "title": "Zvuk", + "noAudioDetections": "Žiadne detekcia zvuku", + "score": "skóre", + "currentRMS": "Aktuálne RMS", + "currentdbFS": "Aktuálne dbFS" + }, + "boundingBoxes": { + "title": "Ohraničujúce rámčeky", + "desc": "Zobraziť ohraničujúce rámčeky okolo sledovaných objektov", + "colors": { + "label": "Farby Ohraničujúcich Rámčekov Objektov", + "info": "
  • Pri spustení bude každému objektovému štítku priradená iná farba.
  • Tenká tmavo modrá čiara označuje, že objekt nie je v danom okamihu detekovaný.
  • Tenká šedá čiara znamená, že objekt je detekovaný ako nehybný.
  • Silná čiara je označovaná aktivované).
  • " + } + }, + "timestamp": { + "title": "Časová pečiatka", + "desc": "Prekryť obrázok časovou pečiatkou" + }, + "zones": { + "title": "Zóny", + "desc": "Zobraziť obrys všetkých definovaných zón" + }, + "mask": { + "title": "Masky detekcie pohybu", + "desc": "Zobraziť polygóny masiek detekcie pohybu" + }, + "motion": { + "title": "Rámčeky detekcie pohybu", + "desc": "Zobraziť rámčeky okolo oblastí, kde bol detekovaný pohyb", + "tips": "

    Boxy pohybu


    Červené boxy budú prekryté na miestach snímky, kde je práve detekovaný pohyb.

    " + }, + "regions": { + "title": "Regióny", + "desc": "Zobraziť rámček oblasti záujmu odoslaný do detektora objektov", + "tips": "

    Oblasti regiónov


    Jasnozelené políčka budú prekrývať oblasti záujmu v zábere, ktoré sa odosielajú do detektora objektov.

    " + }, + "paths": { + "title": "Cesty", + "desc": "Zobraziť významné body dráhy sledovaného objektu", + "tips": "

    Cesty


    Čiary a kruhy označujú významné body, ktorými sa sledovaný objekt počas svojho životného cyklu pohyboval.

    " + }, + "objectShapeFilterDrawing": { + "title": "Výkres filtra tvaru objektu", + "desc": "Nakreslite na obrázok obdĺžnik, aby ste zobrazili podrobnosti o ploche a pomere", + "tips": "Povolením tejto možnosti nakreslíte na obraze kamery obdĺžnik, ktorý zobrazuje jeho plochu a pomer strán. Tieto hodnoty sa potom dajú použiť na nastavenie parametrov filtra tvaru objektu vo vašej konfigurácii.", + "score": "Skóre", + "ratio": "Pomer", + "area": "Oblasť" + } + }, + "cameraWizard": { + "title": "Pridať kameru", + "description": "Postupujte podľa pokynov nižšie a pridajte novú kameru na inštaláciu Frigate.", + "steps": { + "nameAndConnection": "Meno a pripojenie", + "streamConfiguration": "Konfigurácia prúdu", + "validationAndTesting": "Platnosť a testovanie", + "probeOrSnapshot": "Probe alebo Snapshot" + }, + "save": { + "success": "Úspešne zachránil novú kameru {{cameraName}}.", + "failure": "Úspora chýb {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Rozlíšenie", + "video": "Video", + "audio": "Zvuk", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Uveďte platnú adresu streamu", + "testFailed": "Test Stream zlyhal: {{error}}" + }, + "step1": { + "description": "Zadajte detaily kamery a vyskúšajte pripojenie.", + "cameraName": "Názov kamery", + "cameraNamePlaceholder": "e.g., front_door alebo Back Yard Prehľad", + "host": "Hostia / IP adresa", + "port": "Prístav", + "username": "Používateľské meno", + "usernamePlaceholder": "Voliteľné", + "password": "Heslo", + "passwordPlaceholder": "Voliteľné", + "selectTransport": "Vyberte dopravný protokol", + "cameraBrand": "Značka kamery", + "selectBrand": "Vyberte značku kamery pre URL šablónu", + "customUrl": "Vlastné Stream URL", + "brandInformation": "Informácie o značke", + "brandUrlFormat": "Pre kamery s formátom RTSP URL ako: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "Testovacie pripojenie", + "testSuccess": "Test pripojenia úspešný!", + "testFailed": "Test pripojenia zlyhal. Skontrolujte svoj vstup a skúste to znova.", + "streamDetails": "Detaily vysielania", + "warnings": { + "noSnapshot": "Nemožno načítať snímku z konfigurovaného vysielania." + }, + "errors": { + "brandOrCustomUrlRequired": "Buď vyberte značku kamery s hostiteľom / IP alebo si vyberte \"Iný\" s vlastnou URL", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí byť 64 znakov alebo menej", + "invalidCharacters": "Názov kamery obsahuje neplatné znaky", + "nameExists": "Názov kamery už existuje", + "brands": { + "reolink-rtsp": "Reolink RTSP sa neodporúča. Odporúča sa povoliť HTTP v nastavení kamery a reštartovať sprievodca kamery." + }, + "customUrlRtspRequired": "Vlastné URL musia začať s \"rtsp / \"\". Manuálna konfigurácia je potrebná pre non-RTSP kamerové prúdy." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Skúmanie metadát kamery...", + "fetchingSnapshot": "Načítava sa snímka z kamery..." + }, + "connectionSettings": "Nastavenie pripojenia", + "detectionMethod": "Stream Detekcia Metóda", + "onvifPort": "ONVIF Port", + "probeMode": "Probe kamera", + "manualMode": "Ručný výber", + "detectionMethodDescription": "Vyskúša cez ONVIF (ak je podporovaný) nájsť kamery streamové adresy, alebo ručne vyberte značku kamery a jej preddefinované URL. Ak chcete zadať vlastnú URL RTSP, vyberte manuálne zadanie a označte \"Ostatné\".", + "onvifPortDescription": "Pre kamery, ktoré podporujú ONVIF, to je zvyčajne 80 alebo 8080.", + "useDigestAuth": "Použite overenie súhrnu", + "useDigestAuthDescription": "Použite HTTP stráviteľné overenie pre ONVIF. Niektoré kamery môžu vyžadovať vyhradený ONVIF užívateľské meno/password namiesto štandardného správcu." + }, + "step2": { + "description": "Vyhľadajte dostupné streamy z kamery alebo nakonfigurujte manuálne nastavenia na základe zvolenej metódy detekcie.", + "streamsTitle": "Kamerové prúdy", + "addStream": "Pridať Stream", + "addAnotherStream": "Pridať ďalší Stream", + "streamTitle": "Stream {{number}}", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "Rozlíšenie", + "selectResolution": "Vyberte rozlíšenie", + "quality": "Kvalita", + "selectQuality": "Vyberte kvalitu", + "roles": "Roly", + "roleLabels": { + "detect": "Detekcia objektov", + "record": "Nahrávanie", + "audio": "Zvuk" + }, + "testStream": "Testovacie pripojenie", + "testSuccess": "Test pripojenia bol úspešný!", + "testFailed": "Test pripojenia zlyhal. Skontrolujte zadané údaje a skúste to znova.", + "testFailedTitle": "Test Zlyhal", + "connected": "Pripojené", + "notConnected": "Nie je pripojený", + "featuresTitle": "Vlastnosti", + "go2rtc": "Znížte počet pripojení ku kamere", + "detectRoleWarning": "Aspoň jeden prúd musí mať \"detekt\" úlohu pokračovať.", + "rolesPopover": { + "title": "Roly streamu", + "detect": "Hlavné krmivo pre detekciu objektu.", + "record": "Ukladá segmenty video kanála na základe nastavení konfigurácie.", + "audio": "Kŕmenie pre detekciu zvuku." + }, + "featuresPopover": { + "title": "Funkcie streamu", + "description": "Použite prekrytie go2rtc na zníženie pripojenia k fotoaparátu." + }, + "streamDetails": "Detaily vysielania", + "probing": "Skúmajúca kamera...", + "retry": "Skúste to znova", + "testing": { + "probingMetadata": "Skúmanie metadát kamery...", + "fetchingSnapshot": "Načítava sa snímka z fotoaparátu..." + }, + "probeFailed": "Nepodarilo sa otestovať kameru: {{error}}", + "probingDevice": "Snímacie zariadenie...", + "probeSuccessful": "Sonda úspešná", + "probeError": "Chyba sondy", + "probeNoSuccess": "Sonda neúspešná", + "deviceInfo": "Informácie o zariadení", + "manufacturer": "Výrobca", + "model": "Model", + "firmware": "Firmvér", + "profiles": "Profily", + "ptzSupport": "PTZ Podpora", + "autotrackingSupport": "Podpora automatického sledovania", + "presets": "Prestavby", + "rtspCandidates": "RTSP kandidátov", + "rtspCandidatesDescription": "Z kamery boli nájdené nasledujúce adresy URL RTSP. Otestujte pripojenie a zobrazte metadáta streamu.", + "noRtspCandidates": "Z kamery sa nenašli žiadne URL adresy RTSP. Vaše prihlasovacie údaje môžu byť nesprávne alebo kamera nepodporuje protokol ONVIF alebo metódu použitú na získanie URL adries RTSP. Vráťte sa späť a zadajte URL adresu RTSP manuálne.", + "candidateStreamTitle": "Kandidát {{number}}", + "useCandidate": "Použitie", + "uriCopy": "Kopírovať", + "uriCopied": "URI skopírované do schránky", + "testConnection": "Testovacie pripojenie", + "toggleUriView": "Kliknutím prepnete zobrazenie celého URI", + "errors": { + "hostRequired": "Vyžaduje sa hostiteľská/IP adresa" + } + }, + "step3": { + "connectStream": "Pripojiť", + "connectingStream": "Pripája", + "disconnectStream": "Odpojiť", + "estimatedBandwidth": "Odhadovaná šírka pásma", + "roles": "Roly", + "none": "Žiadny", + "error": "Chyba", + "streamValidated": "Stream {{number}} úspešne overený", + "streamValidationFailed": "Stream {{number}} validácia zlyhala", + "saveAndApply": "Uložiť novú kameru", + "saveError": "Neplatná konfigurácia. Skontrolujte nastavenia.", + "issues": { + "title": "Stream Platnosť", + "videoCodecGood": "Kód videa {{codec}}.", + "audioCodecGood": "Audio kódc je {{codec}}.", + "noAudioWarning": "Žiadne audio zistené pre tento prúd, nahrávanie nebude mať audio.", + "audioCodecRecordError": "AAC audio kodek je potrebný na podporu audio v záznamoch.", + "audioCodecRequired": "Zvukový prúd je povinný podporovať detekciu zvuku.", + "restreamingWarning": "Zníženie pripojenia ku kamery pre rekordný prúd môže mierne zvýšiť využitie CPU.", + "dahua": { + "substreamWarning": "Substream 1 je uzamknutý na nízke rozlíšenie. Mnoho Dahua / Amcrest / EmpireTech kamery podporujú ďalšie podstreamy, ktoré musia byť povolené v nastavení kamery. Odporúča sa skontrolovať a využiť tie prúdy, ak je k dispozícii." + }, + "hikvision": { + "substreamWarning": "Substream 1 je uzamknutý na nízke rozlíšenie. Mnoho Hikvision kamery podporujú ďalšie podstreamy, ktoré musia byť povolené v nastavení kamery. Odporúča sa skontrolovať a využiť tie prúdy, ak je k dispozícii." + }, + "resolutionHigh": "Rozlíšenie {{resolution}} môže spôsobiť zvýšenú spotrebu zdrojov.", + "resolutionLow": "Rozlíšenie {{resolution}} môže byť príliš nízka pre spoľahlivú detekciu malých objektov." + }, + "description": "Nakonfigurujte role streamov a pridajte ďalšie streamy pre vašu kameru.", + "validationTitle": "Stream Platnosť", + "connectAllStreams": "Pripojte všetky prúdy", + "reconnectionSuccess": "Opätovné pripojenie bolo úspešné.", + "reconnectionPartial": "Niektoré prúdy sa nepodarilo prepojiť.", + "streamUnavailable": "Ukážka streamu nie je k dispozícii", + "reload": "Znovu načítať", + "connecting": "Pripája...", + "streamTitle": "Stream {{number}}", + "valid": "Platné", + "failed": "Zlyhanie", + "notTested": "Netestované", + "streamsTitle": "Kamerové prúdy", + "addStream": "Pridať Stream", + "addAnotherStream": "Pridať ďalší Stream", + "streamUrl": "Stream URL", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Vyberte stream", + "searchCandidates": "Hľadať kandidátov...", + "noStreamFound": "Nenašiel sa žiadny stream", + "url": "URL", + "resolution": "Rozlíšenie", + "selectResolution": "Vyberte rozlíšenie", + "quality": "Kvalita", + "selectQuality": "Vyberte kvalitu", + "roleLabels": { + "detect": "Detekcia objektov", + "record": "Nahrávanie", + "audio": "Zvuk" + }, + "testStream": "Testovanie pripojenia", + "testSuccess": "Stream test úspešné!", + "testFailed": "Stream test zlyhal", + "testFailedTitle": "Test Zlyhal", + "connected": "Pripojené", + "notConnected": "Nie je pripojený", + "featuresTitle": "Vlastnosti", + "go2rtc": "Znížte počet pripojení ku kamere", + "detectRoleWarning": "Aspoň jeden prúd musí mať \"detekt\" úlohu pokračovať.", + "rolesPopover": { + "title": "Roly streamu", + "detect": "Hlavné krmivo pre detekciu objektu.", + "record": "Ukladá segmenty video kanála na základe nastavení konfigurácie.", + "audio": "Kŕmenie pre detekciu zvuku." + }, + "featuresPopover": { + "title": "Funkcie streamu", + "description": "Použite prekrytie go2rtc na zníženie pripojenia k fotoaparátu." + } + }, + "step4": { + "description": "Záverečné overenie a analýza pred uložením nového fotoaparátu. Pripojte každý prúd pred uložením.", + "validationTitle": "Stream Platnosť", + "connectAllStreams": "Pripojte všetky prúdy", + "reconnectionSuccess": "Opätovné pripojenie bolo úspešné.", + "reconnectionPartial": "Niektoré prúdy sa nepodarilo prepojiť.", + "streamUnavailable": "Ukážka streamu nie je k dispozícii", + "reload": "Znovu načítať", + "connecting": "Pripája...", + "streamTitle": "Stream {{number}}", + "valid": "Platné", + "failed": "Zlyhanie", + "notTested": "Netestované", + "connectStream": "Pripojiť", + "connectingStream": "Pripája", + "disconnectStream": "Odpojiť", + "estimatedBandwidth": "Odhadovaná šírka pásma", + "roles": "Roly", + "ffmpegModule": "Použite režim kompatibility prúdu", + "ffmpegModuleDescription": "Ak sa stream nenačíta ani po niekoľkých pokusoch, skúste túto funkciu povoliť. Keď je táto funkcia povolená, Frigate použije modul ffmpeg s go2rtc. To môže poskytnúť lepšiu kompatibilitu s niektorými streammi z kamier.", + "none": "Žiadne", + "error": "Chyba", + "streamValidated": "Stream {{number}} úspešne overený", + "streamValidationFailed": "Stream {{number}} validácia zlyhala", + "saveAndApply": "Uložiť novú kameru", + "saveError": "Neplatná konfigurácia. Skontrolujte nastavenia.", + "issues": { + "title": "Platnosť Streamu", + "videoCodecGood": "Kód videa je {{codec}}.", + "audioCodecGood": "Audio kódc je {{codec}}.", + "resolutionHigh": "Rozlíšenie {{resolution}} môže spôsobiť zvýšenú spotrebu zdrojov.", + "resolutionLow": "Rozlíšenie {{resolution}} môže byť príliš nízka pre spoľahlivú detekciu malých objektov.", + "noAudioWarning": "Žiadne audio nebolo detekovane pre tento prúd, nahrávanie nebude mať audio.", + "audioCodecRecordError": "AAC audio kodek je potrebný na podporu audio v záznamoch.", + "audioCodecRequired": "Zvukový prúd je povinný podporovať detekciu zvuku.", + "restreamingWarning": "Zníženie pripojenia ku kamery pre rekordný prúd môže mierne zvýšiť využitie CPU.", + "brands": { + "reolink-rtsp": "Reolink RTSP sa neodporúča. Odporúča sa povoliť HTTP v nastavení kamery a reštartovať sprievodca kamery." + }, + "dahua": { + "substreamWarning": "Čiastkový stream 1 je uzamknutý na nízke rozlíšenie. Mnoho kamier Dahua / Amcrest / EmpireTech podporuje ďalšie čiastkové streamy, ktoré je potrebné povoliť v nastaveniach kamery. Odporúča sa skontrolovať a využiť tieto streamy, ak sú k dispozícii." + }, + "hikvision": { + "substreamWarning": "Čiastkový stream 1 je uzamknutý na nízke rozlíšenie. Mnoho kamier Hikvision podporuje ďalšie čiastkové streamy, ktoré je potrebné povoliť v nastaveniach kamery. Odporúča sa skontrolovať a využiť tieto streamy, ak sú k dispozícii." + } + } + } + }, + "cameraManagement": { + "title": "Správa kamier", + "addCamera": "Pridať novu kameru", + "editCamera": "Upraviť kameru:", + "selectCamera": "Vyberte kameru", + "backToSettings": "Späť na nastavenia kamery", + "streams": { + "title": "Enable / Disable kamery", + "desc": "Dočasne deaktivujte kameru, kým sa Frigate nereštartuje. Deaktivácia kamery úplne zastaví spracovanie streamov z tejto kamery aplikáciou Frigate. Detekcia, nahrávanie a ladenie nebudú k dispozícii.
    Poznámka: Toto nezakáže restreamy go2rtc." + }, + "cameraConfig": { + "add": "Pridať kameru", + "edit": "Upraviť kameru", + "description": "Konfigurovať nastavenia kamery, vrátane vstupov streamu a rolí.", + "name": "Názov kamery", + "nameRequired": "Názov kamery je povinný", + "nameLength": "Názov kamery musí byť menšia ako 64 znakov.", + "namePlaceholder": "e.g., predne_dvere alebo Prehľad Záhrady", + "enabled": "Povoliť", + "ffmpeg": { + "inputs": "Vstupné streamy", + "path": "Cesta streamu", + "pathRequired": "Cesta k streamu je povinná", + "pathPlaceholder": "rtsp://...", + "roles": "Roly", + "rolesRequired": "Je vyžadovaná aspoň jedna rola", + "rolesUnique": "Každá rola (audio, detekcia, záznam) môže byť priradená iba k jednému streamu", + "addInput": "Pridať vstupný stream", + "removeInput": "Odobrať vstupný stream", + "inputsRequired": "Je vyžadovaný aspoň jeden vstupný stream" + }, + "go2rtcStreams": "go2rtc Streamy", + "streamUrls": "Stream URLs", + "addUrl": "Pridať URL", + "addGo2rtcStream": "Pridať go2rtc Stream", + "toast": { + "success": "Kamera {{cameraName}} bola úspešne uložená" + } + } + }, + "cameraReview": { + "title": "Nastavenie recenzie kamery", + "object_descriptions": { + "title": "Generatívne popisy objektov umelej inteligencie", + "desc": "Dočasne umožňujú/disable Generovať opisy objektu AI pre tento fotoaparát. Keď je zakázané, AI vygenerované popisy nebudú požiadané o sledovanie objektov na tomto fotoaparáte." + }, + "review_descriptions": { + "title": "Popisy generatívnej umelej inteligencie", + "desc": "Dočasne povoliť/disable Genive AI opisy pre tento fotoaparát. Keď je zakázané, AI vygenerované popisy nebudú požiadané o preskúmanie položiek na tomto fotoaparáte." + }, + "review": { + "title": "Recenzia", + "desc": "Dočasne umožňujú/disable upozornenia a detekcia pre tento fotoaparát až do reštartu Frigate. Pri vypnutých, nebudú vygenerované žiadne nové položky preskúmania. ", + "alerts": "Upozornenia ", + "detections": "Detekcie " + }, + "reviewClassification": { + "title": "Preskúmať klasifikáciu", + "desc": "Frigate kategorizuje položky recenzií ako Upozornenia a Detekcie. Predvolene sa všetky objekty typu osoba a auto považujú za Upozornenia. Kategorizáciu položiek recenzií môžete spresniť konfiguráciou požadovaných zón pre ne.", + "noDefinedZones": "Pre túto kameru nie sú definované žiadne zóny.", + "objectAlertsTips": "Všetky objekty {{alertsLabels}} na {{cameraName}} sa zobrazia ako Upozornenia.", + "zoneObjectAlertsTips": "Všetky objekty {{alertsLabels}} detekované v {{zone}} na {{cameraName}} budú zobrazené ako Upozornenia.", + "objectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "zoneObjectDetectionsTips": { + "text": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{zone}} na kamere {{cameraName}}, budú zobrazené ako detekcie.", + "notSelectDetections": "Všetky objekty typu {{detectionsLabels}} detekované v zóne {{zone}} na kamere {{cameraName}}, ktoré nie sú zaradené do kategórie Upozornenia, sa zobrazia ako Detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú.", + "regardlessOfZoneObjectDetectionsTips": "Všetky objekty {{detectionsLabels}}, ktoré nie sú zaradené do kategórie {{cameraName}}, sa zobrazia ako detekcie bez ohľadu na to, v ktorej zóne sa nachádzajú." + }, + "unsavedChanges": "Nezaradené Nastavenie hodnotenia pre {{camera}}", + "selectAlertsZones": "Vyberte zóny pre upozornenia", + "selectDetectionsZones": "Vyberte zóny pre detekcie", + "limitDetections": "Obmedziť detekciu na konkrétne zóny", + "toast": { + "success": "Konfigurácia klasifikácie bola uložená. Reštartujte Frigate, aby sa zmeny prejavili." + } + } + }, + "users": { + "title": "Používatelia", + "management": { + "title": "Správa používateľov", + "desc": "Spravovať používateľské účty tejto inštancie Frigate." + }, + "addUser": "Pridať používateľa", + "updatePassword": "Aktualizovať heslo", + "toast": { + "success": { + "createUser": "Užívateľ {{user}} úspešne vytvorený", + "deleteUser": "Užívateľ {{user}} úspešne odobraný", + "updatePassword": "Heslo úspešne aktualizované.", + "roleUpdated": "Aktualizovaná rola pre používateľa {{user}}" + }, + "error": { + "setPasswordFailed": "Nepodarilo sa uložiť heslo: {{errorMessage}}", + "createUserFailed": "Nepodarilo sa vytvoriť používateľa: {{errorMessage}}", + "deleteUserFailed": "Nepodarilo sa odstrániť používateľa: {{errorMessage}}", + "roleUpdateFailed": "Nepodarilo sa aktualizovať rolu: {{errorMessage}}" + } + }, + "table": { + "username": "Používateľské meno", + "actions": "Akcie", + "role": "Rola", + "noUsers": "Nenašli sa žiadni používatelia.", + "changeRole": "Zmeniť rolu používateľa", + "password": "Heslo", + "deleteUser": "Odstrániť používateľa" + }, + "dialog": { + "form": { + "user": { + "title": "Používateľské meno", + "desc": "Povolené sú iba písmená, čísla, bodky a podčiarkovníky.", + "placeholder": "Zadajte používateľské meno" + }, + "password": { + "title": "Heslo", + "placeholder": "Zadajte heslo", + "confirm": { + "title": "Potvrdiť heslo", + "placeholder": "Potvrdiť heslo" + }, + "strength": { + "title": "Sila hesla: ", + "weak": "Slabý", + "medium": "Stredná", + "strong": "Silný", + "veryStrong": "Veľmi silný" + }, + "match": "Heslá sa zhodujú", + "notMatch": "Heslá sa nezhodujú" + }, + "newPassword": { + "title": "Nové heslo", + "placeholder": "Zadajte nové heslo", + "confirm": { + "placeholder": "Znovu zadajte nové heslo" + } + }, + "usernameIsRequired": "Vyžaduje sa používateľské meno", + "passwordIsRequired": "Heslo je povinné" + }, + "createUser": { + "title": "Vytvorenie nového užívateľa", + "desc": "Pridajte nový používateľský účet a zadajte rolu pre prístup k oblastiam používateľského rozhrania Frigate.", + "usernameOnlyInclude": "Používateľské meno môže obsahovať iba písmená, číslice, . alebo _", + "confirmPassword": "Potvrďte svoje heslo" + }, + "deleteUser": { + "title": "Odstrániť užívateľa", + "desc": "Túto akciu nie je možné vrátiť späť. Týmto sa natrvalo odstráni používateľský účet a odstránia sa všetky súvisiace údaje.", + "warn": "Naozaj chcete odstrániť používateľa {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Heslo nemôže byť prázdne", + "doNotMatch": "Heslá sa nezhodujú", + "updatePassword": "Aktualizácia hesla pre {{username}}", + "setPassword": "Nastaviť heslo", + "desc": "Vytvorte si silné heslo na zabezpečenie tohto účtu." + }, + "changeRole": { + "title": "Zmeniť rolu používateľa", + "select": "Vyberte rolu", + "desc": "Aktualizovať povolenia pre používateľa {{username}}", + "roleInfo": { + "intro": "Vyberte príslušnú rolu pre tohto používateľa:", + "admin": "Správca", + "adminDesc": "Úplný prístup ku všetkým funkciám.", + "viewer": "Divák", + "viewerDesc": "Obmedzené iba na živé dashboardy, funkcie Review, Explore a Exports.", + "customDesc": "Vlastná rola so špecifickým prístupom k kamere." + } + } + } + }, + "roles": { + "management": { + "title": "Správa roly diváka", + "desc": "Spravujte vlastné roly divákov a ich povolenia na prístup ku kamere pre túto inštanciu Frigate." + }, + "addRole": "Pridať rolu", + "table": { + "role": "Rola", + "cameras": "Kamery", + "actions": "Akcie", + "noRoles": "Neboli nájdené žiadne vlastné role.", + "editCameras": "Editovať kamery", + "deleteRole": "Odstrániť rolu" + }, + "toast": { + "success": { + "createRole": "Rola {{role}} bola úspešne vytvorená", + "updateCameras": "Kamery aktualizované pre rolu {{role}}", + "deleteRole": "Rola {{role}} bola úspešne odstránená", + "userRolesUpdated_one": "", + "userRolesUpdated_few": "", + "userRolesUpdated_other": "{{count}} užívatelia priradené tejto úlohe boli aktualizované pre \"viewer\", ktorý má prístup ku všetkým kamerám." + }, + "error": { + "createRoleFailed": "Nepodarilo sa vytvoriť rolu: {{errorMessage}}", + "updateCamerasFailed": "Nepodarilo sa aktualizovať kamery: {{errorMessage}}", + "deleteRoleFailed": "Nepodarilo sa odstrániť rolu: {{errorMessage}}", + "userUpdateFailed": "Nepodarilo sa aktualizovať používateľské role: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Vytvoriť novú rolu", + "desc": "Pridajte novú úlohu a zadajte prístup k kamerám." + }, + "editCameras": { + "title": "Editovať Rolu Kamery", + "desc": "Aktualizujte prístup k kamere pre rolu {{role}}." + }, + "deleteRole": { + "title": "Odstrániť rolu", + "desc": "Túto akciu nie je možné vrátiť späť. Týmto sa rola natrvalo odstráni a všetci používatelia s touto rolou budú priradení k role „pozerač“, ktorá umožní divákovi prístup ku všetkým kamerám.", + "warn": "Ste si istí, že chcete odstrániť {{role}}?", + "deleting": "Odstraňuje sa..." + }, + "form": { + "role": { + "title": "Názov role", + "placeholder": "Zadajte názov roly", + "desc": "Povolené sú iba písmená, čísla, bodky a podčiarkovníky.", + "roleIsRequired": "Vyžaduje sa názov roly", + "roleOnlyInclude": "Názov role môže obsahovať iba písmená, čísla, . alebo _", + "roleExists": "Úloha s týmto menom už existuje." + }, + "cameras": { + "title": "Kamery", + "desc": "Vyberte kamery, ku ktorým má táto rola prístup. Vyžaduje sa aspoň jedna kamera.", + "required": "Aspoň jedna kamera musí byť vybraná." + } + } + } + }, + "notification": { + "title": "Notifikacie", + "notificationSettings": { + "title": "Nastavenia notifikácií", + "desc": "Frigate dokáže natívne odosielať push notifikácie do vášho zariadenia, keď je spustený v prehliadači alebo nainštalovaný ako PWA." + }, + "notificationUnavailable": { + "title": "Notifikacie su nedostupné", + "desc": "Webové push notifikácie vyžadujú zabezpečený kontext (https://…). Ide o obmedzenie prehliadača. Ak chcete používať notifikácie, pristupujte k Frigate bezpečne." + }, + "globalSettings": { + "title": "Globálne nastavenia", + "desc": "Dočasne pozastaviť upozornenia pre konkrétne kamery na všetkých registrovaných zariadeniach." + }, + "email": { + "title": "E-mail", + "placeholder": "e.g. príklad@email.com", + "desc": "Vyžaduje sa platný e-mail, ktorý bude použitý na upozornenie v prípade akýchkoľvek problémov so službou push." + }, + "cameras": { + "title": "Kamery", + "noCameras": "K dispozícii nie sú žiadne kamery", + "desc": "Vyberte, na ktoré kamery umožňujú notifikácie." + }, + "deviceSpecific": "Špecifické nastavenia zariadenia", + "registerDevice": "Registrovať toto zariadenie", + "unregisterDevice": "Zrušte registráciu tohto zariadenia", + "sendTestNotification": "Odoslať testovacie oznámenie", + "unsavedRegistrations": "Neuložené registrácie oznámení", + "unsavedChanges": "Neuložené zmeny upozornení", + "active": "Upozornenia sú aktívne", + "suspended": "Oznámenie pozastavuju {{time}}", + "suspendTime": { + "suspend": "Pozastaviť", + "5minutes": "Pozastaviť na 5 minút", + "10minutes": "Pozastaviť na 10 minút", + "30minutes": "Pozastaviť na 30 minút", + "1hour": "Pozastaviť na 1 hodinu", + "12hours": "Pozastaviť na 12 hodín", + "24hours": "Pozastaviť na 24 hodín", + "untilRestart": "Pozastaviť do reštartovania" + }, + "cancelSuspension": "Zrušiť pozastavenie", + "toast": { + "success": { + "registered": "Úspešne zaregistrované pre upozornenia. Pred odoslaním akýchkoľvek upozornení (vrátane testovacieho upozornenia) je potrebné reštartovať Frigate.", + "settingSaved": "Nastavenie oznámenia boli uložené." + }, + "error": { + "registerFailed": "Uloženie registrácie upozornenia zlyhalo." + } + } + }, + "frigatePlus": { + "title": "Nastavenie Frigate+", + "apiKey": { + "title": "Frigate + API kľúč", + "validated": "Frigate + API kľúč je detekovaný a overený", + "notValidated": "Frigate + API kľúč nie je detekovaný alebo nie je overený", + "desc": "Frigate+ API kľúč umožňuje integráciu s Frigate+ služby.", + "plusLink": "Prečítajte si viac o Frigate+" + }, + "snapshotConfig": { + "title": "Konfigurácia snímky", + "desc": "Odosielanie do Frigate+ vyžaduje, aby boli v konfigurácii povolené snímky aj snímky clean_copy.", + "cleanCopyWarning": "Niektoré kamery majú povolené snímky, ale voľba clean_copy je zakázaná. Pre možnosť odosielania snímok z týchto kamier do služby Frigate+ je nutné túto voľbu povoliť v konfigurácii snímok.", + "table": { + "camera": "Kamera", + "snapshots": "Snímky", + "cleanCopySnapshots": "clean_copy Snímky" + } + }, + "modelInfo": { + "title": "Informácie o Modele", + "modelType": "Typ Modelu", + "trainDate": "Dátum Tréningu", + "baseModel": "Základný Model", + "plusModelType": { + "baseModel": "Základný Model", + "userModel": "Doladené" + }, + "supportedDetectors": "Podporované Detektory", + "cameras": "Kamery", + "loading": "Načítavam informácie o modeli…", + "error": "Chyba načítania informácií o modeli", + "availableModels": "Dostupné Moduly", + "loadingAvailableModels": "Načítavam dostupné modely…", + "modelSelect": "Tu môžete vybrať dostupné modely zo služby Frigate+. Upozorňujeme, že je možné zvoliť iba modely kompatibilné s aktuálnou konfiguráciou detektora." + }, + "unsavedChanges": "Neuložené zmeny nastavenia Frigate+", + "restart_required": "Vyžadovaný reštart (model Frigate+ zmenený)", + "toast": { + "success": "Nastavenia Frigate+ boli uložené. Reštartujte Frigate+ pre aplikovanie zmien.", + "error": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Spúšťače", + "semanticSearch": { + "title": "Sémantické vyhľadávanie je vypnuté", + "desc": "Na používanie spúšťačov musí byť povolené sémantické vyhľadávanie." + }, + "management": { + "title": "Spúšťače", + "desc": "Správa spúšťa {{camera}}. Použite typ miniatúry, aby ste spustili na podobných miniatúr na vybraných tracked objekt, a typ popisu, aby ste spustili podobné popisy na text, ktorý určíte." + }, + "addTrigger": "Pridať Spúšťač", + "table": { + "name": "Meno", + "type": "Typ", + "content": "Obsah", + "threshold": "Prah", + "actions": "Akcie", + "noTriggers": "Pre túto kameru nie sú nakonfigurované žiadne spúšťače.", + "edit": "Upraviť", + "deleteTrigger": "Odstrániť spúšťač", + "lastTriggered": "Naposledy spustené" + }, + "type": { + "thumbnail": "Náhľad", + "description": "Popis" + }, + "actions": { + "notification": "Poslať upozornenie", + "sub_label": "Pridať vedľajší štítok", + "attribute": "Pridať atribút" + }, + "dialog": { + "createTrigger": { + "title": "Vytvoriť spúšťač", + "desc": "Vytvorte spúšť pre kameru {{camera}}" + }, + "editTrigger": { + "title": "Upraviť spúšťač", + "desc": "Upraviť nastavenia spúšťača na kamere {{camera}}" + }, + "deleteTrigger": { + "title": "Odstrániť spúšťač", + "desc": "Naozaj chcete odstrániť spúšťač {{triggerName}}? Túto akciu nie je možné vrátiť späť." + }, + "form": { + "name": { + "title": "Meno", + "placeholder": "Zadajte meno pre spúšťača", + "description": "Zadajte jedinečné meno alebo popis na identifikáciu tohto spúšťania", + "error": { + "minLength": "Názov musí mať aspoň 2 znaky.", + "invalidCharacters": "Meno môže obsahovať iba písmená, číslice, podčiarkovníky a pomlčky.", + "alreadyExists": "Spúšťač s týmto názvom už pre túto kameru existuje." + } + }, + "enabled": { + "description": "Povoliť alebo zakázať tento spúšťač" + }, + "type": { + "title": "Typ", + "placeholder": "Vybrať typ spúšťača", + "description": "Spustiť, keď sa zistí podobný popis sledovaného objektu", + "thumbnail": "Spustiť, keď sa zistí podobná miniatúra sledovaného objektu" + }, + "content": { + "title": "Obsah", + "imagePlaceholder": "Vyberte miniatúru", + "textPlaceholder": "Zadajte obsah textu", + "imageDesc": "Zobrazujú sa iba posledné 100 miniatúr. Ak nemôžete nájsť požadovanú miniatúru, prečítajte si skôr objekty v preskúmať a nastaviť spúšťací z ponuky tam.", + "textDesc": "Zadajte text, aby ste spustili túto akciu, keď je detekovaný podobný popis objektu.", + "error": { + "required": "Obsah je potrebný." + } + }, + "threshold": { + "title": "Prah", + "desc": "Nastavte prah podobnosti pre tento spúšťač. Vyšší prah znamená, že na spustenie spúšťača je potrebná bližšia zhoda.", + "error": { + "min": "Threshold musí byť aspoň 0", + "max": "Threshold musí byť na väčšine 1" + } + }, + "actions": { + "title": "Akcie", + "desc": "V predvolenom nastavení Frigate odosiela MQTT správu pre všetky spúšťače. Zvoľte dodatočnú akciu, ktorá sa má vykonať, keď sa tento spúšťač aktivuje.", + "error": { + "min": "Musí byť vybraná aspoň jedna akcia." + } + } + } + }, + "wizard": { + "title": "Vytvoriť spúšťač", + "step1": { + "description": "Konfigurujte základné nastavenia pre vašu spúšť." + }, + "step2": { + "description": "Nastavte obsah, ktorý spustí túto akciu." + }, + "step3": { + "description": "Konfigurovať prah a akcie pre tento spúšťač." + }, + "steps": { + "nameAndType": "Meno a typ", + "configureData": "Konfigurovať údaje", + "thresholdAndActions": "Prah a akcie" + } + }, + "toast": { + "success": { + "createTrigger": "Spúšťač {{name}} bol úspešne vytvorený.", + "updateTrigger": "Spúšťač {{name}} bol úspešne aktualizovaný.", + "deleteTrigger": "Spúšťač {{name}} bol úspešne zmazaný." + }, + "error": { + "createTriggerFailed": "Nepodarilo sa vytvoriť spúšťač: {{errorMessage}}", + "updateTriggerFailed": "Nepodarilo sa aktualizovať spúšťač: {{errorMessage}}", + "deleteTriggerFailed": "Nepodarilo sa zmazať spúšťač: {{errorMessage}}" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sk/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/sk/views/system.json new file mode 100644 index 0000000..94afc91 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sk/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "cameras": "Štatistiky kamier - Frigate", + "storage": "Štatistiky úložiska - Frigate", + "general": "Všeobecné štatistiky - Frigate", + "enrichments": "Štatistiky obohatenia - Frigate", + "logs": { + "frigate": "Protokoly Frigate - Frigate", + "go2rtc": "Protokoly Go2RTC - Frigate", + "nginx": "Protokoly Ngnix - Frigate" + } + }, + "title": "System", + "metrics": "Systémové metriky", + "logs": { + "download": { + "label": "Stiahnúť záznamy" + }, + "copy": { + "label": "Kopírovať do schránky", + "success": "Protokoly boli skopírované do schránky", + "error": "Nepodarilo sa skopírovať protokoly do schránky" + }, + "type": { + "label": "Typ", + "timestamp": "Časová pečiatka", + "tag": "Šťitok (Tag)", + "message": "Správa" + }, + "tips": "Záznamy sa streamujú zo servera", + "toast": { + "error": { + "fetchingLogsFailed": "Chyba pri načítaní protokolov: {{errorMessage}}", + "whileStreamingLogs": "Chyba pri streamovaní protokolov: {{errorMessage}}" + } + } + }, + "general": { + "title": "Hlavný", + "detector": { + "title": "Detektory", + "inferenceSpeed": "Detekčná rýchlosť", + "temperature": "Detekčná teplota", + "cpuUsage": "Detektor využitia CPU", + "memoryUsage": "Detektor využitia pamäte", + "cpuUsageInformation": "CPU použitý na prípravu vstupných a výstupných údajov do/z detekčných modelov. Táto hodnota nemeria využitie inferencie, a to ani v prípade použitia GPU alebo akcelerátora." + }, + "hardwareInfo": { + "title": "Informácie o hardvéri", + "gpuUsage": "Využitie GPU", + "gpuMemory": "Pamäť GPU", + "gpuEncoder": "GPU kódovač", + "gpuDecoder": "GPU dekodér", + "gpuInfo": { + "vainfoOutput": { + "title": "Výstup Vainfo", + "returnCode": "Návratový kód: {{code}}", + "processOutput": "Výstup procesu:", + "processError": "Chyba procesu:" + }, + "nvidiaSMIOutput": { + "title": "Výstup Nvidia SMI", + "name": "Meno: {{name}}", + "driver": "Vodič: {{driver}}", + "cudaComputerCapability": "Výpočtové možnosti CUDA: {{cuda_compute}}", + "vbios": "Informácie o VBiose: {{vbios}}" + }, + "closeInfo": { + "label": "Zatvorte informácie o GPU" + }, + "copyInfo": { + "label": "Kopírovať informácie o GPU" + }, + "toast": { + "success": "Informácie o grafickej karte boli skopírované do schránky" + } + }, + "npuUsage": "Použitie NPU", + "npuMemory": "Pamäť NPU", + "intelGpuWarning": { + "title": "Intel GPU Stats Upozornenie", + "message": "Štatistiky GPU nedostupné", + "description": "Toto je známa chyba v Štatistike správ Intel (intel_gpu_top) kde sa rozpadne a opakovane vráti používanie GPU 0% aj v prípadoch, keď hardvér detekcie objektov správne beží na (i)GPU. Toto nie je Frigate chyba. Môžete reštartovať a tak dočasne opraviť problém a potvrdiť, že GPU funguje správne. Toto nemá vplyv na výkon." + } + }, + "otherProcesses": { + "title": "Iné procesy", + "processCpuUsage": "Proces využitia CPU", + "processMemoryUsage": "Procesné využitie pamäte" + } + }, + "storage": { + "title": "Skladovanie", + "overview": "Prehľad", + "recordings": { + "title": "Nahrávky", + "tips": "Táto hodnota predstavuje celkové úložisko, ktoré používajú nahrávky v databáze Frigate. Frigate nesleduje využitie úložiska pre všetky súbory na vašom disku.", + "earliestRecording": "Najstaršia dostupná nahrávka:" + }, + "shm": { + "title": "Alokácia SHM (zdieľanej pamäte)", + "warning": "Aktuálna veľkosť SHM {{total}}MB je príliš malá. Zvýšte ju aspoň na {{min_shm}}MB." + }, + "cameraStorage": { + "title": "Úložisko kamery", + "camera": "Kamera", + "unusedStorageInformation": "Nepoužité informácie o úložisku", + "storageUsed": "Skladovanie", + "percentageOfTotalUsed": "Percento z celkového počtu", + "bandwidth": "Šírka pásma", + "unused": { + "title": "Nepoužité", + "tips": "Táto hodnota nemusí presne zodpovedať voľnému miestu dostupnému pre Frigate, ak máte na disku uložené aj iné súbory okrem nahrávok Frigate. Frigate nesleduje využitie úložiska mimo svojich nahrávok." + } + } + }, + "cameras": { + "title": "Kamery", + "overview": "Prehľad", + "info": { + "aspectRatio": "pomer strán", + "cameraProbeInfo": "{{camera}} Informácie o sonde kamery", + "streamDataFromFFPROBE": "Údaje zo streamu sa získavajú pomocou príkazu ffprobe.", + "fetching": "Načítavajú sa údaje z kamery", + "stream": "Stream {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Rozlíšenie:", + "fps": "FPS:", + "unknown": "Neznámy", + "audio": "Zvuk:", + "error": "Chyba: {{error}}", + "tips": { + "title": "Informácie o kamerovej sonde" + } + }, + "framesAndDetections": "Rámy / Detekcie", + "label": { + "camera": "kamera", + "detect": "odhaliť", + "skipped": "preskočené", + "ffmpeg": "FFmpeg", + "capture": "zachytiť", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "zachytiť{{camName}}", + "cameraDetect": "Detekcia {{camName}}", + "overallFramesPerSecond": "celkový počet snímok za sekundu", + "overallDetectionsPerSecond": "celkový počet detekcií za sekundu", + "overallSkippedDetectionsPerSecond": "celkový počet vynechaných detekcií za sekundu", + "cameraFramesPerSecond": "{{camName}}snimky za sekundu", + "cameraDetectionsPerSecond": "{{camName}}detekcie za sekundu", + "cameraSkippedDetectionsPerSecond": "{{camName}} vynechaných detekcií za sekundu" + }, + "toast": { + "success": { + "copyToClipboard": "Dáta sondy boli skopírované do schránky." + }, + "error": { + "unableToProbeCamera": "Nepodarilo sa overiť kameru: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Naposledy obnovené: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} má vysoké využitie CPU vo formáte FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} má vysoké využitie CPU pri detekcii ({{detectAvg}}%)", + "healthy": "Systém je zdravý", + "reindexingEmbeddings": "Preindexovanie vložených prvkov (dokončené na {{processed}} %)", + "cameraIsOffline": "{{camera}} je offline", + "detectIsSlow": "{{detect}} je pomalý ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je veľmi pomalý ({{speed}} ms)", + "shmTooLow": "Alokácia /dev/shm ({{total}} MB) by sa mala zvýšiť aspoň na {{min}} MB." + }, + "enrichments": { + "title": "Obohatenia", + "infPerSecond": "Inferencie za sekundu", + "embeddings": { + "image_embedding": "Vkladanie obrázkov", + "text_embedding": "Vkladanie textu", + "face_recognition": "Rozpoznávanie tváre", + "plate_recognition": "Rozpoznávanie ŠPZ", + "image_embedding_speed": "Rýchlosť vkladania obrázkov", + "face_embedding_speed": "Rýchlosť vkladania tváre", + "face_recognition_speed": "Rýchlosť rozpoznávania tváre", + "plate_recognition_speed": "Rýchlosť rozpoznávania ŠPZ", + "text_embedding_speed": "Rýchlosť vkladania textu", + "yolov9_plate_detection_speed": "YOLOv9 rýchlosť detekcie ŠPZ", + "yolov9_plate_detection": "YOLOv9 Detekcia ŠPZ", + "review_description": "Popis recenzie", + "review_description_speed": "Popis recenzie Rýchlosťi", + "review_description_events_per_second": "Popis", + "object_description": "Popis objektu", + "object_description_speed": "Popis objektu Rýchlosť", + "object_description_events_per_second": "Popis objektu" + }, + "averageInf": "Priemerný čas inferencie" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/audio.json b/sam2-cpu/frigate-dev/web/public/locales/sl/audio.json new file mode 100644 index 0000000..bf5482c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/audio.json @@ -0,0 +1,144 @@ +{ + "speech": "Govor", + "babbling": "Blebetanje", + "yell": "Kričanje", + "whispering": "Šepetanje", + "laughter": "Smejanje", + "crying": "Jokanje", + "sigh": "Vzdih", + "singing": "Petje", + "yodeling": "Jodlanje", + "rapping": "Rapanje", + "run": "Tek", + "whistling": "Žvižganje", + "breathing": "Dihanje", + "snoring": "Smrčanje", + "cough": "Kašelj", + "animal": "Žival", + "pets": "Ljubljenčki", + "dog": "Pes", + "cat": "Maček", + "meow": "Mijav", + "horse": "Konj", + "moo": "Muu", + "cowbell": "Kravji zvonec", + "pig": "Pujs", + "goat": "Koza", + "sheep": "Ovca", + "chicken": "Kokoš", + "turkey": "Puran", + "duck": "Raca", + "goose": "Gos", + "bird": "Ptič", + "radio": "Radio", + "television": "Televizija", + "footsteps": "Stopinje", + "bus": "Avtobus", + "train": "Vlak", + "toothbrush": "Ščetka za zobe", + "bark": "Lajanje", + "mouse": "Miš", + "keyboard": "Tipkovnica", + "boat": "Ladja", + "vehicle": "Prevozno sredstvo", + "car": "Avto", + "motorcycle": "Motor", + "bicycle": "Kolo", + "skateboard": "Skejt", + "door": "Vrata", + "sink": "Umivalnik", + "blender": "Sekljalnik", + "hair_dryer": "Fen", + "scissors": "Škarje", + "clock": "Ura", + "camera": "Kamera", + "bellow": "Spodaj", + "whoop": "Ups", + "musical_instrument": "Glasbeni inštrument", + "choir": "Zbor", + "burping": "Riganje", + "hiccup": "Kolcanje", + "fart": "Prdenje", + "hands": "Roke", + "finger_snapping": "Tleskanje s prsti", + "clapping": "Ploskanje", + "heartbeat": "Utrip srca", + "cheering": "Navijanje", + "applause": "Aplavz", + "crowd": "Množica", + "children_playing": "Igranje otrok", + "howl": "Auuu", + "purr": "Predenje", + "hiss": "Sikanje", + "livestock": "Živina", + "cattle": "Govedo", + "quack": "Ga-ga", + "cluck": "Kokodak", + "cock_a_doodle_doo": "Kikiriki", + "bleat": "Mee", + "neigh": "I-ha ha", + "chirp": "Čiv-čiv", + "pigeon": "Golob", + "coo": "Gru-gru", + "crow": "Vrana", + "caw": "Kra", + "owl": "Sova", + "hoot": "Hu-hu", + "flapping_wings": "Plapolanje kril", + "dogs": "Psi", + "rats": "Podgane", + "insect": "Insekt", + "cricket": "Čriček", + "mosquito": "Komar", + "fly": "Muha", + "frog": "Žaba", + "snake": "Kača", + "music": "Glasba", + "guitar": "Kitara", + "electric_guitar": "Električna kitara", + "bass_guitar": "Bas kitara", + "acoustic_guitar": "Akustična kitara", + "strum": "Brenkanje", + "banjo": "Bendžo", + "sitar": "Sitar", + "mandolin": "Mandolina", + "ukulele": "Ukulele", + "piano": "Klavir", + "electric_piano": "Digitalni klavir", + "organ": "Orgle", + "electronic_organ": "Digitalne orgle", + "chant": "Spev", + "mantra": "Mantra", + "child_singing": "Otroško petje", + "synthetic_singing": "Sintetično petje", + "humming": "Brenčanje", + "groan": "Stok", + "grunt": "Godrnjanje", + "wheeze": "Zadihan izdih", + "gasp": "Glasen Vzdih", + "pant": "Sopihanje", + "snort": "Smrkanje", + "throat_clearing": "Odkašljevanje", + "sneeze": "Kihanje", + "sniff": "Vohljaj", + "chewing": "Žvečenje", + "biting": "Grizenje", + "gargling": "Grgranje", + "stomach_rumble": "Grmotanje v Želodcu", + "heart_murmur": "Šum na Srcu", + "chatter": "Klepetanje", + "yip": "Jip", + "growling": "Rjovenje", + "whimper_dog": "Pasje Cviljenje", + "oink": "Oink", + "gobble": "Zvok Purana", + "wild_animals": "Divje Živali", + "roaring_cats": "Rjoveče Mačke", + "roar": "Rjovenje Živali", + "squawk": "Krik", + "patter": "Klepetanje", + "croak": "Kvakanje", + "rattle": "Ropotanje", + "whale_vocalization": "Kitova Vokalizacija", + "plucked_string_instrument": "Trgani Godalni Instrument" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/common.json b/sam2-cpu/frigate-dev/web/public/locales/sl/common.json new file mode 100644 index 0000000..25bc064 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/common.json @@ -0,0 +1,311 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilRestart": "Do ponovnega zagona", + "ago": "{{timeAgo}} nazaj", + "justNow": "Zdaj", + "untilForRestart": "Dokler se Frigate ne zažene ponovno.", + "thisWeek": "Ta teden", + "lastWeek": "Prejšnji teden", + "thisMonth": "Ta mesec", + "year_one": "{{time}} leto", + "year_two": "{{time}} leti", + "year_few": "{{time}} leta", + "year_other": "{{time}} let", + "second_one": "{{time}} sekunda", + "second_two": "{{time}} sekundi", + "second_few": "{{time}} sekunde", + "second_other": "{{time}} sekund", + "month_one": "{{time}} mesec", + "month_two": "{{time}} meseca", + "month_few": "{{time}} meseci", + "month_other": "{{time}} mesecev", + "day_one": "{{time}} dan", + "day_two": "{{time}} dneva", + "day_few": "{{time}} dnevi", + "day_other": "{{time}} dni", + "hour_one": "{{time}} ura", + "hour_two": "{{time}} uri", + "hour_few": "{{time}} ure", + "hour_other": "{{time}} ur", + "minute_one": "{{time}} minuta", + "minute_two": "{{time}} minuti", + "minute_few": "{{time}} minute", + "minute_other": "{{time}} minut", + "10minutes": "10 minut", + "lastMonth": "Prejšnji mesec", + "5minutes": "5 minut", + "today": "Danes", + "yesterday": "Včeraj", + "last7": "Zadnjih 7 dni", + "last14": "Zadnjih 14 dni", + "last30": "Zadnjih 30 dni", + "1hour": "1 ura", + "12hours": "12 ur", + "24hours": "24 ur", + "30minutes": "30 minut", + "am": "am", + "pm": "pm", + "mo": "{{time}}mes", + "d": "{{time}}d", + "h": "{{time}}h", + "m": "{{time}}m", + "s": "{{time}}s", + "yr": "{{time}}l.", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "invalidStartTime": "Napačen čas začetka", + "invalidEndTime": "Napačen čas konca", + "inProgress": "V teku" + }, + "menu": { + "live": { + "cameras": { + "count_one": "{{count}} kamera", + "count_two": "{{count}} kameri", + "count_few": "{{count}} kamere", + "count_other": "{{count}} kamer", + "title": "Kamere" + }, + "allCameras": "Vse Kamere", + "title": "V Živo" + }, + "explore": "Brskanje", + "theme": { + "nord": "Nord", + "label": "Teme", + "blue": "Modra", + "green": "Zelena", + "red": "Rdeča", + "highcontrast": "Visok Kontrast", + "default": "Privzeto" + }, + "review": "Pregled", + "system": "Sistem", + "systemMetrics": "Sistemske metrike", + "configuration": "Konfiguracija", + "systemLogs": "Sistemski dnevniki", + "settings": "Nastavitve", + "configurationEditor": "Urejevalnik Konfiguracije", + "languages": "Jeziki", + "language": { + "en": "English (angleščina)", + "es": "Español (španščina)", + "zhCN": "简体中文 (poenostavljena kitajščina)", + "hi": "हिन्दी (hindijščina)", + "fr": "Français (francoščina)", + "ar": "العربية (arabščina)", + "pt": "Português (portugalščina)", + "ru": "Русский (ruščina)", + "de": "Deutsch (nemščina)", + "ja": "日本語 (japonščina)", + "tr": "Türkçe (turščina)", + "it": "Italiano (italijanščina)", + "nl": "Nederlands (nizozemščina)", + "sv": "Svenska (švedščina)", + "cs": "Čeština (češčina)", + "nb": "Norsk Bokmål (norveščina, bokmal)", + "ko": "한국어 (korejščina)", + "vi": "Tiếng Việt (vietnamščina)", + "fa": "فارسی (perzijščina)", + "pl": "Polski (poljščina)", + "uk": "Українська (ukrajinščina)", + "he": "עברית (hebrejščina)", + "el": "Ελληνικά (grščina)", + "ro": "Română (romunščina)", + "hu": "Magyar (madžarščina)", + "fi": "Suomi (finščina)", + "da": "Dansk (danščina)", + "sk": "Slovenčina (slovaščina)", + "yue": "粵語 (kantonščina)", + "th": "ไทย (tajščina)", + "sr": "Српски (srbščina)", + "sl": "Slovenščina (Slovenščina )", + "bg": "Български (bulgarščina)", + "withSystem": { + "label": "Uporabi sistemske nastavitve za jezik" + }, + "ptBR": "Português brasileiro (Brazilska portugalščina)", + "ca": "Català (Katalonščina)", + "lt": "Lietuvių (Litovščina)", + "gl": "Galego (Galicijščina)", + "id": "Bahasa Indonesia (Indonezijščina)", + "ur": "اردو (Urdujščina)" + }, + "appearance": "Izgled", + "darkMode": { + "label": "Temni Način", + "light": "Svetlo", + "dark": "Temno", + "withSystem": { + "label": "Uporabi sistemske nastavitve za svetel ali temen način" + } + }, + "withSystem": "Sistem", + "help": "Pomoč", + "documentation": { + "title": "Dokumentacija", + "label": "Frigate dokumentacija" + }, + "restart": "Znova Zaženi Frigate", + "export": "Izvoz", + "faceLibrary": "Zbirka Obrazov", + "user": { + "title": "Uporabnik", + "account": "Račun", + "current": "Trenutni Uporabnik: {{user}}", + "anonymous": "anonimen", + "logout": "Odjava", + "setPassword": "Nastavi Geslo" + }, + "uiPlayground": "UI Peskovnik", + "classification": "Klasifikacija" + }, + "button": { + "apply": "Uporabi", + "reset": "Ponastavi", + "done": "Končano", + "disable": "Izklopi", + "close": "Zapri", + "back": "Nazaj", + "pictureInPicture": "Slika v Sliki", + "history": "Zgodovina", + "disabled": "Onemogočeno", + "copy": "Kopiraj", + "exitFullscreen": "Izhod iz Celozaslonskega načina", + "enabled": "Omogočen", + "enable": "Vklopi", + "save": "Shrani", + "saving": "Shranjevanje …", + "cancel": "Prekliči", + "fullscreen": "Celozaslonski način", + "twoWayTalk": "Dvosmerni Pogovor", + "cameraAudio": "Zvok Kamere", + "on": "Vključen", + "off": "Izključen", + "edit": "Uredi", + "copyCoordinates": "Kopiraj koordinate", + "delete": "Izbriši", + "yes": "Da", + "no": "Ne", + "download": "Prenesi", + "info": "Info", + "suspended": "Začasno ustavljeno", + "unsuspended": "Obnovi", + "play": "Predvajaj", + "unselect": "Odznači", + "export": "Izvoz", + "deleteNow": "Izbriši Zdaj", + "next": "Naprej", + "continue": "Nadaljuj" + }, + "unit": { + "speed": { + "kph": "km/h", + "mph": "mi/h" + }, + "length": { + "feet": "čevelj", + "meters": "metri" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/uro", + "mbph": "MB/uro", + "gbph": "GB/uro" + } + }, + "label": { + "back": "Pojdi nazaj", + "hide": "Skrij {{item}}", + "show": "Prikaži {{item}}", + "ID": "ID", + "none": "Brez", + "all": "Vse" + }, + "pagination": { + "next": { + "label": "Pojdi na naslednjo stran", + "title": "Naprej" + }, + "label": "paginacija", + "previous": { + "title": "Prejšnji", + "label": "Pojdi na prejšnjo stran" + }, + "more": "Več strani" + }, + "selectItem": "Izberi {{item}}", + "toast": { + "copyUrlToClipboard": "Povezava kopirana v odložišče.", + "save": { + "title": "Shrani", + "error": { + "title": "Napaka pri shranjevanju sprememb: {{errorMessage}}", + "noMessage": "Napaka pri shranjevanju sprememb konfiguracije" + } + } + }, + "role": { + "title": "Vloga", + "admin": "Administrator", + "viewer": "Gledalec", + "desc": "Administratorji imajo poln dostop do vseh funkcij Frigate uporabniškega vmesnika. Gledalci so omejeni na gledanje kamer, zgodovine posnetkov in pregledovanje dogodkov." + }, + "accessDenied": { + "documentTitle": "Dostop zavrnjen - Frigate", + "title": "Dostop Zavrnjen", + "desc": "Nimate pravic za ogled te strani." + }, + "notFound": { + "documentTitle": "Ni Najdeno - Frigate", + "title": "404", + "desc": "Stran ni najdena" + }, + "readTheDocumentation": "Preberite dokumentacijo", + "list": { + "two": "{{0}} in {{1}}", + "many": "{{items}}, in {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Izbirno", + "internalID": "Interni ID, ki ga Frigate uporablja v konfiguraciji in podatkovni bazi" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/auth.json new file mode 100644 index 0000000..547381c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "Uporabniško ime", + "password": "Geslo", + "login": "Prijava", + "errors": { + "usernameRequired": "Uporabniško ime je potrebno", + "passwordRequired": "Geslo je zahtevano", + "rateLimit": "Preveč poskusov, poskusite znova kasneje.", + "loginFailed": "Prijava ni uspela", + "unknownError": "Neznana napaka. Preverite dnevnike.", + "webUnknownError": "Neznana napaka. Preverite dnevnike konzole." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/camera.json new file mode 100644 index 0000000..10414fe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Skupine kamer", + "add": "Dodaj skupino kamer", + "edit": "Uredi skupino kamer", + "delete": { + "label": "Izbriši skupino kamer", + "confirm": { + "title": "Potrdite izbris", + "desc": "Ali ste prepričani, da želite izbrisati skupino kamer z imenom {{name}}?" + } + }, + "camera": { + "setting": { + "desc": "Spremeni možnosti prenosa v živo za nadzorno ploščo te skupine kamer. Te nastavitve so specifične za napravo/brskalnik.", + "streamMethod": { + "method": { + "smartStreaming": { + "desc": "Pametno pretakanje bo posodabljalo sliko vaše kamere enkrat na minuto, kadar ni zaznane nobene aktivnosti, da prihrani pasovno širino in vire. Ko je zaznana aktivnost, se slika brez prekinitve preklopi na prenos v živo.", + "label": "Pametno pretakanje (priporočeno)" + }, + "continuousStreaming": { + "desc": { + "warning": "Neprekinjeno pretakanje lahko povzroči visoko porabo pasovne širine in težave z zmogljivostjo. Uporabljajte previdno.", + "title": "Slika kamere bo na nadzorni plošči vedno prenos v živo, tudi če ni zaznane nobene aktivnosti." + }, + "label": "Neprekinjeno pretakanje" + }, + "noStreaming": { + "desc": "Slike kamere se bodo posodabljale enkrat na minuto.", + "label": "Brez pretakanja" + } + }, + "label": "Metoda pretakanja", + "placeholder": "Izberiti metodo pretakanja" + }, + "audio": { + "tips": { + "title": "Izhod za zvok mora biti nastavljen v go2rtc za ta tok.", + "document": "Preberite dokumentacijo " + } + }, + "label": "Nastavitve pretakanja kamer", + "title": "Nastavitve pretakanja kamere {{cameraName}}", + "audioIsAvailable": "Zvok za ta tok je na voljo", + "audioIsUnavailable": "Zvok za ta tok ni na voljo", + "compatibilityMode": { + "label": "Način združjivosti", + "desc": "To možnost omogočite le, če se v prenosu v živo vaše kamere pojavljajo barvni artefakti in diagonalna črta na desni strani slike." + }, + "placeholder": "Izberite tok", + "stream": "Tok" + }, + "birdseye": "Ptičji pogled" + }, + "name": { + "label": "Ime", + "placeholder": "Vpišite ime …", + "errorMessage": { + "mustLeastCharacters": "Ime skupine kamer mora imeti vsaj 2 znaka.", + "exists": "Skupina kamer s tem imenom že obstaja.", + "nameMustNotPeriod": "Ime skupine kamer ne sme vsebovati pike.", + "invalid": "Neveljavno ime skupine kamer." + } + }, + "cameras": { + "label": "Kamere", + "desc": "Izberite kamere za to skupino." + }, + "icon": "Ikona", + "success": "Skupina kamer z imenom ({{name}}) je bila shranjena." + }, + "debug": { + "options": { + "label": "Nastavitve", + "title": "Lastnosti", + "showOptions": "Prikaži lastnosti", + "hideOptions": "Skrij lastnosti" + }, + "boundingBox": "Omejitve okvirja", + "timestamp": "Časovni žig", + "zones": "Območja", + "mask": "Maska", + "motion": "Gibanje", + "regions": "Regije" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/dialog.json new file mode 100644 index 0000000..f0284ee --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "title": "Ali ste prepričani, da želite ponovno zagnati Frigate?", + "button": "Ponovni zagon", + "restarting": { + "title": "Frigate se ponovno zaganja", + "content": "Ta stran se bo osvežila čez {{countdown}}.", + "button": "Osveži zdaj" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_full": "Ali je ta objekt {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Potrdi to oznako za Frigate Plus", + "ask_a": "Ali je ta objekt {{label}}?", + "ask_an": "Ali je ta objekt {{label}}?" + }, + "state": { + "submitted": "Oddano" + } + }, + "submitToPlus": { + "label": "Pošlji v Frigate+", + "desc": "Predmeti na lokacijah, ki se jim želite izogniti, niso lažni alarmi. Če jih označite kot lažne alarme, boste zmedli model." + } + }, + "video": { + "viewInHistory": "Poglej zgodovino" + } + }, + "export": { + "time": { + "lastHour_one": "Zadnja {{count}} ura", + "lastHour_two": "Zadnji {{count}} uri", + "lastHour_few": "Zadnje {{count}} ure", + "lastHour_other": "Zadnjih {{count}} ur", + "fromTimeline": "Izberi s Časovnice", + "custom": "Po meri", + "start": { + "title": "Začetni čas", + "label": "Izberi Začetni Čas" + }, + "end": { + "title": "Končni Čas", + "label": "Izberi Končni Čas" + } + }, + "name": { + "placeholder": "Poimenujte Izvoz" + }, + "select": "Izberi", + "export": "Izvoz", + "selectOrExport": "Izberi ali Izvozi", + "toast": { + "success": "Izvoz se je uspešno začel. Datoteko si oglejte v izvozih.", + "error": { + "failed": "Npaka pri začetku izvoza: {{error}}", + "endTimeMustAfterStartTime": "Končni čas mora biti po začetnem čase", + "noVaildTimeSelected": "Ni izbranega veljavnega časovnega obdobja" + } + }, + "fromTimeline": { + "saveExport": "Shrani Izvoz", + "previewExport": "Predogled Izvoza" + } + }, + "streaming": { + "label": "Pretakanje", + "restreaming": { + "disabled": "Ponovno pretakanje za to kamero ni omogočeno.", + "desc": { + "title": "Za dodatne možnosti ogleda v živo in zvoka za to kamero nastavite go2rtc.", + "readTheDocumentation": "Preberi dokumentacijo" + } + }, + "showStats": { + "label": "Prikaži statistiko pretoka", + "desc": "Omogočite to možnost, če želite prikazati statistiko pretoka videa kamere." + }, + "debugView": "Pogled za Odpravljanje Napak" + }, + "search": { + "saveSearch": { + "label": "Varno Iskanje", + "desc": "Vnesite ime za to shranjeno iskanje.", + "placeholder": "Vnesite ime za iskanje", + "overwrite": "{{searchName}} že obstaja. Shranjevanje bo prepisalo obstoječo vrednost.", + "success": "Iskanje ({{searchName}}) je bilo shranjeno.", + "button": { + "save": { + "label": "Shrani to iskanje" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "Potrdi Brisanje", + "desc": { + "selected": "Ali ste prepričani, da želite izbrisati vse posnete videoposnetke, povezane s tem elementom pregleda?

    Držite tipko Shift, da se v prihodnje izognete temu pogovornemu oknu." + }, + "toast": { + "success": "Videoposnetek, povezan z izbranimi elementi pregleda, je bil uspešno izbrisan.", + "error": "Brisanje ni uspelo: {{error}}" + } + }, + "button": { + "export": "Izvoz", + "markAsReviewed": "Označi kot pregledano", + "deleteNow": "Izbriši Zdaj", + "markAsUnreviewed": "Označi kot nepregledano" + } + }, + "imagePicker": { + "selectImage": "Izberite sličico sledenega predmeta", + "search": { + "placeholder": "Iskanje po oznaki ali podoznaki..." + }, + "noImages": "Za to kamero ni bilo najdenih sličic" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/filter.json new file mode 100644 index 0000000..5a33b97 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Filter", + "labels": { + "label": "Oznake", + "all": { + "title": "Vse oznake", + "short": "Oznake" + }, + "count_one": "{{count}} oznaka", + "count_other": "{{count}} oznak" + }, + "dates": { + "selectPreset": "Izberite nastavitev …", + "all": { + "title": "Vsi datumi", + "short": "Datumi" + } + }, + "more": "Več filtrov", + "explore": { + "settings": { + "defaultView": { + "summary": "Povzetek", + "title": "Privzeti Pogled", + "desc": "Če filtri niso izbrani, prikaži povzetek najnovejših sledenih objektov na oznako ali prikaži nefiltrirano mrežo.", + "unfilteredGrid": "Nefiltrirana Mreža" + }, + "title": "Nastavitve", + "gridColumns": { + "title": "Mrežni Stolpci", + "desc": "Izberite število stolpcev v pogledu mreže." + }, + "searchSource": { + "label": "Iskanje Vira", + "desc": "Izberite, ali želite iskati po sličicah ali opisih sledenih objektov.", + "options": { + "thumbnailImage": "Sličica", + "description": "Opis" + } + } + }, + "date": { + "selectDateBy": { + "label": "Izberite datum za filtriranje" + } + } + }, + "subLabels": { + "all": "Vse podoznake", + "label": "Podoznake" + }, + "sort": { + "relevance": "Ustreznost", + "dateAsc": "Datum (naraščajoče)", + "label": "Sortiraj", + "dateDesc": "Datum (Padajoče)", + "scoreAsc": "Ocena Predmeta (Naraščajoče)", + "scoreDesc": "Ocena predmeta (Padajoče)", + "speedAsc": "Ocenjena Hitrost (Naraščajoče)", + "speedDesc": "Ocenjena Hitrost (Padajoče)" + }, + "zones": { + "label": "Cone", + "all": { + "title": "Vse cone", + "short": "Cone" + } + }, + "timeRange": "Časovno obdobje", + "reset": { + "label": "Ponastavi filtre na privzete vrednosti" + }, + "logSettings": { + "disableLogStreaming": "Izklopite zapisovanje dnevnika", + "allLogs": "Vsi dnevniki", + "label": "Level Filtra Dnevnika", + "filterBySeverity": "Filtriraj dnevnike po resnosti", + "loading": { + "title": "Nalaganje", + "desc": "Ko se podokno dnevnika pomakne čisto na dno, se novi dnevniki samodejno prikažejo, ko so dodani." + } + }, + "trackedObjectDelete": { + "title": "Potrdite brisanje", + "desc": "Izbris teh {{objectLength}} sledenih predmetov odstrani pripadajoče slikovne posneteke, shranjene vstavke in povezane vnose življenskega cikla predmetov. Posnetki teh sledenih predmetov v pogledu Zgodovina se NE bodo izbrisali.

    Ste prepričani, da želite nadaljevati?

    Pritisnite tipko Shift , da v prihodnje preskočite dialog.", + "toast": { + "success": "Uspešno izbrisani sledeni predmeti.", + "error": "Ni uspelo izbrisati sledenih predmetov: {{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "Filtrirajte po maski območja" + }, + "classes": { + "label": "Razredi", + "all": { + "title": "Vsi Razredi" + }, + "count_one": "{{count}} Razred", + "count_other": "{{count}} Razredov" + }, + "score": "Ocena", + "estimatedSpeed": "Ocenjena Hitrost ({{unit}})", + "features": { + "label": "Lastnosti", + "hasSnapshot": "Ima sliko", + "hasVideoClip": "Ima posnetek", + "submittedToFrigatePlus": { + "label": "Poslano na Frigate+", + "tips": "Najprej morate filtrirati po sledenih objektih, ki imajo sliko.

    Slednih objektov brez slike ni mogoče poslati v Frigate+." + } + }, + "cameras": { + "label": "Filtri Kamere", + "all": { + "title": "Vse Kamere", + "short": "Kamere" + } + }, + "review": { + "showReviewed": "Prikaži Pregledano" + }, + "motion": { + "showMotionOnly": "Prikaži Samo Gibanje" + }, + "recognizedLicensePlates": { + "title": "Prepoznane Registrske Tablice", + "loadFailed": "Prepoznanih registrskih tablic ni bilo mogoče naložiti.", + "loading": "Nalaganje prepoznanih registrskih tablic…", + "placeholder": "Iskanje registrskih tablic…", + "noLicensePlatesFound": "Nobena registrska tablica ni bila najdena.", + "selectPlatesFromList": "Na seznamu izberite eno ali več registrskih tablic.", + "selectAll": "Izberi vse", + "clearAll": "Počisti vse" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/icons.json new file mode 100644 index 0000000..94e9743 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Izberite ikono", + "search": { + "placeholder": "Išči ikono .…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/input.json new file mode 100644 index 0000000..820677c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Prenesi video", + "toast": { + "success": "Izbrani posnetek se je začel prenašati." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/sl/components/player.json new file mode 100644 index 0000000..cc144a5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Posnetki niso bili najdeni", + "noPreviewFound": "Predogled ni bil najden", + "noPreviewFoundFor": "Predogled za {{cameraName}} ni na voljo", + "submitFrigatePlus": { + "title": "Želite poslati ta okvir na Frigate+?", + "submit": "Pošlji" + }, + "stats": { + "streamType": { + "title": "Tip pretoka:", + "short": "Tip" + }, + "bandwidth": { + "title": "Pasovna širina:", + "short": "Pasovna širina" + }, + "latency": { + "value": "{{seconds}} sekund", + "title": "Zakasnitev:", + "short": { + "value": "{{seconds}} s", + "title": "Zakasnitev" + } + }, + "totalFrames": "Skupno število sličic:", + "droppedFrames": { + "title": "Izpuščene sličice:", + "short": { + "title": "Izpuščeno", + "value": "{{droppedFrames}} sličic" + } + }, + "decodedFrames": "Dekodirane sličice:", + "droppedFrameRate": "Stopnja izpuščenih sličic:" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 je zahteven za ta tip pretoka.", + "streamOffline": { + "title": "Pretok ni na voljo", + "desc": "Na toku detect kamere {{cameraName}} ni bilo prejetih nobenih sličic, preverite dnevnik napak" + }, + "cameraDisabled": "Kamera je onemogočena", + "toast": { + "success": { + "submittedFrigatePlus": "Sličica je bila uspešno poslana v Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Pošiljanje sličice v Frigate+ ni uspelo" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/objects.json b/sam2-cpu/frigate-dev/web/public/locales/sl/objects.json new file mode 100644 index 0000000..19b21bf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/objects.json @@ -0,0 +1,120 @@ +{ + "cat": "Maček", + "sheep": "Ovca", + "bird": "Ptič", + "animal": "Žival", + "goat": "Koza", + "horse": "Konj", + "dog": "Pes", + "skis": "Smuči", + "surfboard": "Surf", + "laptop": "Prenosnik", + "tennis_racket": "Teniški lopar", + "deer": "Srna", + "waste_bin": "Koš za smeti", + "skateboard": "Skejt", + "baseball_glove": "Bejzbol rokavica", + "snowboard": "Snežna deska", + "bottle": "Flaša", + "squirrel": "Veverica", + "raccoon": "Rakun", + "robot_lawnmower": "Robotska kosilnica", + "person": "Oseba", + "bicycle": "Kolo", + "car": "Avto", + "motorcycle": "Motor", + "airplane": "Letalo", + "bus": "Avtobus", + "train": "Vlak", + "boat": "Ladja", + "traffic_light": "Semafor", + "fire_hydrant": "Hidrant", + "street_sign": "Prometni znak", + "stop_sign": "Stop znak", + "parking_meter": "Parkomat", + "bench": "Klop", + "cow": "Krava", + "elephant": "Slon", + "bear": "Medved", + "zebra": "Zebra", + "giraffe": "Žirafa", + "hat": "Kapa", + "backpack": "Nahrbtnik", + "umbrella": "Dežnik", + "shoe": "Čevelj", + "eye_glasses": "Očala", + "handbag": "Torbica", + "tie": "Kravata", + "suitcase": "Aktovka", + "frisbee": "Frizbi", + "sports_ball": "Žoga", + "kite": "Kajt", + "baseball_bat": "Bejzbol kij", + "plate": "Pladenj", + "wine_glass": "Kozarec za vino", + "cup": "Šalica", + "fork": "Vilica", + "knife": "Nož", + "spoon": "Žlica", + "bowl": "Skleda", + "banana": "Banana", + "apple": "Jabolka", + "sandwich": "Sendvič", + "orange": "Pomaranča", + "broccoli": "Brokoli", + "carrot": "Korenček", + "hot_dog": "Hot dog", + "pizza": "Pica", + "donut": "Krof", + "cake": "Torta", + "chair": "Stol", + "couch": "Kavč", + "potted_plant": "Lončnica", + "bed": "Postelja", + "mirror": "Ogledalo", + "dining_table": "Jedilna miza", + "window": "Okno", + "desk": "Miza", + "toilet": "Stranišče", + "door": "Vrata", + "tv": "Televizija", + "mouse": "Miš", + "remote": "Daljinec", + "keyboard": "Tipkovnica", + "cell_phone": "Telefon", + "microwave": "Mikrovalovna pečica", + "oven": "Pečica", + "toaster": "Opekač", + "sink": "Umivalnik", + "refrigerator": "Zmrzovalnik", + "blender": "Sekljalnik", + "book": "Knjiga", + "clock": "Ura", + "vase": "Vaza", + "scissors": "Škarje", + "teddy_bear": "Plišasti medvedek", + "hair_dryer": "Fen", + "toothbrush": "Ščetka za zobe", + "hair_brush": "Krtača za lase", + "vehicle": "Prevozno sredstvo", + "bark": "Lajanje", + "fox": "Lisica", + "rabbit": "Zajec", + "on_demand": "Na Zahtevo", + "face": "Obraz", + "license_plate": "Registerska tablica", + "package": "Paket", + "bbq_grill": "Roštilj", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Čistilec", + "postnl": "PostNL", + "nzpost": "NSPost", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/classificationModel.json new file mode 100644 index 0000000..aceedb0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/classificationModel.json @@ -0,0 +1,50 @@ +{ + "description": { + "invalidName": "Neveljavno ime. Ime lahko vsebuje črke, števila, presledke, narekovaje, podčrtaje in pomišljaje." + }, + "categories": "Razredi", + "createCategory": { + "new": "Naredi nov razred" + }, + "button": { + "renameCategory": "Preimenuj razred", + "deleteCategory": "Zbriši razred", + "deleteImages": "Zbriši slike", + "trainModel": "Treniraj model" + }, + "toast": { + "success": { + "deletedCategory": "Izbrisan razred", + "deletedImage": "Zbrisane slike", + "trainedModel": "Uspešno treniranje modela.", + "trainingModel": "Uspešen začetek treniranje modela." + }, + "error": { + "deleteImageFailed": "Neuspešno brisanje: {{errorMessage}}", + "deleteCategoryFailed": "Neuspešno brisanje razreda: {{errorMessage}}", + "trainingFailed": "Neuspešen začetek treniranje modela: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Zbriši razred" + }, + "deleteTrainImages": { + "title": "Zbriši slike za treniranje", + "desc": "Ali ste prepričani, da želite izbrisati {{count}} slik? Tega dejanja ni mogoče razveljaviti." + }, + "renameCategory": { + "title": "Preimenuj razred", + "desc": "Vnesite novo ime za {{name}}. Model bo treba znova naučiti, da bo sprememba imena začela veljati." + }, + "train": { + "title": "Nedavne razvrstitve", + "aria": "Izberi nedavne razvrstitve" + }, + "categorizeImageAs": "Razvrsti sliko kot:", + "categorizeImage": "Razvrsti sliko", + "noModels": { + "object": { + "title": "Ni modelov za razvrščanje objektov" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/configEditor.json new file mode 100644 index 0000000..5c69cc1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "Urejevalnik konfiguracij - Frigate", + "configEditor": "Urejevalnik konfiguracij", + "copyConfig": "Kopiraj konfiguracijo", + "saveAndRestart": "Shrani & ponovno zaženi", + "saveOnly": "Shani", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija kopirana v odložišče." + }, + "error": { + "savingError": "Napaka pri shranjevanju konfiguracije" + } + }, + "confirm": "Izhod brez shranjevanja?", + "safeConfigEditor": "Urejevalnik konfiguracij (Varni Način)", + "safeModeDescription": "Frigate je v varnem načinu zaradi napake pri preverjanju konfiguracije." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/events.json new file mode 100644 index 0000000..a0570b9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/events.json @@ -0,0 +1,38 @@ +{ + "detected": "zaznanih", + "events": { + "noFoundForTimePeriod": "Za to časovno obdobje ni bilo najdenih dogodkov.", + "label": "Dogodki", + "aria": "Izberi dogodke" + }, + "allCameras": "Vse kamere", + "empty": { + "motion": "Ni najdenih podatkov o gibanju", + "alert": "Ni opozoril za pregled", + "detection": "Ni zaznanih elementov za pregled" + }, + "recordings": { + "documentTitle": "Posnetki - Frigate" + }, + "camera": "Kamera", + "documentTitle": "Pregled - Frigate", + "alerts": "Opozorila", + "detections": "Zaznavanja", + "motion": { + "label": "Premik", + "only": "Samo premik" + }, + "timeline": "Časovnica", + "timeline.aria": "Izberi časovnico", + "calendarFilter": { + "last24Hours": "Zadnjih 24 ur" + }, + "markAsReviewed": "Označi kot Pregledano", + "markTheseItemsAsReviewed": "Označi te elemente kot pregledane", + "newReviewItems": { + "label": "Ogled novih elementov za pregled", + "button": "Novi elementi za pregled" + }, + "selected_one": "{{count}} izbranih", + "selected_other": "{{count}} izbranih" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/explore.json new file mode 100644 index 0000000..70fee30 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/explore.json @@ -0,0 +1,209 @@ +{ + "exploreIsUnavailable": { + "title": "Funkcija razišči ni na voljo", + "downloadingModels": { + "setup": { + "visionModel": "Model vida", + "visionModelFeatureExtractor": "Pridobivanje lastnosti modela vida", + "textModel": "Besedilni model", + "textTokenizer": "Tokenizator besedila" + }, + "context": "Frigate prenaša potrebne modele vdelave za podporo funkcije semantičnega iskanja. To lahko traja nekaj minut, odvisno od hitrosti vaše omrežne povezave.", + "tips": { + "context": "Morda boste želeli ponovno indeksirati vdelave (embeddings) svojih sledenih objektov, ko bodo modeli preneseni.", + "documentation": "Preberi dokumentacijo" + }, + "error": "Prišlo je do napake. Preverite dnevnike Frigate." + }, + "embeddingsReindexing": { + "step": { + "descriptionsEmbedded": "Vdelani opisi: ", + "trackedObjectsProcessed": "Obdelani sledeni predmeti: ", + "thumbnailsEmbedded": "Vdelane sličice: " + }, + "context": "Funkcija Explore se lahko uporablja, ko je ponovno indeksiranje vgraditev(embeddings) sledenih objektov končano.", + "startingUp": "Zagon…", + "estimatedTime": "Ocenjeni preostali čas:", + "finishingShortly": "Kmalu končano" + } + }, + "documentTitle": "Razišči - Frigate", + "generativeAI": "Generativna UI", + "exploreMore": "Razišči več {{label}} objektov", + "details": { + "button": { + "regenerate": { + "label": "Regeneriraj opise sledenih predmetov", + "title": "Regeneriraj" + }, + "findSimilar": "Najdi podobno" + }, + "camera": "Kamera", + "estimatedSpeed": "Ocenjena hitrost", + "description": { + "placeholder": "Opis sledenega predmeta", + "label": "Opis", + "aiTips": "Frigate od vašega ponudnika generativne UI ne bo zahteval opisa, dokler se življenjski cikel sledenega objekta ne konča." + }, + "recognizedLicensePlate": "Prepoznana registrska tablica", + "objects": "Predmeti", + "zones": "Območja", + "timestamp": "Časovni žig", + "item": { + "button": { + "share": "Deli ta element mnenja", + "viewInExplore": "Poglej v Razišči Pogledu" + }, + "tips": { + "hasMissingObjects": "Prilagodite konfiguracijo, če želite, da Frigate shranjuje sledene objekte za naslednje oznake: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "Od ponudnika {{provider}} je bil zahtevan nov opis. Glede na hitrost vašega ponudnika lahko regeneracija novega opisa traja nekaj časa.", + "updatedSublabel": "Podoznaka je bila uspešno posodobljena.", + "updatedLPR": "Registrska tablica je bila uspešno posodobljena.", + "audioTranscription": "Zahteva za zvočni prepis je bila uspešno izvedena." + }, + "error": { + "regenerate": "Klic ponudniku {{provider}} za nov opis ni uspel: {{errorMessage}}", + "updatedSublabelFailed": "Posodobitev podoznake ni uspela: {{errorMessage}}", + "updatedLPRFailed": "Posodobitev registrske tablice ni uspela: {{errorMessage}}", + "audioTranscription": "Zahteva za prepis zvoka ni uspela: {{errorMessage}}" + } + }, + "title": "Preglej Podrobnosti Elementa", + "desc": "Preglej podrobnosti elementa" + }, + "label": "Oznaka", + "editSubLabel": { + "title": "Uredi podoznako", + "desc": "Vnesite novo podoznako za {{label}}", + "descNoLabel": "Vnesite novo podoznako za ta sledeni objekt" + }, + "editLPR": { + "title": "Uredi registrsko tablico", + "desc": "Vnesite novo vrednost registrske tablice za {{label}}", + "descNoLabel": "Vnesite novo vrednost registrske tablice za ta sledeni objekt" + }, + "snapshotScore": { + "label": "Ocena Slike" + }, + "topScore": { + "label": "Najboljša Ocena", + "info": "Najboljša ocena je najvišji mediani rezultat za sledeni objekt, zato se lahko razlikuje od rezultata, prikazanega na sličici rezultata iskanja." + }, + "expandRegenerationMenu": "Razširi meni regeneracije", + "tips": { + "descriptionSaved": "Opis uspešno shranjen", + "saveDescriptionFailed": "Opisa ni bilo mogoče posodobiti: {{errorMessage}}" + } + }, + "itemMenu": { + "findSimilar": { + "aria": "Najdi podobne sledene predmete", + "label": "Najdi podobno" + }, + "submitToPlus": { + "label": "Predloži v Frigate+", + "aria": "Predloži v Frigate Plus" + }, + "viewInHistory": { + "label": "Poglej v zgodovini", + "aria": "Poglej v zgodovini" + }, + "deleteTrackedObject": { + "label": "Izbriši ta sledeni predmet" + }, + "viewObjectLifecycle": { + "aria": "Pokaži življenjski cikel predmeta", + "label": "Poglej življenjski cikel predmeta" + }, + "downloadVideo": { + "label": "Prenesi video", + "aria": "Prenesi video" + }, + "downloadSnapshot": { + "label": "Prenesi posnetek", + "aria": "Prenesi posnetek" + }, + "addTrigger": { + "label": "Dodaj sprožilec", + "aria": "Dodaj sprožilec za ta sledeni objekt" + }, + "audioTranscription": { + "label": "Prepis", + "aria": "Zahtevajte prepis zvoka" + } + }, + "dialog": { + "confirmDelete": { + "title": "Potrdi brisanje" + } + }, + "trackedObjectDetails": "Podrobnosti Sledenega Objekta", + "type": { + "details": "podrobnosti", + "snapshot": "posnetek", + "video": "video", + "object_lifecycle": "življenjski cikel objekta" + }, + "objectLifecycle": { + "title": "Življenjski Cikel Objekta", + "noImageFound": "Za ta čas ni bila najdena nobena slika.", + "createObjectMask": "Ustvarite Masko Objekta", + "adjustAnnotationSettings": "Prilagodi nastavitve opomb", + "scrollViewTips": "Pomaknite se, da si ogledate pomembne trenutke življenjskega cikla tega predmeta.", + "count": "{{first}} od {{second}}", + "trackedPoint": "Sledena točka", + "lifecycleItemDesc": { + "visible": "{{label}} zaznan", + "entered_zone": "{{label}} je vstopil/a v {{zones}}", + "active": "{{label}} je postal aktiven", + "stationary": "{{label}} je postal nepremičen", + "attribute": { + "faceOrLicense_plate": "{{attribute}} je bil zaznan za {{label}}", + "other": "{{label}} zaznan kot {{attribute}}" + }, + "gone": "{{label}} levo", + "heard": "{{label}} slišano", + "external": "{{label}} zaznan", + "header": { + "zones": "Cone", + "ratio": "Razmerje", + "area": "Območje" + } + }, + "annotationSettings": { + "title": "Nastavitve Anotacij", + "showAllZones": { + "title": "Prikaži Vse Cone", + "desc": "Vedno prikaži območja na okvirjih, kjer so predmeti vstopili v območje." + }, + "offset": { + "label": "Anotacijski Odmik", + "documentation": "Preberi dokumentacijo ", + "millisecondsToOffset": "Odmik zaznanih anotacij v milisekundah. Privzeto: 0", + "tips": "NASVET: Predstavljajte si posnetek dogodka, v katerem oseba hodi od leve proti desni. Če je okvir dogodka na časovnici preveč levo od osebe, je treba vrednost zmanjšati. Podobno je treba vrednost povečati, če oseba hodi od leve proti desni in je okvir preveč pred njo.", + "toast": { + "success": "Odmik anotacij za {{camera}} je bil shranjen v konfiguracijsko datoteko. Znova zaženite Frigate, da uveljavite spremembe." + } + } + }, + "carousel": { + "previous": "Prejšnji diapozitiv", + "next": "Naslednji diapozitiv" + }, + "autoTrackingTips": "Položaji okvirjev bodo za kamere s samodejnim sledenjem netočni." + }, + "noTrackedObjects": "Ni Najdenih Sledenih Objektov", + "fetchingTrackedObjectsFailed": "Napaka pri pridobivanju sledenih objektov: {{errorMessage}}", + "searchResult": { + "tooltip": "Ujemanje {{type}} pri {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Sledeni objekt je bil uspešno izbrisan.", + "error": "Brisanje sledenega predmeta ni uspelo: {{errorMessage}}" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/exports.json new file mode 100644 index 0000000..59ca521 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Iskanje", + "noExports": "Izovzi niso najdeni", + "deleteExport": "Izbriši izvoz", + "deleteExport.desc": "Ali ste prepričani, da želite izbrisati {{exportName}}?", + "editExport": { + "title": "Preimenuj izvoz", + "desc": "Vpišite novo ime za ta izvoz.", + "saveExport": "Shrani izvoz" + }, + "toast": { + "error": { + "renameExportFailed": "Napaka pri preimenovanju izvoza: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/faceLibrary.json new file mode 100644 index 0000000..c41520f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/faceLibrary.json @@ -0,0 +1,105 @@ +{ + "description": { + "addFace": "Dodajanje nove zbirke v knjižnico obrazov z nalaganjem slike.", + "placeholder": "Vnesite ime za to zbirko", + "invalidName": "Neveljavno ime. Ime lahko vsebuje črke, števila, presledke, narekovaje, podčrtaje in pomišljaje." + }, + "details": { + "person": "Oseba", + "unknown": "Nenznano", + "timestamp": "Časovni žig", + "subLabelScore": "Ocena Podoznake", + "scoreInfo": "Rezultat podoznake je utežena ocena vseh stopenj gotovosti prepoznanih obrazov, zato se lahko razlikuje od ocene, prikazane na posnetku.", + "face": "Podrobnosti Obraza", + "faceDesc": "Podrobnosti sledenega objekta, ki je ustvaril ta obraz" + }, + "uploadFaceImage": { + "title": "Naloži nov obraz", + "desc": "Naloži sliko za iskanje obrazov in vključitev v {{pageToggle}}" + }, + "deleteFaceAttempts": { + "desc_one": "Ali ste prepričani, da želite izbrisati {{count}} obraz? Tega dejanja ni mogoče razveljaviti.", + "desc_two": "Ali ste prepričani, da želite izbrisati {{count}} obraza? Tega dejanja ni mogoče razveljaviti.", + "desc_few": "Ali ste prepričani, da želite izbrisati {{count}} obraze? Tega dejanja ni mogoče razveljaviti.", + "desc_other": "Ali ste prepričani, da želite izbrisati {{count}} obrazov? Tega dejanja ni mogoče razveljaviti.", + "title": "Izbriši Obraze" + }, + "toast": { + "success": { + "deletedFace_one": "Uspešno izbrisan {{count}} obraz.", + "deletedFace_two": "Uspešno izbrisana {{count}} obraza.", + "deletedFace_few": "Uspešno izbrisani {{count}} obrazi.", + "deletedFace_other": "Uspešno izbrisanih {{count}} obrazov.", + "deletedName_one": "{{count}} je bil uspešno izbrisan.", + "deletedName_two": "{{count}} obraza sta bila uspešno izbrisana.", + "deletedName_few": "{{count}} obrazi so bili uspešno izbrisani.", + "deletedName_other": "{{count}} obrazov je bilo uspešno izbrisanih.", + "uploadedImage": "Slika je bila uspešno naložena.", + "addFaceLibrary": "Oseba {{name}} je bila uspešno dodana v Knjižnico Obrazov!", + "renamedFace": "Obraz uspešno preimenovan v {{name}}", + "trainedFace": "Uspešno treniran obraz.", + "updatedFaceScore": "Ocena obraza je bila uspešno posodobljena." + }, + "error": { + "uploadingImageFailed": "Nalaganje slike ni uspelo: {{errorMessage}}", + "addFaceLibraryFailed": "Neuspešno nastavljanje imena obraza: {{errorMessage}}", + "deleteFaceFailed": "Brisanje ni uspelo: {{errorMessage}}", + "deleteNameFailed": "Brisanje imena ni uspelo: {{errorMessage}}", + "renameFaceFailed": "Preimenovanje obraza ni uspelo: {{errorMessage}}", + "trainFailed": "Treniranje ni uspelo: {{errorMessage}}", + "updateFaceScoreFailed": "Posodobitev ocene obraza ni uspela: {{errorMessage}}" + } + }, + "documentTitle": "Knjižnica obrazov - Frigate", + "collections": "Zbirke", + "createFaceLibrary": { + "title": "Ustvari Zbirko", + "desc": "Ustvari novo zbirko", + "new": "Ustvari Nov Obraz", + "nextSteps": "Za vzpoztavitev trdnih osnov:
  • V zavihku Nedavne prepoznave izberi in uporabi slike za učenje vsake zaznane osebe.
  • Za najboljše rezultate se osredotoči na slike, kjer je obraz obrnjen naravnost; izogibaj se slikam, na katerih so obrazi posneti pod kotom.
  • " + }, + "steps": { + "faceName": "Vnesi Ime Obraza", + "uploadFace": "Naloži Sliko Obraza", + "nextSteps": "Naslednji koraki", + "description": { + "uploadFace": "Naložite sliko osebe {{name}}, ki prikazuje obraz (slikan naravnost in ne iz kota). Slike ni treba obrezati samo na obraz." + } + }, + "train": { + "title": "Nedavne prepoznave", + "aria": "Izberite nedavne prepoznave", + "empty": "Ni nedavnih poskusov prepoznavanja obrazov" + }, + "selectItem": "Izberi {{item}}", + "selectFace": "Izberi Obraz", + "deleteFaceLibrary": { + "title": "Izbriši Ime", + "desc": "Ali ste prepričani, da želite izbrisati zbirko {{name}}? S tem boste trajno izbrisali vse povezane obraze." + }, + "renameFace": { + "title": "Preimenuj Obraz", + "desc": "Vnesi novo ime za {{name}}" + }, + "button": { + "deleteFaceAttempts": "Izbriši Obraze", + "addFace": "Dodaj Obraz", + "renameFace": "Preimenuj Obraz", + "deleteFace": "Izbriši Obraz", + "uploadImage": "Naloži Sliko", + "reprocessFace": "Ponovna Obdelava Obraza" + }, + "imageEntry": { + "validation": { + "selectImage": "Izberite slikovno datoteko." + }, + "dropActive": "Sliko spustite tukaj…", + "dropInstructions": "Povlecite in spustite ali prilepite sliko sem ali kliknite za izbiro", + "maxSize": "Največja velikost: {{size}}MB" + }, + "nofaces": "Noben obraz ni na voljo", + "pixels": "{{area}}px", + "readTheDocs": "Preberi dokumentacijo", + "trainFaceAs": "Treniraj obraz kot:", + "trainFace": "Treniraj Obraz" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/live.json new file mode 100644 index 0000000..5b52618 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/live.json @@ -0,0 +1,171 @@ +{ + "documentTitle": "V živo - Frigate", + "documentTitle.withCamera": "{{camera}} - v živo - Frigate", + "lowBandwidthMode": "Nizkopasovni način", + "twoWayTalk": { + "enable": "Omogoči dvosmerni pogovor", + "disable": "Onemogoči Obojesmerni Pogovor" + }, + "ptz": { + "move": { + "clickMove": { + "disable": "Onemogoči funkcijo klikni in premakni", + "label": "Kliknite v okvir, da postavite kamero na sredino", + "enable": "Omogoči premik s klikom" + }, + "left": { + "label": "Premakni PTZ kamero v levo" + }, + "up": { + "label": "Premakni PTZ kamero gor" + }, + "down": { + "label": "Premakni PTZ kamero navzdol" + }, + "right": { + "label": "Premakni PTZ kamero desno" + } + }, + "zoom": { + "in": { + "label": "Povečaj PTZ kamero" + }, + "out": { + "label": "Pomanjšaj PTZ kamero" + } + }, + "focus": { + "in": { + "label": "Izostri PTZ kamero" + }, + "out": { + "label": "Razostri PTZ kamero" + } + }, + "frame": { + "center": { + "label": "Kliknite v okvir, da postavite PTZ kamero na sredino" + } + }, + "presets": "Prednastavitve PTZ kamere" + }, + "cameraAudio": { + "enable": "Omogoči Zvok Kamere", + "disable": "Onemogoči Zvok Kamere" + }, + "camera": { + "enable": "Omogoči Kamero", + "disable": "Onemogoči Kamero" + }, + "muteCameras": { + "enable": "Utišaj vse kamere", + "disable": "Vklopi Zvok Vsem Kameram" + }, + "detect": { + "enable": "Omogoči Detekcijo", + "disable": "Onemogoči Detekcijo" + }, + "recording": { + "enable": "Omogoči Snemanje", + "disable": "Onemogoči Snemanje" + }, + "snapshots": { + "enable": "Omogoči Slike", + "disable": "Onemogoči Slike" + }, + "audioDetect": { + "enable": "Omogoči Zvočno Detekcijo", + "disable": "Onemogoči Zvočno Detekcijo" + }, + "transcription": { + "enable": "Omogoči Prepisovanje Zvoka v Živo", + "disable": "Onemogoči Prepisovanje Zvoka v Živo" + }, + "autotracking": { + "enable": "Omogoči Samodejno Sledenje", + "disable": "Onemogoči Samodejno Sledenje" + }, + "streamStats": { + "enable": "Prikaži Statistiko Pretočnega Predvajanja", + "disable": "Skrij Statistiko Pretočnega Predvajanja" + }, + "manualRecording": { + "title": "Snemanje na Zahtevo", + "tips": "Začni ročni dogodek na podlagi nastavitev hranjenja posnetkov te kamere.", + "playInBackground": { + "label": "Predvajaj v ozadju", + "desc": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." + }, + "showStats": { + "label": "Prikaži Statistiko", + "desc": "Omogočite to možnost, če želite statistiko pretoka prikazati kot prekrivni sloj na viru kamere." + }, + "debugView": "Pogled za Odpravljanje Napak", + "start": "Začni snemanje na zahtevo", + "started": "Začelo se je ročno snemanje na zahtevo.", + "failedToStart": "Ročnega snemanja na zahtevo ni bilo mogoče začeti.", + "recordDisabledTips": "Ker je snemanje v nastavitvah te kamere onemogočeno ali omejeno, bo shranjena samo slika.", + "end": "Končaj snemanje na zahtevo", + "ended": "Ročno snemanje na zahtevo je končano.", + "failedToEnd": "Ročnega snemanja na zahtevo ni bilo mogoče končati." + }, + "streamingSettings": "Nastavitve Pretakanja", + "notifications": "Obvestila", + "audio": "Zvok", + "suspend": { + "forTime": "Začasno ustavi za: " + }, + "stream": { + "title": "Pretok", + "audio": { + "tips": { + "title": "Zvok mora biti predvajan iz vaše kamere in konfiguriran v go2rtc za ta pretok.", + "documentation": "Preberi Dokumentacijo " + }, + "available": "Za ta pretok je na voljo zvok", + "unavailable": "Zvok za ta pretok ni na voljo" + }, + "twoWayTalk": { + "tips": "Vaša naprava mora podpirati to funkcijo, WebRTC pa mora biti konfiguriran za dvosmerni pogovor.", + "tips.documentation": "Preberi dokumentacijo ", + "available": "Za ta tok je na voljo dvosmerni pogovor", + "unavailable": "Dvosmerni pogovor ni na voljo za ta pretok" + }, + "lowBandwidth": { + "tips": "Pogled v živo je v načinu nizke pasovne širine zaradi napak v nalaganju ali pretoku.", + "resetStream": "Ponastavi pretok" + }, + "playInBackground": { + "label": "Predvajaj v ozadju", + "tips": "Omogočite to možnost, če želite nadaljevati s pretakanjem, ko je predvajalnik skrit." + } + }, + "cameraSettings": { + "title": "{{camera}} Nastavitve", + "cameraEnabled": "Kamera Omogočena", + "objectDetection": "Zaznavanje Objektov", + "recording": "Snemanje", + "snapshots": "Slike", + "audioDetection": "Zvočna Detekcija", + "transcription": "Zvočni Prepis", + "autotracking": "Samodejno Sledenje" + }, + "history": { + "label": "Prikaži stare posnetke" + }, + "effectiveRetainMode": { + "modes": { + "all": "Vse", + "motion": "Gibanje", + "active_objects": "Aktivni Objekti" + }, + "notAllTips": "Vaša konfiguracija hranjenja posnetkov {{source}} je nastavljena na način : {{effectiveRetainMode}}, zato bo ta posnetek na zahtevo hranil samo segmente z {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Uredi Postavitev", + "group": { + "label": "Uredi Skupino Kamere" + }, + "exitEdit": "Izhod iz Urejanja" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/recording.json new file mode 100644 index 0000000..20dacb6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Izvoz", + "calendar": "Koledar", + "filters": "Filtri", + "toast": { + "error": { + "noValidTimeSelected": "Izbrano časovno obdobje ni veljavno", + "endTimeMustAfterStartTime": "Končen čas mora biti po začetnem času" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/search.json new file mode 100644 index 0000000..b2233e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Iskanje", + "savedSearches": "Shranjena iskanja", + "searchFor": "Iskanje za {{inputValue}}", + "button": { + "clear": "Izbriši iskanje", + "save": "Shrani iskanje", + "delete": "Izbriši shranjeno iskanje", + "filterInformation": "Informacije o filtru", + "filterActive": "Aktivirani filtri" + }, + "filter": { + "label": { + "cameras": "Kamere", + "labels": "Oznake", + "zones": "Območja", + "sub_labels": "Podoznake", + "search_type": "Tip iskanja", + "time_range": "Časovni razpon", + "before": "Pred", + "after": "Po", + "min_score": "Najmanj točk", + "max_score": "Največ točk", + "recognized_license_plate": "Prepoznana registrska tablica", + "has_clip": "Ima posnetek", + "max_speed": "Najvišja hitrost", + "min_speed": "Najnižja hitrost", + "has_snapshot": "Ima sliko" + }, + "searchType": { + "thumbnail": "Sličica", + "description": "Opis" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Datum »pred« mora biti poznejši od datuma »po«.", + "afterDatebeEarlierBefore": "Datum »po« mora biti zgodnejši od datuma »pred«.", + "minScoreMustBeLessOrEqualMaxScore": "Polje 'Najmanj točk' mora biti manjše ali enako polju 'Največ točk'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Polje 'Največ točk' mora biti večje ali enako polju 'Najmanj točk'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Polje 'Najvišja hitrost' mora biti večje ali enako polju 'Najnižja hitrost'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Polje 'Najnižja hitrost' mora biti manjše ali enako 'Najvišji hitrosti'." + } + }, + "tips": { + "title": "Kako uporabljati besedilne filtre", + "desc": { + "text": "Filtri vam pomagajo zožati rezultate iskanja. Tukaj je, kako jih uporabiti v vnosnem polju:", + "step1": "Vnesite ime ključa filtra, ki mu sledi dvopičje (npr. »kamere:«).", + "step2": "Izberite vrednost iz predlogov, ali vpišite svojo.", + "step3": "Uporabite več filtrov tako, da jih dodate enega za drugim s presledkom vmes.", + "step4": "Datumski filtri uporabljajo format: {{DateFormat}}.", + "step5": "Časovni filter uporablja format: {{exampleTime}}.", + "step6": "Filter izbrišete s klikom na 'x' poleg njih.", + "exampleLabel": "Primer:" + } + }, + "header": { + "currentFilterType": "Filtriraj vrednosti", + "noFilters": "Filtri", + "activeFilters": "Aktivni filtri" + } + }, + "trackedObjectId": "ID sledečega objekta", + "similaritySearch": { + "title": "Iskanje podobnosti", + "active": "Iskanje podobnosti je aktivno", + "clear": "Izbriši iskanje podobnosti" + }, + "placeholder": { + "search": "Iskanje …" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/settings.json new file mode 100644 index 0000000..d8eff4e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/settings.json @@ -0,0 +1,337 @@ +{ + "documentTitle": { + "default": "Nastavitve - Frigate", + "authentication": "Nastavitve preverjanja pristnosti - Frigate", + "camera": "Nastavitve kamere - Frigate", + "notifications": "Nastavitve obvestil - Frigate", + "masksAndZones": "Urejevalnik mask in območij - Frigate", + "object": "Odpravljanje napak - Frigate", + "general": "Splošne Nastavitve - Frigate", + "frigatePlus": "Frigate+ Nastavitve - Frigate", + "enrichments": "Nastavitve Obogatitev - Frigate", + "motionTuner": "Nastavitev gibanja - Frigate", + "cameraManagement": "Upravljaj kamere - Frigate", + "cameraReview": "Nastavitve pregleda kamer – Frigate" + }, + "menu": { + "ui": "Uporabniški vmesnik", + "enrichments": "Obogatitve", + "cameras": "Nastavitve Kamere", + "masksAndZones": "Maske / Cone", + "debug": "Razhroščevanje", + "users": "Uporabniki", + "notifications": "Obvestila", + "frigateplus": "Frigate+", + "motionTuner": "Nastavitev Gibanja", + "triggers": "Prožilniki", + "cameraManagement": "Upravljanje", + "cameraReview": "Pregled", + "roles": "Vloge" + }, + "masksAndZones": { + "zones": { + "point_one": "{{count}} točka", + "point_two": "{{count}} točki", + "point_few": "{{count}} točke", + "point_other": "{{count}} točk" + }, + "objectMasks": { + "point_one": "{{count}} točka", + "point_two": "{{count}} točki", + "point_few": "{{count}} točke", + "point_other": "{{count}} točk" + }, + "motionMasks": { + "point_one": "{{count}} točka", + "point_two": "{{count}} točki", + "point_few": "{{count}} točke", + "point_other": "{{count}} točk" + } + }, + "dialog": { + "unsavedChanges": { + "title": "Imate neshranjene spremembe.", + "desc": "Ali želite shraniti spremembe, preden nadaljujete?" + } + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Brez Kamere" + }, + "general": { + "title": "Splošne Nastavitve", + "liveDashboard": { + "title": "Nadzorna plošča (v živo)", + "automaticLiveView": { + "label": "Samodejni pogled v živo", + "desc": "Samodejno preklopite na pogled kamere v živo, ko je zaznana aktivnost. Če onemogočite to možnost, se statične slike kamere na nadzorni plošči v živo posodobijo le enkrat na minuto." + }, + "playAlertVideos": { + "label": "Predvajajte opozorilne videoposnetke", + "desc": "Privzeto se nedavna opozorila na nadzorni plošči predvajajo kot kratki ponavljajoči videoposnetki . To možnost onemogočite, če želite, da se v tej napravi/brskalniku prikaže samo statična slika nedavnih opozoril." + } + }, + "storedLayouts": { + "title": "Sharnjene Postavitve", + "desc": "Postaviteve kamer v skupini kamer je mogoče povleči/prilagoditi. Položaji so shranjeni v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Postavitve" + }, + "cameraGroupStreaming": { + "title": "Nastavitve Pretakanja Skupine Kamer", + "desc": "Nastavitve pretakanja za vsako skupino kamer so shranjene v lokalnem pomnilniku vašega brskalnika.", + "clearAll": "Počisti Vse Nastavitve Pretakanja" + }, + "recordingsViewer": { + "title": "Pregledovalnik Posnetkov", + "defaultPlaybackRate": { + "label": "Privzeta Hitrost Predvajanja", + "desc": "Privzeta Hitrost Predvajanja za Shranjene Posnetke." + } + }, + "calendar": { + "title": "Koledar", + "firstWeekday": { + "label": "Prvi dan v tednu", + "desc": "Dan, na katerega se začnejo tedni v koledarju za preglede.", + "sunday": "Nedelja", + "monday": "Ponedeljek" + } + }, + "toast": { + "success": { + "clearStoredLayout": "Shranjena postavitev za {{cameraName}} je bila izbrisana", + "clearStreamingSettings": "Nastavitve pretakanja za vse skupine kamer so bile izbrisane." + }, + "error": { + "clearStoredLayoutFailed": "Shranjene postavitve ni bilo mogoče izbrisati: {{errorMessage}}", + "clearStreamingSettingsFailed": "Nastavitev pretakanja ni bilo mogoče izbrisati: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "Nastavitve Obogatitev", + "unsavedChanges": "Neshranjene Spremembe Nastavitev Obogatitev", + "birdClassification": { + "title": "Klasifikacija ptic", + "desc": "Klasifikacija ptic identificira znane ptice z uporabo kvantiziranega Tensorflow modela. Ko je znana ptica prepoznana, se njeno splošno ime doda kot podoznaka. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila." + }, + "semanticSearch": { + "title": "Semantično Iskanje", + "desc": "Semantično iskanje v Frigate vam omogoča iskanje sledenih objektov znotraj vaših pregledov, pri čemer lahko uporabite izvorno sliko, uporabniško določen besedilni opis ali samodejno ustvarjen opis.", + "readTheDocumentation": "Preberi Dokumentacijo", + "reindexNow": { + "label": "Ponovno Indeksiraj Zdaj", + "desc": "Ponovno indeksiranje bo regeneriralo vdelave (embeddings) za vse sledene objekte. Ta postopek se izvaja v ozadju in lahko zelo obremeni vaš procesor ter traja precej časa, odvisno od števila sledenih objektov, ki jih imate.", + "confirmTitle": "Potrdi Ponovno Indeksiranje", + "confirmDesc": "Ali ste prepričani, da želite ponovno indeksirati vse vdelave (embeddings) sledenih objektov? Ta postopek se bo izvajal v ozadju, vendar lahko zelo obremeni vaš procesor in traja kar nekaj časa. Napredek si lahko ogledate na strani Razišči.", + "confirmButton": "Ponovno Indeksiranje", + "success": "Ponovno indeksiranje se je uspešno začelo.", + "alreadyInProgress": "Ponovno indeksiranje je že v teku.", + "error": "Ponovnega indeksiranja ni bilo mogoče začeti: {{errorMessage}}" + }, + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za vdelave (embeddings) semantičnih iskanj.", + "small": { + "title": "majhen", + "desc": "Uporaba načina small uporablja kvantizirano različico modela, ki porabi manj RAM-a in deluje hitreje na procesorju z zelo zanemarljivo razliko v kakovosti vdelave (embedding)." + }, + "large": { + "title": "velik", + "desc": "Uporaba možnosti large uporablja celoten model Jina in se bo, če je mogoče, samodejno izvajal na grafičnem procesorju." + } + } + }, + "faceRecognition": { + "title": "Prepoznavanje Obrazov", + "desc": "Prepoznavanje obrazov omogoča, da se ljudem dodelijo imena, in ko Frigate prepozna njihov obraz, se detekciji dodeli ime kot podoznako. Te informacije so vključene v uporabniški vmesnik, filtre in obvestila.", + "readTheDocumentation": "Preberi Dokumentacijo", + "modelSize": { + "label": "Velikost Modela", + "desc": "Velikost modela, uporabljenega za prepoznavanje obrazov.", + "small": { + "title": "majhen", + "desc": "Uporaba small uporablja model vdelave (embedding) obrazov FaceNet, ki učinkovito deluje na večini procesorjev." + }, + "large": { + "title": "velik", + "desc": "Uporaba large uporablja model vdelave (embedding) obrazov ArcFace in se bo samodejno zagnala na grafičnem procesorju, če bo to mogoče." + } + } + }, + "licensePlateRecognition": { + "title": "Prepoznavanje Registrskih Tablic", + "desc": "Frigate lahko prepozna registrske tablice na vozilih in samodejno doda zaznane znake v polje recognized_license_plate ali znano ime kot podoznako objektom tipa car. Pogost primer uporabe je lahko branje registrskih tablic avtomobilov, ki se ustavijo na dovozu, ali avtomobilov, ki se peljejo mimo po ulici.", + "readTheDocumentation": "Preberi Dokumentacijo" + }, + "restart_required": "Potreben je ponovni zagon (Nastavitve Obogatitve so bile spremenjene)", + "toast": { + "success": "Nastavitve Obogatitev so shranjene. Znova zaženite Frigate, da uveljavite spremembe.", + "error": "Shranjevanje sprememb konfiguracije ni uspelo: {{errorMessage}}" + } + }, + "camera": { + "title": "Nastavitve Kamere", + "streams": { + "title": "Pretoki" + }, + "object_descriptions": { + "title": "Opisi objektov z uporabo generativne UI", + "desc": "Začasno omogoči/onemogoči opise objektov z uporabo generativne UI za to kamero. Ko so onemogočeni, opisi, ki jih ustvari UI, ne bodo zahtevani za sledene objekte na tej kameri." + }, + "review": { + "title": "Pregled", + "desc": "Začasno omogoči/onemogoči opozorila in zaznavanja za to kamero, dokler se Frigate ne zažene znova. Ko je onemogočeno, ne bodo ustvarjeni novi elementi pregleda. ", + "alerts": "Opozorila ", + "detections": "Detekcije " + }, + "reviewClassification": { + "title": "Pregled Klasifikacij", + "readTheDocumentation": "Preberi Dokumentacijo", + "noDefinedZones": "Za to kamero ni določenih nobenih con.", + "objectAlertsTips": "Vsi objekti {{alertsLabels}} na {{cameraName}} bodo prikazani kot Opozorila.", + "unsavedChanges": "Neshranjene nastavitve Pregleda Klasifikacije za {{camera}}", + "selectAlertsZones": "Izberite cone za Opozorila", + "selectDetectionsZones": "Izberite cone za Zaznavanje", + "limitDetections": "Omejite zaznavanje na določene cone" + }, + "addCamera": "Dodaj Novo Kamero", + "editCamera": "Uredi Kamero:", + "selectCamera": "Izberi Kamero", + "backToSettings": "Nazaj na Nastavitve Kamere", + "cameraConfig": { + "add": "Dodaj Kamero", + "edit": "Uredi Kamero", + "description": "Konfigurirajte nastavitve kamere, vključno z pretočnimi vhodi in vlogami.", + "name": "Ime Kamere", + "nameRequired": "Ime kamere je obvezno", + "nameInvalid": "Ime kamere mora vsebovati samo črke, številke, podčrtaje ali vezaje", + "namePlaceholder": "npr. vhodna_vrata" + } + }, + "cameraWizard": { + "title": "Dodaj kamero", + "description": "Sledi spodnjim korakom, da dodaš novo kamero v svojo namestitev Frigate.", + "steps": { + "nameAndConnection": "Ime & Zbirka", + "streamConfiguration": "Konfiguracija pretoka", + "validationAndTesting": "Uverjanje in testiranje" + }, + "save": { + "success": "Kamera {{cameraName}} je bila uspešno shranjena.", + "failure": "Napaka pri shranjevanju {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Resolucija", + "video": "Video", + "audio": "Zvok", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Prosimo, vnesite veljaven URL pretoka", + "testFailed": "Preizkus pretoka ni uspel: {{error}}" + }, + "step1": { + "description": "Vnesite podatke vaše kamere in preizkusite povezavo.", + "cameraName": "Ime kamere", + "cameraNamePlaceholder": "npr. sprednja_vrata ali Pregled zadnjega dvorišča", + "host": "Gostitelj/IP naslov", + "port": "Vrata", + "username": "Uporabniško ime", + "usernamePlaceholder": "Opcijsko", + "password": "Geslo", + "passwordPlaceholder": "Opcijsko", + "selectTransport": "Izberi transportni protokol", + "cameraBrand": "Znamka kamere", + "selectBrand": "Izberi znamko kamere za predlogo URL-ja", + "customUrl": "Po meri URL za pretok", + "brandInformation": "Informacije o znamki", + "brandUrlFormat": "Za kamere z obliko URL-ja RTSP: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://uporabniškoime:geslo@gostitelj:vrata/pot", + "testConnection": "Preveri povezavo", + "testSuccess": "Test povezave uspešen!", + "testFailed": "Test povezave neuspešen. Prosim preveri vnos in poskusi še enkrat.", + "streamDetails": "Podrobnosti pretoka", + "testing": { + "probingMetadata": "Preiskovanje metapodatkov kamere...", + "fetchingSnapshot": "Pridobivanje posnetka kamere..." + }, + "warnings": { + "noSnapshot": "Ni mogoče pridobiti posnetka iz nastavljenega pretoka." + }, + "errors": { + "nameLength": "Ime kamere mora biti 64 znakov ali manj", + "invalidCharacters": "Ime kamere vsebuje neveljavne znake", + "nameExists": "Ime kamere že obstaja", + "customUrlRtspRequired": "URL-ji po meri se morajo začeti z \"rtsp://\". Za ne-RTSP pretoke kamer je potrebna ročna nastavitev.", + "brands": { + "reolink-rtsp": "RTSP za Reolink ni priporočen. \nV nastavitvah kamere omogočite HTTP in znova zaženite čarovnika." + } + } + }, + "step2": { + "streamUrlPlaceholder": "rtsp://uporabniskoime:geslo@gostitelj:vrata/pot", + "url": "URL", + "resolution": "Resolucija", + "selectResolution": "Izberi resolucijo", + "quality": "Kvaliteta", + "selectQuality": "Izberi kvaliteto", + "roles": "Vloge", + "roleLabels": { + "detect": "Prepoznavanje objektov", + "record": "Snemanje", + "audio": "Zvok" + }, + "testStream": "Preveri povezavo", + "testSuccess": "Test pretoka uspešen!", + "testFailed": "Test pretoka spodletel", + "testFailedTitle": "Test spodletel", + "connected": "Povezan", + "notConnected": "Ni povezave", + "featuresTitle": "Funkcije", + "go2rtc": "Zmanjšaj povezave na kamero", + "detectRoleWarning": "Vsaj en pretok mora imeti vlogo »zaznavanje«, da lahko nadaljuješ.", + "rolesPopover": { + "title": "Vloge pretoka", + "detect": "Glavni vir za zaznavanje objektov.", + "record": "Shranjuje odseke video posnetka glede na nastavitve konfiguracije.", + "audio": "Vir za zaznavanje na podlagi zvoka." + }, + "featuresPopover": { + "title": "Značilnosti pretoka", + "description": "Uporabi ponovno pretakanje go2rtc, da zmanjšaš število povezav s kamero." + } + }, + "step3": { + "description": "Končno preverjanje in analiza pred shranjevanjem nove kamere. Poveži vsak pretok, preden shranjuješ.", + "validationTitle": "Preverjanje pretoka", + "connectAllStreams": "Poveži vse pretoke", + "reconnectionSuccess": "Ponovna povezava uspešna.", + "reconnectionPartial": "Nekateri pretoki se niso ponovno povezali.", + "streamUnavailable": "Predogled pretoka ni na voljo", + "reload": "Ponovno naloži", + "connecting": "Povezujem...", + "streamTitle": "Pretok {{number}}", + "valid": "Veljaven", + "failed": "Spodletel", + "notTested": "Ni testiran", + "connectStream": "Poveži", + "connectingStream": "Povezujem", + "disconnectStream": "Prekini povezavo", + "estimatedBandwidth": "Predvidena pasovna širina", + "roles": "Vloge", + "none": "Noben", + "error": "Napaka", + "streamValidated": "Pretok {{number}} uspešno preverjen", + "streamValidationFailed": "Preverjanje pretoka {{number}} spodletelo", + "saveAndApply": "Shrani novo kamero", + "saveError": "Neveljavna konfiguracija. Prosimo preverite vaše nastavitve.", + "issues": { + "title": "Preverjanje pretoka", + "videoCodecGood": "Video kodek je {{codec}}.", + "audioCodecGood": "Audio kodek je {{codec}}.", + "resolutionHigh": "Resolucija {{resolution}} lahko povzroči povečano porabo virov." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sl/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/sl/views/system.json new file mode 100644 index 0000000..6562321 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sl/views/system.json @@ -0,0 +1,174 @@ +{ + "documentTitle": { + "cameras": "Statistika kamer - Frigate", + "storage": "Statistika prostora - Frigate", + "general": "Statistika - Frigate", + "logs": { + "frigate": "Frigate dnevniki - Frigate", + "go2rtc": "Go2RTC dnevniki - Frigate", + "nginx": "Nginx dnevniki - Frigate" + }, + "enrichments": "Statistika Obogatitev - Frigate" + }, + "logs": { + "download": { + "label": "Prenesi dnevnike" + }, + "copy": { + "label": "Kopiraj v odložišče", + "success": "Dnevniki kopirani v odložišče", + "error": "Dnevnika ni bilo mogoče kopirati v odložišče" + }, + "type": { + "label": "Tip", + "timestamp": "Časovni žig", + "message": "Sporočilo", + "tag": "Oznaka" + }, + "tips": "Dnevniki se pretakajo s strežnika", + "toast": { + "error": { + "fetchingLogsFailed": "Napaka pri pridobivanju dnevnikov: {{errorMessage}}", + "whileStreamingLogs": "Napaka med pretakanjem dnevnikov: {{errorMessage}}" + } + } + }, + "storage": { + "recordings": { + "title": "Posnetki", + "tips": "Ta vrednost predstavlja velikost podatkovne zbirke posnetkov Frigate. Frigate ne spremlja velikost drugih datotek na disku.", + "earliestRecording": "Najstarejši posnetki:" + }, + "title": "Hramba", + "overview": "Pregled", + "cameraStorage": { + "title": "Hramba kamer", + "camera": "Kamera", + "unusedStorageInformation": "Informacija neporabljenega prostora", + "storageUsed": "Hramba", + "percentageOfTotalUsed": "Procent celote", + "bandwidth": "Pasovna širina", + "unused": { + "title": "Neporabljeno", + "tips": "Ta vrednost ne predstavlja dejanske proste kapacitete za Frigate posnetke, če na disku shranjujete še druge datoteke. Frigate ne spremlja velikost drugih datotek na disku." + } + } + }, + "general": { + "hardwareInfo": { + "npuMemory": "Pomnilnik NPE", + "title": "Podatki strojne opreme", + "gpuUsage": "Poraba GPE", + "gpuMemory": "Pomnilnik GPE", + "gpuEncoder": "GPE kodirnik", + "gpuDecoder": "GPE dekoder", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo izpis", + "returnCode": "Povratna koda: {{code}}", + "processOutput": "Izpis procesa:", + "processError": "Napaka procesa:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI izpis", + "name": "Ime: {{name}}", + "driver": "Gonilnik: {{driver}}", + "cudaComputerCapability": "Zmožnost računanja CUDA: {{cuda_compute}}", + "vbios": "VBios info: {{vbios}}" + }, + "closeInfo": { + "label": "Zapri GPU info" + }, + "copyInfo": { + "label": "Kopiraj GPU info" + }, + "toast": { + "success": "GPU informacije kopirane v odložišče" + } + }, + "npuUsage": "Poraba NPE" + }, + "title": "Splošno", + "detector": { + "title": "Detektorji", + "inferenceSpeed": "Hitrost sklepanja detektorja", + "temperature": "Temperatura detektorja", + "cpuUsage": "Poraba CPE detektorja", + "memoryUsage": "Poraba pomnilnika detektorja" + }, + "otherProcesses": { + "title": "Ostali procesi", + "processMemoryUsage": "Poraba pomnilnika", + "processCpuUsage": "Poraba CPE" + } + }, + "title": "Sistem", + "metrics": "Sistemske meritve", + "cameras": { + "title": "Kamere", + "overview": "Pregled", + "info": { + "aspectRatio": "razmerje stranic", + "cameraProbeInfo": "{{camera}} Podrobne Informacije Kamere", + "streamDataFromFFPROBE": "Podatki o pretoku se pridobijo z ukazom ffprobe.", + "fetching": "Pridobivanje Podatkov Kamere", + "stream": "Pretok {{idx}}", + "video": "Video:", + "codec": "Kodek:", + "resolution": "Ločljivost:", + "fps": "FPS:", + "unknown": "Neznano", + "audio": "Zvok:", + "error": "Napaka: {{error}}", + "tips": { + "title": "Podrobne Informacije Kamere" + } + }, + "framesAndDetections": "Okvirji / Zaznave", + "label": { + "camera": "kamera", + "detect": "zaznaj", + "skipped": "preskočeno", + "ffmpeg": "FFmpeg", + "capture": "zajemanje", + "overallFramesPerSecond": "skupno število sličic na sekundo (FPS)", + "overallDetectionsPerSecond": "skupno število zaznav na sekundo", + "overallSkippedDetectionsPerSecond": "skupno število preskočenih zaznav na sekundo", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} zajem", + "cameraDetect": "{{camName}} zaznavanje", + "cameraFramesPerSecond": "{{camName}} sličic na sekundo (FPS)", + "cameraDetectionsPerSecond": "{{camName}} detekcij na sekundo", + "cameraSkippedDetectionsPerSecond": "{{camName}} preskočenih zaznav na sekundo" + }, + "toast": { + "success": { + "copyToClipboard": "Podatki sonde so bili kopirani v odložišče." + }, + "error": { + "unableToProbeCamera": "Ni mogoče preveriti podrobnosti kamere: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Zadnja osvežitev: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} ima visoko porabo procesorja FFmpeg ({{ffmpegAvg}} %)", + "detectHighCpuUsage": "{{camera}} ima visoko porabo procesorja za zaznavanje ({{detectAvg}} %)", + "healthy": "Sistem je zdrav", + "reindexingEmbeddings": "Ponovno indeksiranje vdelanih elementov (embeddings) ({{processed}}% končano)", + "cameraIsOffline": "{{camera}} je nedosegljiva", + "detectIsSlow": "{{detect}} je počasen ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} je zelo počasen ({{speed}} ms)" + }, + "enrichments": { + "title": "Obogatitve", + "infPerSecond": "Inference Na Sekundo", + "embeddings": { + "face_recognition": "Prepoznavanje Obrazov", + "plate_recognition": "Prepoznavanje Registrskih Tablic", + "face_recognition_speed": "Hitrost Prepoznavanja Obrazov", + "plate_recognition_speed": "Hitrost Prepoznavanja Registrskih Tablic", + "yolov9_plate_detection": "YOLOv9 Zaznavanje Registrskih Tablic" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/audio.json b/sam2-cpu/frigate-dev/web/public/locales/sr/audio.json new file mode 100644 index 0000000..63c1c25 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/audio.json @@ -0,0 +1,17 @@ +{ + "speech": "Govor", + "scream": "Vrisak", + "babbling": "Brbljanje", + "bicycle": "Bicikla", + "yell": "Vikati", + "car": "Automobil", + "bellow": "Ispod", + "motorcycle": "Motor", + "whoop": "Opa", + "whispering": "Šaptanje", + "bus": "Autobus", + "laughter": "Smeh", + "train": "Voz", + "boat": "Brod", + "crying": "Plač" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/common.json b/sam2-cpu/frigate-dev/web/public/locales/sr/common.json new file mode 100644 index 0000000..06557f2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/common.json @@ -0,0 +1,32 @@ +{ + "time": { + "untilForTime": "Do {{time}}", + "untilForRestart": "Dok se Frigate ponovo ne pokrene.", + "untilRestart": "Do ponovnog pokretanja", + "ago": "{{timeAgo}} pre", + "justNow": "Upravo sada", + "today": "Danas", + "yesterday": "Juče", + "last7": "Zadnjih 7 dana", + "last14": "Zadnjih 14 dana", + "last30": "Zadnjih 30 dana", + "thisWeek": "Ove nedelje", + "lastWeek": "Prošle nedelje", + "thisMonth": "Ovog meseca", + "lastMonth": "Prošlog meseca", + "5minutes": "5 minuta", + "10minutes": "10 minuta", + "30minutes": "30 minuta", + "1hour": "1 sat", + "12hours": "12 sati", + "24hours": "24 sata", + "pm": "pm", + "am": "am", + "yr": "{{time}}god", + "year_one": "1,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21...", + "year_few": "2,3,4,22,23,24,32,33,34,42,...", + "year_other": "", + "mo": "{{time}}mes" + }, + "readTheDocumentation": "Прочитајте документацију" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/auth.json new file mode 100644 index 0000000..ecaa132 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "Korisničko ime", + "password": "Lozinka", + "login": "Uloguj se", + "errors": { + "usernameRequired": "Korisničko ime je obavezno", + "passwordRequired": "Lozinka je obavezna", + "rateLimit": "Prekoračeno ograničenje brzine. Pokušajte ponovo kasnije.", + "loginFailed": "Prijava nije uspela", + "unknownError": "Nepoznata greška. Proveri logove.", + "webUnknownError": "Nepoznata greška. Proveri logove u konzoli." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/camera.json new file mode 100644 index 0000000..1bb6c30 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/camera.json @@ -0,0 +1,21 @@ +{ + "group": { + "label": "Grupa kamera", + "add": "Dodajte grupu kamera", + "edit": "Uredite grupu kamera", + "delete": { + "label": "Izbrišite grupu kamera", + "confirm": { + "title": "Potvrdi Brisanje", + "desc": "Da li ste sigurni da želite da obrišete grupu kamera {{name}}?" + } + }, + "name": { + "label": "Ime", + "placeholder": "Unesite ime…", + "errorMessage": { + "mustLeastCharacters": "Naziv grupe kamera mora imati bar 2 karaktera." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/dialog.json new file mode 100644 index 0000000..ead50e8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/dialog.json @@ -0,0 +1,24 @@ +{ + "restart": { + "title": "Da li želite da restartujete Frigate?", + "button": "Ponovo pokreni", + "restarting": { + "title": "Frigate se ponovo pokreće", + "content": "Ova stranica će se ponovo učitati za {{countdown}} sekundi.", + "button": "Prisilno ponovno učitavanje" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Pošalji na Frigate+", + "desc": "Objekti na lokacijama koje želite da izbegnete nisu lažno pozitivni. Slanje lažno pozitivnih rezultata će zbuniti model." + }, + "review": { + "question": { + "ask_a": "Da li je ovaj objekat {{label}}?" + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/filter.json new file mode 100644 index 0000000..d7b8323 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/filter.json @@ -0,0 +1,19 @@ +{ + "filter": "Filter", + "labels": { + "label": "Labele", + "all": { + "title": "Sve oznake", + "short": "Oznake" + }, + "count_one": "{{count}} Oznaka", + "count_other": "{{count}} Oznake" + }, + "zones": { + "label": "Zone", + "all": { + "title": "Sve zone", + "short": "Zone" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/icons.json new file mode 100644 index 0000000..4bc5937 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Izaberite ikonu", + "search": { + "placeholder": "Potraži ikonu…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/input.json new file mode 100644 index 0000000..b05c1f6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Sačuvaj video", + "toast": { + "success": "Preuzimanje vašeg videa za recenziju je počelo." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/sr/components/player.json new file mode 100644 index 0000000..e827547 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/components/player.json @@ -0,0 +1,13 @@ +{ + "noRecordingsFoundForThisTime": "Nije pronađen nijedan snimak za ovo vreme", + "noPreviewFound": "Pregled nije pronađen", + "noPreviewFoundFor": "Nije pronađen pregled za {{cameraName}}", + "submitFrigatePlus": { + "title": "Pošaljite ovaj frejm na Frigate+?", + "submit": "Pošalji" + }, + "livePlayerRequiredIOSVersion": "Za ovaj tip prenosa uživo potreban je iOS 17.1 ili noviji.", + "streamOffline": { + "title": "Strim je oflajn" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/objects.json b/sam2-cpu/frigate-dev/web/public/locales/sr/objects.json new file mode 100644 index 0000000..4edf472 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/objects.json @@ -0,0 +1,10 @@ +{ + "person": "Osoba", + "bicycle": "Bicikla", + "car": "Automobil", + "motorcycle": "Motor", + "airplane": "Avion", + "bus": "Autobus", + "train": "Voz", + "boat": "Brod" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/configEditor.json new file mode 100644 index 0000000..a94a6e5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/configEditor.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "Editor Konfiguracije - Frigate", + "configEditor": "Editor konfiguracije", + "copyConfig": "Kopiraj konfiguraciju", + "saveAndRestart": "Sačuvaj & Ponovo pokreni", + "saveOnly": "Samo sačuvaj", + "confirm": "Izađi bez čuvanja?", + "toast": { + "success": { + "copyToClipboard": "Konfiguracija je kopirana u clipboard." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/events.json new file mode 100644 index 0000000..4097e56 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/events.json @@ -0,0 +1,14 @@ +{ + "alerts": "Upozorenja", + "detections": "Detekcije", + "motion": { + "label": "Pokret", + "only": "Samo pokret" + }, + "allCameras": "Sve Kamere", + "empty": { + "alert": "Nema upozorenja za pregled", + "detection": "Nema detekcija za pregled", + "motion": "Nema podataka o pokretu" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/explore.json new file mode 100644 index 0000000..66e8fbf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/explore.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "Istraži - Frigate", + "generativeAI": "Generativni AI", + "exploreMore": "Istražite više {{label}} objekata", + "exploreIsUnavailable": { + "title": "Istraživanje je nedostupno", + "embeddingsReindexing": { + "context": "Istraživanje se može koristiti nakon što se završi reindeksiranje ugrađivanja praćenih objekata.", + "startingUp": "Pokretanje…", + "estimatedTime": "Procenjeno preostalo vreme:" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/exports.json new file mode 100644 index 0000000..ff71c75 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/exports.json @@ -0,0 +1,12 @@ +{ + "documentTitle": "Izvoz - Frigate", + "search": "Pretraga", + "noExports": "Nije pronađen nijedan izvoz", + "deleteExport": "Izbriši izvoz", + "deleteExport.desc": "Da li zaista želite obrisati {{exportName}}?", + "editExport": { + "title": "Preimenuj izvoz", + "desc": "Unesite novo ime za ovaj izvoz.", + "saveExport": "Sačuvaj izvoz" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/faceLibrary.json new file mode 100644 index 0000000..c2aa836 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/faceLibrary.json @@ -0,0 +1,14 @@ +{ + "description": { + "addFace": "Prođite kroz dodavanje nove kolekcije u biblioteku lica.", + "placeholder": "Unesite ime za ovu kolekciju", + "invalidName": "Nevažeće ime. Imena mogu da sadrže samo slova, brojeve, razmake, apostrofe, donje crte i crtice." + }, + "details": { + "person": "Osoba", + "subLabelScore": "Sub Label Skor", + "scoreInfo": "Rezultat podoznake je otežan rezultat za sve prepoznate pouzdanosti lica, tako da se može razlikovati od rezultata prikazanog na snimku.", + "face": "Detalji lica", + "faceDesc": "Detalji praćenog objekta koji je generisao ovo lice" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/live.json new file mode 100644 index 0000000..1374fe1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/live.json @@ -0,0 +1,20 @@ +{ + "documentTitle": "Uživo - Frigate", + "documentTitle.withCamera": "{{camera}} - Uživo - Frigate", + "lowBandwidthMode": "Režim niskog propusnog opsega", + "twoWayTalk": { + "enable": "Omogući dvosmerni razgovor", + "disable": "Onemogućite dvosmerni razgovor" + }, + "cameraAudio": { + "enable": "Omogući zvuk kamere", + "disable": "Onemogući zvuk kamere" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Kliknite na sliku da bi centrirali kameru" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/recording.json new file mode 100644 index 0000000..2a12e9b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filter", + "export": "Izvezi", + "calendar": "Kalendar", + "filters": "Filteri", + "toast": { + "error": { + "noValidTimeSelected": "Nije izabran važeći vremenski opseg", + "endTimeMustAfterStartTime": "Vreme završetka mora biti posle vremena početka" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/search.json new file mode 100644 index 0000000..d72036c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/search.json @@ -0,0 +1,12 @@ +{ + "search": "Pretraga", + "savedSearches": "Sačuvane pretrage", + "searchFor": "Pretraži {{inputValue}}", + "button": { + "clear": "Obriši pretragu", + "save": "Sačuvaj pretragu", + "delete": "Izbrišite sačuvanu pretragu", + "filterInformation": "Filtriraj informacije", + "filterActive": "Aktivni filteri" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/settings.json new file mode 100644 index 0000000..2957af0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/settings.json @@ -0,0 +1,11 @@ +{ + "documentTitle": { + "default": "Podešavanja - Frigate", + "authentication": "Podešavanja autentifikacije - Fregate", + "camera": "Podešavanje kamera - Frigate", + "enrichments": "Podešavanja obogaćivanja - Frigate", + "masksAndZones": "Uređivač maski i zona - Frigate", + "motionTuner": "Tjuner pokreta - Frigate", + "general": "Generalna podešavanja - Frigate" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sr/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/sr/views/system.json new file mode 100644 index 0000000..5cd6faa --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sr/views/system.json @@ -0,0 +1,14 @@ +{ + "documentTitle": { + "cameras": "Statusi kamera - Frigate", + "storage": "Statistika skladištenja - Frigate", + "general": "Opšta statistika - Frigate", + "enrichments": "Statistika obogaćivanja - Frigate", + "logs": { + "frigate": "Frigate logovi - Frigate", + "go2rtc": "Go2RTC dnevnici - Frigate", + "nginx": "Nginx logovi - Frigate" + } + }, + "title": "Sistem" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/audio.json b/sam2-cpu/frigate-dev/web/public/locales/sv/audio.json new file mode 100644 index 0000000..2de942a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/audio.json @@ -0,0 +1,503 @@ +{ + "yell": "Skrik", + "bicycle": "Cykel", + "speech": "Tal", + "car": "Bil", + "bellow": "Vrål", + "motorcycle": "Motorcykel", + "whispering": "Viskning", + "bus": "Buss", + "babbling": "Babblande", + "whoop": "Skrika", + "camera": "Kamera", + "laughter": "Skratt", + "snicker": "Fnittra", + "crying": "Gråt", + "choir": "Kör", + "singing": "Sjunger", + "yodeling": "Joddling", + "chant": "Sång", + "mantra": "Mantra", + "synthetic_singing": "Syntetisk sång", + "rapping": "Rappar", + "groan": "Stöna", + "grunt": "Grymta", + "whistling": "Visslar", + "breathing": "Andas", + "snoring": "Snarkning", + "gasp": "Flämtning", + "pant": "Flämta", + "cough": "Hosta", + "throat_clearing": "Halsrensning", + "sneeze": "Nysa", + "run": "Spring", + "shuffle": "Blanda", + "footsteps": "Fotsteg", + "chewing": "Tugga", + "biting": "Biter", + "gargling": "Gurgling", + "stomach_rumble": "Magljud", + "burping": "Rapning", + "hiccup": "Hicka", + "fart": "Fis", + "hands": "Händer", + "finger_snapping": "Knäppning med fingrar", + "clapping": "Klappar", + "heartbeat": "Hjärtslag", + "heart_murmur": "Blåsljud i hjärtat", + "cheering": "Glädjande", + "applause": "Applåder", + "chatter": "Prat", + "crowd": "Folkmassa", + "animal": "Djur", + "yip": "Japp", + "howl": "Tjut", + "bow_wow": "Bow Wow", + "growling": "Morrande", + "whimper_dog": "Hund gnäll", + "cat": "Katt", + "meow": "Mjau", + "hiss": "Väsa", + "caterwaul": "Kattgräl", + "livestock": "Boskap", + "horse": "Häst", + "clip_clop": "Klipp Clop", + "neigh": "Gnägga", + "cattle": "Boskap", + "oink": "Oink", + "goat": "Get", + "bleat": "Bräka", + "fowl": "Fjäderfä", + "cluck": "Kluck", + "cock_a_doodle_doo": "kukilikuk", + "turkey": "kalkon", + "gobble": "Gobble", + "duck": "Anka", + "quack": "Quack", + "goose": "Gås", + "honk": "Tuta", + "wild_animals": "Vilda djur", + "roaring_cats": "Rytande katter", + "roar": "Rytande", + "bird": "Fågel", + "chirp": "Kvittra", + "squawk": "Skriande", + "pigeon": "Duva", + "caw": "Kraxa", + "owl": "Uggla", + "hoot": "Tuta", + "dogs": "Hundar", + "rats": "Råttor", + "mouse": "Älg", + "music": "Musik", + "sigh": "Suck", + "child_singing": "Barnsång", + "sheep": "Får", + "wheeze": "Väsande", + "dog": "Hund", + "sniff": "Sniffa", + "humming": "Hummar", + "pets": "Husdjur", + "coo": "Kuttra", + "snort": "Fnysa", + "children_playing": "Barn som leker", + "bark": "Skall", + "purr": "Spinna", + "moo": "Muu", + "cowbell": "Koskälla", + "pig": "Gris", + "chicken": "Kyckling", + "crow": "Kråka", + "frog": "Groda", + "patter": "Droppar", + "insect": "Insekt", + "cricket": "Syrsa", + "fly": "Fluga", + "buzz": "Surr", + "croak": "Kvack", + "rattle": "Skallra", + "musical_instrument": "Musikinstrument", + "plucked_string_instrument": "Stränginstrument", + "guitar": "Gitarr", + "electric_guitar": "Elektrisk Gitarr", + "bass_guitar": "Basgitarr", + "steel_guitar": "Stålgitarr", + "tapping": "Knackning", + "snake": "Orm", + "acoustic_guitar": "Aukustisk gitarr", + "mosquito": "Mygga", + "flapping_wings": "Vingslag", + "whale_vocalization": "Val-ljud", + "bass_drum": "Bastrumma", + "timpani": "Pukor", + "tabla": "Tabla", + "hi_hat": "Hi-Hat", + "wood_block": "Träblock", + "tambourine": "Tamburin", + "maraca": "Maracas", + "drum_roll": "Trumvirvel", + "rimshot": "Kantslag", + "snare_drum": "Virveltrumma", + "cymbal": "Cymbal", + "mandolin": "Mandolin", + "boat": "Båt", + "train": "Tåg", + "bowed_string_instrument": "stråkinstrument", + "banjo": "Banjo", + "sitar": "Sitar", + "clock": "Klocka", + "keyboard": "Tangentbord", + "vehicle": "Fordon", + "skateboard": "Skatebord", + "door": "Dörr", + "blender": "Blandare", + "sink": "Vask", + "hair_dryer": "Hårfön", + "toothbrush": "Tandborste", + "scissors": "Sax", + "strum": "Anslag", + "zither": "Citer", + "ukulele": "Ukulele", + "piano": "Piano", + "electric_piano": "Elpiano", + "organ": "Orgel", + "electronic_organ": "Elektronisk orgel", + "hammond_organ": "Hammondorgel", + "synthesizer": "Synthesizer", + "sampler": "Provtagare", + "harpsichord": "Cembalo", + "percussion": "Slagverk", + "drum_kit": "Trumset", + "drum_machine": "Trummaskin", + "drum": "Trumma", + "french_horn": "Franskt horn", + "trumpet": "Trumpet", + "flute": "Flöjt", + "gong": "Gonggong", + "tubular_bells": "Rörklockor", + "mallet_percussion": "Malletinstrument", + "marimba": "Marimba", + "glockenspiel": "Klockspel", + "vibraphone": "Vibrafon", + "steelpan": "Stålpanna", + "orchestra": "Orkester", + "brass_instrument": "Bleckblåsinstrument", + "trombone": "Trombon", + "string_section": "Stråkinstrument", + "violin": "Fiol", + "pizzicato": "Pizzicato", + "cello": "Cello", + "double_bass": "Kontrabas", + "wind_instrument": "Blåsinstrument", + "saxophone": "Saxofon", + "clarinet": "Klarinett", + "harp": "Harpa", + "bell": "Klocka", + "church_bell": "Kyrkklocka", + "jingle_bell": "Bjällerklang", + "bicycle_bell": "Cykelklocka", + "tuning_fork": "Stämgaffel", + "chime": "Klämta", + "wind_chime": "Vindspel", + "harmonica": "Munspel", + "accordion": "Dragspel", + "bagpipes": "Säckpipor", + "didgeridoo": "Didjeridu", + "theremin": "Teremin", + "singing_bowl": "Sjungande skål", + "scratching": "Repa", + "pop_music": "Popmusik", + "hip_hop_music": "Hiphopmusik", + "beatboxing": "Beatboxning", + "rock_music": "Rockmusik", + "heavy_metal": "Heavy Metal musik", + "punk_rock": "Punkrock", + "grunge": "Grunge", + "progressive_rock": "Progressiv rock", + "rock_and_roll": "Rock and roll", + "psychedelic_rock": "Psykedelisk rock", + "rhythm_and_blues": "Rytm och blues", + "soul_music": "Soulmusik", + "reggae": "Reggae", + "country": "Land", + "swing_music": "Swingmusik", + "bluegrass": "Bluegrass", + "funk": "Funk", + "folk_music": "Folkmusik", + "middle_eastern_music": "Mellanösternmusik", + "jazz": "Jazz", + "disco": "Disko", + "classical_music": "Klassisk musik", + "opera": "Opera", + "electronic_music": "Elektronisk musik", + "house_music": "Housemusik", + "techno": "Tekno", + "dubstep": "Dubstep", + "drum_and_bass": "Trumma och bas", + "electronica": "Elektronisk musik", + "electronic_dance_music": "Elektronisk dansmusik", + "ambient_music": "Ambientmusik", + "trance_music": "Trancemusik", + "music_of_latin_america": "Latinamerikansk musik", + "salsa_music": "Salsamusik", + "flamenco": "Flamenco", + "blues": "Blues", + "music_for_children": "Musik för barn", + "new-age_music": "New Age-musik", + "vocal_music": "Vokalmusik", + "a_capella": "A cappella", + "music_of_africa": "Afrikansk musik", + "afrobeat": "Afrobeat", + "christian_music": "Kristen musik", + "gospel_music": "Gospelmusik", + "music_of_asia": "Asiens musik", + "carnatic_music": "Karnatisk musik", + "music_of_bollywood": "Bollywoods musik", + "ska": "Ska", + "traditional_music": "Traditionell musik", + "independent_music": "Oberoende musik", + "song": "Låt", + "background_music": "Bakgrundsmusik", + "theme_music": "Temamusik", + "jingle": "Klingande", + "soundtrack_music": "Soundtrackmusik", + "lullaby": "Vaggvisa", + "video_game_music": "Videospelsmusik", + "christmas_music": "Julmusik", + "dance_music": "Dansmusik", + "wedding_music": "Bröllopsmusik", + "happy_music": "Glad musik", + "sad_music": "Sorglig musik", + "tender_music": "Öm musik", + "exciting_music": "Spännande musik", + "angry_music": "Arg musik", + "scary_music": "Skräckmusik", + "wind": "Vind", + "rustling_leaves": "Prasslande löv", + "wind_noise": "Vindbrus", + "thunderstorm": "Åskväder", + "thunder": "Åska", + "water": "Vatten", + "rain": "Regn", + "raindrop": "Regndroppe", + "rain_on_surface": "Regn på ytan", + "stream": "Strömma", + "waterfall": "Vattenfall", + "ocean": "Hav", + "waves": "Vågor", + "steam": "Ånga", + "gurgling": "Gurglande", + "fire": "Brand", + "crackle": "Spraka", + "sailboat": "Segelbåt", + "rowboat": "Roddbåt", + "motorboat": "Motorbåt", + "ship": "Fartyg", + "motor_vehicle": "Motorfordon", + "power_windows": "Elfönster", + "skidding": "Slirning", + "tire_squeal": "Däckskrik", + "toot": "Tuta", + "car_alarm": "Billarm", + "car_passing_by": "Bil som passerar", + "race_car": "Racerbil", + "truck": "Lastbil", + "air_brake": "Luftbroms", + "air_horn": "Lufthorn", + "reversing_beeps": "Backningljud", + "ice_cream_truck": "Glassbil", + "emergency_vehicle": "Akutbil", + "police_car": "Polisbil", + "ambulance": "Ambulans", + "fire_engine": "Brandbil", + "traffic_noise": "Trafikbuller", + "rail_transport": "Järnvägstransport", + "train_whistle": "Tågvissla", + "train_horn": "Tåghorn", + "railroad_car": "Järnvägsvagn", + "train_wheels_squealing": "Tåghjul skriker", + "subway": "Tunnelbana", + "aircraft": "Flygplan", + "aircraft_engine": "Flygmotor", + "jet_engine": "Jetmotor", + "propeller": "Propeller", + "helicopter": "Helikopter", + "fixed-wing_aircraft": "Flygplan med fasta vingar", + "engine": "Motor", + "light_engine": "Ljusmotor", + "lawn_mower": "Gräsklippare", + "chainsaw": "Motorsåg", + "doorbell": "Dörrklocka", + "electric_toothbrush": "Eltandborste", + "computer_keyboard": "Tangentbord", + "alarm": "Larm", + "telephone": "Telefon", + "ringtone": "Ringsignal", + "dial_tone": "Rington", + "busy_signal": "Upptagetsignal", + "alarm_clock": "Alarmklocka", + "smoke_detector": "Brandvarnare", + "fire_alarm": "Brandlarm", + "dental_drill's_drill": "Tandläkarborr", + "medium_engine": "Medelstor motor", + "heavy_engine": "Tung motor", + "engine_knocking": "Motorknackning", + "engine_starting": "Motor startar", + "idling": "Tomgång", + "accelerating": "Accelererar", + "ding-dong": "Ring-ring", + "sliding_door": "Skjutdörr", + "slam": "Smäll", + "knock": "Knack", + "tap": "Knacka", + "squeak": "Gnissla", + "cupboard_open_or_close": "Skåp öppnas eller stängs", + "drawer_open_or_close": "Låda öppnas eller stängs", + "dishes": "Tallrikar", + "cutlery": "Bestick", + "chopping": "Hackning", + "frying": "Steka", + "microwave_oven": "Mikrovågsugn", + "water_tap": "Vattenkran", + "bathtub": "Badkar", + "toilet_flush": "Toalettspolning", + "vacuum_cleaner": "Dammsugare", + "zipper": "Dragkedja", + "keys_jangling": "Nycklar som klirrar", + "coin": "Mynt", + "electric_shaver": "Elektrisk rakhyvel", + "shuffling_cards": "Blanda kort", + "typing": "Skrivar", + "typewriter": "Skrivmaskin", + "writing": "Skriva", + "telephone_bell_ringing": "Telefonen ringer", + "telephone_dialing": "Ljud för telefonuppringning", + "siren": "Siren", + "civil_defense_siren": "Civilförsvarssiren", + "buzzer": "Summer", + "foghorn": "Mistlur", + "whistle": "Vissla", + "steam_whistle": "Ångvissla", + "mechanisms": "Mekanismer", + "ratchet": "Spärrhake", + "tick": "Tick", + "tick-tock": "Tick Tack", + "gears": "Kugghjul", + "pulleys": "Remskivor", + "sewing_machine": "Symaskin", + "printer": "Skrivare", + "mechanical_fan": "Mekanisk fläkt", + "air_conditioning": "Luftkonditionering", + "cash_register": "Kassaapparat", + "single-lens_reflex_camera": "Enkellinsreflexkamera", + "tools": "Verktyg", + "hammer": "Hammare", + "jackhammer": "Tryckluftsborr", + "sawing": "Sågning", + "filing": "Filning", + "sanding": "Sandning", + "power_tool": "Elverktyg", + "drill": "Borra", + "explosion": "Explosion", + "gunshot": "Skottlossning", + "machine_gun": "Kulspruta", + "fusillade": "Fusillad", + "artillery_fire": "Artillerieeld", + "cap_gun": "Kapsylpistol", + "fireworks": "Fyrverkeri", + "firecracker": "Smällare", + "burst": "Brista", + "eruption": "Utbrott", + "boom": "Pang", + "wood": "Trä", + "chop": "Hugga", + "splinter": "Flisa", + "crack": "Spricka", + "glass": "Glas", + "chink": "Skaka", + "shatter": "Splittras", + "silence": "Tystnad", + "sound_effect": "Ljudeffekt", + "environmental_noise": "Miljöbuller", + "static": "Statisk", + "white_noise": "Vitt brus", + "pink_noise": "Rosa brus", + "television": "Tv", + "radio": "Radio", + "field_recording": "Fältinspelning", + "scream": "Skrika", + "sodeling": "Södling", + "chird": "Ackord", + "change_ringing": "Ljud från myntväxling", + "shofar": "Shofar", + "liquid": "Flytande", + "splash": "Stänk", + "slosh": "Plaska", + "squish": "Stryk", + "drip": "Dropp", + "pour": "Hälla", + "trickle": "Sippra", + "gush": "Välla", + "fill": "Fylla", + "spray": "Sprej", + "pump": "Pump", + "stir": "Rör", + "boiling": "Kokande", + "sonar": "Ekolod", + "arrow": "Pil", + "whoosh": "Svischande", + "thump": "Dunk", + "thunk": "Dunkande", + "electronic_tuner": "Elektronisk stämapparat", + "effects_unit": "Effektenhet", + "chorus_effect": "Chorus-effekt", + "basketball_bounce": "Basketbollstuds", + "bang": "Smäll", + "slap": "Slag", + "whack": "Slog", + "smash": "Smälla", + "breaking": "Brytning", + "bouncing": "Studsande", + "whip": "Piska", + "flap": "Flaxa", + "scratch": "Repa", + "scrape": "Skrapa", + "rub": "Gnugga", + "roll": "Rulla", + "crushing": "Krossa", + "crumpling": "Skrynkliga", + "tearing": "Rivning", + "beep": "Pip", + "ping": "Ping", + "ding": "Ding", + "clang": "Klang", + "squeal": "Skrika", + "creak": "Knarr", + "rustle": "Prassel", + "whir": "Surra", + "clatter": "Slammer", + "sizzle": "Fräsa vid matlagning", + "clicking": "Klickande", + "clickety_clack": "Klickigt klack", + "rumble": "Mullrande", + "plop": "Plopp", + "hum": "Brum", + "zing": "Vinande", + "boing": "Pling", + "crunch": "Knastrande", + "sine_wave": "Sinusvåg", + "harmonic": "Harmonisk", + "chirp_tone": "Kvittringston", + "pulse": "Puls", + "inside": "Inuti", + "outside": "Utanför", + "reverberation": "Eko", + "echo": "Eko", + "noise": "Buller", + "mains_hum": "Huvudbrum", + "distortion": "Distorsion", + "sidetone": "Sidoton", + "cacophony": "Kakofoni", + "throbbing": "Bultande", + "vibration": "Vibration" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/common.json b/sam2-cpu/frigate-dev/web/public/locales/sv/common.json new file mode 100644 index 0000000..a1ad12b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/common.json @@ -0,0 +1,297 @@ +{ + "time": { + "untilForTime": "Till {{time}}", + "untilForRestart": "Tills Frigate startar om.", + "untilRestart": "Tills omstart", + "ago": "{{timeAgo}} sedan", + "justNow": "Just nu", + "today": "Idag", + "yesterday": "Igår", + "last14": "Senaste 14 dagarna", + "thisMonth": "Denna månad", + "lastMonth": "Förra månaden", + "30minutes": "30 minuter", + "1hour": "1 timma", + "12hours": "12 timmar", + "pm": "pm", + "am": "am", + "yr": "{{time}}år", + "mo": "{{time}}må", + "month_one": "{{time}} månad", + "month_other": "{{time}} månader", + "d": "{{time}}d", + "last7": "Senaste 7 dagarna", + "5minutes": "5 minuter", + "last30": "Senaste 30 dagarna", + "thisWeek": "Denna vecka", + "lastWeek": "Förra veckan", + "10minutes": "10 minuter", + "24hours": "24 timmar", + "year_one": "{{time}} år", + "year_other": "{{time}} år", + "second_one": "{{time}} sekund", + "second_other": "{{time}} sekunder", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyy" + }, + "h": "{{time}}t", + "hour_one": "{{time}} timme", + "hour_other": "{{time}} timmar", + "m": "{{time}}m", + "minute_one": "{{time}} minut", + "minute_other": "{{time}} minuter", + "s": "{{time}}s", + "formattedTimestamp": { + "12hour": "d MMM, kl. h:mm:ss a", + "24hour": "d MMM, HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "kl. h:mm a", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "day_one": "{{time}} dag", + "day_other": "{{time}} dagar", + "inProgress": "Pågår", + "invalidStartTime": "Ogiltig starttid", + "invalidEndTime": "Ogiltig sluttid" + }, + "button": { + "save": "Spara", + "enabled": "Aktiverad", + "enable": "Aktivera", + "disabled": "Inaktiverad", + "pictureInPicture": "Bild-i-Bild", + "twoWayTalk": "Tvåvägskommunikation", + "edit": "Redigera", + "copyCoordinates": "Kopiera koordinater", + "suspended": "Pausad", + "play": "Spela", + "unselect": "Avmarkera", + "unsuspended": "Återuppta", + "deleteNow": "Radera nu", + "next": "Nästa", + "apply": "Verkställ", + "reset": "Återställ", + "done": "Klar", + "disable": "Inaktivera", + "saving": "Sparar…", + "cancel": "Avbryt", + "close": "Stäng", + "copy": "Kopiera", + "back": "Tillbaka", + "history": "Historia", + "fullscreen": "Fullskärm", + "exitFullscreen": "Lämna Fullskärm", + "cameraAudio": "Kameraljud", + "on": "PÅ", + "off": "AV", + "delete": "Radera", + "yes": "Ja", + "no": "Nej", + "download": "Ladda ner", + "info": "Info", + "export": "Exportera", + "continue": "Fortsätta" + }, + "menu": { + "language": { + "yue": "粵語 (Kantonesiska)", + "it": "Italiano (Italienska)", + "fr": "Français (Franska)", + "nl": "Nederlands (Nederländska)", + "hi": "हिन्दी (Hindi)", + "pt": "Português (Portugisiska)", + "ru": "Русский (Ryska)", + "pl": "Polski (Polska)", + "el": "Ελληνικά (Grekiska)", + "sk": "Slovenčina (Slovenska)", + "tr": "Türkçe (Turkiska)", + "uk": "Українська (Ukrainska)", + "he": "עברית (Hebreiska)", + "ro": "Română (Romänska)", + "hu": "Magyar (Ungerska)", + "fi": "Suomi (Finska)", + "da": "Dansk (Danska)", + "ar": "العربية (Arabiska)", + "es": "Español (Spanska)", + "zhCN": "简体中文 (Kinesiska)", + "de": "Deutsch (Tyska)", + "ja": "日本語 (Japanska)", + "sv": "Svenska (Svenska)", + "cs": "Čeština (Tjeckiska)", + "nb": "Norsk Bokmål (Norsk Bokmål)", + "ko": "한국어 (Koreanska)", + "vi": "Tiếng Việt (Vietnamesiska)", + "fa": "فارسی (Persiska)", + "th": "ไทย (Thailändska)", + "withSystem": { + "label": "Använd systeminställningarna för språk" + }, + "en": "English (Engelska)", + "ptBR": "Português brasileiro (Brasiliansk Portugisiska)", + "ca": "Català (Katalanska)", + "sr": "Српски (Serbiska)", + "sl": "Slovenščina (Slovenska)", + "lt": "Lietuvių (Litauiska)", + "bg": "Български (Bulgariska)", + "gl": "Galego (Galiciska)", + "id": "Bahasa Indonesia (Indonesiska)", + "ur": "اردو (Urdu)" + }, + "darkMode": { + "withSystem": { + "label": "Använd systeminställningarna för ljust eller mörkt läge" + }, + "label": "Mörk Läge", + "light": "Ljus", + "dark": "Mörk" + }, + "theme": { + "label": "Tema", + "blue": "Blå", + "green": "Grön", + "nord": "North", + "default": "Standard", + "highcontrast": "Hög Kontrast", + "red": "Röd" + }, + "export": "Exportera", + "faceLibrary": "Ansiktsbibliotek", + "user": { + "title": "Användare", + "account": "Konto", + "current": "Nuvarande Användare: {{user}}", + "anonymous": "anonym", + "logout": "Logga ut", + "setPassword": "Sätt Lösenord" + }, + "systemMetrics": "Systemstatus", + "configuration": "Konfiguration", + "explore": "Utforska", + "live": { + "cameras": { + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kameror", + "title": "Kameror" + }, + "allCameras": "Alla kameror", + "title": "Live" + }, + "system": "System", + "systemLogs": "Systemlogg", + "settings": "Inställningar", + "help": "Hjälp", + "documentation": { + "title": "Dokumentation", + "label": "Frigate dokumentation" + }, + "uiPlayground": "UI Testmiljö", + "restart": "Starta om Frigate", + "review": "Granska", + "languages": "Språk", + "configurationEditor": "Konfigurationsredigerare", + "withSystem": "System", + "appearance": "Utseende", + "classification": "Klassificering" + }, + "pagination": { + "next": { + "title": "Nästa", + "label": "Gå till nästa sida" + }, + "previous": { + "title": "Föregående", + "label": "Gå till föregående sida" + }, + "more": "Flera sidor", + "label": "paginering" + }, + "accessDenied": { + "title": "Åtkomst Förbjuden", + "desc": "Du har inte rättigheter att visa den här sidan.", + "documentTitle": "Åtkomst Förbjuden - Frigate" + }, + "role": { + "admin": "Admin", + "viewer": "Tittare", + "desc": "Administratörer har fullständig åtkomst till alla funktioner i Frigates gränssnitt. Tittare kan endast visa kameror, granska objekt och se historiskt videomaterial.", + "title": "Roll" + }, + "notFound": { + "documentTitle": "Hittades Inte - Frigate", + "desc": "Sidan hittades inte", + "title": "404" + }, + "toast": { + "save": { + "title": "Spara", + "error": { + "title": "Misslyckades med att spara konfigurationsändringar: {{errorMessage}}", + "noMessage": "Misslyckades med att spara konfigurationsändringar" + } + }, + "copyUrlToClipboard": "Webbadressen har kopierats till urklipp." + }, + "label": { + "back": "Gå tillbaka", + "hide": "Dölj {{item}}", + "show": "Visa {{item}}", + "ID": "ID", + "none": "Ingen", + "all": "Alla" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/h" + }, + "length": { + "feet": "fot", + "meters": "meter" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/timme", + "mbph": "MB/timme", + "gbph": "GB/timme" + } + }, + "selectItem": "Välj {{item}}", + "readTheDocumentation": "Läs dokumentationen", + "information": { + "pixels": "{{area}}px" + }, + "list": { + "two": "{{0}} och {{1}}", + "many": "{{items}} och {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Valfritt", + "internalID": "Det interna ID som Frigate använder i konfigurationen och databasen" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/auth.json new file mode 100644 index 0000000..1fcf909 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Lösenord", + "user": "Användarnamn", + "login": "Logga in", + "errors": { + "usernameRequired": "Användarnamn är obligatoriskt", + "passwordRequired": "Lösenord är obligatoriskt", + "loginFailed": "Inloggning misslyckades", + "unknownError": "Okänt fel. Kontrollera loggarna.", + "webUnknownError": "Okänt fel. Kontrollera konsol loggarna.", + "rateLimit": "Överskriden anropsgräns. Försök igen senare." + }, + "firstTimeLogin": "Försöker du logga in för första gången? Inloggningsuppgifterna finns angivna i Frigate-loggarna." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/camera.json new file mode 100644 index 0000000..23de947 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "Kameragrupper", + "add": "Lägg till Kameragrupp", + "edit": "Ändra Kameragrupp", + "delete": { + "label": "Radera kameragrupp", + "confirm": { + "title": "Bekräfta borttagning", + "desc": "Är du säker på att du vill ta bort kameragruppen {{name}}?" + } + }, + "success": "Kameragruppen ({{name}}) har sparats.", + "name": { + "label": "Namn", + "placeholder": "Ange ett namn…", + "errorMessage": { + "mustLeastCharacters": "Gruppnamnet för kameror måste vara minst 2 tecken.", + "nameMustNotPeriod": "Kameragruppnamnet får inte innehålla en punkt.", + "invalid": "Ogiltligt kameragruppnamn.", + "exists": "Kameragruppnamnet finns redan." + } + }, + "icon": "Ikon", + "camera": { + "setting": { + "label": "inställningar för kameraströmning", + "title": "{{cameraName}} Streaminginställningar", + "desc": "Ändra alternativen för livestreaming för den här kameragruppens instrumentpanel. Dessa inställningar är enhets-/webbläsarspecifika.", + "audioIsAvailable": "Ljud är tillgängligt för denna kameraström", + "audioIsUnavailable": "Ljud är otillgängligt för denna kameraström", + "audio": { + "tips": { + "title": "Ljud måste sändas från din kamera och konfigureras i go2rtc för den här strömmen.", + "document": "Läs dokumentationen. " + } + }, + "streamMethod": { + "label": "Strömningsmetod", + "method": { + "smartStreaming": { + "desc": "Smart streaming uppdaterar kamerabilden en gång per minut när ingen detekterbar aktivitet sker för att spara bandbredd och resurser. När aktivitet detekteras växlar bilden sömlöst till en livestream.", + "label": "Smart strömning (rekommenderas)" + }, + "continuousStreaming": { + "label": "Kontinuerlig strömning", + "desc": { + "title": "Kamerabilden kommer alltid att vara en liveström när den är synlig på instrumentpanelen, även om ingen aktivitet detekteras.", + "warning": "Kontinuerlig strömning kan orsaka hög bandbreddsanvändning och prestandaproblem. Använd med försiktighet." + } + }, + "noStreaming": { + "label": "Ingen strömning", + "desc": "Kamerabilderna uppdateras bara en gång per minut och ingen livestreaming kommer att ske." + } + }, + "placeholder": "Välj en strömningsmetod" + }, + "stream": "Strömma", + "placeholder": "Välj en ström", + "compatibilityMode": { + "label": "Kompatibilitetsläge", + "desc": "Aktivera endast det här alternativet om kamerans livestream visar färgartefakter och har en diagonal linje på höger sida av bilden." + } + }, + "birdseye": "Fågelöga" + }, + "cameras": { + "desc": "Välj kameror för denna guppen.", + "label": "Kameror" + } + }, + "debug": { + "options": { + "showOptions": "Visa alternativ", + "label": "Inställningar", + "title": "Alternativ", + "hideOptions": "Dölj alternativ" + }, + "boundingBox": "Avgränsningsruta", + "timestamp": "Tidsstämpel", + "zones": "Zoner", + "mask": "Maskera", + "motion": "Rörelse", + "regions": "Regioner" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/dialog.json new file mode 100644 index 0000000..2ef0e88 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/dialog.json @@ -0,0 +1,123 @@ +{ + "restart": { + "button": "Starta om", + "restarting": { + "title": "Frigate startar om", + "content": "Sidan uppdateras om {{countdown}} sekunder.", + "button": "Tvinga omladdning nu" + }, + "title": "Är du säker på att du vill starta om Frigate?" + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "Skicka till Frigate+", + "desc": "Objekt på platser du vill undvika är inte falska positiva resultat. Att skicka in dem som falska positiva resultat kommer att förvirra modellen." + }, + "review": { + "question": { + "ask_a": "Är detta objektet {{label}}?", + "ask_an": "Är detta objektet en {{label}}?", + "ask_full": "Är detta objektet {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "Bekräfta denna etikett för Frigate Plus" + }, + "state": { + "submitted": "Inskickad" + } + } + }, + "video": { + "viewInHistory": "Se i Historik" + } + }, + "export": { + "time": { + "fromTimeline": "Välj från tidslinjen", + "lastHour_one": "Sista timma", + "lastHour_other": "Sista {{count}} timmar", + "start": { + "title": "Start Tid", + "label": "Välj Start Tid" + }, + "end": { + "title": "Slut Tid", + "label": "Välj Sluttid" + }, + "custom": "Anpassad" + }, + "name": { + "placeholder": "Ge exporten ett namn" + }, + "select": "Välj", + "export": "Eksport", + "selectOrExport": "Välj eller exportera", + "toast": { + "success": "Exporten har startats. Visa filen på exportsidan.", + "error": { + "failed": "Misslyckades med att starta exporten: {{error}}", + "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", + "noVaildTimeSelected": "Inget giltigt tidsintervall valt" + }, + "view": "Visa" + }, + "fromTimeline": { + "saveExport": "Spara export", + "previewExport": "Förhandsgranska export" + } + }, + "streaming": { + "label": "Videoström", + "restreaming": { + "disabled": "Omströmning är inte aktiverad för den här kameran.", + "desc": { + "title": "Konfigurera go2rtc för ytterligare livevisningsalternativ och ljud för den här kameran.", + "readTheDocumentation": "Läs dokumentationen" + } + }, + "showStats": { + "label": "Visa strömstatistik", + "desc": "Aktivera det här alternativet för att visa strömstatistik som ett överlägg över kameraflödet." + }, + "debugView": "Felsöknings vy" + }, + "search": { + "saveSearch": { + "overwrite": "{{searchName}} finns redan. Om du sparar skrivs det befintliga värdet över.", + "success": "Sökningen ({{searchName}}) har sparats.", + "button": { + "save": { + "label": "Spara den här sökningen" + } + }, + "label": "Spara Sökning", + "desc": "Ange ett namn för den här sparade sökningen.", + "placeholder": "Ange ett namn för din sökning" + } + }, + "recording": { + "confirmDelete": { + "title": "Bekräfta radering", + "desc": { + "selected": "Är du säker på att du vill radera all inspelad video som är kopplad till det här granskningsobjektet?

    Håll ner Shift-tangenten för att hoppa över den här dialogrutan i framtiden." + }, + "toast": { + "success": "Videoklipp som är kopplade till de valda granskningsobjekten har raderats.", + "error": "Misslyckades med att ta bort: {{error}}" + } + }, + "button": { + "export": "Exportera", + "markAsReviewed": "Markera som granskad", + "deleteNow": "Ta bort nu", + "markAsUnreviewed": "Markera som ogranskad" + } + }, + "imagePicker": { + "selectImage": "Välj miniatyrbilden för ett spårat objekt", + "search": { + "placeholder": "Sök efter etikett eller underetikett..." + }, + "noImages": "Inga miniatyrbilder hittades för den här kameran", + "unknownLabel": "Sparad triggerbild" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/filter.json new file mode 100644 index 0000000..90eda7c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "all": { + "title": "Alla Etiketter", + "short": "Etiketter" + }, + "label": "Etiketter", + "count": "{{count}} Etiketter", + "count_one": "{{count}} Etikett", + "count_other": "{{count}} Etiketter" + }, + "filter": "Filtrera", + "zones": { + "label": "Zoner", + "all": { + "title": "Alla zoner", + "short": "Zoner" + } + }, + "features": { + "hasSnapshot": "Har ögonblicksbild", + "hasVideoClip": "Har ett video klipp", + "submittedToFrigatePlus": { + "label": "Skickat till Frigate+", + "tips": "Du måste först filtrera på spårade objekt som har en ögonblicksbild.

    Spårade objekt utan ögonblicksbild kan inte skickas till Frigate+." + }, + "label": "Detaljer" + }, + "sort": { + "dateAsc": "Datum (Stigande)", + "label": "Sortera", + "scoreAsc": "Objektpoäng (Stigande)", + "speedDesc": "Uppskattad Hastighet (Fallande)", + "relevance": "Relevans", + "dateDesc": "Datum (Fallande)", + "scoreDesc": "Objektpoäng (Fallande)", + "speedAsc": "Uppskattad Hastighet (Stigande)" + }, + "cameras": { + "all": { + "short": "Kameror", + "title": "Alla Kameror" + }, + "label": "Kamerafilter" + }, + "explore": { + "settings": { + "title": "Inställningar", + "defaultView": { + "title": "Standard Vy", + "summary": "Sammanfattning", + "desc": "När inga filter är valda, visa en översikt av de senaste spårade objekten per etikett-typ eller visa ett ofiltrerat rutnät.", + "unfilteredGrid": "Ofiltrerat Rutnät" + }, + "searchSource": { + "options": { + "description": "Beskrivning", + "thumbnailImage": "Miniatyrbild" + }, + "label": "Sökkälla", + "desc": "Välj om du vill söka miniatyrbilderna eller beskrivningarna av de spårade objekten." + }, + "gridColumns": { + "desc": "Välj antal kolumner i rutnätsvy.", + "title": "Kolumner i Rutnät" + } + }, + "date": { + "selectDateBy": { + "label": "Välj datum att filtrera efter" + } + } + }, + "review": { + "showReviewed": "Visa Kontrollerade" + }, + "motion": { + "showMotionOnly": "Visa Endast Rörelse" + }, + "score": "Poäng", + "dates": { + "all": { + "short": "Datum", + "title": "Alla datum" + }, + "selectPreset": "Välj Förval…" + }, + "recognizedLicensePlates": { + "noLicensePlatesFound": "Inga registreringsplåtar hittade.", + "selectPlatesFromList": "Välj en eller flera registreringsplåtar från listan.", + "title": "Igenkända Registreringsskyltar", + "loadFailed": "Misslyckades med att ladda igenkända registreringsskyltar.", + "placeholder": "Skriv för att söka registreringsskyltar…", + "loading": "Laddar igenkända registreringsskyltar…", + "selectAll": "Välj alla", + "clearAll": "Rensa alla" + }, + "more": "Flera filter", + "reset": { + "label": "Nollställ filter" + }, + "subLabels": { + "label": "Under kategori", + "all": "Alla under kategorier" + }, + "estimatedSpeed": "Estimerad hastighet ({{unit}})", + "classes": { + "all": { + "title": "Alla Klasser" + }, + "count_one": "{{count}} Klass", + "count_other": "{{count}} Klasser", + "label": "Klasser" + }, + "timeRange": "Tidsspann", + "logSettings": { + "loading": { + "title": "Laddar", + "desc": "När loggvyn är rullad till slutet, strömmas automatiskt nya loggar till vyn." + }, + "filterBySeverity": "Filtrera logg på allvarlighetsgrad", + "disableLogStreaming": "Inaktivera strömning av logg", + "allLogs": "Alla loggar", + "label": "Filter loggnivå" + }, + "trackedObjectDelete": { + "title": "Bekräfta Borttagning", + "toast": { + "success": "Spårade objekt borttagna.", + "error": "Misslyckades med att ta bort spårade objekt: {{errorMessage}}" + }, + "desc": "Borttagning av dessa {{objectLength}} spårade objekt tar bort ögonblicksbild, sparade inbäddningar, och tillhörande livscykelposter. Inspelat material av dessa spårade objekt i Historievyn kommer INTE att tas bort.

    Vill du verkligen fortsätta?

    Håll ner Skift-tangenten för att hoppa över denna dialog i framtiden." + }, + "zoneMask": { + "filterBy": "Filtrera på zonmaskering" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/icons.json new file mode 100644 index 0000000..afdcfb7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Sök efter en ikon…" + }, + "selectIcon": "Välj en ikon" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/input.json new file mode 100644 index 0000000..b3081bd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Ladda ner Video", + "toast": { + "success": "Din video laddas ner." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/sv/components/player.json new file mode 100644 index 0000000..24f1b1e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/components/player.json @@ -0,0 +1,51 @@ +{ + "noPreviewFound": "Ingen Förhandsvisning Hittad", + "noRecordingsFoundForThisTime": "Inga inspelningar hittade för denna tid", + "noPreviewFoundFor": "Ingen förhandsvisning hittad för {{cameraName}}", + "submitFrigatePlus": { + "title": "Skicka denna bild till Frigate+?", + "submit": "Skicka" + }, + "livePlayerRequiredIOSVersion": "iOS 17.1 eller senare krävs för den här typen av livestream.", + "streamOffline": { + "title": "Ström ej tillgänglig", + "desc": "Inga bildrutor har tagits emot från {{cameraName}}-strömmen detect, kontrollera felloggarna" + }, + "stats": { + "streamType": { + "short": "Typ", + "title": "Strömtyp:" + }, + "bandwidth": { + "title": "Bandbredd:", + "short": "Bandbredd" + }, + "latency": { + "title": "Latens:", + "short": { + "title": "Latens", + "value": "{{seconds}} sek" + }, + "value": "{{seconds}} sekunder" + }, + "totalFrames": "Totala Bildrutor:", + "droppedFrames": { + "title": "Bortfallna bildrutor:", + "short": { + "title": "Bortfallen", + "value": "{{droppedFrames}} Bildrutor" + } + }, + "decodedFrames": "Avkodade bildrutor:", + "droppedFrameRate": "Frekvens för bortfallna bildrutor:" + }, + "cameraDisabled": "Kameran är inaktiverad", + "toast": { + "error": { + "submitFrigatePlusFailed": "Bildruta har skickats till Frigate+ med misslyckat resultat" + }, + "success": { + "submittedFrigatePlus": "Bildruta har skickats till Frigate+ med lyckat resultat" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/objects.json b/sam2-cpu/frigate-dev/web/public/locales/sv/objects.json new file mode 100644 index 0000000..1e2926f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/objects.json @@ -0,0 +1,120 @@ +{ + "car": "Bil", + "person": "Person", + "bicycle": "Cykel", + "motorcycle": "Motorcykel", + "airplane": "Flygplan", + "bus": "Buss", + "horse": "Häst", + "sheep": "Får", + "mouse": "Älg", + "bark": "Skall", + "goat": "Get", + "animal": "Djur", + "dog": "Hund", + "cat": "Katt", + "bird": "Fågel", + "train": "Tåg", + "traffic_light": "Trafiklyse", + "stop_sign": "Stoppskylt", + "cow": "Ko", + "elephant": "Elefant", + "bear": "Björn", + "hat": "Hatt", + "boat": "Båt", + "street_sign": "Gatuskylt", + "shoe": "Sko", + "giraffe": "Giraff", + "fire_hydrant": "Brandpost", + "zebra": "Zebra", + "backpack": "Ryggsäck", + "umbrella": "Paraply", + "bench": "Bänk", + "wine_glass": "vinglas", + "bottle": "Flaska", + "spoon": "Sked", + "orange": "Apelsin", + "carrot": "Morot", + "hot_dog": "Varmkorv", + "broccoli": "Broccoli", + "fork": "Gaffel", + "banana": "Banan", + "apple": "Äpple", + "knife": "Kniv", + "bowl": "Skål", + "cup": "Kopp", + "sandwich": "Smörgås", + "toaster": "Brödrost", + "vehicle": "Fordon", + "postnord": "PostNord", + "refrigerator": "Kylskåp", + "hair_dryer": "Hårfön", + "hair_brush": "Hårborste", + "amazon": "Amazon", + "clock": "Klocka", + "parking_meter": "Parkeringsmätare", + "eye_glasses": "Glasögon", + "handbag": "Handväska", + "tie": "Slips", + "suitcase": "Resväska", + "frisbee": "Frisbee", + "skis": "Skidor", + "snowboard": "Snowboard", + "sports_ball": "Boll", + "kite": "Drake", + "baseball_bat": "Basebollträ", + "baseball_glove": "Baseballhandske", + "skateboard": "Skatebord", + "surfboard": "Surfbräda", + "tennis_racket": "Tennisrack", + "pizza": "Pizza", + "donut": "Munk", + "cake": "Tårta", + "chair": "Stol", + "window": "Fönster", + "couch": "Soffa", + "potted_plant": "Krukväxt", + "bed": "Säng", + "mirror": "Spegel", + "dining_table": "Vardagsrumsbord", + "desk": "Skrivbord", + "toilet": "Toalett", + "tv": "TV", + "laptop": "Bärbar dator", + "remote": "Fjärrkontroll", + "keyboard": "Tangentbord", + "cell_phone": "Mobiltelefon", + "microwave": "Mikrovågsugn", + "sink": "Vask", + "vase": "Vas", + "scissors": "Sax", + "squirrel": "Ekorre", + "deer": "Rådjur", + "fox": "Räv", + "rabbit": "Kanin", + "raccoon": "Tvättbjörn", + "robot_lawnmower": "Robotgräsklippare", + "on_demand": "På begäran", + "face": "Ansikte", + "package": "Paket", + "bbq_grill": "Grill", + "usps": "USPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZPost", + "gls": "GLS", + "dpd": "DPD", + "plate": "Tallrik", + "door": "Dörr", + "oven": "Ugn", + "blender": "Blandare", + "book": "Bok", + "waste_bin": "Soptunna", + "license_plate": "Nummerplåt", + "toothbrush": "Tandborste", + "ups": "UPS", + "teddy_bear": "Nallebjörn" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/classificationModel.json new file mode 100644 index 0000000..2b6c61d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Klassificeringsmodeller - Frigate", + "button": { + "deleteClassificationAttempts": "Ta bort klassificeringsbilder", + "renameCategory": "Byt namn på klass", + "deleteCategory": "Ta bort klass", + "deleteImages": "Ta bort bilder", + "trainModel": "Träna modellen", + "addClassification": "Lägg till klassificering", + "deleteModels": "Ta bort modeller", + "editModel": "Redigera modell" + }, + "toast": { + "success": { + "deletedCategory": "Borttagen klass", + "deletedImage": "Raderade bilder", + "categorizedImage": "Lyckades klassificera bilden", + "trainedModel": "Modellen har tränats.", + "trainingModel": "Modellträning har startat.", + "deletedModel_one": "{{count}} modell har raderats", + "deletedModel_other": "{{count}} modeller har raderats", + "updatedModel": "Uppdaterade modellkonfiguration", + "renamedCategory": "Klassen har bytt namn till {{name}}" + }, + "error": { + "deleteImageFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteCategoryFailed": "Misslyckades med att ta bort klassen: {{errorMessage}}", + "categorizeFailed": "Misslyckades med att kategorisera bilden: {{errorMessage}}", + "trainingFailed": "Modellträningen misslyckades. Kontrollera Frigate loggarna för mer information.", + "deleteModelFailed": "Misslyckades med att ta bort modellen: {{errorMessage}}", + "updateModelFailed": "Misslyckades med att uppdatera modell: {{errorMessage}}", + "trainingFailedToStart": "Misslyckades med att starta modellträning: {{errorMessage}}", + "renameCategoryFailed": "Misslyckades med att byta namn på klassen: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Ta bort klass", + "desc": "Är du säker på att du vill ta bort klassen {{name}}? Detta kommer att ta bort alla associerade bilder permanent och kräva att modellen tränas om.", + "minClassesTitle": "Kan inte ta bort klassen", + "minClassesDesc": "En klassificeringsmodell måste ha minst två klasser. Lägg till ytterligare en klass innan du tar bort den här." + }, + "deleteDatasetImages": { + "title": "Ta bort datamängdsbilder", + "desc_one": "Är du säker på att du vill ta bort {{count}} bild från {{dataset}}? Den här åtgärden kan inte ångras och kräver att modellen tränas om.", + "desc_other": "Är du säker på att du vill ta bort {{count}} bilder från {{dataset}}? Den här åtgärden kan inte ångras och kräver att modellen tränas om." + }, + "deleteTrainImages": { + "title": "Ta bort tränade bilder", + "desc_one": "Är du säker på att du vill ta bort {{count}} bild? Den här åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} bilder? Den här åtgärden kan inte ångras." + }, + "renameCategory": { + "title": "Byt namn på klass", + "desc": "Ange ett nytt namn för {{name}}. Du måste träna om modellen för att namnändringen ska träda i kraft." + }, + "description": { + "invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck." + }, + "train": { + "title": "Nyligen tillagd klassificeringar", + "aria": "Välj senaste klassificeringar", + "titleShort": "Ny" + }, + "categories": "Klasser", + "createCategory": { + "new": "Skapa ny klass" + }, + "categorizeImageAs": "Klassificera bilden som:", + "categorizeImage": "Klassificera bild", + "noModels": { + "object": { + "title": "Inga objektklassificeringsmodeller", + "description": "Skapa en anpassad modell för att klassificera detekterade objekt.", + "buttonText": "Skapa objektmodell" + }, + "state": { + "title": "Inga tillstånd klassificeringsmodeller", + "description": "Skapa en anpassad modell för att övervaka och klassificera tillståndsförändringar i specifika kameraområden.", + "buttonText": "Skapa en tillståndsmodell" + } + }, + "wizard": { + "title": "Skapa ny klassificering", + "steps": { + "nameAndDefine": "Namnge och definiera", + "stateArea": "Stat område", + "chooseExamples": "Välj exempel" + }, + "step1": { + "description": "Tillståndsmodeller övervakar fasta kameraområden för förändringar (t.ex. dörr öppen/stängd). Objektmodeller lägger till klassificeringar till detekterade objekt (t.ex. kända djur, leveranspersoner etc.).", + "name": "Namn", + "namePlaceholder": "Ange modellnamn...", + "type": "Typ", + "typeState": "Tillståndet", + "typeObject": "Objekt", + "objectLabel": "Objektetikett", + "objectLabelPlaceholder": "Välj objekttyp...", + "classificationType": "Klassificeringstyp", + "classificationTypeTip": "Lär dig mer om klassificeringstyper", + "classificationTypeDesc": "Underetiketter lägger till ytterligare text till objektetiketten (t.ex. 'Person: UPS'). Attribut är sökbara metadata som lagras separat i objektmetadata.", + "classificationSubLabel": "Underetikett", + "classificationAttribute": "Attribut", + "classes": "Klasser", + "states": "Tillstånd", + "classesTip": "Lär dig mer om klasser", + "classesStateDesc": "Definiera de olika tillstånd som ditt kameraområde kan vara i. Till exempel: \"öppen\" och \"stängd\" för en garageport.", + "classesObjectDesc": "Definiera de olika kategorierna som detekterade objekt ska klassificeras i. Till exempel: 'leveransperson', 'boende', 'främling' för personklassificering.", + "classPlaceholder": "Ange klassnamn...", + "errors": { + "nameRequired": "Modellnamn krävs", + "nameLength": "Modellnamnet måste vara högst 64 tecken långt", + "nameOnlyNumbers": "Modellnamnet får inte bara innehålla siffror", + "classRequired": "Minst 1 klass krävs", + "classesUnique": "Klassnamn måste vara unika", + "stateRequiresTwoClasses": "Tillståndsmodeller kräver minst två klasser", + "objectLabelRequired": "Välj en objektetikett", + "objectTypeRequired": "Vänligen välj en klassificeringstyp" + } + }, + "step2": { + "description": "Välj kameror och definiera området som ska övervakas för varje kamera. Modellen kommer att klassificera tillståndet för dessa områden.", + "cameras": "Kameror", + "selectCamera": "Välj kamera", + "noCameras": "Klicka på + för att lägga till kameror", + "selectCameraPrompt": "Välj en kamera från listan för att definiera dess övervakningsområde" + }, + "step3": { + "selectImagesPrompt": "Markera alla bilder med: {{className}}", + "selectImagesDescription": "Klicka på bilderna för att välja dem. Klicka på Fortsätt när du är klar med den här klass.", + "generating": { + "title": "Generera exempelbilder", + "description": "Frigate hämtar representativa bilder från dina inspelningar. Det kan ta en stund..." + }, + "training": { + "title": "Träningsmodell", + "description": "Din modell tränas i bakgrunden. Stäng den här dialogrutan så börjar modellen köras så snart träningen är klar." + }, + "retryGenerate": "Försök att generera igen", + "noImages": "Inga exempelbilder genererade", + "classifying": "Klassificering & Träning...", + "trainingStarted": "Träningen har börjat", + "errors": { + "noCameras": "Inga kameror konfigurerade", + "noObjectLabel": "Ingen objektetikett vald", + "generateFailed": "Misslyckades med att generera exempel: {{error}}", + "generationFailed": "Genereringen misslyckades. Försök igen.", + "classifyFailed": "Misslyckades med att klassificera bilder: {{error}}" + }, + "generateSuccess": "Exempelbilder har genererats", + "allImagesRequired_one": "Vänligen klassificera alla bilder. {{count}} bild återstår.", + "allImagesRequired_other": "Vänligen klassificera alla bilder. {{count}} bilder återstår.", + "modelCreated": "Modellen har skapats. Använd vyn Senaste klassificeringar för att lägga till bilder för saknade tillstånd och träna sedan modellen.", + "missingStatesWarning": { + "title": "Exempel på saknade tillstånd", + "description": "Det rekommenderas att välja exempel för alla tillstånd för bästa resultat. Du kan fortsätta utan att välja alla tillstånd, men modellen kommer inte att tränas förrän alla tillstånd har bilder. När du har fortsatt använder du vyn Senaste klassificeringar för att klassificera bilder för de saknade tillstånden och tränar sedan modellen." + } + } + }, + "deleteModel": { + "title": "Ta bort klassificeringsmodell", + "single": "Är du säker på att du vill ta bort {{name}}? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras.", + "desc_one": "Är du säker på att du vill ta bort {{count}} modell? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} modeller? Detta kommer att permanent ta bort all tillhörande data, inklusive bilder och träningsdata. Åtgärden kan inte ångras." + }, + "menu": { + "objects": "Objekt", + "states": "Tillstånd" + }, + "details": { + "scoreInfo": "Poängen representerar den genomsnittliga klassificeringssäkerheten för alla upptäckter av detta objekt." + }, + "edit": { + "title": "Redigera klassificeringsmodell", + "descriptionState": "Redigera klasserna för denna tillståndsklassificeringsmodell. Ändringar kräver omträning av modellen.", + "descriptionObject": "Redigera objekttyp och klassificeringstyp för denna objektklassificeringsmodell.", + "stateClassesInfo": "Observera: För att ändra tillståndsklasser måste modellen omtränas med de uppdaterade klasserna." + }, + "tooltip": { + "trainingInProgress": "Modellen tränar för närvarande", + "noNewImages": "Inga nya bilder att träna. Klassificera fler bilder i datasetet först.", + "noChanges": "Inga ändringar i datamängden sedan senaste träningen.", + "modelNotReady": "Modellen är inte redo för träning" + }, + "none": "Ingen" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/configEditor.json new file mode 100644 index 0000000..7b96ff9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "Kopiera konfiguration", + "saveAndRestart": "Spara & Starta om", + "saveOnly": "Spara", + "toast": { + "success": { + "copyToClipboard": "Konfiguration kopierad till urklipp." + }, + "error": { + "savingError": "Problem att spara konfiguration" + } + }, + "documentTitle": "Ändra konfiguration - Frigate", + "configEditor": "Ändra konfiguration", + "confirm": "Avsluta utan att spara?", + "safeConfigEditor": "Konfigurationsredigeraren (felsäkert läge)", + "safeModeDescription": "Fregate är i felsäkert läge på grund av ett konfigurationsvalideringsfel." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/events.json new file mode 100644 index 0000000..f19596e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/events.json @@ -0,0 +1,63 @@ +{ + "detections": "Detektioner", + "alerts": "Varningar", + "motion": { + "label": "Rörelse", + "only": "Endast rörelse" + }, + "allCameras": "Alla kameror", + "empty": { + "alert": "Det finns inga varningar att granska", + "detection": "Det finns inga detekteringar att granska", + "motion": "Ingen rörelsedata hittad" + }, + "documentTitle": "Granska - Frigate", + "timeline": "Tidslinje", + "events": { + "noFoundForTimePeriod": "Inga hädelser hittade för denna tidsperiod.", + "aria": "Välj händelse", + "label": "Händelse" + }, + "recordings": { + "documentTitle": "Inspelningar - Frigate" + }, + "newReviewItems": { + "label": "Visa nya objekt att granska", + "button": "Nya objekt att granska" + }, + "markAsReviewed": "Markera som granskad", + "calendarFilter": { + "last24Hours": "Senaste 24 timmarna" + }, + "timeline.aria": "Välj tidslinje", + "camera": "Kamera", + "markTheseItemsAsReviewed": "Markera dessa objekt som granskade", + "detected": "upptäckt", + "selected_one": "{{count}} valda", + "selected_other": "{{count}} valda", + "suspiciousActivity": "Misstänkt aktivitet", + "threateningActivity": "Hotande aktivitet", + "detail": { + "noDataFound": "Inga detaljerade data att granska", + "aria": "Växla detaljvy", + "trackedObject_one": "{{count}} objekt", + "trackedObject_other": "{{count}} objekt", + "noObjectDetailData": "Inga objektdetaljdata tillgängliga.", + "label": "Detalj", + "settings": "Detaljvy inställningar", + "alwaysExpandActive": { + "title": "Expandera alltid aktivt", + "desc": "Expandera alltid objektinformationen för det aktiva granskningsobjektet när den är tillgänglig." + } + }, + "objectTrack": { + "trackedPoint": "Spårad punkt", + "clickToSeek": "Klicka för att söka till den här tiden" + }, + "zoomIn": "Zooma in", + "zoomOut": "Zooma ut", + "normalActivity": "Normal", + "needsReview": "Behöver granskas", + "securityConcern": "Säkerhetsproblem", + "select_all": "Alla" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/explore.json new file mode 100644 index 0000000..62ea933 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/explore.json @@ -0,0 +1,293 @@ +{ + "generativeAI": "Generativ AI", + "documentTitle": "Utforska - Frigate", + "exploreIsUnavailable": { + "embeddingsReindexing": { + "startingUp": "Startar upp…", + "estimatedTime": "Beräknad återstående tid:", + "finishingShortly": "Snart klar", + "context": "Utforskaren kan användas efter inbäddade spårade objekt har slutat återindexerat.", + "step": { + "thumbnailsEmbedded": "Miniatyrbilder inbäddad: ", + "descriptionsEmbedded": "Beskrivningar inbäddade: ", + "trackedObjectsProcessed": "Spårade objekt bearbetad: " + } + }, + "title": "Utforska är inte tillgänglig", + "downloadingModels": { + "setup": { + "textModel": "Text modell", + "visionModel": "Visionsmodell", + "visionModelFeatureExtractor": "Funktionsutdragare för visionsmodell", + "textTokenizer": "Texttokeniserare" + }, + "tips": { + "documentation": "Läs dokumentationen", + "context": "Du kanske vill omindexera inbäddningarna av dina spårade objekt när modellerna har laddats ner." + }, + "error": "Ett fel har inträffat. Kontrollera Frigate loggarna.", + "context": "Frigate laddar ner de nödvändiga inbäddningsmodellerna för att stödja den semantiska sökfunktionen. Detta kan ta flera minuter beroende på hastigheten på din nätverksanslutning." + } + }, + "details": { + "timestamp": "tidsstämpel", + "item": { + "title": "Granska objektinformation", + "desc": "Granska objektinformation", + "button": { + "share": "Dela den här recensionen", + "viewInExplore": "Visa i Utforska" + }, + "tips": { + "mismatch_one": "{{count}} otillgängligt objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller detektering, eller så har de redan rensats/raderats.", + "mismatch_other": "{{count}} otillgängliga objekt upptäcktes och inkluderades i detta granskningsobjekt. Dessa objekt kvalificerade sig antingen inte som en varning eller upptäckt, eller så har de redan rensats/raderats.", + "hasMissingObjects": "Justera din konfiguration om du vill att Frigate ska spara spårade objekt för följande etiketter: {{objects}}" + }, + "toast": { + "success": { + "regenerate": "En ny beskrivning har begärts från {{provider}}. Beroende på din leverantörs hastighet kan det ta lite tid att generera den nya beskrivningen.", + "updatedSublabel": "Underetiketten har uppdaterats.", + "updatedLPR": "Nummerplåt har uppdaterats.", + "audioTranscription": "Ljudtranskription har begärts. Beroende på hastigheten på din Frigate-server kan transkriptionen ta lite tid att slutföra." + }, + "error": { + "regenerate": "Kunde inte ringa {{provider}} för en ny beskrivning: {{errorMessage}}", + "updatedSublabelFailed": "Misslyckades med att uppdatera underetiketten: {{errorMessage}}", + "audioTranscription": "Misslyckades med att begära ljudtranskription: {{errorMessage}}", + "updatedLPRFailed": "Misslyckades med att uppdatera nummerplåten: {{errorMessage}}" + } + } + }, + "label": "Märka", + "editSubLabel": { + "title": "Redigera underetikett", + "desc": "Ange en ny underetikett för denna {{label}}", + "descNoLabel": "Ange en ny underetikett för det här spårade objektet" + }, + "editLPR": { + "title": "Redigera nummerplåt", + "desc": "Ange ett nytt nummerplåt för denna {{label}}", + "descNoLabel": "Ange ett nytt nummerplåt för detta spårade objekt" + }, + "snapshotScore": { + "label": "Ögonblicksbildspoäng" + }, + "topScore": { + "label": "Högsta poäng", + "info": "Topppoängen är den högsta medianpoängen för det spårade objektet, så denna kan skilja sig från poängen som visas på miniatyrbilden av sökresultatet." + }, + "score": { + "label": "Poäng" + }, + "recognizedLicensePlate": "Erkänd nummerplåt", + "estimatedSpeed": "Uppskattad hastighet", + "objects": "Objekt", + "camera": "Kamera", + "zones": "Zoner", + "button": { + "findSimilar": "Hitta liknande", + "regenerate": { + "title": "Regenerera", + "label": "Återskapa beskrivningen av spårat objekt" + } + }, + "description": { + "label": "Beskrivning", + "placeholder": "Beskrivning av det spårade objektet", + "aiTips": "Frigate kommer inte att begära en beskrivning från din generativa AI-leverantör förrän det spårade objektets livscykel har avslutats." + }, + "expandRegenerationMenu": "Expandera regenereringsmenyn", + "regenerateFromSnapshot": "Återskapa från ögonblicksbild", + "regenerateFromThumbnails": "Återskapa från miniatyrbilder", + "tips": { + "descriptionSaved": "Beskrivningen har sparats", + "saveDescriptionFailed": "Misslyckades med att uppdatera beskrivningen: {{errorMessage}}" + } + }, + "exploreMore": "Utforska fler {{label}} objekt", + "type": { + "details": "detaljer", + "video": "video", + "snapshot": "ögonblicksbild", + "object_lifecycle": "objektets livscykel", + "thumbnail": "miniatyrbild", + "tracking_details": "spårningsdetaljer" + }, + "trackedObjectDetails": "Detaljer om spårade objekt", + "objectLifecycle": { + "title": "Objektets livscykel", + "noImageFound": "Ingen bild hittades för denna tidsstämpel.", + "createObjectMask": "Skapa objektmask", + "adjustAnnotationSettings": "Justera annoteringsinställningar", + "scrollViewTips": "Scrolla för att se de viktiga ögonblicken i detta objekts livscykel.", + "autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.", + "count": "{{first}} av {{second}}", + "lifecycleItemDesc": { + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Proportion", + "area": "Område" + }, + "visible": "{{label}} upptäckt", + "entered_zone": "{{label}} gick in i {{zones}}", + "active": "{{label}} blev aktiv", + "stationary": "{{label}} blev stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} upptäckt för {{label}}", + "other": "{{label}} igenkänd som {{attribute}}" + }, + "gone": "{{label}} vänster", + "heard": "{{label}} hört" + }, + "annotationSettings": { + "title": "Annoteringsinställningar", + "showAllZones": { + "title": "Visa alla zoner", + "desc": "Visa alltid zoner på ramar där objekt har kommit in i en zon." + }, + "offset": { + "label": "Annoteringsförskjutning", + "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Fältet annotation_offset kan dock användas för att justera detta.", + "documentation": "Läs dokumentationen ", + "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", + "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.", + "toast": { + "success": "Annoterings förskjutningen för {{camera}} har sparats i konfigurationsfilen. Starta om Frigate för att tillämpa dina ändringar." + } + } + }, + "trackedPoint": "Spårad punkt", + "carousel": { + "previous": "Föregående bild", + "next": "Nästa bild" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "Ladda ner video", + "aria": "Ladda ner video" + }, + "downloadSnapshot": { + "label": "Ladda ner ögonblicksbild", + "aria": "Ladda ner ögonblicksbild" + }, + "viewObjectLifecycle": { + "label": "Visa objektets livscykel", + "aria": "Visa objektets livscykel" + }, + "findSimilar": { + "label": "Hitta liknande", + "aria": "Hitta liknande spårade objekt" + }, + "addTrigger": { + "label": "Lägg till utlösare", + "aria": "Lägg till en utlösare för det här spårade objektet" + }, + "audioTranscription": { + "label": "Transkribera", + "aria": "Begär ljudtranskribering" + }, + "submitToPlus": { + "label": "Skicka till Frigate+", + "aria": "Skicka till Frigate Plus" + }, + "viewInHistory": { + "label": "Visa i historik", + "aria": "Visa i historik" + }, + "deleteTrackedObject": { + "label": "Ta bort det här spårade objektet" + }, + "showObjectDetails": { + "label": "Visa objektets plats" + }, + "viewTrackingDetails": { + "label": "Visa spårningsinformation", + "aria": "Visa spårningsdetaljerna" + }, + "hideObjectDetails": { + "label": "Dölj objektsökväg" + }, + "downloadCleanSnapshot": { + "label": "Ladda ner ren ögonblicksbild", + "aria": "Ladda ner ren ögonblicksbild" + } + }, + "dialog": { + "confirmDelete": { + "title": "Bekräfta radering", + "desc": "Om du tar bort det här spårade objektet tas ögonblicksbilden, alla sparade inbäddningar och alla tillhörande spårningsdetaljer bort. Inspelade bilder av det här spårade objektet i historikvyn kommer INTE att raderas.

    Är du säker på att du vill fortsätta?" + } + }, + "noTrackedObjects": "Inga spårade objekt hittades", + "fetchingTrackedObjectsFailed": "Fel vid hämtning av spårade objekt: {{errorMessage}}", + "trackedObjectsCount_one": "{{count}} spårat objekt ", + "trackedObjectsCount_other": "{{count}} spårade objekt ", + "searchResult": { + "tooltip": "Matchade {{type}} vid {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "Spårat objekt har raderats.", + "error": "Misslyckades med att ta bort spårat objekt: {{errorMessage}}" + } + }, + "previousTrackedObject": "Föregående spårade objekt", + "nextTrackedObject": "Nästa spårade objekt" + }, + "aiAnalysis": { + "title": "AI-analys" + }, + "concerns": { + "label": "Oro" + }, + "trackingDetails": { + "title": "Spårningsdetaljer", + "noImageFound": "Ingen bild hittades för denna tidsstämpel.", + "createObjectMask": "Skapa objektmask", + "adjustAnnotationSettings": "Justera annoteringsinställningar", + "scrollViewTips": "Klicka för att se de viktiga ögonblicken i detta objekts livscykel.", + "autoTrackingTips": "Begränsningsrutornas positioner kommer att vara felaktiga för autospårningskameror.", + "count": "{{first}} av {{second}}", + "trackedPoint": "Spårad punkt", + "lifecycleItemDesc": { + "visible": "{{label}} upptäckt", + "entered_zone": "{{label}} gick in i {{zones}}", + "active": "{{label}} blev aktiv", + "stationary": "{{label}} blev stationär", + "attribute": { + "faceOrLicense_plate": "{{attribute}} upptäckt för {{label}}", + "other": "{{label}} igenkänd som {{attribute}}" + }, + "gone": "{{label}} lämnade", + "heard": "{{label}} hördes", + "external": "{{label}} upptäckt", + "header": { + "zones": "Zoner", + "ratio": "Förhållandet", + "area": "Område", + "score": "Resultat" + } + }, + "annotationSettings": { + "title": "Annoteringsinställningar", + "showAllZones": { + "title": "Visa alla zoner", + "desc": "Visa alltid zoner på ramar där objekt har kommit in i en zon." + }, + "offset": { + "label": "Annoteringsförskjutning", + "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Du kan använda den här inställningen för att förskjuta anteckningarna framåt eller bakåt i tiden för att bättre anpassa dem till det inspelade materialet.", + "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", + "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.", + "toast": { + "success": "Annoteringsförskjutningen för {{camera}} har sparats i konfigurationsfilen." + } + } + }, + "carousel": { + "previous": "Föregående bild", + "next": "Nästa bild" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/exports.json new file mode 100644 index 0000000..da2bc13 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Sök", + "documentTitle": "Export - Frigate", + "noExports": "Inga exporter hittade", + "deleteExport": "Radera export", + "deleteExport.desc": "Är du säker att du vill radera {{exportName}}?", + "editExport": { + "desc": "Ange ett nytt namn för denna export.", + "title": "Byt namn på Export", + "saveExport": "Spara Export" + }, + "toast": { + "error": { + "renameExportFailed": "Misslyckades att byta namn på export: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Dela export", + "downloadVideo": "Ladda ner video", + "editName": "Redigera namn", + "deleteExport": "Ta bort export" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/faceLibrary.json new file mode 100644 index 0000000..45a80bf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "details": { + "person": "Person", + "confidence": "Säkerhet", + "face": "Ansiktsdetaljer", + "timestamp": "tidsstämpel", + "faceDesc": "Detaljer om det spårade objektet som genererade detta ansikte", + "unknown": "Okänt", + "subLabelScore": "Underetikettpoäng", + "scoreInfo": "Underetikettpoängen är den viktade poängen för alla igenkända ansiktskonfidenser, så detta kan skilja sig från poängen som visas på ögonblicksbilden." + }, + "description": { + "placeholder": "Ange ett namn för denna samling", + "addFace": "Lägg till en ny samling i ansiktsbiblioteket genom att ladda upp din första bild.", + "invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck." + }, + "documentTitle": "Ansiktsbibliotek - Frigate", + "steps": { + "faceName": "Ange namn", + "uploadFace": "Ladda upp bild på ansikte", + "nextSteps": "Nästa steg", + "description": { + "uploadFace": "Ladda upp en bild på {{name}} som visar deras ansikte framifrån. Bilden behöver inte beskäras till bara deras ansikte." + } + }, + "createFaceLibrary": { + "title": "Skapa samling", + "desc": "Skapa ny samling", + "nextSteps": "För att bygga en stark grund:
  • Använd fliken Senaste Igenkänningar för att välja och träna bilder för varje detekterad person.
  • Fokusera på raka bilder för bästa resultat; undvik att träna bilder som fångar ansikten i en vinkel.
  • ", + "new": "Skapa nytt ansikte" + }, + "train": { + "title": "Senaste Igenkänningar", + "aria": "Välj senaste igenkänningar", + "empty": "Det finns inga ny försök till ansiktsigenkänning", + "titleShort": "Ny" + }, + "uploadFaceImage": { + "title": "Ladda upp ansiktsbild", + "desc": "Ladda upp en bild för att skanna efter ansikte och inkludera {{pageToggle}}" + }, + "selectItem": "Välj {{item}}", + "collections": "Samlingar", + "selectFace": "Välj ansikte", + "deleteFaceLibrary": { + "title": "Ta bort namn", + "desc": "Är du säker på att du vill ta bort samlingen {{name}}? Detta kommer att ta bort alla associerade ansikten permanent." + }, + "deleteFaceAttempts": { + "title": "Ta bort ansikten", + "desc_one": "Är du säker på att du vill ta bort {{count}} ansikte? Den här åtgärden kan inte ångras.", + "desc_other": "Är du säker på att du vill ta bort {{count}} ansikten? Den här åtgärden kan inte ångras." + }, + "imageEntry": { + "dropActive": "Släpp bilden här…", + "dropInstructions": "Dra och släpp eller klistra in en bild här, eller klicka för att välja", + "maxSize": "Maxstorlek: {{size}}MB", + "validation": { + "selectImage": "Välj en bildfil." + } + }, + "nofaces": "Inga ansikten tillgängliga", + "pixels": "{{area}}px", + "readTheDocs": "Läs dokumentationen", + "trainFaceAs": "Träna ansikte som:", + "trainFace": "Träna ansikte", + "toast": { + "success": { + "uploadedImage": "Bilden har laddats upp.", + "addFaceLibrary": "{{name}} har lagts till i ansiktsbiblioteket!", + "deletedFace_one": "{{count}} ansikte har raderats.", + "deletedFace_other": "{{count}} ansikten har raderats.", + "deletedName_one": "{{count}} ansikte har raderats.", + "deletedName_other": "{{count}} ansikten har raderats.", + "renamedFace": "Ansiktet har bytt namn till {{name}}", + "trainedFace": "Ansikte är tränant.", + "updatedFaceScore": "Ansikts poängen har uppdaterats." + }, + "error": { + "uploadingImageFailed": "Misslyckades med att ladda upp bilden: {{errorMessage}}", + "addFaceLibraryFailed": "Misslyckades med att ange ansiktsnamn: {{errorMessage}}", + "deleteFaceFailed": "Misslyckades med att ta bort: {{errorMessage}}", + "deleteNameFailed": "Misslyckades med att ta bort namnet: {{errorMessage}}", + "renameFaceFailed": "Misslyckades med att byta namn på ansikte: {{errorMessage}}", + "trainFailed": "Misslyckades med att träna: {{errorMessage}}", + "updateFaceScoreFailed": "Misslyckades med att uppdatera ansiktspoäng: {{errorMessage}}" + } + }, + "renameFace": { + "title": "Byt namn på ansikte", + "desc": "Ange ett nytt namn för {{name}}" + }, + "button": { + "deleteFaceAttempts": "Ta bort ansikten", + "addFace": "Lägg till ansikte", + "renameFace": "Byt namn på ansikte", + "deleteFace": "Ta bort ansikte", + "uploadImage": "Ladda upp bild", + "reprocessFace": "Återbearbeta ansiktet" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/live.json new file mode 100644 index 0000000..b0873ae --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Live - Frigate", + "documentTitle.withCamera": "{{camera}} - Live - Frigate", + "twoWayTalk": { + "enable": "Aktivera tvåvägssamtal", + "disable": "Avaktivera tvåvägssamtal" + }, + "cameraAudio": { + "disable": "Inaktivera kameraljud", + "enable": "Aktivera kameraljud" + }, + "ptz": { + "zoom": { + "in": { + "label": "Zooma in PTZ kamera" + }, + "out": { + "label": "Zooma ut PTZ kamera" + } + }, + "move": { + "up": { + "label": "Flytta PTZ kamera uppåt" + }, + "right": { + "label": "Flytta PTZ kamera åt höger" + }, + "clickMove": { + "disable": "Inaktivera klick för att flytta", + "label": "Klicka i bilden för att centrera kameran", + "enable": "Aktivera klick för att flytta" + }, + "left": { + "label": "Flytta PTZ kamera till vänster" + }, + "down": { + "label": "Flytta PTZ kamera nedåt" + } + }, + "frame": { + "center": { + "label": "Klicka i bilden för att centrera PTZ kamera" + } + }, + "presets": "PTZ kamera förinställningar", + "focus": { + "in": { + "label": "Fokusera PTZ-kameran in" + }, + "out": { + "label": "Fokusera PTZ-kameran ut" + } + } + }, + "streamStats": { + "enable": "Visa videostatistik", + "disable": "Dölj videostatistik" + }, + "detect": { + "enable": "Aktivera detektering", + "disable": "Avaktivera detektering" + }, + "recording": { + "enable": "Aktivera inspelning", + "disable": "Avaktivera inspelning" + }, + "snapshots": { + "enable": "Aktivera ögonblicksbilder", + "disable": "Avaktivera ögonblicksbilder" + }, + "audioDetect": { + "enable": "Aktivera ljudaktivering", + "disable": "Avaktivera ljudaktivering" + }, + "autotracking": { + "enable": "Aktivera Autospårning", + "disable": "Avaktivera Autospårning" + }, + "notifications": "Notifikationer", + "audio": "Ljud", + "lowBandwidthMode": "Läge för låg bandbredd", + "manualRecording": { + "failedToEnd": "Misslyckades med att avsluta manuell vid behov-inspelning.", + "started": "Starta manuell inspelning vid behov.", + "title": "Vid behov", + "tips": "Ladda ner en omedelbar ögonblicksbild eller starta en manuell händelse baserat på kamerans inställningar för inspelningslagring.", + "playInBackground": { + "label": "Spela upp i bakgrunden", + "desc": "Strömma vidare när spelaren inte visas." + }, + "showStats": { + "label": "Visa Statistik", + "desc": "Visa statistik ovanpå kamerabilden." + }, + "debugView": "Felsökningsvy", + "start": "Starta inspelning vid behov", + "failedToStart": "Misslyckades med att starta manuell inspelning vid behov.", + "recordDisabledTips": "Eftersom inspelning är inaktiverad eller begränsad i konfigurationen för den här kameran kommer endast en ögonblicksbild att sparas.", + "end": "Avsluta vid behov-inspelning", + "ended": "Avslutade manuell vid behov-inspelning." + }, + "cameraSettings": { + "audioDetection": "Ljuddetektering", + "title": "{{kamera}} inställningar", + "cameraEnabled": "Kamera Aktiverad", + "objectDetection": "Objektsdetektering", + "recording": "Inspelning", + "snapshots": "Ögonblicksbilder", + "autotracking": "Autospårning", + "transcription": "Ljudtranskription" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Aktiva Objekt", + "all": "Allt", + "motion": "Rörelse" + }, + "notAllTips": "Din lagringskonfiguration för {{source}}-inspelning är inställd på läge: {{effectiveRetainMode}}, så den här inspelningen vid behov kommer endast att behålla segment av {{effectiveRetainModeName}}." + }, + "editLayout": { + "label": "Redigera Layout", + "group": { + "label": "Redigera Kameragrupp" + }, + "exitEdit": "Lämna Redigering" + }, + "camera": { + "enable": "Aktivera Kamera", + "disable": "Inaktivera Kamera" + }, + "muteCameras": { + "enable": "Slå av ljudet på alla kameror", + "disable": "Slå på ljudet på alla kameror" + }, + "streamingSettings": "Inställningar för Streaming", + "suspend": { + "forTime": "Pausa för: " + }, + "stream": { + "title": "Ström", + "audio": { + "tips": { + "title": "Ljud måste skickas ut från din kamera och konfigureras i go2rtc för den här strömmen.", + "documentation": "Läs dokumentationen: " + }, + "available": "Ljud är tillgängligt för den här strömmen", + "unavailable": "Ljud är inte tillgängligt för den här strömmen" + }, + "twoWayTalk": { + "tips": "Din enhet måste stödja funktionen och WebRTC måste vara konfigurerat för tvåvägskommunikation.", + "tips.documentation": "Läs dokumentationen: ", + "available": "tvåvägskommunikation är tillgängligt för den här strömmen", + "unavailable": "Tvåvägskommunikation är inte tillgängligt för den här strömmen" + }, + "lowBandwidth": { + "tips": "Livevisningen är i lägebandbreddsläge på grund av buffring eller strömningsfel.", + "resetStream": "Återställ ström" + }, + "playInBackground": { + "label": "Spela i bakgrunden", + "tips": "Aktivera det här alternativet för att fortsätta strömma när spelaren är dold." + }, + "debug": { + "picker": "Strömval är inte tillgängligt i felsökningsläge. Felsökningsvyn använder alltid den ström som tilldelats detekteringsrollen." + } + }, + "history": { + "label": "Visa historiskt videomaterial" + }, + "transcription": { + "enable": "Aktivera live-ljudtranskription", + "disable": "Inaktivera live-ljudtranskription" + }, + "noCameras": { + "title": "Inga kameror konfigurerade", + "description": "Börja med att ansluta en kamera till Frigate.", + "buttonText": "Lägg till kamera", + "restricted": { + "title": "Inga kameror tillgängliga", + "description": "Du har inte behörighet att visa några kameror i den här gruppen." + } + }, + "snapshot": { + "takeSnapshot": "Ladda ner omedelbar ögonblicksbild", + "noVideoSource": "Ingen videokälla tillgänglig för ögonblicksbilden.", + "captureFailed": "Misslyckades med att ta en ögonblicksbild.", + "downloadStarted": "Nedladdning av ögonblicksbild har startat." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/recording.json new file mode 100644 index 0000000..b4bfaf2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Export", + "filter": "Filtrera", + "calendar": "Kalender", + "filters": "Filter", + "toast": { + "error": { + "noValidTimeSelected": "Inget giltigt tidsintervall valt", + "endTimeMustAfterStartTime": "Sluttid måste vara efter starttid" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/search.json new file mode 100644 index 0000000..5fd24ab --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/search.json @@ -0,0 +1,72 @@ +{ + "savedSearches": "Sparade Sökningar", + "searchFor": "Sök efter {{inputValue}}", + "search": "Sök", + "button": { + "clear": "Radera sökning", + "save": "Spara sökning", + "filterActive": "Aktiva filter", + "delete": "Ta bort sparad sökning", + "filterInformation": "Filterinformation" + }, + "filter": { + "tips": { + "desc": { + "step1": "Skriv ett filtreringsnyckelnamn följt av ett kolon (t.ex. \"kameror:\").", + "step2": "Välj ett värde utifrån alternativen eller skriv in ditt egna.", + "step3": "Använd flera filter genom att addera dom en efter en med ett mellanslag emellan dom.", + "step4": "Datumfilter (före: och efter:) använd {{DateFormat}} format.", + "step5": "Tidsintervaller använder {{exampleTime}} format.", + "exampleLabel": "Exempel:", + "step6": "Ta bort filter genom att klicka på 'x\" bredvid dom.", + "text": "Filter hjälper dig att begränsa dina sökresultat. Så här använder du dem i inmatningsfältet:" + }, + "title": "Hur du använder filter" + }, + "header": { + "noFilters": "Filter", + "activeFilters": "Aktiva Filter", + "currentFilterType": "Filtervärden" + }, + "label": { + "after": "Efter", + "min_speed": "Minsta Hastighet", + "zones": "Zoner", + "min_score": "Minsta Poäng", + "cameras": "Kameror", + "sub_labels": "Underetiketter", + "search_type": "Söktyp", + "time_range": "Tidsintervall", + "before": "Före", + "max_speed": "Högsta Hastighet", + "recognized_license_plate": "Identifierad registreringsskylt", + "has_clip": "Har klipp", + "has_snapshot": "Har Ögonblicksbild", + "labels": "Etiketter", + "max_score": "Högsta Poäng" + }, + "searchType": { + "thumbnail": "Miniatyrbild", + "description": "Beskrivning" + }, + "toast": { + "error": { + "afterDatebeEarlierBefore": "Datumet \"efter\" måste vara tidigare än datumet \"före\".", + "minScoreMustBeLessOrEqualMaxScore": "\"min_score\" måste vara mindre än eller lika med \"max_score\".", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "\"max_speed\" måste vara större än eller lika med \"min_speed\".", + "beforeDateBeLaterAfter": "Datumet \"före\" måste vara senare än datumet \"efter\".", + "maxScoreMustBeGreaterOrEqualMinScore": "\"max_score\" måste vara större än eller lika med \"min_score\".", + "minSpeedMustBeLessOrEqualMaxSpeed": "\"min_speed\" måste vara mindre än eller lika med \"max_speed\"." + } + } + }, + "similaritySearch": { + "title": "Likhetssökning", + "active": "Likhetsökning aktiv", + "clear": "Rensa likhetsökning" + }, + "placeholder": { + "search": "Sök…" + }, + "trackedObjectId": "Spårad Objekts ID" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/settings.json new file mode 100644 index 0000000..99f74a4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/settings.json @@ -0,0 +1,1238 @@ +{ + "documentTitle": { + "camera": "Kamerainställningar - Frigate", + "default": "Inställningar - Frigate", + "general": "Användargränssnitt Inställningar - Frigate", + "authentication": "Autentiseringsinställningar - Frigate", + "classification": "Klassificeringsinställningar - Frigate", + "masksAndZones": "Maskerings- och zonverktyg - Frigate", + "enrichments": "Förbättringsinställningar - Frigate", + "frigatePlus": "Frigate+ Inställningar - Frigate", + "notifications": "Notifikations Inställningar - Frigate", + "motionTuner": "Rörelse inställning - Frigate", + "object": "Felsöka - Frigate", + "cameraManagement": "Hantera kameror - Frigate", + "cameraReview": "Kameragranskningsinställningar - Frigate" + }, + "general": { + "title": "UI inställningar", + "liveDashboard": { + "automaticLiveView": { + "desc": "Automatiskt byte till kamera där aktivitet registreras. Inaktivering av denna inställning gör att en statisk bild visas i Live Panelen som uppdateras en gång per minut.", + "label": "Automatisk Live Visning" + }, + "playAlertVideos": { + "desc": "Som standard visas varningar på Live panelen som små loopande klipp. Inaktivera denna inställning för att bara visa en statisk bild av nya varningar på denna enhet/webbläsare.", + "label": "Spela upp Varnings videor" + }, + "title": "Live Panel", + "displayCameraNames": { + "label": "Visa alltid kameranamn", + "desc": "Visa alltid kameranamnen i ett chip i instrumentpanelen för livevisning med flera kameror." + }, + "liveFallbackTimeout": { + "label": "Live spelare reserv timeout", + "desc": "När en kameras högkvalitativa liveström inte är tillgänglig, återgå till lågbandbreddsläge efter så här många sekunder. Standard: 3." + } + }, + "storedLayouts": { + "title": "Sparade Layouter", + "desc": "Layouten av kameror i en grupp kan dras för att ändra storlek. Positionerna lagras lokalt i din webbläsare.", + "clearAll": "Rensa Alla Layouter" + }, + "cameraGroupStreaming": { + "desc": "Streaming inställningar för varje kameragrupp lagras lokalt i din webbläsare.", + "clearAll": "Rensa Alla Streaming Inställningar", + "title": "Kamera Grupp Streaming Inställningar" + }, + "recordingsViewer": { + "title": "Inspelningsvisare", + "defaultPlaybackRate": { + "desc": "Standard uppspelningshastighet för inspelningar.", + "label": "Standard Uppspelningshastighet" + } + }, + "calendar": { + "firstWeekday": { + "sunday": "Söndag", + "monday": "Måndag", + "label": "Första Veckodag", + "desc": "Den dag då veckorna i översynskalendern börjar." + }, + "title": "Kalender" + }, + "toast": { + "success": { + "clearStoredLayout": "Rensa lagrad layout för {{cameraName}}", + "clearStreamingSettings": "Rensa streaminginställningar för samtliga kameragrupper." + }, + "error": { + "clearStoredLayoutFailed": "Misslyckades att rensa lagrad layout för: {{errorMessage}}", + "clearStreamingSettingsFailed": "Misslyckades med att rensa streaminginställningar: {{errorMessage}}" + } + } + }, + "cameraSetting": { + "noCamera": "Ingen Kamera", + "camera": "Kamera" + }, + "enrichments": { + "unsavedChanges": "Osparade Förbättringsinställningar", + "birdClassification": { + "title": "Fågel klassificering", + "desc": "Fågelklassificering identifierar kända fåglar med hjälp av en kvantiserad Tensorflow-modell. När en känd fågel känns igen läggs dess vanliga namn till som en underetikett. Denna information inkluderas i användargränssnittet, filter och i aviseringar." + }, + "title": "Förbättringsinställningar", + "semanticSearch": { + "title": "Semantisk sökning", + "desc": "Semantisk sökning i Frigate låter dig hitta spårade objekt i dina granskningsobjekt med hjälp av antingen själva bilden, en användardefinierad textbeskrivning eller en automatiskt genererad.", + "readTheDocumentation": "Läs dokumentationen", + "reindexNow": { + "label": "Omindexera nu", + "desc": "Omindexering kommer att generera inbäddningar för alla spårade objekt. Den här processen körs i bakgrunden och kan maximera din CPU och ta en hel del tid beroende på antalet spårade objekt du har.", + "confirmTitle": "Bekräfta omindexering", + "confirmDesc": "Är du säker på att du vill omindexera alla spårade objektinbäddningar? Den här processen körs i bakgrunden men den kan maximera din processor och ta en hel del tid. Du kan se förloppet på Utforska-sidan.", + "confirmButton": "Omindexera", + "success": "Omindexeringen har startat.", + "alreadyInProgress": "Omindexering pågår redan.", + "error": "Misslyckades med att starta omindexering: {{errorMessage}}" + }, + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för semantiska sökinbäddningar.", + "small": { + "title": "små", + "desc": "Att använda small använder en kvantiserad version av modellen som använder mindre RAM och körs snabbare på CPU med en mycket försumbar skillnad i inbäddningskvalitet." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder hela Jina-modellen och körs automatiskt på GPU:n om tillämpligt." + } + } + }, + "faceRecognition": { + "desc": "Ansiktsigenkänning gör att personer kan tilldelas namn och när deras ansikte känns igen kommer Frigate att tilldela personens namn som en underetikett. Denna information finns i användargränssnittet, filter och i aviseringar.", + "readTheDocumentation": "Läs dokumentationen", + "modelSize": { + "label": "Modellstorlek", + "desc": "Storleken på modellen som används för ansiktsigenkänning.", + "small": { + "title": "små", + "desc": "Att använda small använder en FaceNet-modell för ansiktsinbäddning som körs effektivt på de flesta processorer." + }, + "large": { + "title": "stor", + "desc": "Att använda large använder en ArcFace-modell för ansiktsinbäddning och körs automatiskt på GPU:n om tillämpligt." + } + }, + "title": "Ansikts igenkänning" + }, + "licensePlateRecognition": { + "title": "Nummerplåt Erkännande", + "desc": "Frigate kan känna igen nummerplåt på fordon och automatiskt lägga till de upptäckta tecknen i fältet recognized_license_plate eller ett känt namn som en underetikett till objekt av typen bil. Ett vanligt användningsfall kan vara att läsa nummerplåtor på bilar som kör in på en uppfart eller bilar som passerar på en gata.", + "readTheDocumentation": "Läs dokumentationen" + }, + "restart_required": "Omstart krävs (berikningsinställningar har ändrats)", + "toast": { + "success": "Inställningarna för berikning har sparats. Starta om Frigate för att tillämpa dina ändringar.", + "error": "Kunde inte spara konfigurationsändringarna: {{errorMessage}}" + } + }, + "menu": { + "ui": "Användargränssnitt", + "cameras": "Kamera Inställningar", + "masksAndZones": "Masker / Områden", + "users": "Användare", + "notifications": "Notifikationer", + "frigateplus": "Frigate+", + "enrichments": "Förbättringar", + "motionTuner": "Rörelsemottagare", + "debug": "Felsök", + "triggers": "Utlösare", + "roles": "Roller", + "cameraManagement": "Hantering", + "cameraReview": "Granska" + }, + "dialog": { + "unsavedChanges": { + "title": "Du har osparade ändringar.", + "desc": "Vill du spara dina ändringar innan du fortsätter?" + } + }, + "camera": { + "title": "Kamera inställningar", + "streams": { + "title": "Videoströmmar", + "desc": "Inaktivera tillfälligt en kamera tills Frigate startar om. Om du inaktiverar en kamera helt stoppas Frigates bearbetning av kamerans strömmar. Detektering, inspelning och felsökning kommer inte att vara tillgängliga.
    Obs! Detta inaktiverar inte go2rtc-återströmmar." + }, + "object_descriptions": { + "title": "Generativa AI-objektbeskrivningar", + "desc": "Aktivera/inaktivera tillfälligt generativa AI-objektbeskrivningar för den här kameran. När den är inaktiverad kommer AI-genererade beskrivningar inte att begäras för spårade objekt på den här kameran." + }, + "review_descriptions": { + "title": "Beskrivningar av generativa AI-granskningar", + "desc": "Aktivera/inaktivera tillfälligt genererade AI-granskningsbeskrivningar för den här kameran. När det är inaktiverat kommer AI-genererade beskrivningar inte att begäras för granskningsobjekt på den här kameran." + }, + "review": { + "title": "Recensera", + "desc": "Tillfälligt Aktivera/avaktivera varningar och detekteringar för den här kameran tills Frigate startar om. När den är avaktiverad genereras inga nya granskningsobjekt. ", + "alerts": "Aviseringar ", + "detections": "Detektioner " + }, + "reviewClassification": { + "title": "Granska klassificering", + "desc": "Frigate kategoriserar granskningsobjekt som varningar och detekteringar. Som standard betraktas alla person- och bil-objekt som varningar. Du kan förfina kategoriseringen av dina granskningsobjekt genom att konfigurera obligatoriska zoner för dem.", + "noDefinedZones": "Inga zoner är definierade för den här kameran.", + "objectAlertsTips": "Alla {{alertsLabels}}-objekt på {{cameraName}} kommer att visas som varningar.", + "zoneObjectAlertsTips": "Alla {{alertsLabels}} objekt som upptäcks i {{zone}} på {{cameraName}} kommer att visas som varningar.", + "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som detektioner oavsett vilken zon de befinner sig i.", + "zoneObjectDetectionsTips": { + "text": "Alla {{detectionsLabels}}-objekt som inte kategoriseras i {{zone}} på {{cameraName}} kommer att visas som detektioner.", + "notSelectDetections": "Alla {{detectionsLabels}} objekt som upptäckts i {{zone}} på {{cameraName}} och som inte kategoriserats som varningar kommer att visas som detekteringar oavsett vilken zon de befinner sig i.", + "regardlessOfZoneObjectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som detektioner oavsett vilken zon de befinner sig i." + }, + "unsavedChanges": "Osparade inställningar för granskningsklassificering för {{camera}}", + "selectAlertsZones": "Välj zoner för aviseringar", + "selectDetectionsZones": "Välj zoner för detektioner", + "limitDetections": "Begränsa detektioner till specifika zoner", + "toast": { + "success": "Konfigurationen för granskning av klassificering har sparats. Starta om Frigate för att tillämpa ändringarna." + } + }, + "addCamera": "Lägg till ny kamera", + "editCamera": "Redigera kamera:", + "selectCamera": "Välj en kamera", + "backToSettings": "Tillbaka till kamera inställningar", + "cameraConfig": { + "add": "Lägg till kamera", + "edit": "Redigera kamera", + "description": "Konfigurera kamerainställningar inklusive strömingångar och roller.", + "name": "Kamera namn", + "nameRequired": "Kamera namn krävs", + "nameInvalid": "Kamera namnet får endast innehålla bokstäver, siffror, understreck, eller bindestreck", + "namePlaceholder": "t.ex. fram_dörr", + "enabled": "Aktiverad", + "ffmpeg": { + "inputs": "Ingångsströmmar", + "path": "Strömväg", + "pathRequired": "Strömningsväg krävs", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst en roll krävs", + "rolesUnique": "Varje roll (ljud, detektering, inspelning) kan bara tilldelas en ström", + "addInput": "Lägg till inmatningsström", + "removeInput": "Ta bort inmatningsström", + "inputsRequired": "Minst en indataström krävs" + }, + "toast": { + "success": "Kamera {{cameraName}} sparades" + }, + "nameLength": "Namnet på kameran måste vara kortare än 24 tecken." + } + }, + "masksAndZones": { + "filter": { + "all": "Alla masker och zoner" + }, + "restart_required": "Omstart krävs (masker/zoner har ändrats)", + "toast": { + "success": { + "copyCoordinates": "Kopierade koordinaterna för {{polyName}} till urklipp." + }, + "error": { + "copyCoordinatesFailed": "Kunde inte kopiera koordinaterna till urklipp." + } + }, + "motionMaskLabel": "Rörelsemask {{number}}", + "objectMaskLabel": "Objektmask {{number}} ({{label}})", + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Zonnamnet måste vara minst 2 tecken långt.", + "mustNotBeSameWithCamera": "Zonnamnet får inte vara detsamma som kameranamnet.", + "alreadyExists": "En zon med detta namn finns redan för den här kameran.", + "mustNotContainPeriod": "Zonnamnet får inte innehålla punkter.", + "hasIllegalCharacter": "Zonnamnet innehåller ogiltiga tecken.", + "mustHaveAtLeastOneLetter": "Zonnamnet måste ha minst en bokstav." + } + }, + "distance": { + "error": { + "text": "Avståndet måste vara större än eller lika med 0,1.", + "mustBeFilled": "Alla avståndsfält måste fyllas i för att hastighetsuppskattning ska kunna användas." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Trögheten måste vara över 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Uppehållstiden måste vara större än eller lika med 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Gränsvärdet för hastigheten måste vara större eller lika med 0.1." + } + }, + "polygonDrawing": { + "removeLastPoint": "Ta bort senaste punkten", + "reset": { + "label": "Rensa alla punkter" + }, + "snapPoints": { + "true": "Fäst punkter", + "false": "Fäst inte punkter" + }, + "delete": { + "title": "Bekräfta borttagning", + "desc": "Är du säker på att du vill ta bort {{type}} {{name}}?", + "success": "{{name}} har raderats." + }, + "error": { + "mustBeFinished": "Polygonritningen måste vara klar innan du sparar." + } + } + }, + "zones": { + "label": "Zoner", + "documentTitle": "Redigera zon - Frigate", + "desc": { + "documentation": "Dokumentation", + "title": "Zoner låter dig definiera ett specifikt område av bilden så att du kan avgöra om ett objekt befinner sig inom ett visst område eller inte." + }, + "add": "Lägg till zon", + "edit": "Redigera zon", + "name": { + "title": "Namn", + "inputPlaceHolder": "Ange ett namn…", + "tips": "Namnet måste vara minst 2 tecken långt, måste innehålla minst en bokstav och får inte vara namnet på en kamera eller någon annan zon på den här kameran." + }, + "inertia": { + "title": "Momentum", + "desc": "Anger hur många bildrutor ett objekt måste finnas i en zon innan de räknas som en del av zonen. Standard: 3" + }, + "objects": { + "title": "Objekt", + "desc": "Lista över objekt som gäller för den här zonen." + }, + "allObjects": "Alla objekt", + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "loiteringTime": { + "title": "Tid någon hänger omkring", + "desc": "Ställer in en minsta tid i sekunder som objektet måste vara i zonen för att det ska aktiveras. Standard: 0" + }, + "speedEstimation": { + "title": "Hastighetsuppskattning", + "desc": "Aktivera hastighetsuppskattning för objekt i den här zonen. Zonen måste ha exakt fyra punkter.", + "lineADistance": "Avstånd till linje A ({{unit}})", + "lineBDistance": "Avstånd till linje B ({{unit}})", + "lineCDistance": "Avstånd till linje C ({{unit}})", + "lineDDistance": "Avstånd till linje D ({{unit}})" + }, + "speedThreshold": { + "title": "Hastighetsgräns ({{unit}})", + "desc": "Anger en lägsta hastighet för objekt som ska beaktas i denna zon.", + "toast": { + "error": { + "pointLengthError": "Hastighetsuppskattning har inaktiverats för den här zonen. Zoner med hastighetsuppskattning måste ha exakt 4 punkter.", + "loiteringTimeError": "Zoner med uppehållstider större än 0 bör inte användas vid hastighetsuppskattning." + } + } + }, + "toast": { + "success": "Zonen ({{zoneName}}) har sparats." + } + }, + "motionMasks": { + "label": "Rörelsemask", + "documentTitle": "Redigera rörelsemask - Frigate", + "desc": { + "title": "Rörelsemasker används för att förhindra att oönskade typer av rörelser utlöser detektering. Övermaskering gör det svårare att spåra objekt.", + "documentation": "Dokumentation" + }, + "add": "Ny rörelsemask", + "edit": "Redigera rörelsemask", + "context": { + "title": "Rörelsemasker används för att förhindra att oönskade typer av rörelser utlöser detektering (till exempel: trädgrenar, kameratidsstämplar). Rörelsemasker bör användas mycket sparsamt, övermaskering gör det svårare att spåra objekt." + }, + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "polygonAreaTooLarge": { + "title": "Rörelsemasken täcker {{polygonArea}}% av kamerabilden. Stora rörelsemasker rekommenderas inte.", + "tips": "Rörelsemasker förhindrar inte att objekt upptäcks. Du bör använda en obligatorisk zon istället." + }, + "toast": { + "success": { + "title": "{{polygonName}} har sparats.", + "noName": "Rörelsemasken har sparats." + } + } + }, + "objectMasks": { + "label": "Objektmasker", + "documentTitle": "Redigera objektmask - Frigate", + "point_one": "{{count}} poäng", + "point_other": "{{count}} poäng", + "desc": { + "title": "Objektfiltermasker används för att filtrera bort falska positiva resultat för en given objekttyp baserat på plats.", + "documentation": "Dokumentation" + }, + "add": "Lägg till objektmask", + "edit": "Redigera objektmask", + "context": "Objektfiltermasker används för att filtrera bort falska positiva resultat för en given objekttyp baserat på plats.", + "clickDrawPolygon": "Klicka för att rita en polygon på bilden.", + "objects": { + "title": "Objekt", + "desc": "Objekttypen som gäller för den här objektmasken.", + "allObjectTypes": "Alla objekttyper" + }, + "toast": { + "success": { + "title": "{{polygonName}} har sparats.", + "noName": "Objektmasken har sparats." + } + } + } + }, + "motionDetectionTuner": { + "title": "Rörelsedetekteringstuner", + "unsavedChanges": "Osparade ändringar i Motion Tuner ({{camera}})", + "desc": { + "title": "Frigate använder rörelsedetektering som en första kontroll för att se om det händer något i bilden som är värt att kontrollera med objektdetektering.", + "documentation": "Läs guiden för rörelsejustering" + }, + "Threshold": { + "title": "Tröskel", + "desc": "Tröskelvärdet anger hur mycket förändring i en pixels luminans som krävs för att betraktas som rörelse. Standard: 30" + }, + "contourArea": { + "title": "Konturområde", + "desc": "Konturareans värde används för att avgöra vilka grupper av ändrade pixlar som kvalificeras som rörelse. Standard: 10" + }, + "improveContrast": { + "title": "Förbättra kontrasten", + "desc": "Förbättra kontrasten för mörkare scener. Standard: PÅ" + }, + "toast": { + "success": "Rörelseinställningarna har sparats." + } + }, + "debug": { + "title": "Felsök", + "detectorDesc": "Fregate använder dina detektorer ({{detectors}}) för att upptäcka objekt i din kameras videoström.", + "desc": "Felsökningsvyn visar en realtidsvy av spårade objekt och deras statistik. Objektlistan visar en tidsfördröjd sammanfattning av upptäckta objekt.", + "openCameraWebUI": "Öppna {{camera}}s webbgränssnitt", + "debugging": "Felsökning", + "objectList": "Objektlista", + "noObjects": "Inga föremål", + "audio": { + "title": "Ljud", + "noAudioDetections": "Inga ljuddetekteringar", + "score": "betyg", + "currentRMS": "Nuvarande RMS", + "currentdbFS": "Nuvarande dbFS" + }, + "boundingBoxes": { + "title": "Avgränsande rutor", + "desc": "Visa avgränsningsrutor runt spårade objekt", + "colors": { + "label": "Färger för objektgränser", + "info": "
  • Vid uppstart tilldelas olika färger till varje objektetikett
  • En mörkblå tunn linje indikerar att objektet inte detekteras vid denna aktuella tidpunkt
  • En grå tunn linje indikerar att objektet detekteras som stillastående
  • En tjock linje indikerar att objektet är föremål för autospårning (när det är aktiverat)
  • " + } + }, + "timestamp": { + "title": "Tidsstämpel", + "desc": "Lägg en tidsstämpel över bilden" + }, + "zones": { + "title": "Zoner", + "desc": "Visa en översikt över alla definierade zoner" + }, + "mask": { + "title": "Rörelsemasker", + "desc": "Visa rörelsemaskpolygoner" + }, + "motion": { + "title": "Rörelseboxar", + "desc": "Visa rutor runt områden där rörelse detekteras", + "tips": "

    Rörelserutor


    Röda rutor kommer att läggas över områden i bilden där rörelse för närvarande detekteras

    " + }, + "regions": { + "title": "Regioner", + "desc": "Visa en ruta med det intresseområde som skickats till objektdetektorn", + "tips": "

    Regionsrutor


    Ljusgröna rutor kommer att läggas över intressanta områden i bilden som skickas till objektdetektorn.

    " + }, + "paths": { + "title": "Vägar", + "desc": "Visa viktiga punkter i det spårade objektets bana", + "tips": "

    Vägar


    Linjer och cirklar indikerar viktiga punkter som det spårade objektet har flyttat under sin livscykel.

    " + }, + "objectShapeFilterDrawing": { + "title": "Ritning av objektformfilter", + "desc": "Rita en rektangel på bilden för att visa detaljer om area och förhållande", + "tips": "Aktivera det här alternativet för att rita en rektangel på kamerabilden för att visa dess area och förhållande. Dessa värden kan sedan användas för att ställa in parametrar för objektformsfilter i din konfiguration.", + "score": "Betyg", + "ratio": "Förhållandet", + "area": "Område" + } + }, + "users": { + "title": "Användare", + "management": { + "title": "Användarhantering", + "desc": "Hantera användarkonton för denna Frigate-instans." + }, + "addUser": "Lägg till användare", + "updatePassword": "Uppdatera lösenord", + "toast": { + "success": { + "createUser": "Användaren {{user}} har skapats", + "deleteUser": "Användaren {{user}} har raderats", + "updatePassword": "Lösenordet har uppdaterats.", + "roleUpdated": "Rollen uppdaterades för {{user}}" + }, + "error": { + "setPasswordFailed": "Misslyckades med att spara lösenordet: {{errorMessage}}", + "createUserFailed": "Misslyckades med att skapa användare: {{errorMessage}}", + "deleteUserFailed": "Misslyckades med att ta bort användaren: {{errorMessage}}", + "roleUpdateFailed": "Misslyckades med att uppdatera rollen: {{errorMessage}}" + } + }, + "table": { + "username": "Användarnamn", + "actions": "Åtgärder", + "role": "Roll", + "noUsers": "Inga användare hittades.", + "changeRole": "Ändra användarroll", + "password": "Lösenord", + "deleteUser": "Ta bort användare" + }, + "dialog": { + "form": { + "user": { + "title": "Användarnamn", + "desc": "Endast bokstäver, siffror, punkter och understreck är tillåtna.", + "placeholder": "Ange användarnamn" + }, + "password": { + "title": "Lösenord", + "strength": { + "title": "Lösenordsstyrka: ", + "weak": "Svag", + "medium": "Mellanstark", + "strong": "Stark", + "veryStrong": "Mycket stark" + }, + "match": "Lösenorden matchar", + "notMatch": "Lösenorden matchar inte", + "placeholder": "Ange lösenord", + "confirm": { + "title": "Bekräfta lösenord", + "placeholder": "Bekräfta lösenord" + }, + "show": "Visa lösenord", + "hide": "Dölj lösenord", + "requirements": { + "title": "Lösenordskrav:", + "length": "Minst 8 tecken", + "uppercase": "Minst en stor bokstav", + "digit": "Minst en siffra", + "special": "Minst ett specialtecken (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "Nytt lösenord", + "placeholder": "Ange nytt lösenord", + "confirm": { + "placeholder": "Ange nytt lösenord igen" + } + }, + "usernameIsRequired": "Användarnamn krävs", + "passwordIsRequired": "Lösenord krävs", + "currentPassword": { + "title": "Nuvarande lösenord", + "placeholder": "Ange ditt nuvarande lösenord" + } + }, + "createUser": { + "title": "Skapa ny användare", + "desc": "Lägg till ett nytt användarkonto och ange en roll för åtkomst till områden i Frigate gränssnittet.", + "usernameOnlyInclude": "Användarnamnet får endast innehålla bokstäver, siffror, . eller _", + "confirmPassword": "Vänligen bekräfta ditt lösenord" + }, + "deleteUser": { + "title": "Ta bort användare", + "desc": "Den här åtgärden kan inte ångras. Detta kommer att permanent radera användarkontot och all tillhörande data.", + "warn": "Är du säker på att du vill ta bort {{username}}?" + }, + "passwordSetting": { + "cannotBeEmpty": "Lösenordet får inte vara tomt", + "doNotMatch": "Lösenorden matchar inte", + "updatePassword": "Uppdatera lösenord för {{username}}", + "setPassword": "Ange lösenord", + "desc": "Skapa ett starkt lösenord för att säkra det här kontot.", + "currentPasswordRequired": "Nuvarande lösenord krävs", + "incorrectCurrentPassword": "Nuvarande lösenord är felaktigt", + "passwordVerificationFailed": "Misslyckades med att verifiera lösenordet", + "multiDeviceWarning": "Alla andra enheter där du är inloggad måste logga in igen inom {{refresh_time}}.", + "multiDeviceAdmin": "Du kan också tvinga alla användare att autentisera om sig omedelbart genom att rotera din JWT-hemlighet." + }, + "changeRole": { + "title": "Ändra användarroll", + "select": "Välj en roll", + "desc": "Uppdatera behörigheter för {{username}}", + "roleInfo": { + "intro": "Välj lämplig roll för den här användaren:", + "admin": "Administratör", + "adminDesc": "Full åtkomst till alla funktioner.", + "viewer": "Åskådare", + "viewerDesc": "Begränsat till Live-dashboards, Review, Explore, och Exports bara.", + "customDesc": "Anpassad roll med specifik kameraåtkomst." + } + } + } + }, + "notification": { + "title": "Aviseringar", + "notificationSettings": { + "title": "Aviseringsinställningar", + "desc": "Frigate kan skicka push-notiser till din enhet när den körs i webbläsare eller installerad som PWA." + }, + "globalSettings": { + "title": "Övergripande inställningar", + "desc": "Stäng tillfälligt av aviseringar för specifika kameror på alla registrerade enheter." + }, + "email": { + "title": "E-post", + "placeholder": "t.ex. exempel@epost.se", + "desc": "En giltig e-postadress krävs och kommer att användas för att meddela dig om det uppstår problem med push-tjänsten." + }, + "cameras": { + "title": "Kameror", + "noCameras": "Inga kameror tillgängliga", + "desc": "Välj vilka kameror som notifikationer ska aktiveras för." + }, + "unregisterDevice": "Avregistrera enheten", + "sendTestNotification": "Skicka testnotis", + "active": "Aviseringar är aktiva", + "notificationUnavailable": { + "title": "Meddelanden otillgängliga", + "desc": "Webb push-meddelanden kräver en säker kontext (https://…). Detta är en begränsning i webbläsaren. Få säker åtkomst till Frigate för att använda meddelanden." + }, + "deviceSpecific": "Enhetsspecifika inställningar", + "registerDevice": "Registrera den här enheten", + "unsavedRegistrations": "Osparade aviseringsregistreringar", + "unsavedChanges": "Osparade ändringar till aviseringar", + "suspended": "Aviseringar avstängda {{time}}", + "suspendTime": { + "suspend": "Pausa", + "5minutes": "Pausa i 5 minuter", + "10minutes": "Pausa i 10 minuter", + "30minutes": "Pausa i 30 minuter", + "1hour": "Pausa i 1 timme", + "12hours": "Pausa i 12 timmar", + "24hours": "Pausa i 24 timmar", + "untilRestart": "Pausa tills omstart" + }, + "cancelSuspension": "Avbryt pausning", + "toast": { + "success": { + "registered": "Registreringen för aviseringar har lyckats. Omstart av Frigate krävs innan några aviseringar (inklusive en testavisering) kan skickas.", + "settingSaved": "Aviseringsinställningarna har sparats." + }, + "error": { + "registerFailed": "Det gick inte att spara aviseringsregistreringen." + } + } + }, + "roles": { + "addRole": "Lägg till roll", + "table": { + "role": "Roll", + "cameras": "Kameror", + "noRoles": "Inga anpassade roller hittades.", + "editCameras": "Redigera kameror", + "deleteRole": "Radera roll", + "actions": "Åtgärder" + }, + "toast": { + "success": { + "createRole": "Roll {{role}} skapad", + "updateCameras": "Kameror uppdaterade för roll {{role}}", + "deleteRole": "Roll {{role}} raderad", + "userRolesUpdated_one": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror.", + "userRolesUpdated_other": "{{count}} användare som tilldelats den här rollen har uppdaterats till 'tittare', vilket har åtkomst till alla kameror." + }, + "error": { + "createRoleFailed": "Misslyckades att skapa roll: {{errorMessage}}", + "updateCamerasFailed": "Misslyckades att uppdatera kameror: {{errorMessage}}", + "deleteRoleFailed": "Misslyckades att radera roll: {{errorMessage}}", + "userUpdateFailed": "Misslyckades att uppdatera användar-roller: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Skapa ny roll", + "desc": "Skapa en ny roll och ange kamera åtkomstbehörigheter." + }, + "deleteRole": { + "title": "Radera roll", + "deleting": "Raderar...", + "desc": "Den här åtgärden kan inte ångras. Detta kommer att ta bort rollen permanent och tilldela alla användare med rollen 'tittare', vilket ger tittaren åtkomst till alla kameror.", + "warn": "Är du säker på att du vill ta bort {{role}}?" + }, + "form": { + "role": { + "placeholder": "Ange rollens namn", + "desc": "Enbart bokstäver, siffror, punkter och understreck tillåtna.", + "roleIsRequired": "Rollens namn krävs", + "roleExists": "En roll med detta namn finns redan.", + "title": "Rollnamn", + "roleOnlyInclude": "Rollnamnet får endast innehålla bokstäver, siffror, . eller _" + }, + "cameras": { + "title": "Kameror", + "required": "Minst en kamera måste väljas.", + "desc": "Välj kameror som den här rollen har åtkomst till. Minst en kamera krävs." + } + }, + "editCameras": { + "title": "Redigera rollkameror", + "desc": "Uppdatera kameraåtkomst för rollen {{role}}." + } + }, + "management": { + "title": "Hantering av tittarroller", + "desc": "Hantera anpassade tittarroller och deras kameraåtkomstbehörigheter för den här Frigate instansen." + } + }, + "frigatePlus": { + "title": "Frigate+ Inställningar", + "apiKey": { + "notValidated": "Frigate+ API-nyckeln upptäcktes inte eller validerades inte", + "desc": "Frigate+ API-nyckeln möjliggör integration med Frigate+-tjänsten.", + "plusLink": "Läs mer om Frigate+", + "title": "Frigate+ API-nyckel", + "validated": "Frigate+ API-nyckeln har upptäckts och validerats" + }, + "snapshotConfig": { + "title": "Ögonblicksbild konfiguration", + "desc": "Att skicka till Frigate+ kräver att både snapshots och clean_copy snapshots är aktiverade i din konfiguration.", + "cleanCopyWarning": "Vissa kameror har aktiverade ögonblicksbilder men har ren kopia inaktiverad. Du måste aktivera clean_copy i din ögonblicksbild konfiguration för att kunna skicka bilder från dessa kameror till Frigate+.", + "table": { + "camera": "Kamera", + "snapshots": "Ögonblicksbilder", + "cleanCopySnapshots": "clean_copy Ögonblicksbilder" + } + }, + "modelInfo": { + "title": "Modellinformation", + "modelType": "Modelltyp", + "trainDate": "Träningsdatum", + "baseModel": "Basmodell", + "plusModelType": { + "baseModel": "Basmodell", + "userModel": "Finjusterad" + }, + "supportedDetectors": "Detektorer som stöds", + "cameras": "Kameror", + "loading": "Laddar modellinformation…", + "error": "Misslyckades med att ladda modellinformationen", + "availableModels": "Tillgängliga modeller", + "loadingAvailableModels": "Laddar tillgängliga modeller…", + "modelSelect": "Dina tillgängliga modeller på Frigate+ kan väljas här. Observera att endast modeller som är kompatibla med din nuvarande detektorkonfiguration kan väljas." + }, + "unsavedChanges": "Osparade ändringar av inställningar för Frigate+", + "restart_required": "Omstart krävs (Frigate+ modell ändrad)", + "toast": { + "success": "Inställningarna för Frigate+ har sparats. Starta om Frigate för att tillämpa ändringarna.", + "error": "Kunde inte spara konfigurationsändringarna: {{errorMessage}}" + } + }, + "triggers": { + "documentTitle": "Utlösare", + "management": { + "title": "Utlösare", + "desc": "Hantera utlösare för {{camera}}. Använd miniatyrtypen för att utlösa liknande miniatyrer som ditt valda spårade objekt och beskrivningstypen för att utlösa liknande beskrivningar av text du anger." + }, + "addTrigger": "Lägg till utlösare", + "table": { + "name": "Namn", + "type": "Typ", + "content": "Innehåll", + "threshold": "Tröskel", + "actions": "Åtgärder", + "noTriggers": "Inga utlösare konfigurerade för den här kameran.", + "edit": "Redigera", + "deleteTrigger": "Ta bort utlösare", + "lastTriggered": "Senast utlöst" + }, + "type": { + "thumbnail": "Miniatyrbild", + "description": "Beskrivning" + }, + "actions": { + "notification": "Skicka avisering", + "alert": "Markera som Varning", + "sub_label": "Lägg till underetikett", + "attribute": "Lägg till attribut" + }, + "dialog": { + "createTrigger": { + "title": "Skapa utlösare", + "desc": "Skapa en utlösare för kamera {{camera}}" + }, + "editTrigger": { + "title": "Redigera utlösare", + "desc": "Redigera inställningarna för utlösare på kameran {{camera}}" + }, + "deleteTrigger": { + "title": "Ta bort utlösare", + "desc": "Är du säker på att du vill ta bort utlösaren {{triggerName}}? Den här åtgärden kan inte ångras." + }, + "form": { + "name": { + "title": "Namn", + "placeholder": "Namnge denna utlösare", + "error": { + "minLength": "Fältet måste vara minst 2 tecken långt.", + "invalidCharacters": "Fältet får bara innehålla bokstäver, siffror, understreck och bindestreck.", + "alreadyExists": "En utlösare med detta namn finns redan för den här kameran." + }, + "description": "Ange ett unikt namn eller en unik beskrivning för att identifiera den här utlösaren" + }, + "enabled": { + "description": "Aktivera eller inaktivera den här utlösaren" + }, + "type": { + "title": "Typ", + "placeholder": "Välj utlösartyp", + "description": "Utlöses när en liknande beskrivning av spårat objekt detekteras", + "thumbnail": "Utlöses när en liknande miniatyrbild av ett spårat objekt upptäcks" + }, + "content": { + "title": "Innehåll", + "imagePlaceholder": "Välj en miniatyrbild", + "textPlaceholder": "Ange textinnehåll", + "imageDesc": "Endast de senaste 100 miniatyrerna visas. Om du inte hittar önskad miniatyr kan du granska tidigare objekt i Utforska och skapa en utlösare från menyn där.", + "textDesc": "Ange text för att utlösa den här åtgärden när en liknande beskrivning av spårat objekt upptäcks.", + "error": { + "required": "Innehåll krävs." + } + }, + "threshold": { + "title": "Tröskel", + "error": { + "min": "Tröskelvärdet måste vara minst 0", + "max": "Tröskelvärdet får vara högst 1" + }, + "desc": "Ställ in likhetströskeln för denna utlösare. En högre tröskel innebär att en bättre matchning krävs för att utlösaren ska aktiveras." + }, + "actions": { + "title": "Åtgärder", + "desc": "Som standard utlöser Frigate ett MQTT-meddelande för alla utlösare. Underetiketter lägger till utlösarnamnet till objektetiketten. Attribut är sökbara metadata som lagras separat i de spårade objektmetadata.", + "error": { + "min": "Minst en åtgärd måste väljas." + } + }, + "friendly_name": { + "title": "Vänligt namn", + "placeholder": "Namnge eller beskriv denna utlösare", + "description": "Ett valfritt vänligt namn eller en beskrivande text för denna utlösare." + } + } + }, + "toast": { + "success": { + "createTrigger": "Utlösaren {{name}} har skapats.", + "updateTrigger": "Utlösaren {{name}} har uppdaterats.", + "deleteTrigger": "Utlösaren {{name}} har raderats." + }, + "error": { + "createTriggerFailed": "Misslyckades med att skapa utlösaren: {{errorMessage}}", + "updateTriggerFailed": "Misslyckades med att uppdatera utlösaren: {{errorMessage}}", + "deleteTriggerFailed": "Misslyckades med att ta bort utlösaren: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Semantisk sökning är inaktiverad", + "desc": "Semantisk sökning måste vara aktiverad för att använda Utlösare." + }, + "wizard": { + "title": "Skapa utlösare", + "step1": { + "description": "Konfigurera grundinställningarna för din trigger." + }, + "step2": { + "description": "Ställ in innehållet som ska utlösa den här åtgärden." + }, + "step3": { + "description": "Konfigurera tröskelvärdet och åtgärderna för den här utlösaren." + }, + "steps": { + "nameAndType": "Namn och typ", + "configureData": "Konfigurera data", + "thresholdAndActions": "Tröskelvärde och åtgärder" + } + } + }, + "cameraWizard": { + "title": "Lägg till kamera", + "description": "Följ stegen nedan för att lägga till en ny kamera i din Frigate-installation.", + "steps": { + "nameAndConnection": "Namn och anslutning", + "streamConfiguration": "Strömkonfiguration", + "validationAndTesting": "Validering och testning", + "probeOrSnapshot": "Prob eller ögonblicksbild" + }, + "save": { + "success": "Ny kamera {{cameraName}} har sparats.", + "failure": "Fel vid sparning av {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Upplösning", + "video": "Video", + "audio": "Ljud", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Ange en giltig strömnings länk", + "testFailed": "Strömtest misslyckades: {{error}}" + }, + "step1": { + "description": "Ange dina kamerauppgifter och välj att undersöka kameran eller manuellt välja märke.", + "cameraName": "Kameranamn", + "cameraNamePlaceholder": "t.ex. ytterdörr eller Översikt över bakgård", + "host": "Värd-/IP-adress", + "port": "Portnummer", + "username": "Användarnamn", + "usernamePlaceholder": "Frivillig", + "password": "Lösenord", + "passwordPlaceholder": "Frivillig", + "selectTransport": "Välj transportprotokoll", + "cameraBrand": "Kameramärke", + "selectBrand": "Välj kameramärke för URL-mall", + "customUrl": "Anpassad ström länk", + "brandInformation": "Varumärkesinformation", + "brandUrlFormat": "För kameror med RTSP URL-formatet: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://användarnamn:passord@värd:port/text", + "testConnection": "Testa anslutning", + "testSuccess": "Anslutningstestet lyckades!", + "testFailed": "Anslutningstestet misslyckades. Kontrollera dina indata och försök igen.", + "streamDetails": "Streamdetaljer", + "warnings": { + "noSnapshot": "Det gick inte att hämta en ögonblicksbild från den konfigurerade strömmen." + }, + "errors": { + "brandOrCustomUrlRequired": "Välj antingen ett kameramärke med värd/IP eller välj \"Annat\" med en anpassad URL", + "nameRequired": "Kameranamn krävs", + "nameLength": "Kameranamnet måste vara högst 64 tecken långt", + "invalidCharacters": "Kameranamnet innehåller ogiltiga tecken", + "nameExists": "Kameranamnet finns redan", + "brands": { + "reolink-rtsp": "Reolink RTSP rekommenderas inte. Aktivera HTTP i kamerans firmwareinställningar och starta om guiden." + }, + "customUrlRtspRequired": "Anpassade webbadresser måste börja med \"rtsp://\". Manuell konfiguration krävs för kameraströmmar som inte använder RTSP." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Undersöker kamerans metadata...", + "fetchingSnapshot": "Hämtar kamerabild..." + }, + "connectionSettings": "Anslutningsinställningar", + "detectionMethod": "Strömdetekteringsmetod", + "onvifPort": "ONVIF-port", + "probeMode": "Undersök kameran", + "manualMode": "Manuellt val", + "detectionMethodDescription": "Undersök kameran med ONVIF (om det stöds) för att hitta kameraströms-URL:er, eller välj kameramärke manuellt för att använda fördefinierade URL:er. För att ange en anpassad RTSP-URL, välj den manuella metoden och välj \"Annat\".", + "onvifPortDescription": "För kameror som stöder ONVIF är detta vanligtvis 80 eller 8080.", + "useDigestAuth": "Använd digest-autentisering", + "useDigestAuthDescription": "Använd HTTP-sammanfattningsautentisering för ONVIF. Vissa kameror kan kräva ett dedikerat ONVIF-användarnamn/lösenord istället för standardadministratörsanvändaren." + }, + "step2": { + "description": "Undersök kameran efter tillgängliga strömmar eller konfigurera manuella inställningar baserat på din valda detekteringsmetod.", + "streamsTitle": "Kameraströmmar", + "addStream": "Lägg till ström", + "addAnotherStream": "Lägg till ytterligare en ström", + "streamTitle": "Ström {{number}}", + "streamUrl": "Ström URL", + "streamUrlPlaceholder": "rtsp://användarnamn:lösenord@värd:portnummer/plats", + "url": "URL", + "resolution": "Upplösning", + "selectResolution": "Välj upplösning", + "quality": "Kvalitet", + "selectQuality": "Välj kvalitet", + "roles": "Roller", + "roleLabels": { + "detect": "Objektdetektering", + "record": "Inspelning", + "audio": "Ljud" + }, + "testStream": "Testa anslutning", + "testSuccess": "Anslutningstestet lyckades!", + "testFailed": "Anslutningstestet misslyckades. Kontrollera dina indata och försök igen.", + "testFailedTitle": "Testet misslyckades", + "connected": "Ansluten", + "notConnected": "Inte ansluten", + "featuresTitle": "Funktioner", + "go2rtc": "Minska anslutningar till kameran", + "detectRoleWarning": "Minst en ström måste ha rollen \"upptäcka\" för att fortsätta.", + "rolesPopover": { + "title": "Ström-roller", + "detect": "Huvud video ström för objektdetektering.", + "record": "Sparar segment av videoflödet baserat på konfigurationsinställningar.", + "audio": "Flöde för ljudbaserad detektering." + }, + "featuresPopover": { + "title": "Strömfunktioner", + "description": "Använd go2rtc-omströmning för att minska anslutningar till din kamera." + }, + "streamDetails": "Streamdetaljer", + "probing": "Undersöker kameran...", + "retry": "Försöka igen", + "testing": { + "probingMetadata": "Undersöker kamerans metadata...", + "fetchingSnapshot": "Hämtar kamerabild..." + }, + "probeFailed": "Misslyckades med att undersöka kameran: {{error}}", + "probingDevice": "Undersöker enheten...", + "probeSuccessful": "Kontroll lyckades", + "probeError": "Kontroll fel", + "probeNoSuccess": "Kontroll misslyckades", + "deviceInfo": "Enhetsinformation", + "manufacturer": "Tillverkare", + "model": "Modell", + "firmware": "Inbyggd programvara", + "profiles": "Profiler", + "ptzSupport": "PTZ-stöd", + "autotrackingSupport": "Stöd för Autospårning", + "presets": "Förinställningar", + "rtspCandidates": "RTSP-kandidater", + "rtspCandidatesDescription": "Följande RTSP-URL:er hittades från kamera kontrollen. Testa anslutningen för att visa strömmetadata.", + "noRtspCandidates": "Inga RTSP-URL:er hittades från kameran. Dina inloggningsuppgifter kan vara felaktiga, eller så kanske kameran inte stöder ONVIF eller metoden som används för att hämta RTSP-URL:er. Gå tillbaka och ange RTSP-URL:en manuellt.", + "candidateStreamTitle": "Kandidat {{number}}", + "useCandidate": "Använda", + "uriCopy": "Kopiera", + "uriCopied": "URI kopierad till urklipp", + "testConnection": "Testa anslutning", + "toggleUriView": "Klicka för att växla mellan fullständig URI-vy", + "errors": { + "hostRequired": "Värd-/IP-adress krävs" + } + }, + "step3": { + "description": "Konfigurera strömningsroller och lägg till ytterligare strömmar för din kamera.", + "validationTitle": "Strömvalidering", + "connectAllStreams": "Anslut alla strömmar", + "reconnectionSuccess": "Återanslutningen lyckades.", + "reconnectionPartial": "Vissa strömmar kunde inte återanslutas.", + "streamUnavailable": "Förhandsgranskning av strömmen är inte tillgänglig", + "reload": "Ladda om", + "connecting": "Ansluter...", + "streamTitle": "Ström {{number}}", + "valid": "Giltig", + "failed": "Misslyckades", + "notTested": "Inte testad", + "connectStream": "Ansluta", + "connectingStream": "Ansluter", + "disconnectStream": "Koppla från", + "estimatedBandwidth": "Uppskattad bandbredd", + "roles": "Roller", + "none": "Ingen", + "error": "Fel", + "streamValidated": "Ström {{number}} har validerats", + "streamValidationFailed": "Validering av ström {{number}} misslyckades", + "saveAndApply": "Spara ny kamera", + "saveError": "Ogiltig konfiguration. Kontrollera dina inställningar.", + "issues": { + "title": "Strömvalidering", + "videoCodecGood": "Videokodeken är {{codec}}.", + "audioCodecGood": "Ljudkodeken är {{codec}}.", + "noAudioWarning": "Inget ljud upptäcktes för den här strömmen, inspelningarna kommer inte att ha något ljud.", + "audioCodecRecordError": "AAC-ljudkodeken krävs för att stödja ljud i inspelningar.", + "audioCodecRequired": "En ljudström krävs för att stödja ljuddetektering.", + "restreamingWarning": "Att minska anslutningarna till kameran för inspelningsströmmen kan öka CPU-användningen något.", + "dahua": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Dahua / Amcrest / EmpireTech kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "hikvision": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Hikvision kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "resolutionHigh": "En upplösning på {{resolution}} kan orsaka ökad resursanvändning.", + "resolutionLow": "En upplösning på {{resolution}} kan vara för låg för tillförlitlig detektering av små objekt." + }, + "ffmpegModule": "Använd läge för strömkompatibilitet", + "ffmpegModuleDescription": "Om strömmen inte läses in efter flera försök, prova att aktivera detta. När det är aktiverat kommer Frigate att använda ffmpeg-modulen med go2rtc. Detta kan ge bättre kompatibilitet med vissa kameraströmmar.", + "streamsTitle": "Kameraströmmar", + "addStream": "Lägg till ström", + "addAnotherStream": "Lägg till ytterligare en ström", + "streamUrl": "Stream-URL", + "streamUrlPlaceholder": "rtsp://användarnamn:lösenord@värd:portnummer/plats", + "selectStream": "Välj en ström", + "searchCandidates": "Sök kandidater...", + "noStreamFound": "Ingen ström hittades", + "url": "URL", + "resolution": "Upplösning", + "selectResolution": "Välj upplösning", + "quality": "Kvalitet", + "selectQuality": "Välj kvalitet", + "roleLabels": { + "detect": "Objektdetektering", + "record": "Inspelning", + "audio": "Ljud" + }, + "testStream": "Testa anslutning", + "testSuccess": "Streamtestet lyckades!", + "testFailed": "Strömtestet misslyckades", + "testFailedTitle": "Testet misslyckades", + "connected": "Ansluten", + "notConnected": "Inte ansluten", + "featuresTitle": "Funktioner", + "go2rtc": "Minska anslutningar till kameran", + "detectRoleWarning": "Minst en ström måste ha rollen \"upptäck\" för att fortsätta.", + "rolesPopover": { + "title": "Stream-roller", + "detect": "Huvud kamera flöde för objektdetektering.", + "record": "Sparar segment av videoflödet baserat på konfigurationsinställningar.", + "audio": "Flöde för ljudbaserad detektering." + }, + "featuresPopover": { + "title": "Streamfunktioner", + "description": "Använd go2rtc-omströmning för att minska anslutningar till din kamera." + } + }, + "step4": { + "description": "Slutgiltig validering och analys innan du sparar din nya kamera. Anslut varje ström innan du sparar.", + "validationTitle": "Ström validering", + "connectAllStreams": "Anslut alla strömmar", + "reconnectionSuccess": "Återanslutningen lyckades.", + "reconnectionPartial": "Vissa strömmar kunde inte återanslutas.", + "streamUnavailable": "Förhandsgranskning av strömmen är inte tillgänglig", + "reload": "Ladda om", + "connecting": "Ansluter...", + "streamTitle": "Ström {{number}}", + "valid": "Giltig", + "failed": "Misslyckades", + "notTested": "Inte testad", + "connectStream": "Ansluta", + "connectingStream": "Ansluter", + "disconnectStream": "Koppla från", + "estimatedBandwidth": "Uppskattad bandbredd", + "roles": "Roller", + "ffmpegModule": "Använd strömkompatibilitetsläge", + "ffmpegModuleDescription": "Om strömmen inte laddas efter flera försök, försök att aktivera detta. När det är aktiverat kommer Frigate att använda ffmpeg-modulen med go2rtc. Detta kan ge bättre kompatibilitet med vissa kameraströmmar.", + "none": "Ingen", + "error": "Fel", + "streamValidated": "Ström {{number}} har validerats", + "streamValidationFailed": "Validering av ström {{number}} misslyckades", + "saveAndApply": "Spara ny kamera", + "saveError": "Ogiltig konfiguration. Kontrollera dina inställningar.", + "issues": { + "title": "Ström validering", + "videoCodecGood": "Videokodeken är {{codec}}.", + "audioCodecGood": "Ljudkodeken är {{codec}}.", + "resolutionHigh": "En upplösning på {{resolution}} kan orsaka ökad resursanvändning.", + "resolutionLow": "En upplösning på {{resolution}} kan vara för låg för tillförlitlig detektering av små objekt.", + "noAudioWarning": "Inget ljud upptäcktes för den här strömmen, inspelningarna kommer inte att ha ljud.", + "audioCodecRecordError": "AAC-ljudkodeken krävs för att stödja ljud i inspelningar.", + "audioCodecRequired": "En ljudström krävs för att stödja ljuddetektering.", + "restreamingWarning": "Att minska anslutningarna till kameran för inspelningsströmmen kan öka CPU-användningen något.", + "brands": { + "reolink-rtsp": "Reolink RTSP rekommenderas inte. Aktivera HTTP i kamerans firmwareinställningar och starta om guiden.", + "reolink-http": "Reolink HTTP-strömmar bör använda FFmpeg för bättre kompatibilitet. Aktivera \"Använd strömkompatibilitetsläge\" för den här strömmen." + }, + "dahua": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Dahua/Amcrest/EmpireTech-kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + }, + "hikvision": { + "substreamWarning": "Delström 1 är låst till en låg upplösning. Många Hikvision-kameror stöder ytterligare delströmmar som måste aktiveras i kamerans inställningar. Det rekommenderas att kontrollera och använda dessa strömmar om de är tillgängliga." + } + } + } + }, + "cameraManagement": { + "title": "Hantera kameror", + "addCamera": "Lägg till ny kamera", + "editCamera": "Redigera kamera:", + "selectCamera": "Välj en kamera", + "backToSettings": "Tillbaka till kamerainställningar", + "streams": { + "title": "Aktivera/avaktivera kameror", + "desc": "Inaktivera tillfälligt en kamera tills Frigate startar om. Om du inaktiverar en kamera helt stoppas Frigates bearbetning av kamerans strömmar. Detektering, inspelning och felsökning kommer inte att vara tillgängliga.
    Obs! Detta inaktiverar inte go2rtc-återströmmar." + }, + "cameraConfig": { + "add": "Lägg till kamera", + "edit": "Redigera kamera", + "description": "Konfigurera kamerainställningar inklusive strömingångar och roller.", + "name": "Kameranamn", + "nameRequired": "Kameranamn krävs", + "nameLength": "Kameranamnet måste vara kortare än 64 tecken.", + "namePlaceholder": "t.ex. ytterdörr eller Översikt över bakgård", + "enabled": "Aktiverad", + "ffmpeg": { + "inputs": "Ingångsströmmar", + "path": "Strömväg", + "pathRequired": "Strömväg krävs", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "Minst en roll krävs", + "rolesUnique": "Varje roll (ljud, detektering, inspelning) kan bara tilldelas en ström", + "addInput": "Lägg till inmatningsström", + "removeInput": "Ta bort inmatningsström", + "inputsRequired": "Minst en indataström krävs" + }, + "go2rtcStreams": "go2rtc-strömmar", + "streamUrls": "Ström-URL:er", + "addUrl": "Lägg till URL", + "addGo2rtcStream": "Lägg till go2rtc-ström", + "toast": { + "success": "Kamera {{cameraName}} sparades" + } + } + }, + "cameraReview": { + "title": "Inställningar för kameragranskning", + "object_descriptions": { + "title": "Generativa AI-objektbeskrivningar", + "desc": "Aktivera/inaktivera tillfälligt generativa AI-objektbeskrivningar för den här kameran. När den är inaktiverad kommer AI-genererade beskrivningar inte att begäras för spårade objekt på den här kameran." + }, + "review_descriptions": { + "title": "Beskrivningar av generativa AI-granskningar", + "desc": "Tillfälligt aktivera/inaktivera genererade AI-granskningsbeskrivningar för den här kameran. När det är inaktiverat kommer AI-genererade beskrivningar inte att begäras för granskningsobjekt på den här kameran." + }, + "review": { + "title": "Granska", + "desc": "Tillfälligt aktivera/avaktivera varningar och detekteringar för den här kameran tills Frigate startar om. När den är avaktiverad genereras inga nya granskningsobjekt. ", + "alerts": "Aviseringar ", + "detections": "Detektioner " + }, + "reviewClassification": { + "title": "Granska klassificering", + "desc": "Frigate kategoriserar granskningsobjekt som Varningar och Detekteringar. Som standard betraktas alla person- och bil-objekt som Varningar. Du kan förfina kategoriseringen av dina granskningsobjekt genom att konfigurera obligatoriska zoner för dem.", + "noDefinedZones": "Inga zoner är definierade för den här kameran.", + "objectAlertsTips": "Alla {{alertsLabels}}-objekt på {{cameraName}} kommer att visas som Varningar.", + "zoneObjectAlertsTips": "Alla {{alertsLabels}} objekt som upptäcks i {{zone}} på {{cameraName}} kommer att visas som Varningar.", + "objectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som Detektioner oavsett vilken zon de befinner sig i.", + "zoneObjectDetectionsTips": { + "text": "Alla {{detectionsLabels}}-objekt som inte kategoriseras i {{zone}} på {{cameraName}} kommer att visas som Detektioner.", + "notSelectDetections": "Alla {{detectionsLabels}} objekt som upptäckts i {{zone}} på {{cameraName}} och som inte kategoriserats som Varningar kommer att visas som Detekteringar oavsett vilken zon de befinner sig i.", + "regardlessOfZoneObjectDetectionsTips": "Alla {{detectionsLabels}}-objekt som inte kategoriseras på {{cameraName}} kommer att visas som Detektioner oavsett vilken zon de befinner sig i." + }, + "unsavedChanges": "Osparade inställningar för granskningsklassificering för {{camera}}", + "selectAlertsZones": "Välj zoner för Varningar", + "selectDetectionsZones": "Välj zoner för Detektioner", + "limitDetections": "Begränsa detektioner till specifika zoner", + "toast": { + "success": "Konfigurationen för granskning av klassificering har sparats. Starta om Frigate för att tillämpa ändringarna." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/sv/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/sv/views/system.json new file mode 100644 index 0000000..2056b38 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/sv/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "storage": "Lagringsstatistik - Frigate", + "general": "Allmän statistik - Frigate", + "cameras": "Kamerastatistik - Frigate", + "logs": { + "frigate": "Frigate-loggar - Frigate", + "go2rtc": "Go2RTC loggar - Frigate", + "nginx": "Nginx loggar - Frigate" + }, + "enrichments": "Förbättringsstatistik - Frigate" + }, + "logs": { + "copy": { + "label": "Kopiera till urklipp", + "success": "Kopierat loggarna till utklippstavlan", + "error": "Kunde inte kopiera loggarna till utklippstavlan" + }, + "download": { + "label": "Ladda ned logg" + }, + "type": { + "label": "Typ", + "timestamp": "Tidsstämpel", + "message": "Meddelande", + "tag": "Tagg" + }, + "tips": "Loggarna strömmas från Server", + "toast": { + "error": { + "fetchingLogsFailed": "Fel vid hämtning av loggar: {{errorMessage}}", + "whileStreamingLogs": "Fel vid uppspelning av loggar: {{errorMessage}}" + } + } + }, + "title": "System", + "metrics": "Systemdetaljer", + "general": { + "title": "Generellt", + "detector": { + "title": "Detektorer", + "inferenceSpeed": "Detektorns inferenshastighet", + "temperature": "Detektor temperatur", + "cpuUsage": "Detektorns CPU-användning", + "memoryUsage": "Detektor minnes användning", + "cpuUsageInformation": "CPU som används för att förbereda in- och utdata till/från detekteringsmodeller. Detta värde mäter inte inferensanvändning, även om en GPU eller accelerator används." + }, + "hardwareInfo": { + "title": "Hårdvaruinformation", + "gpuUsage": "GPU-användning", + "gpuMemory": "GPU-minne", + "gpuEncoder": "GPU-kodare", + "gpuDecoder": "GPU-avkodare", + "gpuInfo": { + "nvidiaSMIOutput": { + "vbios": "VBios-information: {{vbios}}", + "title": "Nvidia SMI utdata", + "name": "Namn: {{name}}", + "driver": "Drivrutin: {{driver}}", + "cudaComputerCapability": "CUDA beräknings kapacitet: {{cuda_compute}}" + }, + "closeInfo": { + "label": "Stäng GPU-info" + }, + "copyInfo": { + "label": "Kopiera GPU-info" + }, + "toast": { + "success": "Kopierade GPU-info till urklipp" + }, + "vainfoOutput": { + "title": "Vainfo resultat", + "returnCode": "Returkod: {{code}}", + "processOutput": "Bearbeta utdata:", + "processError": "Processfel:" + } + }, + "npuUsage": "NPU-användning", + "npuMemory": "NPU-minne", + "intelGpuWarning": { + "title": "Intel GPU statistik varning", + "message": "GPU statistik otillgänglig", + "description": "Detta är en känd bugg i Intels GPU-statistikrapporteringsverktyg (intel_gpu_top) där den slutar fungera och upprepade gånger returnerar en GPU-användning på 0 %, även i fall där hårdvaruacceleration och objektdetektering körs korrekt på (i)GPU:n. Detta är inte en Frigate-bugg. Du kan starta om värden för att tillfälligt åtgärda problemet och bekräfta att GPU:n fungerar korrekt. Detta påverkar inte prestandan." + } + }, + "otherProcesses": { + "title": "Övriga processer", + "processCpuUsage": "Process CPU-användning", + "processMemoryUsage": "Processminnesanvändning" + } + }, + "storage": { + "cameraStorage": { + "storageUsed": "Lagring", + "percentageOfTotalUsed": "Procentandel av totalt", + "bandwidth": "Bandbredd", + "unused": { + "title": "Oanvänt", + "tips": "Det här värdet kanske inte korrekt representerar det lediga utrymmet tillgängligt för Frigate om du har andra filer lagrade på din hårddisk utöver Frigates inspelningar. Frigate spårar inte lagringsanvändning utanför sina egna inspelningar." + }, + "title": "Kamera lagring", + "camera": "Kamera", + "unusedStorageInformation": "Information om oanvänd lagring" + }, + "title": "Lagring", + "overview": "Översikt", + "recordings": { + "title": "Inspelningar", + "tips": "Detta värde representerar den totala lagringsmängden som används av inspelningarna i Frigates databas. Frigate spårar inte lagringsanvändningen för alla filer på din disk.", + "earliestRecording": "Tidigast tillgängliga inspelning:" + }, + "shm": { + "title": "SHM-allokering (delat minne)", + "warning": "Den nuvarande SHM-storleken på {{total}}MB är för liten. Öka den till minst {{min_shm}}MB." + } + }, + "cameras": { + "title": "Kameror", + "overview": "Översikt", + "info": { + "aspectRatio": "bildförhållande", + "cameraProbeInfo": "{{camera}} Kamerasondinformation", + "streamDataFromFFPROBE": "Strömdata erhålls med ffprobe.", + "codec": "Codec:", + "resolution": "Upplösning:", + "fps": "FPS:", + "unknown": "Okänd", + "audio": "Ljud:", + "error": "Fel: {{error}}", + "tips": { + "title": "Kamera sond information" + }, + "fetching": "Hämtar kamera data", + "stream": "Ström {{idx}}", + "video": "Video:" + }, + "label": { + "detect": "detektera", + "camera": "kamera", + "skipped": "hoppade över", + "ffmpeg": "FFmpeg", + "capture": "spela in", + "overallFramesPerSecond": "totalt antal bilder per sekund", + "overallDetectionsPerSecond": "totala detektioner per sekund", + "overallSkippedDetectionsPerSecond": "totalt antal hoppade detekteringar per sekund", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} inspelning", + "cameraDetect": "{{camName}} upptäcka", + "cameraFramesPerSecond": "{{camName}} bildrutor per sekund", + "cameraDetectionsPerSecond": "{{camName}} detekteringar per sekund", + "cameraSkippedDetectionsPerSecond": "{{camName}} hoppade över detekteringar per sekund" + }, + "framesAndDetections": "Ramar / Detektioner", + "toast": { + "success": { + "copyToClipboard": "Kopierade probdata till urklipp." + }, + "error": { + "unableToProbeCamera": "Kunde inte undersöka kameran: {{errorMessage}}" + } + } + }, + "lastRefreshed": "Senast uppdaterad: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} har hög FFmpeg CPU-användning ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} har hög CPU-användning vid detektering ({{detectAvg}}%)", + "healthy": "Systemet är hälsosamt", + "reindexingEmbeddings": "Omindexering av inbäddningar ({{processed}}% klar)", + "cameraIsOffline": "{{camera}} är urkopplad", + "detectIsSlow": "{{detect}} är långsam ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} är väldigt långsam ({{speed}} ms)", + "shmTooLow": "/dev/shm allokeringen ({{total}} MB) bör ökas till minst {{min}} MB." + }, + "enrichments": { + "title": "Berikningar", + "infPerSecond": "Slutsatser per sekund", + "embeddings": { + "image_embedding": "Bildinbäddning", + "text_embedding": "Textinbäddning", + "face_recognition": "Ansiktsigenkänning", + "plate_recognition": "Nummerplåt igenkänning", + "image_embedding_speed": "Bildinbäddningshastighet", + "face_embedding_speed": "Ansikts inbäddnings hastighet", + "face_recognition_speed": "Ansiktsigenkänningshastighet", + "plate_recognition_speed": "Hastighet för igenkänning av nummerplåtar", + "text_embedding_speed": "Textinbäddningshastighet", + "yolov9_plate_detection_speed": "YOLOv9 nummerplåt detekterings hastighet", + "yolov9_plate_detection": "YOLOv9 nummerplåt detektering", + "review_description": "Recensionsbeskrivning", + "review_description_speed": "Recensionsbeskrivning Hastighet", + "review_description_events_per_second": "Recensionsbeskrivning", + "object_description": "Objekt beskrivning", + "object_description_speed": "Objekt beskrivning hastighet", + "object_description_events_per_second": "Objekt beskrivning" + }, + "averageInf": "Genomsnittlig inferenstid" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ta/audio.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/audio.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/common.json b/sam2-cpu/frigate-dev/web/public/locales/ta/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/common.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/auth.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/auth.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/camera.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/camera.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/dialog.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/dialog.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/filter.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/filter.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/icons.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/icons.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/input.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/input.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ta/components/player.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/components/player.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ta/objects.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/objects.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/configEditor.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/configEditor.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/events.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/events.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/explore.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/explore.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/exports.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/exports.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/faceLibrary.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/faceLibrary.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/live.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/live.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/recording.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/recording.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/search.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/search.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/settings.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ta/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ta/views/system.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ta/views/system.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/audio.json b/sam2-cpu/frigate-dev/web/public/locales/th/audio.json new file mode 100644 index 0000000..d6da104 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "พูด", + "yell": "ตะโกน", + "babbling": "พูดไม่ชัด", + "bellow": "ตะโกนเสียงต่ำ", + "whoop": "โห่ร้อง", + "singing": "ร้องเพลง", + "choir": "คณะนักร้องประสานเสียง", + "yodeling": "การร้องเพลงแบบโยเดิล", + "chant": "สวดมนต์", + "mantra": "มนต์", + "child_singing": "เด็กกำลังร้องเพลง", + "throat_clearing": "กระแอม", + "sneeze": "จาม", + "sniff": "สูดจมูก", + "run": "[ยังไม่แปล: Run]", + "shuffle": "[ยังไม่แปล: Shuffle]", + "footsteps": "[ยังไม่แปล: Footsteps]", + "biting": "[ยังไม่แปล: Biting]", + "gargling": "[ยังไม่แปล: Gargling]", + "stomach_rumble": "[ยังไม่แปล: Stomach Rumble]", + "burping": "[ยังไม่แปล: Burping]", + "hiccup": "[ยังไม่แปล: Hiccup]", + "heart_murmur": "[ยังไม่แปล: Heart Murmur]", + "cheering": "[ยังไม่แปล: Cheering]", + "applause": "[ยังไม่แปล: Applause]", + "chatter": "[ยังไม่แปล: Chatter]", + "crowd": "[ยังไม่แปล: Crowd]", + "children_playing": "[ยังไม่แปล: Children Playing]", + "animal": "สัตว์", + "pets": "[ยังไม่แปล: Pets]", + "dog": "สุนัข", + "bark": "เสียงหอบ", + "yip": "[ยังไม่แปล: Yip]", + "howl": "[ยังไม่แปล: Howl]", + "bow_wow": "[ยังไม่แปล: Bow Wow]", + "growling": "[ยังไม่แปล: Growling]", + "whimper_dog": "[ยังไม่แปล: Dog Whimper]", + "chirp": "[ยังไม่แปล: Chirp]", + "squawk": "[ยังไม่แปล: Squawk]", + "pigeon": "[ยังไม่แปล: Pigeon]", + "coo": "[ยังไม่แปล: Coo]", + "dogs": "[ยังไม่แปล: Dogs]", + "rats": "[ยังไม่แปล: Rats]", + "mouse": "เมาส์", + "patter": "[ยังไม่แปล: Patter]", + "insect": "[ยังไม่แปล: Insect]", + "cricket": "[ยังไม่แปล: Cricket]", + "frog": "[ยังไม่แปล: Frog]", + "croak": "[ยังไม่แปล: Croak]", + "snake": "[ยังไม่แปล: Snake]", + "snare_drum": "[ยังไม่แปล: Snare Drum]", + "rimshot": "[ยังไม่แปล: Rimshot]", + "trumpet": "[ยังไม่แปล: Trumpet]", + "trombone": "[ยังไม่แปล: Trombone]", + "bowed_string_instrument": "[ยังไม่แปล: Bowed String Instrument]", + "tuning_fork": "[ยังไม่แปล: Tuning Fork]", + "chime": "[ยังไม่แปล: Chime]", + "wind_chime": "[ยังไม่แปล: Wind Chime]", + "harmonica": "[ยังไม่แปล: Harmonica]", + "accordion": "[ยังไม่แปล: Accordion]", + "bagpipes": "[ยังไม่แปล: Bagpipes]", + "rock_music": "[ยังไม่แปล: Rock Music]", + "heavy_metal": "[ยังไม่แปล: Heavy Metal]", + "punk_rock": "[ยังไม่แปล: Punk Rock]", + "psychedelic_rock": "[ยังไม่แปล: Psychedelic Rock]", + "rhythm_and_blues": "[ยังไม่แปล: Rhythm and Blues]", + "soul_music": "[ยังไม่แปล: Soul Music]", + "reggae": "[ยังไม่แปล: Reggae]", + "country": "[ยังไม่แปล: Country]", + "swing_music": "[ยังไม่แปล: Swing Music]", + "bluegrass": "[ยังไม่แปล: Bluegrass]", + "funk": "[ยังไม่แปล: Funk]", + "folk_music": "[ยังไม่แปล: Folk Music]", + "middle_eastern_music": "[ยังไม่แปล: Middle Eastern Music]", + "jazz": "[ยังไม่แปล: Jazz]", + "disco": "[ยังไม่แปล: Disco]", + "independent_music": "[ยังไม่แปล: Independent Music]", + "song": "[ยังไม่แปล: Song]", + "jet_engine": "[ยังไม่แปล: Jet Engine]", + "propeller": "[ยังไม่แปล: Propeller]", + "helicopter": "[ยังไม่แปล: Helicopter]", + "fixed-wing_aircraft": "[ยังไม่แปล: Fixed-Wing Aircraft]", + "bicycle": "จักรยาน", + "accelerating": "[ยังไม่แปล: Accelerating]", + "door": "ประตู", + "slam": "[ยังไม่แปล: Slam]", + "computer_keyboard": "[ยังไม่แปล: Computer Keyboard]", + "radio": "[ยังไม่แปล: Radio]", + "field_recording": "[ยังไม่แปล: Field Recording]", + "scream": "[ยังไม่แปล: Scream]", + "whispering": "กระซิบ", + "laughter": "เสียงหัวเราะ", + "snicker": "หัวเราะเยาะ", + "crying": "ร้องไห้", + "sigh": "ถอนหายใจ", + "synthetic_singing": "การร้องเพลงสังเคราะห์", + "rapping": "การแร็พ", + "humming": "ฮัมเพลง", + "groan": "ครวญคราง", + "grunt": "เสียงคราง", + "whistling": "ผิวปาก", + "breathing": "การหายใจ", + "wheeze": "หายใจเสียงวี้ด", + "snoring": "กรน", + "gasp": "หอบ", + "pant": "หายใจหอบ", + "snort": "เสียงสูดจมูก", + "cough": "ไอ", + "chewing": "[ยังไม่แปล: Chewing]", + "fart": "[ยังไม่แปล: Fart]", + "hands": "[ยังไม่แปล: Hands]", + "finger_snapping": "[ยังไม่แปล: Finger Snapping]", + "clapping": "[ยังไม่แปล: Clapping]", + "heartbeat": "[ยังไม่แปล: Heartbeat]", + "cat": "แมว", + "purr": "[ยังไม่แปล: Purr]", + "meow": "[ยังไม่แปล: Meow]", + "hiss": "[ยังไม่แปล: Hiss]", + "caterwaul": "[ยังไม่แปล: Caterwaul]", + "livestock": "[ยังไม่แปล: Livestock]", + "horse": "ม้า", + "clip_clop": "[ยังไม่แปล: Clip Clop]", + "neigh": "[ยังไม่แปล: Neigh]", + "cattle": "[ยังไม่แปล: Cattle]", + "moo": "[ยังไม่แปล: Moo]", + "cowbell": "[ยังไม่แปล: Cowbell]", + "pig": "[ยังไม่แปล: Pig]", + "oink": "[ยังไม่แปล: Oink]", + "goat": "แพะ", + "bleat": "[ยังไม่แปล: Bleat]", + "sheep": "แกะ", + "fowl": "[ยังไม่แปล: Fowl]", + "chicken": "[ยังไม่แปล: Chicken]", + "cluck": "[ยังไม่แปล: Cluck]", + "cock_a_doodle_doo": "[ยังไม่แปล: Cock-a-Doodle-Doo]", + "turkey": "[ยังไม่แปล: Turkey]", + "gobble": "[ยังไม่แปล: Gobble]", + "duck": "[ยังไม่แปล: Duck]", + "quack": "[ยังไม่แปล: Quack]", + "goose": "[ยังไม่แปล: Goose]", + "honk": "[ยังไม่แปล: Honk]", + "wild_animals": "[ยังไม่แปล: Wild Animals]", + "roaring_cats": "[ยังไม่แปล: Roaring Cats]", + "roar": "[ยังไม่แปล: Roar]", + "bird": "นก", + "crow": "[ยังไม่แปล: Crow]", + "caw": "[ยังไม่แปล: Caw]", + "owl": "[ยังไม่แปล: Owl]", + "hoot": "[ยังไม่แปล: Hoot]", + "flapping_wings": "[ยังไม่แปล: Flapping Wings]", + "mosquito": "[ยังไม่แปล: Mosquito]", + "fly": "[ยังไม่แปล: Fly]", + "buzz": "[ยังไม่แปล: Buzz]", + "rattle": "[ยังไม่แปล: Rattle]", + "whale_vocalization": "[ยังไม่แปล: Whale Vocalization]", + "music": "[ยังไม่แปล: Music]", + "musical_instrument": "[ยังไม่แปล: Musical Instrument]", + "plucked_string_instrument": "[ยังไม่แปล: Plucked String Instrument]", + "guitar": "[ยังไม่แปล: Guitar]", + "electric_guitar": "[ยังไม่แปล: Electric Guitar]", + "bass_guitar": "[ยังไม่แปล: Bass Guitar]", + "acoustic_guitar": "[ยังไม่แปล: Acoustic Guitar]", + "steel_guitar": "[ยังไม่แปล: Steel Guitar]", + "tapping": "[ยังไม่แปล: Tapping]", + "strum": "[ยังไม่แปล: Strum]", + "banjo": "[ยังไม่แปล: Banjo]", + "sitar": "[ยังไม่แปล: Sitar]", + "mandolin": "[ยังไม่แปล: Mandolin]", + "zither": "[ยังไม่แปล: Zither]", + "ukulele": "[ยังไม่แปล: Ukulele]", + "keyboard": "คีย์บอร์ด", + "piano": "[ยังไม่แปล: Piano]", + "electric_piano": "[ยังไม่แปล: Electric Piano]", + "organ": "[ยังไม่แปล: Organ]", + "electronic_organ": "[ยังไม่แปล: Electronic Organ]", + "hammond_organ": "[ยังไม่แปล: Hammond Organ]", + "synthesizer": "[ยังไม่แปล: Synthesizer]", + "sampler": "[ยังไม่แปล: Sampler]", + "harpsichord": "[ยังไม่แปล: Harpsichord]", + "percussion": "[ยังไม่แปล: Percussion]", + "drum_kit": "[ยังไม่แปล: Drum Kit]", + "drum_machine": "[ยังไม่แปล: Drum Machine]", + "drum": "[ยังไม่แปล: Drum]", + "drum_roll": "[ยังไม่แปล: Drum Roll]", + "bass_drum": "[ยังไม่แปล: Bass Drum]", + "timpani": "[ยังไม่แปล: Timpani]", + "tabla": "[ยังไม่แปล: Tabla]", + "cymbal": "[ยังไม่แปล: Cymbal]", + "hi_hat": "[ยังไม่แปล: Hi-Hat]", + "wood_block": "[ยังไม่แปล: Wood Block]", + "tambourine": "[ยังไม่แปล: Tambourine]", + "maraca": "[ยังไม่แปล: Maraca]", + "gong": "[ยังไม่แปล: Gong]", + "tubular_bells": "[ยังไม่แปล: Tubular Bells]", + "mallet_percussion": "[ยังไม่แปล: Mallet Percussion]", + "marimba": "[ยังไม่แปล: Marimba]", + "glockenspiel": "[ยังไม่แปล: Glockenspiel]", + "vibraphone": "[ยังไม่แปล: Vibraphone]", + "steelpan": "[ยังไม่แปล: Steelpan]", + "orchestra": "[ยังไม่แปล: Orchestra]", + "brass_instrument": "[ยังไม่แปล: Brass Instrument]", + "church_bell": "[ยังไม่แปล: Church Bell]", + "jingle_bell": "[ยังไม่แปล: Jingle Bell]", + "french_horn": "[ยังไม่แปล: French Horn]", + "string_section": "[ยังไม่แปล: String Section]", + "violin": "[ยังไม่แปล: Violin]", + "bicycle_bell": "[ยังไม่แปล: Bicycle Bell]", + "pizzicato": "[ยังไม่แปล: Pizzicato]", + "cello": "[ยังไม่แปล: Cello]", + "double_bass": "[ยังไม่แปล: Double Bass]", + "wind_instrument": "[ยังไม่แปล: Wind Instrument]", + "flute": "[ยังไม่แปล: Flute]", + "saxophone": "[ยังไม่แปล: Saxophone]", + "clarinet": "[ยังไม่แปล: Clarinet]", + "harp": "[ยังไม่แปล: Harp]", + "bell": "[ยังไม่แปล: Bell]", + "didgeridoo": "[ยังไม่แปล: Didgeridoo]", + "theremin": "[ยังไม่แปล: Theremin]", + "singing_bowl": "[ยังไม่แปล: Singing Bowl]", + "scratching": "[ยังไม่แปล: Scratching]", + "pop_music": "[ยังไม่แปล: Pop Music]", + "hip_hop_music": "[ยังไม่แปล: Hip-Hop Music]", + "beatboxing": "[ยังไม่แปล: Beatboxing]", + "grunge": "[ยังไม่แปล: Grunge]", + "progressive_rock": "[ยังไม่แปล: Progressive Rock]", + "rock_and_roll": "[ยังไม่แปล: Rock and Roll]", + "classical_music": "[ยังไม่แปล: Classical Music]", + "opera": "[ยังไม่แปล: Opera]", + "electronic_music": "[ยังไม่แปล: Electronic Music]", + "house_music": "[ยังไม่แปล: House Music]", + "drum_and_bass": "[ยังไม่แปล: Drum and Bass]", + "techno": "[ยังไม่แปล: Techno]", + "dubstep": "[ยังไม่แปล: Dubstep]", + "electronica": "[ยังไม่แปล: Electronica]", + "electronic_dance_music": "[ยังไม่แปล: Electronic Dance Music]", + "ambient_music": "[ยังไม่แปล: Ambient Music]", + "trance_music": "[ยังไม่แปล: Trance Music]", + "music_of_latin_america": "[ยังไม่แปล: Music of Latin America]", + "salsa_music": "[ยังไม่แปล: Salsa Music]", + "flamenco": "[ยังไม่แปล: Flamenco]", + "blues": "[ยังไม่แปล: Blues]", + "music_for_children": "[ยังไม่แปล: Music for Children]", + "new-age_music": "[ยังไม่แปล: New Age Music]", + "vocal_music": "[ยังไม่แปล: Vocal Music]", + "a_capella": "[ยังไม่แปล: A Capella]", + "music_of_africa": "[ยังไม่แปล: Music of Africa]", + "afrobeat": "[ยังไม่แปล: Afrobeat]", + "christian_music": "[ยังไม่แปล: Christian Music]", + "gospel_music": "[ยังไม่แปล: Gospel Music]", + "music_of_asia": "[ยังไม่แปล: Music of Asia]", + "carnatic_music": "[ยังไม่แปล: Carnatic Music]", + "music_of_bollywood": "[ยังไม่แปล: Music of Bollywood]", + "ska": "[ยังไม่แปล: Ska]", + "traditional_music": "[ยังไม่แปล: Traditional Music]", + "background_music": "[ยังไม่แปล: Background Music]", + "theme_music": "[ยังไม่แปล: Theme Music]", + "jingle": "[ยังไม่แปล: Jingle]", + "soundtrack_music": "[ยังไม่แปล: Soundtrack Music]", + "lullaby": "[ยังไม่แปล: Lullaby]", + "video_game_music": "[ยังไม่แปล: Video Game Music]", + "christmas_music": "[ยังไม่แปล: Christmas Music]", + "dance_music": "[ยังไม่แปล: Dance Music]", + "wedding_music": "[ยังไม่แปล: Wedding Music]", + "happy_music": "[ยังไม่แปล: Happy Music]", + "sad_music": "[ยังไม่แปล: Sad Music]", + "angry_music": "[ยังไม่แปล: Angry Music]", + "scary_music": "[ยังไม่แปล: Scary Music]", + "tender_music": "[ยังไม่แปล: Tender Music]", + "exciting_music": "[ยังไม่แปล: Exciting Music]", + "wind": "[ยังไม่แปล: Wind]", + "rustling_leaves": "[ยังไม่แปล: Rustling Leaves]", + "wind_noise": "[ยังไม่แปล: Wind Noise]", + "thunderstorm": "[ยังไม่แปล: Thunderstorm]", + "thunder": "[ยังไม่แปล: Thunder]", + "water": "[ยังไม่แปล: Water]", + "rain": "[ยังไม่แปล: Rain]", + "raindrop": "[ยังไม่แปล: Raindrop]", + "rain_on_surface": "[ยังไม่แปล: Rain on Surface]", + "stream": "[ยังไม่แปล: Stream]", + "ocean": "[ยังไม่แปล: Ocean]", + "waterfall": "[ยังไม่แปล: Waterfall]", + "waves": "[ยังไม่แปล: Waves]", + "steam": "[ยังไม่แปล: Steam]", + "gurgling": "[ยังไม่แปล: Gurgling]", + "fire": "[ยังไม่แปล: Fire]", + "crackle": "[ยังไม่แปล: Crackle]", + "vehicle": "ยานพาหนะ", + "boat": "เรือ", + "sailboat": "[ยังไม่แปล: Sailboat]", + "rowboat": "[ยังไม่แปล: Rowboat]", + "motorboat": "[ยังไม่แปล: Motorboat]", + "ship": "[ยังไม่แปล: Ship]", + "motor_vehicle": "[ยังไม่แปล: Motor Vehicle]", + "car": "รถยนต์", + "toot": "[ยังไม่แปล: Toot]", + "car_alarm": "[ยังไม่แปล: Car Alarm]", + "power_windows": "[ยังไม่แปล: Power Windows]", + "skidding": "[ยังไม่แปล: Skidding]", + "tire_squeal": "[ยังไม่แปล: Tire Squeal]", + "car_passing_by": "[ยังไม่แปล: Car Passing By]", + "race_car": "[ยังไม่แปล: Race Car]", + "truck": "[ยังไม่แปล: Truck]", + "air_brake": "[ยังไม่แปล: Air Brake]", + "air_horn": "[ยังไม่แปล: Air Horn]", + "reversing_beeps": "[ยังไม่แปล: Reversing Beeps]", + "ice_cream_truck": "[ยังไม่แปล: Ice Cream Truck]", + "bus": "รถประจำทาง", + "emergency_vehicle": "[ยังไม่แปล: Emergency Vehicle]", + "police_car": "[ยังไม่แปล: Police Car]", + "ambulance": "[ยังไม่แปล: Ambulance]", + "fire_engine": "[ยังไม่แปล: Fire Engine]", + "motorcycle": "มอเตอร์ไซค์", + "traffic_noise": "[ยังไม่แปล: Traffic Noise]", + "rail_transport": "[ยังไม่แปล: Rail Transport]", + "train": "รถไฟ", + "train_whistle": "[ยังไม่แปล: Train Whistle]", + "train_horn": "[ยังไม่แปล: Train Horn]", + "railroad_car": "[ยังไม่แปล: Railroad Car]", + "train_wheels_squealing": "[ยังไม่แปล: Train Wheels Squealing]", + "subway": "[ยังไม่แปล: Subway]", + "aircraft": "[ยังไม่แปล: Aircraft]", + "aircraft_engine": "[ยังไม่แปล: Aircraft Engine]", + "skateboard": "สเก็ตบอร์ด", + "engine": "[ยังไม่แปล: Engine]", + "light_engine": "[ยังไม่แปล: Light Engine]", + "dental_drill's_drill": "[ยังไม่แปล: Dental Drill]", + "lawn_mower": "[ยังไม่แปล: Lawn Mower]", + "chainsaw": "[ยังไม่แปล: Chainsaw]", + "medium_engine": "[ยังไม่แปล: Medium Engine]", + "heavy_engine": "[ยังไม่แปล: Heavy Engine]", + "engine_knocking": "[ยังไม่แปล: Engine Knocking]", + "engine_starting": "[ยังไม่แปล: Engine Starting]", + "idling": "[ยังไม่แปล: Idling]", + "doorbell": "[ยังไม่แปล: Doorbell]", + "ding-dong": "[ยังไม่แปล: Ding-Dong]", + "sliding_door": "[ยังไม่แปล: Sliding Door]", + "knock": "[ยังไม่แปล: Knock]", + "tap": "[ยังไม่แปล: Tap]", + "squeak": "[ยังไม่แปล: Squeak]", + "drawer_open_or_close": "[ยังไม่แปล: Drawer Open or Close]", + "cupboard_open_or_close": "[ยังไม่แปล: Cupboard Open or Close]", + "dishes": "[ยังไม่แปล: Dishes]", + "cutlery": "[ยังไม่แปล: Cutlery]", + "chopping": "[ยังไม่แปล: Chopping]", + "frying": "[ยังไม่แปล: Frying]", + "microwave_oven": "[ยังไม่แปล: Microwave Oven]", + "blender": "เครื่องปั่น", + "water_tap": "[ยังไม่แปล: Water Tap]", + "sink": "อ่างล้างจาน", + "bathtub": "[ยังไม่แปล: Bathtub]", + "hair_dryer": "ไดร์เป่าผม", + "toilet_flush": "[ยังไม่แปล: Toilet Flush]", + "toothbrush": "แปรงสีฟัน", + "electric_toothbrush": "[ยังไม่แปล: Electric Toothbrush]", + "vacuum_cleaner": "[ยังไม่แปล: Vacuum Cleaner]", + "zipper": "[ยังไม่แปล: Zipper]", + "keys_jangling": "[ยังไม่แปล: Keys Jangling]", + "coin": "[ยังไม่แปล: Coin]", + "scissors": "กรรไกร", + "electric_shaver": "[ยังไม่แปล: Electric Shaver]", + "shuffling_cards": "[ยังไม่แปล: Shuffling Cards]", + "typing": "[ยังไม่แปล: Typing]", + "typewriter": "[ยังไม่แปล: Typewriter]", + "writing": "[ยังไม่แปล: Writing]", + "alarm": "[ยังไม่แปล: Alarm]", + "ringtone": "[ยังไม่แปล: Ringtone]", + "telephone": "[ยังไม่แปล: Telephone]", + "telephone_bell_ringing": "[ยังไม่แปล: Telephone Bell Ringing]", + "telephone_dialing": "[ยังไม่แปล: Telephone Dialing]", + "dial_tone": "[ยังไม่แปล: Dial Tone]", + "busy_signal": "[ยังไม่แปล: Busy Signal]", + "alarm_clock": "[ยังไม่แปล: Alarm Clock]", + "siren": "[ยังไม่แปล: Siren]", + "civil_defense_siren": "[ยังไม่แปล: Civil Defense Siren]", + "buzzer": "[ยังไม่แปล: Buzzer]", + "smoke_detector": "[ยังไม่แปล: Smoke Detector]", + "fire_alarm": "[ยังไม่แปล: Fire Alarm]", + "foghorn": "[ยังไม่แปล: Foghorn]", + "whistle": "[ยังไม่แปล: Whistle]", + "steam_whistle": "[ยังไม่แปล: Steam Whistle]", + "mechanisms": "[ยังไม่แปล: Mechanisms]", + "ratchet": "[ยังไม่แปล: Ratchet]", + "clock": "นาฬิกา", + "tick": "[ยังไม่แปล: Tick]", + "tick-tock": "[ยังไม่แปล: Tick-Tock]", + "gears": "[ยังไม่แปล: Gears]", + "pulleys": "[ยังไม่แปล: Pulleys]", + "sewing_machine": "[ยังไม่แปล: Sewing Machine]", + "mechanical_fan": "[ยังไม่แปล: Mechanical Fan]", + "air_conditioning": "[ยังไม่แปล: Air Conditioning]", + "cash_register": "[ยังไม่แปล: Cash Register]", + "printer": "[ยังไม่แปล: Printer]", + "camera": "กล้อง", + "single-lens_reflex_camera": "[ยังไม่แปล: Single-Lens Reflex Camera]", + "tools": "[ยังไม่แปล: Tools]", + "hammer": "[ยังไม่แปล: Hammer]", + "sawing": "[ยังไม่แปล: Sawing]", + "jackhammer": "[ยังไม่แปล: Jackhammer]", + "filing": "[ยังไม่แปล: Filing]", + "sanding": "[ยังไม่แปล: Sanding]", + "power_tool": "[ยังไม่แปล: Power Tool]", + "drill": "[ยังไม่แปล: Drill]", + "explosion": "[ยังไม่แปล: Explosion]", + "gunshot": "[ยังไม่แปล: Gunshot]", + "machine_gun": "[ยังไม่แปล: Machine Gun]", + "fusillade": "[ยังไม่แปล: Fusillade]", + "artillery_fire": "[ยังไม่แปล: Artillery Fire]", + "cap_gun": "[ยังไม่แปล: Cap Gun]", + "fireworks": "[ยังไม่แปล: Fireworks]", + "firecracker": "[ยังไม่แปล: Firecracker]", + "burst": "[ยังไม่แปล: Burst]", + "eruption": "[ยังไม่แปล: Eruption]", + "boom": "[ยังไม่แปล: Boom]", + "wood": "[ยังไม่แปล: Wood]", + "chop": "[ยังไม่แปล: Chop]", + "splinter": "[ยังไม่แปล: Splinter]", + "crack": "[ยังไม่แปล: Crack]", + "glass": "[ยังไม่แปล: Glass]", + "chink": "[ยังไม่แปล: Chink]", + "shatter": "[ยังไม่แปล: Shatter]", + "silence": "[ยังไม่แปล: Silence]", + "sound_effect": "[ยังไม่แปล: Sound Effect]", + "environmental_noise": "[ยังไม่แปล: Environmental Noise]", + "static": "[ยังไม่แปล: Static]", + "white_noise": "[ยังไม่แปล: White Noise]", + "pink_noise": "[ยังไม่แปล: Pink Noise]", + "television": "[ยังไม่แปล: Television]" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/common.json b/sam2-cpu/frigate-dev/web/public/locales/th/common.json new file mode 100644 index 0000000..b920787 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/common.json @@ -0,0 +1,251 @@ +{ + "time": { + "today": "วันนี้", + "justNow": "ตอนนี้", + "yesterday": "เมื่อวาน", + "lastWeek": "สัปดาห์ที่แล้ว", + "untilForRestart": "จนกว่า Frigate รีสตาร์ท.", + "untilRestart": "จนกว่า รีสตาร์ท", + "ago": "{{timeAgo}} ที่แล้ว", + "last7": "7 วันที่แล้ว", + "last14": "14 วันที่แล้ว", + "last30": "30 วันที่แล้ว", + "thisWeek": "สัปดาห์นี้", + "untilForTime": "จนกว่า {{time}}", + "thisMonth": "เดือนนี้", + "lastMonth": "เดือนที่แล้ว", + "5minutes": "5 นาที", + "30minutes": "30 นาที", + "12hours": "12 ชั่วโมง", + "10minutes": "10 นาที", + "1hour": "1 ชั่วโมง", + "24hours": "24 ชั่วโมง", + "pm": "หลังเที่ยง", + "year_other": "{{time}} ปี", + "month_other": "{{time}} เดือน", + "day_other": "{{time}} วัน", + "h": "{{time}} ชั่วโมง", + "hour_other": "{{time}} ชั่วโมง", + "m": "{{time}} นาที", + "s": "{{time}} วินาที", + "mo": "{{time}} เดือน", + "minute_other": "{{time}} นาที", + "am": "ก่อนเที่ยง", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "yr": "{{time}} ปี", + "d": "{{time}} วัน", + "second_other": "{{time}} วินาที", + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "24hour": "HH:mm:ss", + "12hour": "h:mm:ss aaa" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestampMonthDay": "MMM d" + }, + "label": { + "back": "ย้อนกลับ" + }, + "button": { + "apply": "ใช้งาน", + "reset": "รีเซ็ต", + "done": "เรียบร้อย", + "enabled": "เปิดใช้งานแล้ว", + "enable": "เปิดใช้งาน", + "disable": "ปิดใช้งาน", + "disabled": "ปิดใช้งานแล้ว", + "save": "บันทึก", + "saving": "กำลังบันทึก…", + "cancel": "ยกเลิก", + "close": "ปิด", + "copy": "คัดลอก", + "back": "กลับ", + "history": "ประวัติ", + "fullscreen": "เต็มจอ", + "exitFullscreen": "ออกเต็มจอ", + "pictureInPicture": "ภาพซ้อนภาพ", + "cameraAudio": "เสียงกล้อง", + "on": "เปิด", + "off": "ปิด", + "edit": "แก้ไข", + "delete": "ลบ", + "yes": "ใช่", + "no": "ไม่", + "download": "ดาวน์โหลด", + "info": "ข้อมูล", + "play": "เล่น", + "unselect": "ไม่ได้เลือก", + "export": "ส่งออก", + "deleteNow": "ลบตอนนี้", + "next": "ต่อไป", + "twoWayTalk": "พูดคุยสองทาง", + "copyCoordinates": "คัดลอกพิกัด", + "suspended": "ถูกระงับ", + "unsuspended": "ยกเลิกถูกระงับ" + }, + "menu": { + "restart": "รีสตาร์ท Frigate", + "user": { + "logout": "ออกจากระบบ", + "title": "ผู้ใช้", + "account": "บัญชี", + "current": "ผู้ใช้ปัจจุบัน: {{user}}", + "anonymous": "ไม่ระบุตัวตน", + "setPassword": "ตั้งรหัสผ่าน" + }, + "live": { + "cameras": { + "count_other": "{{count}} กล้อง", + "title": "กล้อง" + }, + "title": "สด", + "allCameras": "กล้องทั้งหมด" + }, + "configurationEditor": "ตัวแก้ไขการกำหนดค่า", + "export": "ส่งออก", + "system": "ระบบ", + "configuration": "การกำหนดค่า", + "systemLogs": "บันทึกระบบ", + "settings": "ตั้งค่า", + "languages": "ภาษา", + "language": { + "withSystem": { + "label": "ใช้ภาษาของระบบ" + }, + "en": "English (อังกฤษ)", + "zhCN": "简体中文 (ภาษาจีนตัวย่อ)", + "hi": "हिन्दी (ฮินดี)", + "fr": "Français (ฝรั่งเศส)", + "ar": "العربية (อาหรับ)", + "pt": "Português (โปรตุเกส)", + "ru": "Русский (รัสเซีย)", + "de": "Deutsch (เยอรมัน)", + "ja": "日本語 (ญี่ปุ่น)", + "tr": "Türkçe (ตุรกี)", + "it": "Italiano (อิตาเลียน)", + "nl": "Nederlands (ดัตช์)", + "sv": "Svenska (สวีเดน)", + "cs": "Čeština (เช็ก)", + "nb": "Norsk Bokmål (นอร์เวย์ บ็อกมอล)", + "ko": "한국어 (เกาหลี)", + "fa": "فارسی (เปอร์เซีย)", + "he": "עברית (ฮีบรู)", + "el": "Ελληνικά (กรีก)", + "ro": "Română (โรมาเนีย)", + "hu": "Magyar (ฮังการี)", + "fi": "Suomi (ฟินแลนด์)", + "da": "Dansk (เดนมาร์ก)", + "es": "Español (สเปน)", + "sk": "Slovenčina (สโลวัก)", + "uk": "Українська (ยูเครน)", + "vi": "Tiếng Việt (เวียดนาม)", + "yue": "粵語 (กวางตุ้ง)", + "pl": "Polski (ขัด)", + "th": "ไทย (ไทย)" + }, + "darkMode": { + "label": "โหมดมืด", + "light": "สว่าง", + "dark": "มืด", + "withSystem": { + "label": "ใช้ของระบบสำหรับโหมดสว่างหรือมืด" + } + }, + "withSystem": "ระบบ", + "theme": { + "label": "ธีม", + "blue": "น้ำเงิน", + "green": "เขียว", + "red": "แดง", + "highcontrast": "คอนทราสต์สูง", + "default": "เริ่มต้น", + "nord": "ฟ้า" + }, + "review": "รีวิว", + "explore": "สำรวจ", + "uiPlayground": "UI สนามเด็กเล่น", + "faceLibrary": "ที่เก็บหน้า", + "help": "ช่วยเหลือ", + "documentation": { + "title": "เอกสาร", + "label": "เอกสาร Frigate" + }, + "systemMetrics": "ตัวชี้วัดของระบบ", + "appearance": "หน้าตา" + }, + "role": { + "viewer": "ผู้ชม", + "title": "บทบาท", + "admin": "ผู้ดูแล", + "desc": "ผู้ดูแลสามารถเข้าถึงระบบได้ทั้งหมดใน UI Frigate. ผู้ชมสามารถทำได้แค่ ดูกล้อง, ดูรีวิว, และ ประวัติคลิปใน UI." + }, + "toast": { + "save": { + "error": { + "noMessage": "มีข้อผิดพลาดในการกำหนดค่า", + "title": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "title": "บันทึก" + }, + "copyUrlToClipboard": "คัดลอก URL ใส่ คลิปบอร์ดแล้ว" + }, + "pagination": { + "previous": { + "title": "ก่อนหน้า", + "label": "ไปหน้าที่แล้ว" + }, + "next": { + "title": "ต่อไป", + "label": "ไปหน้าต่อไป" + }, + "more": "หน้าเพิ่มเติม", + "label": "แบ่งหน้า" + }, + "accessDenied": { + "documentTitle": "ไม่สามารถเข้าถึงได้ - Frigate", + "title": "ไม่สามารถเข้าถึงได้", + "desc": "คุณไม่มีสิทธิ์ในการเข้าถึงหน้านี้" + }, + "notFound": { + "documentTitle": "ไม่พบ - Frigate", + "desc": "ไม่พบหน้านี้", + "title": "๔๐๔" + }, + "selectItem": "เลือก {{item}}", + "unit": { + "speed": { + "mph": "ไมล์ต่อชั่วโมง", + "kph": "กิโลเมตรต่อชั่วโมง" + }, + "length": { + "feet": "ฟุต", + "meters": "เมตร" + } + }, + "readTheDocumentation": "อ่านเอกสาร" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/auth.json new file mode 100644 index 0000000..7a24ffe --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "user": "ชื่อผู้ใช้", + "errors": { + "webUnknownError": "ข้อผิดพลาดที่ไม่รู้จัก. ตรวจสอบที่ console logs.", + "rateLimit": "เกินขีดจำกัด. โปรดลองอีกครั้งในภายหลัง.", + "loginFailed": "ล็อกอินไม่สำเร็จ", + "unknownError": "ข้อผิดพลาดที่ไม่รู้จัก. ตรวจสอบที่ logs.", + "passwordRequired": "ต้องการรหัสผ่าน", + "usernameRequired": "ต้องการชื่อผู้ใช้" + }, + "login": "ล็อกอิน", + "password": "รหัสผ่าน" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/camera.json new file mode 100644 index 0000000..3be4d79 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/camera.json @@ -0,0 +1,86 @@ +{ + "group": { + "camera": { + "setting": { + "title": "{{cameraName}} ตั้งค่าการสตรีมมิ่ง", + "audioIsAvailable": "เสียงสามารถใช้งานได้ในสตรีมนี้", + "audioIsUnavailable": "เสียงไม่สามารถใช้ได้สําหรับสตรีมนี้", + "audio": { + "tips": { + "title": "เสียงต้องส่งออกจากกล้องของคุณและกําหนดค่าบน go2rtc สําหรับสตรีมนี้.", + "document": "อ่านเอกสาร " + } + }, + "stream": "สตรีม", + "placeholder": "เลือกสตรีม", + "streamMethod": { + "label": "วิธีสตรีมมิ่ง", + "placeholder": "เลือกวิธีสตรีมมิ่ง", + "method": { + "noStreaming": { + "label": "ไม่มีการสตรีม", + "desc": "ภาพของกล้องจะอัปเดตนาทีต่อครั้งและไม่มีสตรีมสด." + }, + "smartStreaming": { + "label": "สมาร์ทสตรีม (แนะนํา)", + "desc": "สมาร์ทสตรีมจะอัปเดตกล้องของคุณทุกหนึ่งนาทีเมื่อไม่มีการตรวจพบกิจกรรมที่เกิดขึ้นเพื่อรักษาแบนด์วิดท์และทรัพยากร. เมื่อมีการตรวจพบกิจกรรม, ภาพจะถูกเปลี่ยนเป็นสตรีมสด." + }, + "continuousStreaming": { + "label": "สตรีมมิ่ง", + "desc": { + "title": "ภาพของกล้องจะแสดงตลอดเมื่ออยู่บนแดชบอร์ด, แม้ว่าไม่มีกิจกรรม.", + "warning": "สตรีมแบบต่อเนื่องอาจทําให้เกิดแบนด์วิดท์สูงและเกิดปัญหาประสิทธิภาพการทํางาน, โปรดใช้ความระมัดระวัง." + } + } + } + }, + "desc": "เปลี่ยนตัวเลือกสตรีมสดสําหรับแผงควบคุมของกลุ่มกล้องนี้. การตั้งค่าเหล่านั้นเฉพาะอุปกรณ์/เบราว์เซอร์", + "compatibilityMode": { + "label": "โหมด", + "desc": "เปิดใช้งานตัวเลือกนี้เฉพาะเมื่อถ้ากล้องของคุณถ่ายทอดสดเป็นการแสดงสีแปลกๆและมีเส้นด้านข้าง." + }, + "label": "การตั้งค่าสตรีมกล้อง" + } + }, + "label": "กลุ่มกล้อง", + "add": "เพิ่มกลุ่มกล้อง", + "edit": "แก้ไขกลุ่มกล้อง", + "delete": { + "label": "ลบกลุ่มกล้อง", + "confirm": { + "title": "ยืนยันการลบ", + "desc": "คุณแน่ใจหรือไม่ว่าต้องการลบกลุ่มกล้อง {{name}}?" + } + }, + "name": { + "label": "ชื่อ", + "placeholder": "ป้อนชื่อ…", + "errorMessage": { + "mustLeastCharacters": "ชื่อกลุ่มกล้องต้องมีอย่างน้อย 2 ตัวอักษร", + "exists": "ชื่อกลุ่มกล้องมีอยู่แล้ว", + "nameMustNotPeriod": "ชื่อกลุ่มกล้องต้องไม่มีจุด", + "invalid": "ชื่อกลุ่มกล้องไม่ถูกต้อง" + } + }, + "cameras": { + "label": "กล้อง", + "desc": "เลือกกล้องสำหรับกลุ่มนี้" + }, + "icon": "ไอคอน", + "success": "บันทึกกลุ่มกล้อง ({{name}}) เรียบร้อยแล้ว" + }, + "debug": { + "options": { + "label": "การตั้งค่า", + "title": "ตัวเลือก", + "showOptions": "แสดงตัวเลือก", + "hideOptions": "ซ่อนไฟล์ตัวเลือก" + }, + "boundingBox": "กรอบตรวจจับ", + "timestamp": "เวลาปัจจุบัน", + "zones": "โซน", + "mask": "หน้ากาก", + "motion": "การเคลื่อนไหว", + "regions": "พื้นที่" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/dialog.json new file mode 100644 index 0000000..d1a85ec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/dialog.json @@ -0,0 +1,105 @@ +{ + "search": { + "saveSearch": { + "label": "บันทึกค้นหา", + "success": "ค้นหา {{searchName}} ถูกบันทึกเรียบร้อยแล้ว", + "desc": "ระบุชื่อสำหรับการค้นหาบันทึกนี้", + "button": { + "save": { + "label": "บันทึกการค้นหานี้" + } + }, + "overwrite": "{{searchName}} มีอยู่แล้ว. การบันทึกจะทำการทับของเดิมลงไป.", + "placeholder": "ใส่ชื่อสําหรับการค้นหาของคุณ" + } + }, + "export": { + "fromTimeline": { + "saveExport": "บันทึกส่งออก", + "previewExport": "ตัวอย่างส่งออก" + }, + "toast": { + "error": { + "endTimeMustAfterStartTime": "เวลาสิ้นสุดต้องอยู่หลังเวลาเริ่มต้น", + "noVaildTimeSelected": "ไม่ได้เลือกช่วงเวลาที่ถูกต้อง", + "failed": "เริ่มต้นการส่งออกผิดพลาด: {{error}}" + }, + "success": "เริ่มต้นการส่งออก ดูไฟล์ในโฟลเดอร์ /exports." + }, + "time": { + "fromTimeline": "เลือกจากเวลา", + "lastHour_other": "ก่อนหน้านี้ {{count}} ชั่วโมง", + "custom": "กําหนดเอง", + "start": { + "title": "เวลาเริ่มต้น", + "label": "เลือกเวลาเริ่มต้น" + }, + "end": { + "title": "เวลาสิ้นสุด", + "label": "เลือกเวลาสิ้นสุด" + } + }, + "name": { + "placeholder": "ชื่อส่งออก" + }, + "select": "เลือก", + "export": "ส่งออก", + "selectOrExport": "เลือกหรือส่งออก" + }, + "restart": { + "restarting": { + "button": "โหลดหน้าใหม่ตอนนี้", + "title": "Frigate กำลังรีสตาร์ท", + "content": "หน้านี้จะถูกโหลดในอีก {{countdown}} วินาที." + }, + "title": "คุณแน่ใจหรือว่าต้องการรีสตาร์ท Frigate?", + "button": "รีสตาร์ท" + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_full": "วัตถุนี้คือ {{untranslatedLabel}} ({{translatedLabel}})?", + "label": "ยืนยันหมวดหมู่นี้สําหรับ Frigate+", + "ask_a": "วัตถุนี้คือ {{label}}?", + "ask_an": "วัตถุนี้คือ {{label}}?" + }, + "state": { + "submitted": "ส่งเรียบร้อย" + } + }, + "submitToPlus": { + "label": "ส่งไปยัง Frigate+" + } + }, + "video": { + "viewInHistory": "ดูประวัติ" + } + }, + "recording": { + "button": { + "deleteNow": "ลบตอนนี้", + "export": "ส่งออก", + "markAsReviewed": "ทำเครื่องหมายว่ารีวิวแล้ว" + }, + "confirmDelete": { + "title": "ยืนยันการลบ", + "toast": { + "error": "ลบไม่ได้: {{error}}" + } + } + }, + "streaming": { + "label": "สตรีม", + "restreaming": { + "disabled": "รีสตรีมไม่ได้เปิดใช้งานสําหรับกล้องนี้.", + "desc": { + "readTheDocumentation": "อ่านเอกสาร" + } + }, + "showStats": { + "label": "แสดงสถานะสตรีม", + "desc": "เปิดใช้งานตัวเลือกนี้เพื่อจะแสดงสถิติในกล้อง." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/filter.json new file mode 100644 index 0000000..aea9fc5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/filter.json @@ -0,0 +1,86 @@ +{ + "filter": "กรอง", + "zones": { + "label": "โซน", + "all": { + "title": "โซนทั้งหมด", + "short": "โซน" + } + }, + "dates": { + "selectPreset": "เลือกค่าที่ตั้งไว้…", + "all": { + "title": "วันที่ทั้งหมด", + "short": "วันที่" + } + }, + "more": "ตัวกรองเพิ่มเติม", + "reset": { + "label": "รีเซ็ตตัวกรองเป็นค่าเริ่มต้น" + }, + "timeRange": "ช่วงเวลา", + "score": "คะแนน", + "estimatedSpeed": "ความเร็วโดยประมาณ {{unit}}", + "features": { + "label": "คุณสมบัติ", + "submittedToFrigatePlus": { + "label": "ส่งไปยัง Frigate+", + "tips": "คุณต้องกรองครั้งแรกบนวัตถุที่มีภาพ.

    วัตถุที่ไม่มีภาพไม่สามารถส่งออกได้." + }, + "hasSnapshot": "มีภาพ", + "hasVideoClip": "มีวิดีโอ" + }, + "sort": { + "label": "เรียง", + "dateAsc": "วันที่ (จากน้อยไปมาก)", + "scoreDesc": "คะแนน วัตถุ (จากมากไปน้อย)", + "speedDesc": "ความเร็วโดยประมาณ (จากมากไปน้อย)", + "relevance": "สอดคล้อง", + "scoreAsc": "คะแนน วัตถุ (จากน้อยไปมาก)", + "speedAsc": "ความเร็วโดยประมาณ (จากน้อยไปมาก)", + "dateDesc": "วันที่ (จากมากไปน้อย)" + }, + "subLabels": { + "all": "หมวดหมู่ย่อยทั้งหมด", + "label": "หมวดหมู่ย่อย" + }, + "labels": { + "all": { + "title": "หมวดหมู่ทั้งหมด", + "short": "หมวดหมู่" + }, + "count_other": "{{count}} หมวดหมู่", + "count_one": "{{count}} หมวดหมู่" + }, + "cameras": { + "all": { + "short": "กล้อง", + "title": "กล้องทั้งหมด" + }, + "label": "กรองกล้อง" + }, + "review": { + "showReviewed": "แสดงที่รีวิวแล้ว" + }, + "motion": { + "showMotionOnly": "แสดงเฉพาะการเคลื่อนไหวเท่านั้น" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "สรุป", + "unfilteredGrid": "ตารางที่ไม่ได้กรอง", + "title": "มุมเริ่มต้น", + "desc": "เมื่อไม่มีตัวกรอง, แสดงสรุปวัตถุล่าสุดหรือแสดงตารางที่ไม่ได้กรอง." + }, + "gridColumns": { + "title": "ตารางคอลัมน์", + "desc": "เลือกจำนวนคอลัมน์ในตาราง." + }, + "searchSource": { + "label": "ค้นหาแหล่ง" + }, + "title": "การตั้งค่า" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/icons.json new file mode 100644 index 0000000..e224c88 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "เลือกไอคอน", + "search": { + "placeholder": "ค้นหาไอคอน" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/input.json new file mode 100644 index 0000000..ad2edd2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "ดาวน์โหลดวิดีโอ", + "toast": { + "success": "วิดีโอรายการรีวิวของคุณเริ่มดาวน์โหลดแล้ว." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/th/components/player.json new file mode 100644 index 0000000..03e5a1a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/components/player.json @@ -0,0 +1,51 @@ +{ + "streamOffline": { + "desc": "ไม่ได้รับเฟรมบน {{cameraName}} สตรีม detect, ตรวจสอบ error logs", + "title": "สตรีมออฟไลน์" + }, + "noPreviewFound": "ไม่พบตัวอย่าง", + "submitFrigatePlus": { + "title": "ส่งเฟรมนี้ให้ Frigate+ ไหม?", + "submit": "ส่ง" + }, + "livePlayerRequiredIOSVersion": "ต้องใช้ iOS 17.1 ขึ้นไปสำหรับประเภทสตรีมสดนี้", + "cameraDisabled": "กล้องถูกปิดใช้งาน", + "stats": { + "streamType": { + "title": "ประเภทสตรีม:", + "short": "ประเภท" + }, + "latency": { + "title": "ความหน่วง:", + "value": "{{seconds}} วินาที", + "short": { + "title": "ความหน่วง", + "value": "{{seconds}} วิ" + } + }, + "totalFrames": "จำนวนเฟรมทั้งหมด:", + "droppedFrames": { + "short": { + "title": "หายไป", + "value": "{{droppedFrames}} เฟรม" + }, + "title": "เฟรมที่หาย:" + }, + "decodedFrames": "เฟรมถูกถอดรหัส:", + "droppedFrameRate": "อัตราเฟรมที่หายไป:", + "bandwidth": { + "title": "แบนด์วิธ:", + "short": "แบนด์วิธ" + } + }, + "toast": { + "error": { + "submitFrigatePlusFailed": "ไม่สามารถส่งเฟรมไปยัง Frigate+ ได้" + }, + "success": { + "submittedFrigatePlus": "ส่งเฟรมให้ Frigate+ เรียบร้อยแล้ว" + } + }, + "noRecordingsFoundForThisTime": "ไม่เจอการบันทึกในช่วงเวลานี้", + "noPreviewFoundFor": "ไม่พบตัวอย่างสำหรับ {{cameraName}}" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/objects.json b/sam2-cpu/frigate-dev/web/public/locales/th/objects.json new file mode 100644 index 0000000..8d3130d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/objects.json @@ -0,0 +1,118 @@ +{ + "traffic_light": "สัญญาณไฟจราจร", + "hair_dryer": "ไดร์เป่าผม", + "skateboard": "สเก็ตบอร์ด", + "fire_hydrant": "หัวดับเพลิง", + "bicycle": "จักรยาน", + "person": "คน", + "car": "รถยนต์", + "motorcycle": "มอเตอร์ไซค์", + "airplane": "เครื่องบิน", + "bus": "รถประจำทาง", + "train": "รถไฟ", + "cat": "แมว", + "horse": "ม้า", + "bird": "นก", + "boat": "เรือ", + "bench": "ม้านั่ง", + "dog": "สุนัข", + "parking_meter": "มิเตอร์จอดรถ", + "sheep": "แกะ", + "eye_glasses": "แว่นตา", + "sports_ball": "ลูกบอลกีฬา", + "baseball_bat": "ไม้เบสบอล", + "baseball_glove": "ถุงมือเบสบอล", + "tennis_racket": "ไม้เทนนิส", + "wine_glass": "แก้วไวน์", + "vehicle": "ยานพาหนะ", + "clock": "นาฬิกา", + "knife": "มีด", + "bowl": "ชาม", + "hot_dog": "ฮอทดอก", + "potted_plant": "ต้นไม้ในกระถาง", + "dining_table": "โต๊ะกินอาหาร", + "tv": "ทีวิ", + "teddy_bear": "ตุ๊กตาหมี", + "hair_brush": "แปรงหวีผม", + "squirrel": "กระรอก", + "deer": "กวาง", + "face": "ใบหน้า", + "robot_lawnmower": "หุ่นยนต์ตัดหญ้า", + "waste_bin": "ถังขยะ", + "license_plate": "ป้ายทะเบียนรถ", + "amazon": "อเมซอน", + "usps": "ไปรษณีย์สหรัฐ", + "stop_sign": "ป้ายหยุด", + "street_sign": "ป้ายถนน", + "cow": "วัว", + "elephant": "ช้าง", + "bear": "หมี", + "zebra": "ยีราฟ", + "giraffe": "จระเข้น้ำ", + "hat": "หมวก", + "backpack": "กระเป๋าเป้", + "umbrella": "ร่ม", + "shoe": "รองเท้า", + "handbag": "กระเป๋าถือ", + "mouse": "เมาส์", + "tie": "เนคไท", + "suitcase": "กระเป๋าเดินทาง", + "frisbee": "ดิสก์", + "skis": "สกี", + "snowboard": "สโนบอร์ด", + "kite": "ดอกกระดาษ", + "surfboard": "บอร์ดโต้คลื่น", + "bottle": "ขวด", + "plate": "จาน", + "cup": "ถ้วย", + "fork": "ส้อม", + "spoon": "ช้อน", + "banana": "กล้วย", + "apple": "แอปเปิล", + "sandwich": "แซนวิช", + "orange": "ส้ม", + "broccoli": "บรอกโคลี", + "carrot": "แครอท", + "pizza": "พิซซ่า", + "donut": "โดนัท", + "cake": "เค้ก", + "chair": "เก้าอี้", + "couch": "โซฟา", + "bed": "เตียง", + "mirror": "กระจก", + "window": "หน้าต่าง", + "desk": "โต๊ะทำงาน", + "toilet": "ห้องน้ำ", + "door": "ประตู", + "laptop": "แล็ปท็อป", + "remote": "รีโมท", + "oven": "เตาอบ", + "keyboard": "คีย์บอร์ด", + "microwave": "ไมโครเวฟ", + "toaster": "เครื่องทำขนมปัง", + "sink": "อ่างล้างจาน", + "refrigerator": "ตู้เย็น", + "blender": "เครื่องปั่น", + "book": "หนังสือ", + "vase": "แจกัน", + "scissors": "กรรไกร", + "toothbrush": "แปรงสีฟัน", + "animal": "สัตว์", + "bark": "เสียงหอบ", + "fox": "สุนัขจิ้งจอก", + "goat": "แพะ", + "rabbit": "กระต่าย", + "raccoon": "กระรอกน้ำ", + "package": "พัสดุ", + "purolator": "เพรูเลตอร์", + "bbq_grill": "เตาบาร์บีคิว", + "cell_phone": "โทรศัพท์มือถือ", + "ups": "UPS", + "dhl": "DHL", + "gls": "GLS", + "dpd": "DPD", + "postnord": "PostNord", + "nzpost": "NZPost", + "fedex": "FedEx", + "postnl": "PostNL" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/configEditor.json new file mode 100644 index 0000000..d44ae39 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/configEditor.json @@ -0,0 +1,16 @@ +{ + "copyConfig": "คัดลอกการกำหนดค่า", + "saveOnly": "บันทึกเท่านั้น", + "confirm": "ออกโดยที่ไม่บันทึก?", + "toast": { + "error": { + "savingError": "เกิดข้อผิดพลาดในการบันทึกการกำหนดค่า" + }, + "success": { + "copyToClipboard": "คัดลอกการกำหนดค่าไปยังคลิปบอร์ดแล้ว." + } + }, + "saveAndRestart": "บันทึก และ รีสตาร์ท", + "documentTitle": "ตัวแก้ไขการกำหนดค่า - Frigate", + "configEditor": "ตัวแก้ไขการกำหนดค่า" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/events.json new file mode 100644 index 0000000..f303ea6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/events.json @@ -0,0 +1,38 @@ +{ + "motion": { + "label": "เคลื่อนไหว", + "only": "เคลื่อนไหวเท่านั้น" + }, + "allCameras": "กล้องทั้งหมด", + "empty": { + "detection": "ไม่มีการเคลื่อนไหวให้รีวิว", + "motion": "ไม่เจอข้อมูลการเคลื่อนไหว", + "alert": "ไม่มีการแจ้งเตือนให้รีวิว" + }, + "events": { + "label": "กิจกรรม", + "aria": "เลือกกิจกรรม", + "noFoundForTimePeriod": "ไม่เจอกิจกรรมในช่วงเวลานี้" + }, + "recordings": { + "documentTitle": "การบันทึก - Frigate" + }, + "calendarFilter": { + "last24Hours": "24 ชั่วโมงล่าสุด" + }, + "markAsReviewed": "ทำเครื่องหมายว่ารีวิวแล้ว", + "newReviewItems": { + "button": "รายการใหม่ที่จะรีวิว", + "label": "ดูรายการรีวิวใหม่" + }, + "selected_other": "เลือก {{count}} แล้ว", + "camera": "กล้อง", + "detected": "ตรวจพบ", + "timeline": "ไทม์ไลน์", + "markTheseItemsAsReviewed": "ทำเครื่องหมายรายการเหล่านี้ว่าได้รับการรีวิวแล้ว", + "alerts": "การแจ้งเตือน", + "detections": "การตรวจจับ", + "selected_one": "เลือก {{count}} แล้ว", + "timeline.aria": "เลือกไทม์ไลน์", + "documentTitle": "รีวิว - Frigate" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/explore.json new file mode 100644 index 0000000..b74d29e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/explore.json @@ -0,0 +1,32 @@ +{ + "documentTitle": "สํารวจ - Frigate", + "generativeAI": "AI", + "exploreMore": "สํารวจวัตถุ {{label}} เพิ่มเติม", + "exploreIsUnavailable": { + "title": "สํารวจไม่มีให้ใช้งาน", + "embeddingsReindexing": { + "context": "สํารวจสามารถใช้หลังจากติดตามวัตถุเสร็จ.", + "startingUp": "เริ่มต้น…", + "estimatedTime": "ระยะเวลาโดยประมาณ:", + "finishingShortly": "เสร็จเร็วๆนี้" + }, + "downloadingModels": { + "tips": { + "documentation": "อ่านเอกสาร" + } + } + }, + "type": { + "details": "รายละเอียด", + "video": "วิดีโอ" + }, + "objectLifecycle": { + "noImageFound": "ไม่มีภาพสําหรับช่วงเวลานี้.", + "annotationSettings": { + "offset": { + "documentation": "อ่านเอกสาร " + } + } + }, + "trackedObjectsCount_other": "{{count}} วัตถุที่เจอ " +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/exports.json new file mode 100644 index 0000000..698c6f8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "ส่งออก - Frigate", + "search": "ค้นหา", + "noExports": "ไม่เจอการส่งออก", + "deleteExport": "ลบส่งออก", + "deleteExport.desc": "คุณแน่ใจหรอที่จะลบ {{exportName}}?", + "editExport": { + "title": "แก้ชื่อส่งออก", + "desc": "ใส่ชื่อใหม่สำหรับการส่งออกนี้", + "saveExport": "บันทึกการส่งออก" + }, + "toast": { + "error": { + "renameExportFailed": "ผิดพลาดในการแก้ไขชื่อการส่งออก: {{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/faceLibrary.json new file mode 100644 index 0000000..4372d09 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/faceLibrary.json @@ -0,0 +1,56 @@ +{ + "details": { + "person": "คน", + "subLabelScore": "คะแนน Sub Label", + "unknown": "ไม่รู้" + }, + "steps": { + "faceName": "ใส่ชื่อหน้า", + "uploadFace": "ใส่รูปหน้า", + "nextSteps": "ต่อไป", + "description": { + "uploadFace": "อัพโหลดภาพ {{name}} ที่แสดงให้เห็นใบหน้าของเขาจากมุมข้างหน้า. รูปภาพไม่จําเป็นต้องตัดให้เห็นเฉพาะใบหน้าของเขา." + } + }, + "selectFace": "เลือกหน้า", + "deleteFaceLibrary": { + "title": "ลบชื่อ", + "desc": "คุณแน่ใจหรือไม่ว่าต้องการลบคอลเลกชัน {{name}}? การลบนี้จะเป็นการลบอย่างถาวร." + }, + "deleteFaceAttempts": { + "title": "ลบหน้า" + }, + "renameFace": { + "title": "เปลี่ยนชื่อหน้า", + "desc": "ใส่ชื่อใหม่สำหรับ {{name}}" + }, + "button": { + "deleteFaceAttempts": "ลบหน้า", + "addFace": "เพิ่มหน้า", + "renameFace": "แก้ชื่อหน้า", + "deleteFace": "ลบหน้า", + "uploadImage": "อัปโหลดรูป", + "reprocessFace": "คำนวนหน้าใหม่" + }, + "imageEntry": { + "dropActive": "ลากรูปลงที่นี้", + "dropInstructions": "ลากและวางภาพที่นี่, หรือคลิกเลือก" + }, + "selectItem": "เลือก {{item}}", + "createFaceLibrary": { + "new": "สร้างหน้าใหม่", + "nextSteps": "สร้างรากฐานที่แข็งแรง:
  • ใช้แท็บฝึกเพื่อเลือกและฝึกบนภาพแต่ละบุคคล.
  • เน้นไปที่ภาพหน้าตรงเพื่อผลลัพธ์ที่ดีที่สุด; หลีกเลี่ยงภาพที่จับภาพใบหน้าแบบมุม
  • " + }, + "collections": "คอลเลกชัน", + "description": { + "addFace": "ทำตามวิธีการเพิ่มคอลเลกชันใหม่ไปยังที่เก็บหน้า.", + "placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้" + }, + "toast": { + "success": { + "addFaceLibrary": "{{name}} เพิ่มลงที่เก็บหน้าเรียบร้อยแล้ว!", + "deletedName_other": "{{count}} หน้าถูกลบไปเรียบร้อยแล้ว." + } + }, + "readTheDocs": "อ่านเอกสาร" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/live.json new file mode 100644 index 0000000..ccf620b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/live.json @@ -0,0 +1,49 @@ +{ + "stream": { + "playInBackground": { + "label": "เล่นในพื้นหลัง" + }, + "audio": { + "tips": { + "documentation": "อ่านเอกสาร " + } + }, + "twoWayTalk": { + "tips.documentation": "อ่านเอกสาร " + } + }, + "documentTitle": "สด - Frigate", + "lowBandwidthMode": "โหมดแบนด์วิดท์ต่ำ", + "twoWayTalk": { + "enable": "เปิดใช้งานการสนทนาสองทาง", + "disable": "ปิดใช้งานการสนทนาสองทาง" + }, + "cameraAudio": { + "enable": "เปิดเสียงกล้อง", + "disable": "ปิดเสียงกล้อง" + }, + "manualRecording": { + "playInBackground": { + "label": "เล่นในพื้นหลัง" + } + }, + "editLayout": { + "exitEdit": "ออกจากการแก้ไข" + }, + "effectiveRetainMode": { + "modes": { + "all": "ทั้งหมด" + } + }, + "documentTitle.withCamera": "{{camera}} - สด - Frigate", + "streamingSettings": "การตั้งค่าการสตรีม", + "camera": { + "disable": "ปิดกล้อง" + }, + "detect": { + "disable": "ปิดการตรวจจับ" + }, + "recording": { + "disable": "ปิดการบันทึก" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/recording.json new file mode 100644 index 0000000..3a27ea9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "ส่งออก", + "calendar": "ปฎิทิน", + "filter": "กรอง", + "filters": "ตัวกรอง", + "toast": { + "error": { + "noValidTimeSelected": "ไม่ได้เลือกช่วงเวลาที่ถูกต้อง", + "endTimeMustAfterStartTime": "เวลาสิ้นสุดต้องอยู่หลังเวลาเริ่มต้น" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/search.json new file mode 100644 index 0000000..c94d1c7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/search.json @@ -0,0 +1,62 @@ +{ + "search": "ค้นหา", + "button": { + "save": "บันทึกค้นหา", + "delete": "ลบการบันทึกค้นหา", + "clear": "ล้างการค้นหา", + "filterInformation": "ข้อมูลตัวกรอง", + "filterActive": "ตัวกรองที่ใช้งาน" + }, + "savedSearches": "บันทึกการค้นหา", + "searchFor": "ค้นหา {{inputValue}}", + "trackedObjectId": "ติดตามรหัส", + "filter": { + "label": { + "cameras": "กล้อง", + "labels": "หมวดหมู่", + "zones": "โซน", + "sub_labels": "หมวดหมู่ย่อย", + "search_type": "ค้นหาประเภท", + "time_range": "ช่วงเวลา", + "after": "หลัง", + "min_score": "คะแนนต่ำ", + "max_speed": "ความเร็วสูงสุด", + "has_clip": "มีคลิป", + "has_snapshot": "มีภาพ", + "before": "ก่อน", + "max_score": "คะแนนสูงสุด", + "min_speed": "ความเร็วต่ำ" + }, + "searchType": { + "description": "ลักษณะ" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "วันที่ของก่อนต้องอยู่หลังจากวันที่ของหลัง.", + "afterDatebeEarlierBefore": "วันที่ 'หลัง' จะต้องมาก่อนวันที่ 'ก่อน'.", + "minScoreMustBeLessOrEqualMaxScore": "'คะแนนต่ำ' ต้องน้อยกว่าหรือเท่ากับ 'คะแนนสูง'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'ความเร็วต่ำ' จะต้องน้อยกว่าหรือเท่ากับ 'ความเร็วสูงสุด'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'ความเร็วสูงสุด' จะต้องมากกว่าหรือเท่ากับ 'ความเร็วต่ำ'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'คะแนนสูง' ต้องมากกว่าหรือเท่ากับ 'คะแนนต่ำ'." + } + }, + "tips": { + "desc": { + "step1": "ประเภทตัวกรองต้องตามด้วยอัญประกาศ (เช่น \"cameras:\").", + "exampleLabel": "ตัวอย่าง:", + "step4": "วันที่กรอง (before: และ after:)ใช้รูปแบบ {{DateFormat}}.", + "step2": "เลือกค่าจากคําแนะนําหรือพิมพ์ด้วยคุณเอง." + }, + "title": "วิธีการใช้ตัวกรองข้อความ" + }, + "header": { + "activeFilters": "ตัวกรองที่ใช้งาน" + } + }, + "placeholder": { + "search": "ค้นหา…" + }, + "similaritySearch": { + "title": "ค้นหาที่คล้ายกัน" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/settings.json new file mode 100644 index 0000000..4216207 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/settings.json @@ -0,0 +1,248 @@ +{ + "dialog": { + "unsavedChanges": { + "title": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก.", + "desc": "คุณต้องการบันทึกการเปลี่ยนแปลงของคุณก่อนดำเนินการต่อหรือไม่?" + } + }, + "users": { + "dialog": { + "form": { + "newPassword": { + "title": "รหัสผ่านใหม่", + "confirm": { + "placeholder": "ใส่รหัสผ่านใหม่อีกครั้ง" + }, + "placeholder": "ใส่รหัสผ่านใหม่" + }, + "password": { + "notMatch": "รหัสไม่ตรงกัน", + "title": "รหัสผ่าน", + "confirm": { + "title": "ยืนยันรหัสผ่าน", + "placeholder": "ยืนยันรหัสผ่าน" + }, + "strength": { + "title": "ความแข็งแรงของรหัส: ", + "weak": "แย่", + "strong": "ใช้ได้", + "veryStrong": "แข็งแรง", + "medium": "กลาง" + }, + "match": "รหัสตรงกัน", + "placeholder": "ใส่รหัสผ่าน" + }, + "user": { + "placeholder": "ใส่ชื่อผู้ใช้", + "title": "ผู้ใช้" + }, + "passwordIsRequired": "ต้องการรหัสผ่าน", + "usernameIsRequired": "ต้องการชื่อผู้ใช้" + }, + "changeRole": { + "roleInfo": { + "admin": "ผู้ดูแล" + } + } + }, + "toast": { + "success": { + "updatePassword": "อัปเดตรหัสผ่านเรียบร้อย", + "deleteUser": "ลบผู้ใช้ {{user}} เรียบร้อย", + "createUser": "สร้างผู้ใช้ {{user}} เรียบร้อย" + }, + "error": { + "setPasswordFailed": "ผิดพลาดในการบันทึกรหัสผ่าน: {{errorMessage}}", + "deleteUserFailed": "ผิดพลาดในการลบผู้ใช้: {{errorMessage}}", + "createUserFailed": "ผิดพลาดในการสร้างผู้ใช้: {{errorMessage}}" + } + }, + "table": { + "username": "ชื่อผู้ใช้", + "noUsers": "ไม่เจอผู้ใช้", + "password": "รหัสผ่าน", + "deleteUser": "ลบผู้ใช้", + "actions": "การดำเนินการ" + }, + "management": { + "title": "จัดการผู้ใช้", + "desc": "จัดการบัญชีของ Frigate นี้." + }, + "addUser": "แก้ไขผู้ใช้", + "title": "ผู้ใช้", + "updatePassword": "อัปเดตรหัสผ่าน" + }, + "notification": { + "suspendTime": { + "12hours": "ระงับ 12 ชั่วโมง", + "5minutes": "ระงับ 5 นาที", + "10minutes": "ระงับ 10 นาที", + "1hour": "ระงับ 1 ชั่วโมง", + "untilRestart": "ระงับจนกว่ารีสตาร์ท", + "suspend": "ระงับ", + "24hours": "ระงับ 24 ชั่วโมง", + "30minutes": "ระงับ 30 นาที" + }, + "cameras": { + "title": "กล้อง", + "noCameras": "ไม่มีกล้องให้ใช้งาน" + }, + "email": { + "title": "อีเมล" + }, + "notificationSettings": { + "documentation": "อ่านเอกสาร" + }, + "notificationUnavailable": { + "documentation": "อ่านเอกสาร" + } + }, + "documentTitle": { + "default": "ตั้งค่า - Frigate", + "authentication": "การตั้งค่าการตรวจสอบสิทธิ์ - Frigate", + "camera": "การตั้งค่ากล้อง - Frigate", + "classification": "การตั้งค่าการจำแนกประเภท - Frigate", + "masksAndZones": "ตัวแก้ไขแมสและโซน - Frigate", + "general": "การตั้งค่าทั่วไป - Frigate", + "frigatePlus": "การตั้งค่า Frigate+ - Frigate", + "notifications": "การตั้งค่าการแจ้งเตือน - Frigate" + }, + "menu": { + "notifications": "การแจ้งเตือน", + "frigateplus": "Frigate+", + "cameras": "ตั้งค่ากล้อง", + "users": "ผู้ใช้", + "classification": "การจําแนกประเภท", + "masksAndZones": "แมส / โซน", + "ui": "UI" + }, + "cameraSetting": { + "camera": "กล้อง", + "noCamera": "ไม่มีกล้อง" + }, + "general": { + "liveDashboard": { + "title": "แดชบอร์ดสด", + "automaticLiveView": { + "label": "การดูสดอัตโนมัติ", + "desc": "สลับไปที่มุมมองสดของกล้องโดยอัตโนมัติเมื่อตรวจพบกิจกรรม. การปิดใช้งานตัวเลือกนี้จะทำให้ภาพคงที่จากกล้องบนแดชบอร์ดสดอัปเดตเพียงครั้งเดียวต่อนาที." + }, + "playAlertVideos": { + "label": "เล่นวิดีโอการแจ้งเตือน.", + "desc": "ตามค่าเริ่มต้น, แจ้งเตือนล่าสุดบนแดชบอร์ดสด จะเล่นเป็นวิดีโอวนซ้ำขนาดเล็ก. ปิดใช้งานตัวเลือกนี้เพื่อแสดงเฉพาะภาพนิ่งของการแจ้งเตือนล่าสุดบนอุปกรณ์/เบราว์เซอร์นี้เท่านั้น." + } + }, + "storedLayouts": { + "title": "เลย์เอาท์ที่จัดเก็บไว้", + "desc": "คุณสามารถลากหรือปรับขนาดเลย์เอาท์ของกล้องในกลุ่มกล้องได้. ตำแหน่งต่างๆ จะถูกเก็บไว้ในหน่วยความจำภายในของเบราว์เซอร์ของคุณ.", + "clearAll": "ล้างเลย์เอาต์ทั้งหมด" + }, + "calendar": { + "title": "ปฎิทิน", + "firstWeekday": { + "label": "วันธรรมดาวันแรก", + "sunday": "วันอาทิตย์", + "monday": "วันจันทร์", + "desc": "วันที่เริ่มต้นสัปดาห์ของปฏิทินการรีวิว." + } + }, + "title": "การตั้งค่าทั่วไป", + "cameraGroupStreaming": { + "title": "การตั้งค่าสตรีมกล้องแบบกลุ่ม", + "desc": "การตั้งค่าสตรีมมิ่งสําหรับกล้องแต่ละกลุ่มเก็บไว้ในเบราว์เซอร์ของคุณ.", + "clearAll": "ล้างการตั้งค่าสตรีมทั้งหมด" + } + }, + "classification": { + "faceRecognition": { + "title": "การจดจำใบหน้า", + "modelSize": { + "small": { + "title": "เล็ก" + }, + "large": { + "title": "ใหญ่" + } + } + }, + "toast": { + "error": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "semanticSearch": { + "modelSize": { + "small": { + "title": "เล็ก" + }, + "large": { + "title": "ใหญ่" + } + } + }, + "restart_required": "จำเป็นต้องรีสตาร์ท (การตั้งค่าการจำแนกมีการเปลี่ยนแปลง)" + }, + "camera": { + "review": { + "title": "รีวิว", + "alerts": "การแจ้งเตือน ", + "detections": "การเคลื่อนไหว " + }, + "reviewClassification": { + "readTheDocumentation": "อ่านเอกสาร" + } + }, + "masksAndZones": { + "zones": { + "name": { + "tips": "ชื่อต้องมีความยาวอย่างน้อย 2 อักขระ และต้องไม่ใช่ชื่อกล้องหรือโซนอื่น", + "title": "ชื่อ", + "inputPlaceHolder": "ใส่ชื่อ…" + }, + "add": "เพิ่มโซน", + "edit": "แก้โซน", + "point_other": "{{count}} จุด", + "speedEstimation": { + "docs": "อ่านเอกสาร" + } + }, + "motionMasks": { + "point_other": "{{count}} จุด", + "context": { + "documentation": "อ่านเอกสาร" + }, + "polygonAreaTooLarge": { + "documentation": "อ่านเอกสาร" + } + }, + "objectMasks": { + "point_other": "{{count}} จุด" + } + }, + "frigatePlus": { + "title": "การตั้งค่า Frigate+", + "modelInfo": { + "cameras": "กล้อง" + }, + "toast": { + "error": "ผิดพลาดในการบันทึกการกำหนดค่า: {{errorMessage}}" + }, + "snapshotConfig": { + "documentation": "อ่านเอกสาร" + } + }, + "debug": { + "objectShapeFilterDrawing": { + "document": "อ่านเอกสาร " + } + }, + "enrichments": { + "semanticSearch": { + "readTheDocumentation": "อ่านเอกสาร" + }, + "faceRecognition": { + "readTheDocumentation": "อ่านเอกสาร" + }, + "licensePlateRecognition": { + "readTheDocumentation": "อ่านเอกสาร" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/th/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/th/views/system.json new file mode 100644 index 0000000..2084d91 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/th/views/system.json @@ -0,0 +1,59 @@ +{ + "general": { + "title": "ทั่วไป", + "hardwareInfo": { + "gpuInfo": { + "nvidiaSMIOutput": { + "driver": "ไดรเวอร์: {{driver}}", + "name": "ชื่อ: {{name}}" + }, + "copyInfo": { + "label": "คัดลอกข้อมูล GPU" + }, + "closeInfo": { + "label": "ปิดข้อมูล GPU" + }, + "toast": { + "success": "คัดลอกข้อมูล GPU ไปยังคลิปบอร์ดแล้ว" + } + }, + "gpuUsage": "การใช้งาน GPU", + "gpuMemory": "หน่วยความจํา GPU", + "title": "รายละเอียดของอุปกรณ์", + "gpuEncoder": "ใช้ GPU เข้ารหัส", + "gpuDecoder": "ใช้ GPU ถอดรหัส" + }, + "detector": { + "cpuUsage": "ตัวตรวจจับใช้งานหน่วยประมวลผลกลาง", + "title": "ตัวตรวจจับ", + "inferenceSpeed": "ความเร็วในการตรวจจับ", + "temperature": "อุณภูมิตัวตรวจจับ", + "memoryUsage": "ตัวตรวจจับใช้งานหน่วยความจำ" + } + }, + "enrichments": { + "embeddings": { + "face_recognition_speed": "ความเร็วในการจดจำใบหน้า", + "plate_recognition_speed": "ความเร็วในการจดจำป้าย", + "yolov9_plate_detection_speed": "ความเร็วในการตรวจจับป้าย ของ YOLOv9", + "face_recognition": "การจดจำใบหน้า", + "text_embedding_speed": "ความเร็วในการอ่านข้อความ", + "yolov9_plate_detection": "การตรวจจับป้าย ของ YOLOv9" + } + }, + "title": "ระบบ", + "storage": { + "cameraStorage": { + "unused": { + "title": "ไม่ได้ใช้" + }, + "storageUsed": "พื้นที่จัดเก็บ", + "title": "พื้นที่จัดเก็บกล้อง" + }, + "title": "พื้นที่จัดเก็บ" + }, + "stats": { + "cameraIsOffline": "{{camera}} ออฟไลน์", + "detectIsVerySlow": "{{detect}} ช้ามาก ({{speed}} มิลลิวินาที)" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/audio.json b/sam2-cpu/frigate-dev/web/public/locales/tr/audio.json new file mode 100644 index 0000000..34a6f36 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/audio.json @@ -0,0 +1,503 @@ +{ + "mantra": "mantra", + "breathing": "nefes alma", + "snoring": "horlama", + "animal": "hayvan", + "dog": "köpek", + "laughter": "kahkaha", + "yell": "bağırma", + "cat": "kedi", + "whispering": "fısıldama", + "crying": "ağlama", + "bark": "havlama", + "speech": "Konuşma", + "bicycle": "bisiklet", + "horse": "at", + "goat": "keçi", + "mouse": "fare", + "keyboard": "klavye", + "vehicle": "araç", + "boat": "bot", + "car": "araba", + "bus": "otobüs", + "motorcycle": "motosiklet", + "skateboard": "kaykay", + "door": "kapı", + "blender": "mikser", + "sink": "lavabo", + "toothbrush": "diş fırçası", + "clock": "saat", + "scissors": "makas", + "bird": "kuş", + "sheep": "koyun", + "train": "tren", + "hair_dryer": "saç kurutma makinesi", + "babbling": "Agulama", + "snicker": "kıkırdama", + "sigh": "iç çekme", + "bellow": "haykırma", + "whoop": "nara", + "singing": "şarkı söyleme", + "choir": "koro", + "yodeling": "gırtlak naresi", + "grunt": "homurdanma", + "whistling": "ıslık", + "wheeze": "hırıltı", + "gasp": "kesik nefes", + "pant": "soluma", + "cough": "öksürük", + "throat_clearing": "boğaz temizleme", + "chatter": "gevezelik", + "crowd": "kalabalık", + "children_playing": "oynayan çocuklar", + "pets": "evcil hayvan", + "meow": "miyavlama", + "hiss": "tıslama", + "moo": "böğürme", + "cowbell": "inek çanı", + "fowl": "kümes hayvanı", + "chicken": "tavuk", + "cluck": "gıdaklama", + "quack": "vakvaklama", + "coo": "kumru sesi", + "crow": "karga", + "insect": "böcek", + "plucked_string_instrument": "çekmeli telli çalgı", + "guitar": "gitar", + "electric_guitar": "elektro gitar", + "strum": "pena vuruşu", + "organ": "org", + "electronic_organ": "elektronik org", + "hammond_organ": "hammond org", + "percussion": "vurmalı çalgı", + "drum_kit": "bateri", + "drum": "davul", + "gong": "gong", + "tubular_bells": "boru çanlar", + "mallet_percussion": "tokmaklı vurmalılar", + "brass_instrument": "bakır nefesli", + "flute": "flüt", + "church_bell": "kilise çanı", + "didgeridoo": "didgeridoo", + "theremin": "teremin", + "heavy_metal": "heavy metal", + "punk_rock": "punk rock", + "grunge": "grunge", + "reggae": "reggae", + "country": "country müzik", + "middle_eastern_music": "orta doğu müziği", + "jazz": "caz", + "trance_music": "trance müzik", + "music_of_latin_america": "latin amerika müziği", + "music_of_africa": "afrika müziği", + "afrobeat": "afrobeat", + "christian_music": "hristiyan müziği", + "gospel_music": "gospel", + "independent_music": "bağımsız müzik", + "wedding_music": "düğün müziği", + "happy_music": "mutlu müzik", + "scary_music": "korkutucu müzik", + "wind": "rüzgar", + "thunder": "gök gürültüsü", + "water": "su", + "rain": "yağmur", + "ocean": "okyanus", + "waves": "dalgalar", + "steam": "buhar", + "gurgling": "şırıltı", + "fire": "ateş", + "sailboat": "yelkenli", + "rowboat": "sandal", + "motorboat": "motorbot", + "toot": "korna sesi", + "race_car": "yarış arabası", + "ambulance": "ambulans", + "train_horn": "tren kornası", + "jet_engine": "jet motoru", + "accelerating": "hızlanma", + "doorbell": "kapı zili", + "ding-dong": "ding dong", + "cupboard_open_or_close": "dolap açma/kapama", + "drawer_open_or_close": "çekmece açma/kapama", + "dishes": "bulaşık", + "toilet_flush": "sifon çekme", + "zipper": "fermuar", + "shuffling_cards": "kart karıştırma", + "ringtone": "zil sesi", + "telephone_dialing": "numara çevirme", + "dial_tone": "çevir sesi", + "buzzer": "buzzer", + "mechanisms": "mekanizma", + "ratchet": "cırcır", + "tick": "tik", + "filing": "törpüleme", + "burst": "patlama", + "eruption": "püskürme", + "silence": "sessizlik", + "chant": "tezahürat", + "child_singing": "çocuk şarkısı", + "synthetic_singing": "sentetik şarkı", + "rapping": "rap", + "humming": "mırıldanma", + "groan": "inleme", + "snort": "burnundan soluma", + "sneeze": "hapşırma", + "sniff": "burun çekme", + "run": "koşma", + "shuffle": "ayak sürtme", + "footsteps": "adım sesleri", + "chewing": "çiğneme", + "biting": "ısırma", + "gargling": "gargara", + "stomach_rumble": "mide gurultusu", + "burping": "geğirme", + "hiccup": "hıçkırık", + "fart": "gaz çıkarma", + "hands": "el sesi", + "finger_snapping": "parmak şıklatma", + "clapping": "alkışlama", + "heartbeat": "kalp atışı", + "heart_murmur": "kalp üfürümü", + "cheering": "tezahürat", + "applause": "alkış", + "yip": "ince havlama", + "howl": "uluma", + "bow_wow": "havlama", + "growling": "hırlama", + "whimper_dog": "köpek sızlanması", + "purr": "mırlama", + "caterwaul": "kedi çığlığı", + "livestock": "çiftlik hayvanı", + "clip_clop": "nal sesi", + "neigh": "kişneme", + "cattle": "büyükbaş hayvan", + "pig": "domuz", + "oink": "domuz sesi", + "bleat": "meleme", + "cock_a_doodle_doo": "horoz ötüşü", + "turkey": "hindi", + "gobble": "hindi sesi", + "duck": "ördek", + "goose": "kaz", + "honk": "kaz sesi", + "wild_animals": "vahşi hayvan", + "roaring_cats": "kükreyen kedi", + "roar": "kükreme", + "chirp": "cıvıltı", + "squawk": "kuş çığlığı", + "pigeon": "güvercin", + "caw": "karga sesi", + "owl": "baykuş", + "hoot": "baykuş ötüşü", + "flapping_wings": "kanat çırpma", + "dogs": "köpekler", + "rats": "sıçanlar", + "patter": "tıkırtı", + "cricket": "cırcır böceği", + "mosquito": "sivrisinek", + "fly": "sinek", + "frog": "kurbağa", + "croak": "vraklama", + "snake": "yılan", + "rattle": "çıngırak", + "buzz": "vızıltı", + "whale_vocalization": "balina sesi", + "music": "müzik", + "musical_instrument": "müzik aleti", + "bass_guitar": "bas gitar", + "acoustic_guitar": "akustik gitar", + "steel_guitar": "steel gitar", + "tapping": "tıklatma", + "ukulele": "ukulele", + "banjo": "banjo", + "sitar": "sitar", + "mandolin": "mandolin", + "zither": "ziter", + "piano": "piyano", + "electric_piano": "elektro piyano", + "synthesizer": "sentezleyici", + "sampler": "örnekleyici", + "drum_machine": "davul makinesi", + "harpsichord": "klavsen", + "snare_drum": "trampet", + "rimshot": "rimşat", + "drum_roll": "davul geçişi", + "bass_drum": "bas davul", + "timpani": "timpani", + "maraca": "marakas", + "tabla": "tabla", + "cymbal": "zil", + "hi_hat": "hi-hat", + "wood_block": "tahta blok", + "tambourine": "tef", + "marimba": "marimba", + "glockenspiel": "glockenspiel", + "vibraphone": "vibrafon", + "steelpan": "steelpan", + "orchestra": "orkestra", + "french_horn": "korno", + "trumpet": "trompet", + "trombone": "trombon", + "bowed_string_instrument": "yaylı telli çalgı", + "string_section": "yaylılar", + "violin": "keman", + "pizzicato": "pizzicato", + "cello": "viyolonsel", + "double_bass": "kontrbas", + "wind_instrument": "nefesli çalgı", + "saxophone": "saksafon", + "clarinet": "klarnet", + "harp": "arp", + "bell": "çan", + "singing_bowl": "tibet çanağı", + "scratching": "tırmalama", + "pop_music": "pop müzik", + "bicycle_bell": "bisiklet zili", + "tuning_fork": "diyapazon", + "chime": "çan sesi", + "wind_chime": "rüzgar çanı", + "harmonica": "mızıka", + "accordion": "akordeon", + "bagpipes": "gayda", + "hip_hop_music": "hip hop müzik", + "beatboxing": "beatbox", + "rock_music": "rock müzik", + "progressive_rock": "progressive rock", + "rock_and_roll": "rock and roll", + "psychedelic_rock": "psychedelic rock", + "rhythm_and_blues": "rhythm and blues", + "soul_music": "soul müzik", + "swing_music": "swing müzik", + "bluegrass": "bluegrass", + "funk": "funk", + "folk_music": "halk müziği", + "disco": "disko", + "classical_music": "klasik müzik", + "opera": "opera", + "electronic_music": "elektronik müzik", + "house_music": "house müzik", + "techno": "tekno", + "dubstep": "dubstep", + "drum_and_bass": "drum and bass", + "electronica": "electronica", + "electronic_dance_music": "elektronik dans müziği", + "ambient_music": "ambient müzik", + "salsa_music": "salsa müziği", + "flamenco": "flamenko", + "blues": "blues", + "music_for_children": "çocuk müziği", + "new-age_music": "new age müzik", + "vocal_music": "vokal müzik", + "a_capella": "akapella", + "music_of_asia": "asya müziği", + "carnatic_music": "karnatik müzik", + "music_of_bollywood": "bollywood müziği", + "ska": "ska", + "traditional_music": "geleneksel müzik", + "song": "şarkı", + "background_music": "arka plan müziği", + "theme_music": "tema müziği", + "jingle": "jingle", + "soundtrack_music": "film müziği", + "lullaby": "ninni", + "video_game_music": "video oyunu müziği", + "christmas_music": "noel müziği", + "dance_music": "dans müziği", + "sad_music": "hüzünlü müzik", + "tender_music": "yumuşak müzik", + "exciting_music": "heyecanlı müzik", + "angry_music": "öfkeli müzik", + "rustling_leaves": "yaprak hışırtısı", + "wind_noise": "rüzgar gürültüsü", + "thunderstorm": "gök gürültülü fırtına", + "raindrop": "yağmur damlası", + "rain_on_surface": "yüzeye düşen yağmur", + "stream": "dere", + "waterfall": "şelale", + "crackle": "çıtırtı", + "ship": "gemi", + "motor_vehicle": "motorlu taşıt", + "car_alarm": "araç alarmı", + "power_windows": "otomatik cam", + "skidding": "kayma", + "tire_squeal": "lastik gıcırtısı", + "car_passing_by": "geçen araba", + "truck": "kamyon", + "air_brake": "havalı fren", + "air_horn": "havalı korna", + "reversing_beeps": "geri vites bip sesi", + "ice_cream_truck": "dondurma kamyonu", + "emergency_vehicle": "acil durum aracı", + "police_car": "polis arabası", + "fire_engine": "itfaiye aracı", + "traffic_noise": "trafik gürültüsü", + "rail_transport": "raylı ulaşım", + "train_whistle": "tren düdüğü", + "railroad_car": "vagon", + "fixed-wing_aircraft": "sabit kanatlı uçak", + "train_wheels_squealing": "tren tekeri gıcırtısı", + "subway": "metro", + "aircraft": "hava aracı", + "aircraft_engine": "uçak motoru", + "propeller": "pervane", + "helicopter": "helikopter", + "engine": "motor", + "light_engine": "hafif motor", + "dental_drill's_drill": "dişçi matkabı", + "lawn_mower": "çim biçme makinesi", + "chainsaw": "motorlu testere", + "medium_engine": "orta motor", + "heavy_engine": "ağır motor", + "engine_knocking": "motor vuruntusu", + "engine_starting": "motor çalıştırma", + "idling": "rölanti", + "sliding_door": "sürgülü kapı", + "slam": "kapı çarpma", + "knock": "kapı çalma", + "tap": "tıklatma", + "squeak": "gıcırtı", + "cutlery": "çatal bıçak", + "chopping": "doğrama", + "frying": "kızartma", + "microwave_oven": "mikrodalga fırın", + "water_tap": "musluk", + "bathtub": "küvet", + "electric_toothbrush": "elektrikli diş fırçası", + "vacuum_cleaner": "elektrik süpürgesi", + "keys_jangling": "anahtar şıngırtısı", + "coin": "madeni para", + "electric_shaver": "tıraş makinesi", + "typing": "klavyede yazma", + "typewriter": "daktilo", + "computer_keyboard": "bilgisayar klavyesi", + "writing": "yazma", + "alarm": "alarm", + "telephone": "telefon", + "telephone_bell_ringing": "telefon çalması", + "busy_signal": "meşgul sinyali", + "alarm_clock": "çalar saat", + "siren": "siren", + "civil_defense_siren": "sivil savunma sireni", + "smoke_detector": "duman dedektörü", + "fire_alarm": "yangın alarmı", + "foghorn": "sis düdüğü", + "whistle": "düdük", + "steam_whistle": "buhar düdüğü", + "tick-tock": "tik tak", + "gears": "dişliler", + "pulleys": "makaralar", + "sewing_machine": "dikiş makinesi", + "mechanical_fan": "mekanik vantilatör", + "air_conditioning": "klima", + "cash_register": "yazar kasa", + "printer": "yazıcı", + "camera": "kamera", + "single-lens_reflex_camera": "slr kamera", + "tools": "aletler", + "hammer": "çekiç", + "jackhammer": "hilti", + "sawing": "testere ile kesme", + "sanding": "zımparalama", + "power_tool": "elektrikli alet", + "drill": "matkap", + "explosion": "patlama", + "gunshot": "silah sesi", + "machine_gun": "makineli tüfek", + "fusillade": "yaylım ateşi", + "artillery_fire": "topçu ateşi", + "cap_gun": "mantar tabancası", + "fireworks": "havai fişek", + "firecracker": "torpil", + "boom": "gümbürtü", + "wood": "ahşap", + "chop": "kesme", + "splinter": "parçalanma", + "crack": "çatlama", + "glass": "cam", + "chink": "şıngırtı", + "shatter": "kırılma", + "sound_effect": "ses efekti", + "environmental_noise": "çevre gürültüsü", + "static": "parazit", + "white_noise": "beyaz gürültü", + "pink_noise": "pembe gürültü", + "television": "televizyon", + "radio": "radyo", + "field_recording": "alan kaydı", + "scream": "çığlık", + "jingle_bell": "küçük çan", + "sodeling": "Jodel (Yodeling)", + "chird": "Cıvıltı", + "change_ringing": "Sıralı Çan Çalma", + "shofar": "Şofar", + "liquid": "Sıvı", + "splash": "Su Sıçraması", + "slosh": "Çalkalanma", + "squish": "Vıcıklama (Islak Ezilme)", + "drip": "Damlama", + "pour": "Dökülme", + "trickle": "Şırıldama / İnce Akış", + "gush": "Fışkırma", + "fill": "Doldurma", + "spray": "Püskürtme / Sprey", + "pump": "Pompalama", + "stir": "Karıştırma", + "boiling": "Kaynama", + "sonar": "Sonar Sesi", + "arrow": "Ok Sesi", + "whoosh": "Hışırtı (Hızlı Geçiş Sesi)", + "thump": "Küt Sesi (Boğuk)", + "thunk": "Tok Ses", + "electronic_tuner": "Elektronik Akort Cihazı", + "effects_unit": "Efekt Ünitesi", + "chorus_effect": "Chorus (Koro) Efekti", + "basketball_bounce": "Basketbol Topu Sektirme", + "bang": "Gümleme / Patlama", + "slap": "Tokat / Şaplak", + "whack": "Sert Vuruş / Kütletme", + "smash": "Parçalanma", + "breaking": "Kırılma", + "bouncing": "Sekme / Zıplama", + "whip": "Kırbaç", + "flap": "Kanat Çırpma / Pırpır Etme", + "scratch": "Tırmalama / Cızırtı", + "scrape": "Kazıma / Sürtünme", + "rub": "Ovma / Sürtme", + "roll": "Yuvarlanma", + "crushing": "Ezilme (Kuru/Sert)", + "crumpling": "Buruşturma", + "tearing": "Yırtılma", + "beep": "Bip Sesi", + "ping": "Ping Sesi (Çınlama)", + "ding": "Ding (Zil Sesi)", + "clang": "Çangırtı (Metalik)", + "squeal": "Ciyaklama / Acı Gıcırtı", + "creak": "Gıcırdama (Tahta/Kapı)", + "rustle": "Hışırtı (Kağıt/Yaprak)", + "whir": "Vızıltı (Motor/Pervane)", + "clatter": "Takırtı", + "sizzle": "Cızırdayarak Kızarma", + "clicking": "Tıklama", + "clickety_clack": "Takır Tukur Sesi", + "rumble": "Gürleme / Gümbürtü", + "plop": "Lup Sesi (Suya düşme)", + "hum": "Uğultu / Mırıldanma", + "zing": "Vınlama", + "boing": "Boing (Yay Sesi)", + "crunch": "Kıtırdatma / Çıtırdatma", + "sine_wave": "Sinüs Dalgası", + "harmonic": "Harmonik", + "chirp_tone": "Cıvıltı Tonu (Sinyal)", + "pulse": "Darbe / Pulse", + "inside": "İç Mekan", + "outside": "Dış Mekan", + "reverberation": "Yankılanım (Reverb)", + "echo": "Yankı", + "noise": "Gürültü", + "mains_hum": "Şebeke Uğultusu (Elektrik)", + "distortion": "Bozulma / Distorsiyon", + "sidetone": "Yan Ton", + "cacophony": "Kakofoni (Ses Kargaşası)", + "throbbing": "Zonklama", + "vibration": "Titreşim" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/common.json b/sam2-cpu/frigate-dev/web/public/locales/tr/common.json new file mode 100644 index 0000000..6dd7d79 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/common.json @@ -0,0 +1,307 @@ +{ + "time": { + "lastWeek": "Geçen Hafta", + "thisWeek": "Bu Hafta", + "lastMonth": "Geçen Ay", + "untilForRestart": "Frigate yeniden başlatılana kadar.", + "untilRestart": "Yeniden başlatmaya kadar", + "ago": "{{timeAgo}} önce", + "justNow": "Az önce", + "today": "Bugün", + "yesterday": "Dün", + "last7": "Son 7 gün", + "last30": "Son 30 gün", + "thisMonth": "Bu Ay", + "5minutes": "5 dakika", + "10minutes": "10 dakika", + "30minutes": "30 dakika", + "1hour": "1 saat", + "12hours": "12 saat", + "last14": "Son 14 gün", + "24hours": "24 saat", + "formattedTimestamp": { + "24hour": "d MMM, HH:mm:ss", + "12hour": "d MMM, h:mm:ss aaa" + }, + "formattedTimestamp2": { + "24hour": "d MMM HH:mm:ss", + "12hour": "dd/MM h:mm:ssa" + }, + "second_one": "{{time}} saniye", + "second_other": "{{time}} saniye", + "year_one": "{{time}} yıl", + "year_other": "{{time}} yıl", + "hour_one": "{{time}} saat", + "hour_other": "{{time}} saat", + "h": "{{time}} s", + "yr": "{{time}} yıl", + "mo": "{{time}} ay", + "untilForTime": "{{time}}'a kadar", + "pm": "ÖS", + "am": "ÖÖ", + "d": "{{time}} gün", + "day_one": "{{time}} gün", + "day_other": "{{time}} gün", + "m": "{{time}}d", + "minute_one": "{{time}} dakika", + "minute_other": "{{time}} dakika", + "formattedTimestampWithYear": { + "12hour": "%-d %b %Y, %I:%M %p", + "24hour": "%-d %b %Y, %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%-d %b", + "formattedTimestampExcludeSeconds": { + "12hour": "%-d %b, %I:%M %p", + "24hour": "%-d %b, %H:%M" + }, + "s": "{{time}}sn", + "month_one": "{{time}} ay", + "month_other": "{{time}} ay", + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-ss" + }, + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "inProgress": "Devam ediyor", + "invalidStartTime": "Geçersiz başlangıç zamanı", + "invalidEndTime": "Geçersiz bitiş zamanı" + }, + "button": { + "off": "KAPALI", + "next": "Sonraki", + "saving": "Kaydediliyor…", + "back": "Geri", + "unselect": "Seçimi kaldır", + "info": "Bilgi", + "enable": "Aç", + "disabled": "Kapalı", + "disable": "Kapat", + "history": "Geçmiş", + "cameraAudio": "Kamera Sesi", + "on": "AÇIK", + "suspended": "Askıya alınmış", + "unsuspended": "Askıdan çıkart", + "export": "Dışa aktar", + "download": "İndir", + "edit": "Düzenle", + "fullscreen": "Tam Ekran", + "deleteNow": "Şimdi Sil", + "apply": "Uygula", + "reset": "Sıfırla", + "done": "Bitti", + "enabled": "Açık", + "save": "Kaydet", + "exitFullscreen": "Tam Ekrandan Çık", + "pictureInPicture": "Pencere içinde pencere", + "copyCoordinates": "Koordinatları kopyala", + "yes": "Evet", + "play": "Oynat", + "no": "Hayır", + "copy": "Kopyala", + "cancel": "İptal", + "twoWayTalk": "Çift Yönlü Ses", + "close": "Kapat", + "delete": "Sil", + "continue": "Devam Et" + }, + "menu": { + "systemLogs": "Sistem günlükleri", + "user": { + "anonymous": "anonim", + "account": "Hesap", + "current": "Mevcut kullanıcı: {{user}}", + "setPassword": "Parola Belirle", + "logout": "Oturumu Kapat", + "title": "Kullanıcı" + }, + "configuration": "Yapılandırma", + "languages": "Diller", + "language": { + "en": "İngilizce", + "zhCN": "简体中文 (Basitleştirilmiş Çince)", + "withSystem": { + "label": "Dil için sistem tercihini kullan" + }, + "hi": "हिन्दी (Hintçe)", + "fr": "Français (Fransızca)", + "pt": "Português (Portekizce)", + "de": "Deutsch (Almanca)", + "ja": "日本語 (Japonca)", + "tr": "Türkçe (Türkçe)", + "it": "Italiano (İtalyanca)", + "nl": "Nederlands (Felemenkçe)", + "sv": "Svenska (İsveççe)", + "cs": "Čeština (Çekçe)", + "nb": "Norsk Bokmål (Bokmål Norveç Dili)", + "ko": "한국어 (Korece)", + "vi": "Tiếng Việt (Vietnamca)", + "pl": "Polski (Lehçe)", + "uk": "Українська (Ukraynaca)", + "he": "עברית (İbranice)", + "el": "Ελληνικά (Yunanca)", + "ro": "Română (Rumence)", + "hu": "Magyar (Macarca)", + "fi": "Suomi (Fince)", + "da": "Dansk (Danimarka Dili)", + "sk": "Slovenčina (Slovakça)", + "fa": "فارسی (Farsça)", + "es": "Español (İspanyolca)", + "ar": "العربية (Arapça)", + "ru": "Русский (Rusça)", + "yue": "粵語 (Kantonca)", + "th": "ไทย (Tayca)", + "ca": "Català (Katalanca)", + "ptBR": "Português brasileiro (Brezilya Portekizcesi)", + "sr": "Српски (Sırpça)", + "sl": "Slovenščina (Slovence)", + "lt": "Lietuvių (Litvanyaca)", + "bg": "Български (Bulgarca)", + "gl": "Galego (Galiçyaca)", + "id": "Bahasa Indonesia (Endonezce)", + "ur": "اردو (Urduca)" + }, + "withSystem": "Sistem", + "theme": { + "label": "Tema", + "blue": "Mavi", + "contrast": "Yüksek Karşıtlık", + "green": "Yeşil", + "red": "Kırmızı", + "default": "Varsayılan", + "nord": "Kuzey", + "highcontrast": "Yüksek Karşıtlık" + }, + "restart": "Frigate'i yeniden başlat", + "live": { + "title": "Canlı", + "allCameras": "Tüm Kameralar", + "cameras": { + "title": "Kameralar", + "count_one": "{{count}} Kamera", + "count_other": "{{count}} Kamera" + } + }, + "review": "İncele", + "explore": "Keşfet", + "system": "Sistem", + "documentation": { + "title": "Dökümantasyon", + "label": "Frigate dökümantasyonu" + }, + "settings": "Ayarlar", + "appearance": "Görünüm", + "darkMode": { + "label": "Karanlık Mod", + "withSystem": { + "label": "Karanlık tema için sistem tercihini kullan" + }, + "light": "Açık", + "dark": "Koyu" + }, + "export": "Dışa Aktar", + "configurationEditor": "Yapılandırma düzenleyicisi", + "help": "Yardım", + "faceLibrary": "Yüz Veritabanı", + "systemMetrics": "Sistem metrikleri", + "uiPlayground": "UI Deneme Alanı", + "classification": "Sınıflandırma" + }, + "label": { + "back": "Geri", + "hide": "{{item}} öğesini gizle", + "show": "{{item}} öğesini göster", + "ID": "ID", + "none": "Yok", + "all": "Tümü" + }, + "notFound": { + "documentTitle": "Bulunamadı - Frigate", + "desc": "Sayfa bulunamadı", + "title": "404" + }, + "unit": { + "speed": { + "mph": "mph", + "kph": "km/s" + }, + "length": { + "feet": "feet", + "meters": "metre" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/saat", + "mbph": "MB/saat", + "gbph": "GB/saat" + } + }, + "pagination": { + "next": { + "title": "Sonraki", + "label": "Sonraki sayfaya git" + }, + "previous": { + "label": "Önceki sayfaya git", + "title": "Önceki" + }, + "label": "sayfalandırma", + "more": "Daha fazla" + }, + "accessDenied": { + "title": "Erişim Reddedildi", + "desc": "Bu sayfayı görüntüleme yetkiniz yok.", + "documentTitle": "Erişim Reddedildi - Frigate" + }, + "toast": { + "copyUrlToClipboard": "URL panoya kopyalandı.", + "save": { + "title": "Kaydet", + "error": { + "noMessage": "Yapılandırma değişiklikleri kaydedilemedi", + "title": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + } + } + }, + "selectItem": "{{item}} seçin", + "role": { + "title": "Rol", + "viewer": "Görüntüleyici", + "admin": "Yönetici", + "desc": "Yöneticiler Frigate arayüzündeki bütün özelliklere tam erişim sahibidir. Görüntüleyiciler ise yalnızca kameraları, eski görüntüleri ve inceleme öğelerini görüntülemekle sınırlıdır." + }, + "readTheDocumentation": "Dökümantasyonu oku", + "list": { + "two": "{{0}} ve {{1}}", + "many": "{{items}} ve {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "İsteğe bağlı", + "internalID": "Frigate’ın yapılandırma ve veritabanında kullandığı Dahili Kimlik" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/auth.json new file mode 100644 index 0000000..5d99dcd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "password": "Parola", + "login": "Oturum Aç", + "errors": { + "webUnknownError": "Bilinmeyen hata. Konsol günlüklerini kontrol edin.", + "usernameRequired": "Kullanıcı adı gereklidir", + "loginFailed": "Oturum açma başarısız", + "passwordRequired": "Parola gereklidir", + "rateLimit": "İstek sınırı aşıldı. Daha sonra tekrar deneyin.", + "unknownError": "Bilinmeyen hata. Günlükleri kontrol edin." + }, + "user": "Kullanıcı Adı", + "firstTimeLogin": "İlk kez giriş yapmayı mı deniyorsunuz? Giriş bilgileri Frigate loglarında görüntülenir." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/camera.json new file mode 100644 index 0000000..7885c26 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "name": { + "placeholder": "Bir isim girin…", + "label": "İsim", + "errorMessage": { + "nameMustNotPeriod": "Kamera grubu ismi nokta içeremez.", + "invalid": "Geçersiz kamera grubu ismi.", + "mustLeastCharacters": "Kamera grubu ismi en az iki karakterden oluşmalıdır.", + "exists": "Bu isimle bir kamera grubu zaten var." + } + }, + "success": "{{name}} adlı kamera grubu kaydedildi.", + "camera": { + "setting": { + "title": "{{cameraName}} Yayın Ayarları", + "audio": { + "tips": { + "document": "Dökümantasyonu oku ", + "title": "Bu yayın için kameranızın yayın çıkışında ses olması ve go2rtc'de ayarlanmış olması gerekmektedir." + } + }, + "label": "Kamera Yayın Ayarları", + "desc": "Bu kamera grubunun kontrol paneli için canlı yayın seçeneklerini değiştirin. Bu ayarlar cihaz/tarayıcı özelindedir.", + "audioIsUnavailable": "Bu yayında ses yok", + "audioIsAvailable": "Bu yayında ses kullanılabilir", + "streamMethod": { + "label": "Yayın Yöntemi", + "method": { + "noStreaming": { + "label": "Yayın Yok", + "desc": "Kamera görüntüsü dakikda bir güncellenecektir ve canlı yayın yapılmayacaktır." + }, + "continuousStreaming": { + "desc": { + "title": "Kamera görüntüsü panelde görüldüğü sürece, kamerada aktivite olmasa bile, canlı yayın şeklinde olacaktır.", + "warning": "Sürekli yayın yüksek internet kullanımına ve performans sorunlarına yol açabilir. Dikkatli kullanın." + }, + "label": "Sürekli Yayın" + }, + "smartStreaming": { + "label": "Akıllı yayın (önerilir)", + "desc": "Akıllı yayın özelliği, internet ve diğer kaynaklardan tasarruf için aktivite yokken yayının yerine dakikada bir güncellenen sabit resim gösterir. Kamerada aktivite tespit edildiğinde görüntü sabit resimden canlı yayına geçer." + } + }, + "placeholder": "Bir yayın metodu seçin" + }, + "compatibilityMode": { + "label": "Uyumluluk modu", + "desc": "Bu özelliği sadece kamera akışında renkli mozaiklenme yahut resmin sağ tarafında çizgi görüyorsanız etkinleştirin." + }, + "placeholder": "Bir yayın seçin", + "stream": "Yayın" + }, + "birdseye": "Kuş Bakışı" + }, + "icon": "Simge", + "add": "Kamera Grubu Ekle", + "label": "Kamera Grupları", + "delete": { + "confirm": { + "desc": "{{name}} isimli kamera grubunu silmek istediğinizden emin misiniz?", + "title": "Silmeyi Onayla" + }, + "label": "Kamera Grubunu Sil" + }, + "edit": "Kamera Grubunu Düzenle", + "cameras": { + "desc": "Bu gruba dahil olacak kameraları seçin.", + "label": "Kameralar" + } + }, + "debug": { + "options": { + "label": "Ayarlar", + "title": "Seçenekler", + "hideOptions": "Seçenekleri Gizle", + "showOptions": "Seçenekleri Göster" + }, + "boundingBox": "Çerçeve", + "timestamp": "Zaman Damgası", + "zones": "Alanlar", + "mask": "Maske", + "motion": "Hareket", + "regions": "Tespit Bölgeleri" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/dialog.json new file mode 100644 index 0000000..35fe451 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/dialog.json @@ -0,0 +1,133 @@ +{ + "restart": { + "title": "Frigate'i yeniden başlatmak istediğinize emin misiniz?", + "button": "Yeniden Başlat", + "restarting": { + "content": "Bu sayfa {{countdown}} saniye sonra yeniden yüklenecektir.", + "title": "Frigate Yeniden Başlatılıyor", + "button": "Şimdi Yeniden Yükle" + } + }, + "explore": { + "plus": { + "review": { + "state": { + "submitted": "Gönderildi" + }, + "true": { + "true_one": "Bu bir {{label}}", + "true_other": "Bu bir {{label}}", + "label": "Frigate+ için bu etiketi onaylayın" + }, + "false": { + "label": "Bu etiketi Frigate+ için onaylamaktan vazgeç", + "false_one": "Bu bir {{label}} değil", + "false_other": "Bu bir {{label}} değil" + }, + "question": { + "ask_an": "Bu nesne bir {{label}} nesnesi mi?", + "label": "Bu etiketi Frigate+ için onaylayın", + "ask_a": "Bu nesne bir {{label}} mi?", + "ask_full": "Bu nesne bir {{untranslatedLabel}} ({{translatedLabel}}) nesnesi mi?" + } + }, + "submitToPlus": { + "label": "Frigate+'ya Gönder", + "desc": "Görülmesini istemediğiniz yerlerdeki nesneler yanlış pozitif değildir. Bunları yanlış pozitif olarak göndermek modeli yanıltacaktır." + } + }, + "video": { + "viewInHistory": "Geçmiş Görünümünde Görüntüle" + } + }, + "export": { + "time": { + "end": { + "label": "Bitiş Zamanını Seç", + "title": "Bitiş Zamanı" + }, + "lastHour_one": "Son 1 Saat", + "lastHour_other": "Son {{count}} Saat", + "start": { + "title": "Başlangıç Zamanı", + "label": "Başlangıç Zamanını Seç" + }, + "fromTimeline": "Zaman Şeridinde Seç", + "custom": "Özel" + }, + "select": "Seç", + "export": "Dışa Aktar", + "selectOrExport": "Seç veya Dışa Aktar", + "toast": { + "success": "Dışa aktarma başarıyla başlatıldı. Dosyayı dışa aktarmalar sayfasında görüntüleyebilirsiniz.", + "error": { + "failed": "Dışa aktarım başlatılamadı: {{error}}", + "endTimeMustAfterStartTime": "Bitiş zamanı başlangıç zamanından sonra olmalıdır", + "noVaildTimeSelected": "Geçerli bir zaman aralığı seçilmedi" + }, + "view": "Görüntüle" + }, + "fromTimeline": { + "saveExport": "Dışa Aktarımı Kaydet", + "previewExport": "Dışa Aktarımı Önizle" + }, + "name": { + "placeholder": "Dışa Aktarımı Adlandırın" + } + }, + "streaming": { + "label": "Akış", + "restreaming": { + "disabled": "Bu kamera için Yeniden Akış devre dışı.", + "desc": { + "readTheDocumentation": "Dökümantasyonu oku", + "title": "Bu kameradan ek canlı gösterim seçenekleri ve sesli yayın almak için go2rtc'yi yapılandırın." + } + }, + "showStats": { + "label": "Akış istatistiklerini göster", + "desc": "Kamera akışının üzerinde akış istastistiklerini görmek için bu seçeneği aktifleştirin." + }, + "debugView": "Hata Ayıklama Görünümü" + }, + "search": { + "saveSearch": { + "desc": "Kayıtlı aramaya bir isim verin.", + "placeholder": "Aramanız için bir isim girin", + "overwrite": "{{searchName}} zaten var. Bu isimle kaydetmek mevcut olanın üzerine yazacaktır.", + "label": "Aramayı Kaydet", + "button": { + "save": { + "label": "Bu aramayı kaydet" + } + }, + "success": "Arama ({{searchName}}) kaydedildi." + } + }, + "recording": { + "confirmDelete": { + "title": "Silmeyi Onayla", + "desc": { + "selected": "Bu inceleme öğesiyle ilişkili tüm kaydedilmiş videoları silmek istediğinizden emin misiniz?

    Gelecekte bu diyaloğu pas geçmek için Shift tuşuna basılı tutarak tıklatın." + }, + "toast": { + "success": "Seçili incele öğelerinin ilişkili olduğu video kayıtları başarıyla silinmiştir.", + "error": "Siliemedi: {{error}}" + } + }, + "button": { + "export": "Dışa Aktar", + "markAsReviewed": "İncelendi olarak işaretle", + "deleteNow": "Şimdi Sil", + "markAsUnreviewed": "Gözden geçirilmedi olarak işaretle" + } + }, + "imagePicker": { + "selectImage": "Takip edilen nesnenin küçük resmini seçin", + "noImages": "Bu kamera için küçük resim bulunamadı", + "search": { + "placeholder": "Etiket/alt etiket kullanarak arama yapın..." + }, + "unknownLabel": "Kaydedilen Tetikleme Görseli" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/filter.json new file mode 100644 index 0000000..c71110b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/filter.json @@ -0,0 +1,137 @@ +{ + "labels": { + "label": "Etiketler", + "all": { + "title": "Bütün Etiketler", + "short": "Etiketler" + }, + "count": "{{count}} Etiket", + "count_one": "{{count}} Etiket", + "count_other": "{{count}} Etiket" + }, + "dates": { + "all": { + "title": "Tüm Tarihler", + "short": "Tarihler" + }, + "selectPreset": "Ön ayar seçin…" + }, + "sort": { + "label": "Sırala", + "dateAsc": "Tarih (Artan)", + "dateDesc": "Tarih (Azalan)", + "scoreAsc": "Nesne Skoru (Artan)", + "scoreDesc": "Nesne Skoru (Azalan)", + "speedAsc": "Tahmini Hız (Artan)", + "relevance": "Alaka", + "speedDesc": "Tahmini Hız (Azalan)" + }, + "filter": "Filtre", + "zones": { + "all": { + "short": "Alanlar", + "title": "Bütün Alanlar" + }, + "label": "Alanlar" + }, + "reset": { + "label": "Filtreleri varsayılanlara sıfırla" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Öncelikle izlenen nesneler içinde fotoğrafı olanlar için filtre uygulamalısınız.

    Fotoğrafı olmayan nesneler Frigate+’a gönderilemez.", + "label": "Frigate+'a Gönderildi" + }, + "hasVideoClip": "Video klibi var", + "hasSnapshot": "Fotoğrafı var", + "label": "Özellikler" + }, + "score": "Skor", + "estimatedSpeed": "Tahmini Hız ({{unit}})", + "timeRange": "Zaman Aralığı", + "subLabels": { + "all": "Tüm Alt Etiketler", + "label": "Alt Etiketler" + }, + "more": "Daha Fazla Filtre", + "cameras": { + "all": { + "short": "Kameralar", + "title": "Tüm Kameralar" + }, + "label": "Kameraları Filtrele" + }, + "review": { + "showReviewed": "İncelenenleri de Göster" + }, + "explore": { + "settings": { + "defaultView": { + "summary": "Özet", + "title": "Varsayılan Görünüm", + "unfilteredGrid": "Filtresiz Izgara", + "desc": "Filtre seçilmediğinde, her etiket için en son izlenen nesnelerin özeti ya da filtresiz ızgara görünümü gösterilir." + }, + "gridColumns": { + "title": "Izgara Sütun Sayısı", + "desc": "Izgara görünümüde gösterilecek sütun sayısı." + }, + "title": "Ayarlar", + "searchSource": { + "options": { + "thumbnailImage": "Küçük Resim", + "description": "Metin Açıklaması" + }, + "label": "Arama kaynağı", + "desc": "Aramanızda küçük resimleri mi yoksa açıklamaları mı kullanacağınızı seçin." + } + }, + "date": { + "selectDateBy": { + "label": "Filtrelemek için tarih seçin" + } + } + }, + "logSettings": { + "filterBySeverity": "Önceliğe göre günlükleri filtrele", + "disableLogStreaming": "Günlük akışını devre dışı bırak", + "allLogs": "Tüm günlükler", + "loading": { + "title": "Günlük Akışı", + "desc": "Günlükler sayfası en aşağıya kaydırıldığında yeni günlük satırları geldikçe aşağıya eklenir." + }, + "label": "Düzeye göre günlükleri filtrele" + }, + "trackedObjectDelete": { + "toast": { + "success": "Takip edilen nesneler başarıyla silindi.", + "error": "Takip edilen nesneler silinemedi: {{errorMessage}}" + }, + "title": "Silmeyi onayla", + "desc": "Bu {{objectLength}} adet izlenen nesneyi sildiğinizde ilgili tüm fotoğraflar, kaydedilmiş tüm gömüler ve ilişkili tüm nesne yaşam döngüsü kayıtları kaldırılır. Bu izlenen nesnelere ait Geçmiş görünümündeki kayıtlı görüntüler SİLİNMEYECEKTİR.

    Devam etmek istediğinize emin misiniz?

    Gelecekte bu diyaloğu pas geçmek için Shift tuşuna basılı tutarak tıklayın." + }, + "recognizedLicensePlates": { + "selectPlatesFromList": "Listeden bir veya birden fazla plaka seçin.", + "placeholder": "Plaka ara…", + "loading": "Tanınan plakalar yükleniyor…", + "title": "Tanınan Plakalar", + "noLicensePlatesFound": "Plaka bulunamadı.", + "loadFailed": "Tanınan plakalar yüklenemedi.", + "selectAll": "Tümünü seç", + "clearAll": "Tümünü temizle" + }, + "motion": { + "showMotionOnly": "Yalnızca Hareket Olanları Göster" + }, + "zoneMask": { + "filterBy": "Alana göre filtrele" + }, + "classes": { + "count_one": "{{count}} Sınıf", + "count_other": "{{count}} Sınıf", + "label": "Sınıflar", + "all": { + "title": "Tüm Sınıflar" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/icons.json new file mode 100644 index 0000000..30c0e1d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "search": { + "placeholder": "Bir simge arayın…" + }, + "selectIcon": "Bir simge belirleyin" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/input.json new file mode 100644 index 0000000..bbaa987 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Videoyu İndir", + "toast": { + "success": "İncele öğesinin videosu indirilmeye başlandı." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/tr/components/player.json new file mode 100644 index 0000000..6a79503 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/components/player.json @@ -0,0 +1,51 @@ +{ + "streamOffline": { + "title": "Yayın Çevrimdışı", + "desc": "{{cameraName}} isimli kameranın tespit akışından hiç bir görüntü alınamadı, hata günlüklerini kontrol edin" + }, + "stats": { + "streamType": { + "title": "Yayın Türü:", + "short": "Tür" + }, + "latency": { + "value": "{{seconds}} saniye", + "short": { + "title": "Gecikme", + "value": "{{seconds}} sn" + }, + "title": "Gecikme:" + }, + "bandwidth": { + "title": "Bant genişliği:", + "short": "Bant genişliği" + }, + "decodedFrames": "Çözülen kareler:", + "droppedFrameRate": "Atlanan Kare Oranı:", + "totalFrames": "Toplam Kareler:", + "droppedFrames": { + "short": { + "title": "Atlanan", + "value": "{{droppedFrames}} kare" + }, + "title": "Atlanan Kareler:" + } + }, + "noPreviewFound": "Önizleme Bulunamadı", + "toast": { + "success": { + "submittedFrigatePlus": "Kare başarıyla Frigate+'a gönderildi" + }, + "error": { + "submitFrigatePlusFailed": "Kare Frigate+'a gönderilemedi" + } + }, + "noRecordingsFoundForThisTime": "Bu zaman aralığı için kayıt bulunamadı", + "cameraDisabled": "Kamera devre dışı bırakıldı", + "noPreviewFoundFor": "{{cameraName}} için Önizleme Bulunamadı", + "livePlayerRequiredIOSVersion": "Bu canlı yayın türü için iOS 17.1 veya daha yeni sürüm gereklidir.", + "submitFrigatePlus": { + "title": "Bu kare Frigate+'ya gönderilsin mi?", + "submit": "Gönder" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/objects.json b/sam2-cpu/frigate-dev/web/public/locales/tr/objects.json new file mode 100644 index 0000000..fb07a09 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "köpek", + "cat": "kedi", + "animal": "hayvan", + "bark": "havlama", + "blender": "mikser", + "mouse": "fare", + "door": "kapı", + "sink": "lavabo", + "boat": "bot", + "skateboard": "kaykay", + "sheep": "koyun", + "bicycle": "bisiklet", + "motorcycle": "motosiklet", + "bird": "kuş", + "car": "araba", + "bus": "otobüs", + "goat": "keçi", + "hair_dryer": "saç kurutma makinesi", + "clock": "saat", + "scissors": "makas", + "vehicle": "araç", + "keyboard": "klavye", + "toothbrush": "diş fırçası", + "horse": "at", + "train": "tren", + "handbag": "El Çantası", + "umbrella": "Şemsiye", + "shoe": "Ayakkabı", + "eye_glasses": "Gözlük", + "desk": "Masa", + "remote": "Uzaktan Kumanda", + "refrigerator": "Buzdolabı", + "book": "Kitap", + "hair_brush": "Saç Fırçası", + "squirrel": "Sincap", + "waste_bin": "Çöp Kutusu", + "face": "Yüz", + "license_plate": "Araç Plakası", + "an_post": "An Post", + "sandwich": "Sandviç", + "couch": "Koltuk", + "baseball_glove": "Beyzbol Eldiveni", + "donut": "Donut", + "bed": "Yatak", + "backpack": "Sırt Çantası", + "parking_meter": "Parkmetre", + "stop_sign": "Dur Tabelası", + "person": "İnsan", + "bear": "Ayı", + "hat": "Şapka", + "orange": "Portakal", + "dining_table": "Yemek Masası", + "traffic_light": "Trafik Lambası", + "giraffe": "Zürafa", + "fire_hydrant": "Yangın Musluğu", + "street_sign": "Sokak Tabelası", + "mirror": "Ayna", + "banana": "Muz", + "carrot": "Havuç", + "pizza": "Pizza", + "vase": "Vazo", + "nzpost": "NZPost", + "bench": "Bank", + "elephant": "Fil", + "spoon": "Kaşık", + "laptop": "Dizüstü Bilgisayar", + "frisbee": "Frizbi", + "skis": "Kayak", + "kite": "Uçurtma", + "bottle": "Şişe", + "cup": "Fincan", + "knife": "Bıçak", + "bowl": "Kase", + "package": "Paket", + "airplane": "Uçak", + "snowboard": "Kar Kayağı", + "usps": "USPS", + "ups": "UPS", + "cow": "İnek", + "zebra": "Zebra", + "suitcase": "Bavul", + "sports_ball": "Spor Topu", + "baseball_bat": "Beyzbol Sopası", + "surfboard": "Sörf Tahtası", + "plate": "Plaka", + "broccoli": "Brokoli", + "tv": "Televizyon", + "cell_phone": "Cep Telefonu", + "teddy_bear": "Ayıcık", + "deer": "Geyik", + "fox": "Tilki", + "purolator": "Purolator", + "fork": "Çatal", + "toilet": "Tuvalet", + "window": "Pencere", + "microwave": "Mikrodalga Fırın", + "hot_dog": "Sosisli Sandviç", + "wine_glass": "Şarap Bardağı", + "cake": "Kek", + "potted_plant": "Saksı Bitkisi", + "bbq_grill": "Izgara", + "tennis_racket": "Tenis Raketi", + "dhl": "DHL", + "raccoon": "Rakun", + "robot_lawnmower": "Robot Çim Biçme Makinesi", + "toaster": "Tost Makinesi", + "apple": "Elma", + "amazon": "Amazon", + "rabbit": "Tavşan", + "chair": "Sandalye", + "postnl": "PostNL", + "oven": "Fırın", + "fedex": "FedEx", + "on_demand": "İstenildiğinde", + "tie": "Kravat", + "dpd": "DPD", + "gls": "GLS", + "postnord": "PostNord" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/classificationModel.json new file mode 100644 index 0000000..535e5b1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/classificationModel.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "Sınıflandırma Modelleri - Frigate", + "details": { + "scoreInfo": "Skor, modelin nesneyi tespit ettiği tüm durumlar için ortalama güven düzeyini gösterir." + }, + "button": { + "deleteClassificationAttempts": "Sınıflandırma Fotoğraflarını Sil", + "renameCategory": "Sınıfı Yeniden Adlandır", + "deleteCategory": "Sınıfı Sil", + "deleteImages": "Fotoğrafları Sil", + "trainModel": "Modeli Eğit", + "addClassification": "Sınıflandırma Ekle", + "deleteModels": "Modelleri Sil", + "editModel": "Modeli Düzenle" + }, + "toast": { + "success": { + "deletedCategory": "Silinmiş Sınıf", + "deletedImage": "Silinmiş Fotoğraflar", + "deletedModel_one": "{{count}} model başarıyla silindi", + "deletedModel_other": "{{count}} model başarıyla silindi", + "categorizedImage": "Fotoğraf Başarıyla Sınıflandırıldı", + "trainedModel": "Model başarıyla eğitildi.", + "trainingModel": "Model eğitimi başarıyla başladı.", + "updatedModel": "Model yapılandırması başarıyla güncellendi", + "renamedCategory": "Sınıf başarıyla {{name}} olarak yeniden adlandırıldı" + }, + "error": { + "deleteImageFailed": "Silinemedi: {{errorMessage}}", + "deleteModelFailed": "Model silinirken hata oluştu: {{errorMessage}}", + "categorizeFailed": "Görsel sınıflandırılamadı: {{errorMessage}}", + "trainingFailed": "Model eğitimi başarısız oldu. Ayrıntılar için Frigate günlüklerini kontrol edin.", + "deleteCategoryFailed": "Sınıf silinemedi: {{errorMessage}}", + "trainingFailedToStart": "Model eğitimi başlatılamadı: {{errorMessage}}", + "updateModelFailed": "Model güncellenemedi: {{errorMessage}}", + "renameCategoryFailed": "Sınıf yeniden adlandırılamadı: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Sınıfı Sil", + "desc": "{{name}} adlı sınıfı silmek istediğinizden emin misiniz? Bu işlem, sınıfa ait tüm görselleri kalıcı olarak silecek ve modelin yeniden eğitilmesini gerektirecektir.", + "minClassesTitle": "Sınıf Silinemiyor", + "minClassesDesc": "Bu sınıfı silmeden önce bir sınıflandırma modelinin en az 2 sınıfa sahip olması gerekir. Bu sınıfı silmeden önce başka bir sınıf ekleyin." + }, + "deleteModel": { + "title": "Sınıflandırma Modelini Sil", + "single": "{{name}} öğesini silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz.", + "desc_one": "{{count}} modeli silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz.", + "desc_other": "{{count}} modeli silmek istediğinizden emin misiniz? Bu işlem, görseller ve eğitim verileri dâhil olmak üzere tüm ilişkili verileri kalıcı olarak silecektir. Bu işlem geri alınamaz." + }, + "deleteDatasetImages": { + "title": "Eğitim verisi görsellerini sil", + "desc_one": "{{dataset}} veri kümesinden {{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve modelin yeniden eğitilmesini gerektirir.", + "desc_other": "{{dataset}} veri kümesinden {{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve modelin yeniden eğitilmesini gerektirir." + }, + "deleteTrainImages": { + "title": "Eğitim Görsellerini Sil", + "desc_one": "{{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "desc_other": "{{count}} görseli silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + }, + "renameCategory": { + "title": "Sınıfı Yeniden Adlandır", + "desc": "{{name}} için yeni bir ad girin. Ad değişikliğinin etkili olabilmesi için modeli yeniden eğitmeniz gerekecektir." + }, + "description": { + "invalidName": "Geçersiz ad. Ad yalnızca harfler, rakamlar, boşluklar, kesme işaretleri (’), alt çizgiler (_) ve tireler (-) içerebilir." + }, + "train": { + "title": "Son Sınıflandırmalar", + "titleShort": "Son", + "aria": "Son Sınıflandırmaları Seç" + }, + "categories": "Sınıflar", + "createCategory": { + "new": "Yeni Sınıf Oluştur" + }, + "categorizeImageAs": "Görseli Şu Şekilde Sınıflandır:", + "categorizeImage": "Görseli Sınıflandır", + "menu": { + "objects": "Nesneler", + "states": "Durumlar" + }, + "noModels": { + "object": { + "title": "Nesne sınıflandırma modeli mevcut değil", + "description": "Algılanan nesneleri sınıflandırmak için özel bir model oluşturun.", + "buttonText": "Nesne Modeli Oluştur" + }, + "state": { + "title": "Durum Sınıflandırma Modeli Yok", + "description": "Belirli kamera alanlarındaki durum değişimlerini izlemek ve sınıflandırmak için özel bir model oluşturun.", + "buttonText": "Durum Modeli Oluştur" + } + }, + "tooltip": { + "trainingInProgress": "Model şu anda eğitiliyor", + "noNewImages": "Eğitilecek yeni görsel bulunmuyor. Önce veri kümesinde daha fazla görseli sınıflandırın.", + "noChanges": "Son eğitimden bu yana veri kümesinde herhangi bir değişiklik yapılmadı.", + "modelNotReady": "Model eğitim için hazır değil" + }, + "edit": { + "title": "Sınıflandırma Modelini Düzenle", + "descriptionState": "Bu durum sınıflandırma modeli için sınıfları düzenleyin. Değişiklikler, modelin yeniden eğitilmesini gerektirecektir.", + "descriptionObject": "Bu nesne sınıflandırma modeli için nesne türünü ve sınıflandırma türünü düzenleyin.", + "stateClassesInfo": "Not: Durum sınıflarını değiştirmek, modelin güncellenmiş sınıflarla yeniden eğitilmesini gerektirir." + }, + "wizard": { + "title": "Yeni Sınıflandırma Oluştur", + "steps": { + "nameAndDefine": "Adlandır ve Tanımla", + "stateArea": "Durum Alanı", + "chooseExamples": "Örnekleri Seç" + }, + "step1": { + "description": "State modelleri, sabit kamera alanlarındaki değişiklikleri (ör. kapının açılması/kapanması) izler. Nesne modelleri ise algılanan nesnelere ek sınıflandırmalar ekler (ör. bilinen hayvanlar, kuryeler vb.).", + "name": "Ad", + "namePlaceholder": "Model adını girin...", + "type": "Tür", + "typeState": "Durum", + "typeObject": "Nesne", + "objectLabel": "Nesne Etiketi", + "objectLabelPlaceholder": "Nesne türünü seçin...", + "classificationType": "Sınıflandırma Türü", + "classificationTypeTip": "Sınıflandırma türleri hakkında bilgi edinin", + "classificationTypeDesc": "Alt etiketleri, nesne etiketine ek metin ekler (örneğin: “Person: UPS”). Öznitelikler (attributes) ise nesne meta verilerinde ayrı olarak saklanan ve aranabilir metadata bilgileridir.", + "classificationSubLabel": "Alt Etiket", + "classificationAttribute": "Özellik", + "classes": "Sınıflar", + "states": "Durumlar", + "classesTip": "Sınıflar hakkında bilgi edinin", + "classesStateDesc": "Kamera alanınızın içinde bulunabileceği farklı durumları tanımlayın. Örneğin: bir garaj kapısı için ‘açık’ ve ‘kapalı’.", + "classesObjectDesc": "Algılanan nesneleri sınıflandırmak için farklı kategorileri tanımlayın. Örneğin: Bir kişi sınıflandırması için ‘teslimat_görevlisi’, ‘sakin’, ‘yabancı’.", + "classPlaceholder": "Sınıf adını girin...", + "errors": { + "nameRequired": "Model adı gerekli", + "nameLength": "Model adı 64 karakter veya daha az olmalıdır", + "nameOnlyNumbers": "Model adı yalnızca rakamlardan oluşamaz", + "classRequired": "En az 1 sınıf gereklidir", + "classesUnique": "Sınıf adları benzersiz olmalıdır", + "stateRequiresTwoClasses": "Durum modelleri en az 2 sınıf gerektirir", + "objectLabelRequired": "Lütfen bir nesne etiketi seçin", + "objectTypeRequired": "Lütfen bir sınıflandırma türü seçin" + } + }, + "step2": { + "description": "İzlenecek alanı her kamera için seçin ve tanımlayın. Model bu alanların durumunu sınıflandıracaktır.", + "cameras": "Kameralar", + "selectCamera": "Kamera Seç", + "noCameras": "Kameraları eklemek için + simgesine tıklayın", + "selectCameraPrompt": "Listedeki bir kamerayı seçerek izlenecek alanı tanımlayın" + }, + "step3": { + "selectImagesPrompt": "{{className}} etiketli tüm görselleri seç", + "selectImagesDescription": "Görselleri seçmek için üzerlerine tıklayın. Bu sınıfla işiniz bittiğinde Devam Et’e tıklayın.", + "allImagesRequired_one": "Lütfen tüm görselleri sınıflandırın. {{count}} görsel kaldı.", + "allImagesRequired_other": "Lütfen tüm görselleri sınıflandırın. {{count}} görsel kaldı.", + "generating": { + "title": "Örnek Görseller Oluşturuluyor", + "description": "Frigate kayıtlarınızdan temsilî görüntüler çekiliyor. Bu işlem biraz zaman alabilir…" + }, + "training": { + "title": "Model Eğitiliyor", + "description": "Modeliniz arka planda eğitiliyor. Bu pencereyi kapatabilirsiniz; eğitim tamamlandığında model otomatik olarak çalışmaya başlayacaktır." + }, + "retryGenerate": "Oluşturmayı Yeniden Dene", + "noImages": "Örnek görsel oluşturulamadı", + "classifying": "Sınıflandırılıyor ve Eğitiliyor...", + "trainingStarted": "Eğitim başarıyla başlatıldı", + "modelCreated": "Model başarıyla oluşturuldu. Eksik durumlar için görseller eklemek üzere Son Sınıflandırmalar görünümünü kullanın ve ardından modeli eğitin.", + "errors": { + "noCameras": "Hiç kamera yapılandırılmadı", + "noObjectLabel": "Nesne etiketi seçilmedi", + "generateFailed": "Örnekler oluşturulamadı: {{error}}", + "generationFailed": "Oluşturma başarısız oldu. Lütfen tekrar deneyin.", + "classifyFailed": "Görseller sınıflandırılamadı: {{error}}" + }, + "generateSuccess": "Örnek görseller başarıyla oluşturuldu", + "missingStatesWarning": { + "title": "Eksik Durum Örnekleri", + "description": "En iyi sonuçlar için tavsiye edilir: Tüm durumlar (state) için örnekler seçin. Tüm durumlar için örnek seçmeden devam edebilirsiniz, ancak model, tüm durumlara ait görüntüler eklenene kadar eğitilmeyecektir. Devam ettikten sonra, eksik durumlar için görüntüleri sınıflandırmak ve ardından modeli eğitmek için Son Sınıflandırmalar (Recent Classifications) görünümünü kullanın." + } + } + }, + "none": "Yok" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/configEditor.json new file mode 100644 index 0000000..32ffdb2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "saveOnly": "Sadece Kaydet", + "toast": { + "error": { + "savingError": "Yapılandırma kaydedilirken hata" + }, + "success": { + "copyToClipboard": "Yapılandırma panoya kopyalandı." + } + }, + "copyConfig": "Yapılandırmayı Kopyala", + "configEditor": "Yapılandırma Düzenleyicisi", + "documentTitle": "Yapılandırma Düzenleyicisi - Frigate", + "saveAndRestart": "Kaydet & Yeniden Başlat", + "confirm": "Kaydetmeden çıkılsın mı?", + "safeConfigEditor": "Yapılandırma Düzenleyicisi (Güvenli Mod)", + "safeModeDescription": "Frigate, yapılandırmanızdaki bir hata nedeniyle güvenli moda geçti." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/events.json new file mode 100644 index 0000000..e15c11b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/events.json @@ -0,0 +1,63 @@ +{ + "camera": "kamera", + "alerts": "Alarmlar", + "detections": "Tespitler", + "empty": { + "detection": "İncelenecek tespit öğesi yok", + "alert": "İncelenecek alarm öğesi yok", + "motion": "Hareket verisi bulunamadı" + }, + "timeline": "Zaman şeridi", + "events": { + "aria": "Olayları seçin", + "noFoundForTimePeriod": "Seçili zaman aralığında olay bulunamadı.", + "label": "Olaylar" + }, + "recordings": { + "documentTitle": "Kayıtlar - Frigate" + }, + "calendarFilter": { + "last24Hours": "Son 24 Saat" + }, + "markAsReviewed": "İncelendi Olarak İşaretle", + "newReviewItems": { + "button": "Yeni İncelenecek Öğeler Var", + "label": "Yeni inceleme öğelerini göster" + }, + "documentTitle": "İncele - Frigate", + "motion": { + "label": "Hareket", + "only": "Yalnızca hareket" + }, + "timeline.aria": "Zaman şeridi seçin", + "markTheseItemsAsReviewed": "Bunları incelendi olarak işaretle", + "allCameras": "Tüm Kameralar", + "selected_one": "{{count}} seçildi", + "selected_other": "{{count}} seçildi", + "detected": "algılandı", + "suspiciousActivity": "Şüpheli Etkinlik", + "threateningActivity": "Tehlikeli Etkinlik", + "zoomIn": "Büyüt", + "zoomOut": "Küçült", + "detail": { + "label": "Detay", + "aria": "Ayrıntı görünümünü aç/kapat", + "trackedObject_one": "{{count}} nesne", + "trackedObject_other": "{{count}} nesne", + "noObjectDetailData": "Nesneye ait ayrıntılı veri bulunmuyor.", + "settings": "Ayrıntılı Görünüm Ayarları", + "alwaysExpandActive": { + "title": "Etkin olanı her zaman genişlet", + "desc": "Varsa, etkin inceleme öğesinin nesne ayrıntılarını daima göster." + }, + "noDataFound": "İncelenecek ayrıntılı veri bulunmuyor" + }, + "objectTrack": { + "trackedPoint": "Takip edilen nokta", + "clickToSeek": "Bu zamana gitmek için tıklayın" + }, + "normalActivity": "Normal", + "needsReview": "İnceleme Gerekiyor", + "securityConcern": "Güvenlik endişesi", + "select_all": "Tümü" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/explore.json new file mode 100644 index 0000000..6909c84 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/explore.json @@ -0,0 +1,293 @@ +{ + "documentTitle": "Keşfet - Frigate", + "details": { + "timestamp": "Zaman Damgası", + "item": { + "title": "Nesne Detaylarını İncele", + "desc": "Nesne detaylarıın incele", + "button": { + "share": "Bu incele öğesini paylaş", + "viewInExplore": "Keşfet'te Görüntüle" + }, + "tips": { + "hasMissingObjects": "Eğer Frigate'in {{objects}} etiketine sahip nesneleri kaydetmesini istiyorsanız yapılandırmanızı buna göre ayarlayın", + "mismatch_one": "Tespit edilmiş olan bir nesne bu İncele öğesine dahil edildi. Bu nesne Alarm veya Tespit olarak derecelendirilemedi veya çoktan silindi/temizlendi.", + "mismatch_other": "Tespit edilmiş olan {{count}} adet nesne bu İncele öğesine dahil edildi. Bu nesneler Alarm veya Tespit olarak derecelendirilemedi veya çoktan silindi/temizlendi." + }, + "toast": { + "success": { + "updatedSublabel": "Alt etiket başarıyla gücellendi.", + "regenerate": "Yeni bir açıklama {{provider}} sağlayıcısından talep edildi. Sağlayıcının hızına bağlı olarak yeni açıklamanın oluşturulması biraz zaman alabilir.", + "updatedLPR": "Plaka başarıyla güncellendi.", + "audioTranscription": "Ses dökümü başarıyla istendi. Frigate sunucunuzun hızına bağlı olarak döküm işlemi tamamlanması biraz zaman alabilir." + }, + "error": { + "updatedSublabelFailed": "Alt etiket güncellenemedi: {{errorMessage}}", + "regenerate": "{{provider}} sağlayıcısından yeni açıklama talep edilemedi: {{errorMessage}}", + "updatedLPRFailed": "Plaka güncellenemedi: {{errorMessage}}", + "audioTranscription": "Ses çözümlemesi talep edilemedi: {{errorMessage}}" + } + } + }, + "label": "Etiket", + "editSubLabel": { + "desc": "Bu {{label}} için yeni bir alt etiket girin", + "descNoLabel": "Bu takip edilmiş nesne için yeni bir alt etiket girin", + "title": "Alt etiketi gücelle" + }, + "estimatedSpeed": "Tahmini Hız", + "camera": "Kamera", + "zones": "Alanlar", + "description": { + "label": "Açıklama", + "aiTips": "Frigate, Nesne Geçmişi tamamlanana kadar Üretken Yapay Zeka sağlayıcısından bir resim açıklaması talep etmeyecektir.", + "placeholder": "Takip edilen nesnenin açıklaması" + }, + "expandRegenerationMenu": "Yeniden Üret menüsünü genişlet", + "regenerateFromSnapshot": "Fotoğraftan Üret", + "regenerateFromThumbnails": "Küçük Resimden Üret", + "tips": { + "descriptionSaved": "Açıklama başarıyla kaydedildi", + "saveDescriptionFailed": "Açıklama güncellenemedi: {{errorMessage}}" + }, + "button": { + "regenerate": { + "label": "Nesne açıklaması yeniden üretildi", + "title": "Yeniden Üret" + }, + "findSimilar": "Benzerini Bul" + }, + "topScore": { + "info": "Tepe skor, bir takip edilen nesne için en yüksek ortalama puandır ve arama sonucundaki küçük resimde gösterilen puandan farklı olabilir.", + "label": "Tepe Skor" + }, + "objects": "Nesneler", + "editLPR": { + "title": "Plakayı düzenle", + "desc": "Bu {{label}} için yeni bir plaka değeri girin", + "descNoLabel": "Bu nesne için yeni bir plaka değeri girin" + }, + "recognizedLicensePlate": "Tanınan Plaka", + "snapshotScore": { + "label": "Fotoğraf Skoru" + }, + "score": { + "label": "Skor" + } + }, + "generativeAI": "Üretken Yapay Zeka", + "exploreIsUnavailable": { + "title": "Keşfet şu anda kullanılamıyor", + "embeddingsReindexing": { + "startingUp": "Başlatılıyor…", + "estimatedTime": "Tahmini kalan süre:", + "step": { + "thumbnailsEmbedded": "Gömü eklenen küçük resimler: ", + "descriptionsEmbedded": "Gömü eklenen açıklamalar: ", + "trackedObjectsProcessed": "İşlenen takip edilen nesneler: " + }, + "finishingShortly": "Birazdan tamamlanacak", + "context": "Keşfet sayfası nesnelerin gömülerinin yeniden dizinlemesi tamamlandığında kullanılabilecektir." + }, + "downloadingModels": { + "setup": { + "visionModel": "Görüş modeli", + "visionModelFeatureExtractor": "Görüş modeli özellik çıkarmcısı", + "textModel": "Metin modeli", + "textTokenizer": "Metin tokenizeri" + }, + "error": "Bir hata oluştu. Frigate günlüklerini kontrol edin.", + "tips": { + "documentation": "Dökümantasyonu oku", + "context": "Model indirildikten sonra takip edilmiş nesnelerinizin gömüleri tekrar dizinlemeyi tercih edebilirsiniz." + }, + "context": "Frigate, Anlamsal Arama özelliği için gerekli olan gömü modellerini indiriyor. Ağ bağlantınızın hızına göre bu işlem bir kaç dakika sürebilir." + } + }, + "trackedObjectDetails": "Takip Edilen Nesne Detayları", + "type": { + "details": "detaylar", + "object_lifecycle": "nesne yaşam döngüsü", + "snapshot": "fotoğraf", + "video": "video", + "thumbnail": "küçük resim", + "tracking_details": "izleme ayrıntıları" + }, + "objectLifecycle": { + "title": "Nesne Yaşam Döngüsü", + "noImageFound": "Bu zaman damgası için bir resim bulunamadı.", + "createObjectMask": "Nesne Maskesi Oluştur", + "adjustAnnotationSettings": "Belirteç ayarları", + "lifecycleItemDesc": { + "visible": "{{label}} tespit edildi", + "entered_zone": "{{label}} {{zones}} alanına girdi", + "active": "{{label}} inaktif oldu", + "heard": "{{label}} duyuldu", + "external": "{{label}} tespit edildi", + "stationary": "{{label}} sabit durdu", + "attribute": { + "other": "{{label}} {{attribute}} olarak tespit edildi", + "faceOrLicense_plate": "{{label}} için {{attribute}} tespit edildi" + }, + "gone": "{{label}} ayrıldı", + "header": { + "zones": "Alanlar", + "ratio": "Oran", + "area": "Alan" + } + }, + "annotationSettings": { + "offset": { + "label": "Belirteç telafisi", + "documentation": "Dökümantasyonu oku ", + "millisecondsToOffset": "Belirteç gecikmesi. Varsayılan: 0", + "desc": "Tespit belirteç verisi kameranızın tespit yayınından gelir fakat kameranızın kayıt yayını üzerine çizilir. Bu iki yayın zaman zaman senkrondan kayar. Bunun sonucu olarak görüntü ve belirteç karelerinin zaman uyumu kayabilir. Bunu telafi etmek için annotation_offset alanı kullanılarak gecikmeyi ayarlanabilir.", + "tips": "İPUCU: Videoda bir kişinin soldan sağa doğru yürüdüğünü hayal edin. Eğer belirteç sürekli olarak kişinin solunda/arkasında ise bu değer daha küçük veya negatif olarak ayarlanmalıdır. Benzer şekilde, eğer kişi sağdan sola doğru yürürken belirteç karesi sürekli olarak kişinin önünde/sağında kalıyorsa bu değer daha büyük veya pozitif olarak ayarlanmalıdır.", + "toast": { + "success": "{{camera}} kamerası için belirteç telafi değeri yapılandırma dosyasına kaydedildi. Değişikliklerin uygulamak için Frigate'i yeniden başlatın." + } + }, + "title": "Belirteç Ayarları", + "showAllZones": { + "title": "Tüm Alanları Göster", + "desc": "Nesnelerin bir alana girdiği karelerde her zaman alanları göster." + } + }, + "carousel": { + "next": "Sonraki sayfa", + "previous": "Önceki sayfa" + }, + "scrollViewTips": "Bu nesnenin yaşam döngüsündeki önemli noktaları görmek için kaydırın.", + "autoTrackingTips": "Otomatik takip yapılan kameralarda gösterilen çerçeveler hatalı olacaktır.", + "count": "Toplam {{second}} kerede {{first}} kez", + "trackedPoint": "Takip Edilen Nokta" + }, + "itemMenu": { + "downloadVideo": { + "label": "Videoyu indir", + "aria": "Videoyu indir" + }, + "findSimilar": { + "aria": "Benzer takip edilen nesneleri bul", + "label": "Benzerini bul" + }, + "submitToPlus": { + "label": "Frigate+'a gönder", + "aria": "Frigate+'a gönder" + }, + "viewInHistory": { + "label": "Geçmiş görünümünde görüntüle", + "aria": "Geçmiş görünümünde görüntüle" + }, + "deleteTrackedObject": { + "label": "Bu takip edilen nesneyi sil" + }, + "viewObjectLifecycle": { + "aria": "Nesne yaşam döngüsünü göster", + "label": "Nesne yaşam döngüsünü göster" + }, + "downloadSnapshot": { + "aria": "Fotoğrafı indir", + "label": "Fotoğrafı indir" + }, + "addTrigger": { + "label": "Tetik ekle", + "aria": "Bu takip edilen nesne için bir tetik ekle" + }, + "audioTranscription": { + "label": "Çözümle", + "aria": "Ses çözümlemesi iste" + }, + "downloadCleanSnapshot": { + "label": "Temiz anlık görüntüyü indir", + "aria": "Temiz anlık görüntüyü indir" + }, + "viewTrackingDetails": { + "label": "İzleme ayrıntılarını görüntüle", + "aria": "Takip ayrıntılarını göster" + }, + "showObjectDetails": { + "label": "Nesne yolunu göster" + }, + "hideObjectDetails": { + "label": "Nesne yolunu gizle" + } + }, + "noTrackedObjects": "Takip Edilen Nesne Bulunamadı", + "fetchingTrackedObjectsFailed": "Takip edilen nesneler getirilirken hata: {{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "error": "Takip edilen nesne silinemedi: {{errorMessage}}", + "success": "Takip edilen nesne başarıyla silindi." + } + }, + "tooltip": "Eşleşme: {{type}} (%{{confidence}})", + "previousTrackedObject": "Önceki izlenen nesne", + "nextTrackedObject": "Sonraki izlenen nesne" + }, + "dialog": { + "confirmDelete": { + "desc": "Bu takip edilen nesneyi silmek anlık görüntüyü, kaydedilmiş gömü verilerini ve ilişkili yaşam döngüsü kayıtlarını siler. Geçmiş görünümündeki bu izlenen nesneye ait kayıtlı video görüntüleri SİLİNMEYECEKTİR.

    Devam etmek istediğinizden emin misiniz?", + "title": "Silmeyi onayla" + } + }, + "trackedObjectsCount_one": "{{count}} adet takip edilen nesne ", + "trackedObjectsCount_other": "{{count}} adet takip edilen nesne ", + "exploreMore": "Daha fazla {{label}} nesnesini keşfet", + "aiAnalysis": { + "title": "Yapay Zeka Analizi" + }, + "trackingDetails": { + "title": "Takip Ayrıntıları", + "noImageFound": "Bu zaman damgasına ait bir görsel bulunamadı.", + "createObjectMask": "Nesne Maskesi Oluştur", + "adjustAnnotationSettings": "Etiketleme ayarlarını düzenle", + "scrollViewTips": "Bu nesnenin yaşam döngüsündeki önemli olayları görmek için tıklayın.", + "autoTrackingTips": "Otomatik takip yapan kameralar için sınır kutusu konumları doğru olmayabilir.", + "count": "{{second}}’den {{first}}", + "trackedPoint": "Takip edilen nokta", + "lifecycleItemDesc": { + "visible": "{{label}} tespit edildi", + "entered_zone": "{{label}} {{zones}} bölgesine girdi", + "active": "{{label}} etkin hale geldi", + "stationary": "{{label}} sabit hale geldi", + "attribute": { + "faceOrLicense_plate": "{{label}} için {{attribute}} tespit edildi", + "other": "{{label}}, {{attribute}} olarak tanındı" + }, + "gone": "{{label}} ayrıldı", + "heard": "{{label}} duyuldu", + "external": "{{label}} tespit edildi", + "header": { + "zones": "Bölgeler", + "ratio": "Oran", + "area": "Alan", + "score": "Skor" + } + }, + "annotationSettings": { + "title": "Etiketleme Ayarları", + "showAllZones": { + "title": "Tüm Bölgeleri Göster", + "desc": "Herhangi bir bölgeye nesne girdiğinde, o karede bölgeleri her zaman göster." + }, + "offset": { + "label": "Etiket Kaydırma Değeri", + "desc": "Bu veriler kameranızın algılama akışından gelir ancak kayıt akışındaki görüntülerin üzerine bindirilir. İki akışın tamamen senkronize olması pek olası değildir. Bu nedenle sınır kutusu ile görüntü birebir hizalı olmayabilir. Bu ayarı kullanarak anotasyonları zamansal olarak ileri veya geri kaydırabilir ve kaydedilmiş görüntülerle daha iyi hizalayabilirsiniz.", + "millisecondsToOffset": "Algılama anotasyonlarının kaydırılacağı milisaniye değeri. Varsayılan: 0", + "tips": "Videonun oynatımı kutulardan ve yol noktalarından öndeyse değeri düşürün; geride kalıyorsa değeri artırın. Bu değer negatif olabilir.", + "toast": { + "success": "{{camera}} için anotasyon zaman kaydırması yapılandırma dosyasına kaydedildi." + } + } + }, + "carousel": { + "previous": "Önceki slayt", + "next": "Sonraki slayt" + } + }, + "concerns": { + "label": "Endişeler" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/exports.json new file mode 100644 index 0000000..0c8fec1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Arama", + "documentTitle": "Dışa Aktar - Frigate", + "deleteExport": "Dışa Aktarımı Sil", + "deleteExport.desc": "{{exportName}} adlı dışa aktarımı silmek istediğinize emin misiniz?", + "editExport": { + "saveExport": "Dışa Aktarımı Kaydet", + "desc": "Bu dışa aktarım için yeni bir isim girin.", + "title": "Dışa Aktarımı Yeniden Adlandır" + }, + "toast": { + "error": { + "renameExportFailed": "Dışa aktarım adlandırılamadı: {{errorMessage}}" + } + }, + "noExports": "Dışa aktarım bulunamadı", + "tooltip": { + "shareExport": "Dışa aktarmayı paylaş", + "downloadVideo": "Videoyu İndir", + "editName": "İsmi Düzenle", + "deleteExport": "Dışa Aktarmayı Sil" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/faceLibrary.json new file mode 100644 index 0000000..7f2a1dd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/faceLibrary.json @@ -0,0 +1,101 @@ +{ + "selectItem": "{{item}} seçin", + "description": { + "placeholder": "Bu koleksiyona bir isim verin", + "addFace": "İlk görselinizi yükleyerek Yüz Kütüphanesi’ne yeni bir koleksiyon ekleyin.", + "invalidName": "Geçersiz ad. Ad yalnızca harfler, rakamlar, boşluklar, kesme işaretleri (’), alt çizgiler (_) ve tireler (-) içerebilir." + }, + "details": { + "person": "İnsan", + "faceDesc": "Yüz ve ilişkili nesneye ait detaylar", + "confidence": "Kesinlik", + "timestamp": "Zaman Damgası", + "face": "Yüz Detayları", + "scoreInfo": "Alt etiket skoru, tanınan tüm yüzlerin güvenilirlik değerlerinin ağırlıklı ortalamasından elde edilir, dolayısıyla fotoğraf üzerinde gösterilen skordan farklı olabilir.", + "subLabelScore": "Alt Etiket Puanı", + "unknown": "Bilinmeyen" + }, + "documentTitle": "Yüz Kütüphanesi - Frigate", + "uploadFaceImage": { + "title": "Yüz Resmi Yükle", + "desc": "Yüzleri taramak ve {{pageToggle}} için dahil etmek üzere bir resim yükleyin" + }, + "createFaceLibrary": { + "desc": "Yeni bir yüz koleksiyonu oluşturun", + "new": "Yeni Yüz Oluştur", + "title": "Koleksiyon Oluştur", + "nextSteps": "Sağlam bir temel oluşturmak için:
  • Her tespit edilen kişi için **Recent Recognitions (Son Tanımalar)** sekmesini kullanarak görüntüleri seçin ve eğitim gerçekleştirin.
  • En iyi sonuçlar için doğrudan önden çekilmiş yüz görüntülerine odaklanın; yüzlerin açılı göründüğü fotoğrafları eğitimde kullanmaktan kaçının.
  • " + }, + "train": { + "title": "Son Tanımalar", + "aria": "Son algılanan nesneleri seç", + "empty": "Yakın zamanda yüz tanıma denemesi olmadı", + "titleShort": "Son" + }, + "deleteFaceLibrary": { + "title": "İsmi Sil", + "desc": "{{name}} koleksiyonunu silmek istediğinizden emin misiniz? Bu işlem, ilişkili tüm yüzleri kalıcı olarak silecektir." + }, + "button": { + "deleteFaceAttempts": "Yüzleri Sil", + "addFace": "Yüz Ekle", + "reprocessFace": "Yüzü Yeniden İşle", + "uploadImage": "Resim Yükle", + "renameFace": "Yüzü Yeniden Adlandır", + "deleteFace": "Yüzü Sil" + }, + "imageEntry": { + "dropActive": "Resmi buraya bırakın…", + "maxSize": "Maksimum boyut: {{size}} MB", + "validation": { + "selectImage": "Lütfen bir resim dosyası seçin." + }, + "dropInstructions": "Bir görseli buraya sürükleyip bırakın, yapıştırın ya da seçmek için tıklayın" + }, + "trainFaceAs": "Yüzü şu olarak eğit:", + "toast": { + "success": { + "deletedFace_one": "{{count}} yüz başarıyla silindi.", + "deletedFace_other": "{{count}} yüz başarıyla silindi.", + "deletedName_one": "{{count}} yüz başarıyla silindi.", + "deletedName_other": "{{count}} yüz başarıyla silindi.", + "addFaceLibrary": "{{name}} başarıyla Yüz Kütüphanesi’ne eklendi!", + "trainedFace": "Yüz başarıyla eğitildi.", + "uploadedImage": "Resim başarıyla yüklendi.", + "updatedFaceScore": "Yüz tanıma skoru {{name}} ({{score}}) olarak başarıyla güncellendi.", + "renamedFace": "Yüz başarıyla {{name}} olarak adlandırıldı" + }, + "error": { + "uploadingImageFailed": "Resim yüklenemedi: {{errorMessage}}", + "addFaceLibraryFailed": "Yüz ismi ayarlanamadı: {{errorMessage}}", + "updateFaceScoreFailed": "Yüz skoru güncellenemedi: {{errorMessage}}", + "trainFailed": "Eğitme işlemi başarısız oldu: {{errorMessage}}", + "deleteFaceFailed": "Silme işlemi başarısız: {{errorMessage}}", + "deleteNameFailed": "İsim silinemedi: {{errorMessage}}", + "renameFaceFailed": "Yüz yeniden adlandırılamadı: {{errorMessage}}" + } + }, + "readTheDocs": "Dokümantasyonu oku", + "selectFace": "Yüz Seçin", + "trainFace": "Yüzü Eğit", + "steps": { + "faceName": "Yüze İsim Verin", + "uploadFace": "Yüz Resmi Yükle", + "nextSteps": "Sonraki Adımlar", + "description": { + "uploadFace": "{{name}}'ın yüzünü direkt karşıdan, ön açıdan gösteren bir fotoğraf yükleyin. Fotoğrafı yalnızca yüzü kalacak şekilde kırpmanıza gerek YOKTUR." + } + }, + "renameFace": { + "title": "Yüzü Yeniden Adlandır", + "desc": "{{name}} için yeni bir isim girin" + }, + "collections": "Koleksiyonlar", + "deleteFaceAttempts": { + "title": "Yüzleri Sil", + "desc_one": "Bir adet yüzü silmek istediğinize emin misiniz? Bu işlem geri alınamaz.", + "desc_other": "{{count}} adet yüzü silmek istediğinize emin misiniz? Bu işlem geri alınamaz." + }, + "nofaces": "Yüz bulunamadı", + "pixels": "{{area}}px" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/live.json new file mode 100644 index 0000000..ad7a7e5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Canlı - Frigate", + "documentTitle.withCamera": "{{camera}} - Canlı - Frigate", + "muteCameras": { + "disable": "Tüm Kameraların Sesini Aç", + "enable": "Tüm Kameraları Sustur" + }, + "autotracking": { + "disable": "Otomatik Takibi Kapat", + "enable": "Otomatik Takibi Aç" + }, + "manualRecording": { + "start": "Talep Üzerine Kaydı Başlat", + "failedToEnd": "Manuel talep üzerine kayıt bitirilemedi.", + "recordDisabledTips": "Kamera konfigürasyonunda kayıtlar devre dışı bırakıldığı veya kısıtlandığı için yalnızca bir fotoğraf kaydedilcektir.", + "showStats": { + "desc": "Yayın istatistiklerini göstermek için bu seçeneği açın.", + "label": "İstatistikleri Göster" + }, + "started": "Manuel talep üzerine kayıt başlatıldı.", + "failedToStart": "Manuel talep üzerine kayıt başlatılamadı.", + "title": "İsteğe Bağlı", + "end": "Talep Üzerine Kaydı Bitir", + "debugView": "Hata Ayıklama Görünümü", + "ended": "Manuel talep üzerine kayıt bitirildi.", + "tips": "Bu kameranın kayıt saklama ayarlarına göre anlık bir görüntü indirin veya manuel bir olay başlatın.", + "playInBackground": { + "label": "Arka planda oynat", + "desc": "Yayını oynatıcı arkadayken de devam ettirmek için bu seçeneği açın." + } + }, + "stream": { + "audio": { + "tips": { + "documentation": "Dökümantasyonu oku ", + "title": "Bu yayın için kameranızın yayın çıkışında ses olması ve go2rtc'de ayarlanmış olması gerekmektedir." + }, + "unavailable": "Bu yayında ses yok", + "available": "Bu yayında ses var" + }, + "twoWayTalk": { + "tips": "Çift yönlü ses için cihazınızın ve kameranızın bu özelliği desteklemesi ve WebRTC'nin ayarlanmış olması gereklidir.", + "tips.documentation": "Dökümantasyonu oku ", + "available": "Bu yayında çift yönlü ses var", + "unavailable": "Bu yayında çift yönlü ses yok" + }, + "lowBandwidth": { + "tips": "Canlı görünüm, yayında donmalar veya yayın hataları sebebiyle düşük bant genişliği moduna geçti.", + "resetStream": "Yayını sıfırla" + }, + "playInBackground": { + "label": "Arka planda oynat", + "tips": "Yayını oynatıcı arkadayken de devam ettirmek için bu seçeneği açın." + }, + "title": "Yayın", + "debug": { + "picker": "Debug modunda akış seçimi kullanılamaz. Debug görünümü her zaman “detect” rolüne atanmış akışı kullanır." + } + }, + "cameraSettings": { + "recording": "Kayıt", + "snapshots": "Fotoğraflar", + "title": "{{camera}} Ayarları", + "autotracking": "Otomatik Takip", + "cameraEnabled": "Kamera Açık", + "objectDetection": "Nesne Tespiti", + "audioDetection": "Ses Algılama", + "transcription": "Ses Çözümlemesi" + }, + "effectiveRetainMode": { + "modes": { + "active_objects": "Aktif nesneler", + "all": "Tümü", + "motion": "Hareket" + }, + "notAllTips": "İlgili {{source}} kaynağındaki kayıt saklama politikanız şu moda ayarlı: {{effectiveRetainMode}}. Dolayısıyla şu anda gerçekleştirdiğiniz manuel talep üzerine kayıtta yalnızca {{effectiveRetainModeName}} içeren bölümler yer alacaktır." + }, + "editLayout": { + "label": "Düzeni düzenle", + "group": { + "label": "Kamera Grubunu Düzenle" + }, + "exitEdit": "Düzenlemeden Çık" + }, + "cameraAudio": { + "enable": "Kamera sesini aç", + "disable": "Kamera sesini kapat" + }, + "ptz": { + "move": { + "clickMove": { + "enable": "Tıklamayla gezintiyi aç", + "disable": "Tıklamayla gezintiyi kapat", + "label": "Kamerayı ortalamak için görüntüye tıklatın" + }, + "down": { + "label": "PTZ kamerayı aşağı çevir" + }, + "right": { + "label": "PTZ kamerayı sağa çevir" + }, + "left": { + "label": "PTZ kameryı sağa çevir" + }, + "up": { + "label": "PTZ kamerayı yukarı çevir" + } + }, + "zoom": { + "out": { + "label": "PTZ kamerayı uzaklaştır" + }, + "in": { + "label": "PTZ kamerayı yakınlaştır" + } + }, + "presets": "PTZ kamera ön ayarları", + "frame": { + "center": { + "label": "PTZ kamerayı ortalamak için görüntüye tıklatın" + } + }, + "focus": { + "in": { + "label": "PTZ kamera odağını yakınlaştır" + }, + "out": { + "label": "PTZ kamera odağını uzaklaştır" + } + } + }, + "history": { + "label": "Geçmiş görüntüleri göster" + }, + "camera": { + "enable": "Kamerayı Aç", + "disable": "Kamerayı Kapat" + }, + "suspend": { + "forTime": "Askıya alınma süresi: " + }, + "twoWayTalk": { + "disable": "Çift yönli sesi kapat", + "enable": "Çift yönli sesi aç" + }, + "snapshots": { + "enable": "Resimleri Aç", + "disable": "Resimleri Kapat" + }, + "audioDetect": { + "enable": "Ses Tespitini Aç", + "disable": "Ses Tespitini Kapat" + }, + "streamStats": { + "disable": "Yayın İstatistiklerini Gizel", + "enable": "Yayın İstatistiklerini Göster" + }, + "lowBandwidthMode": "Düşük bant genişliği modu", + "streamingSettings": "Yayın ayarları", + "audio": "Ses", + "recording": { + "enable": "Kaydı Aç", + "disable": "Kaydı Kapat" + }, + "notifications": "Bildirimler", + "detect": { + "disable": "Tespiti Kapat", + "enable": "Tespiti Aç" + }, + "transcription": { + "enable": "Canlı Ses Çözümlemeyi Aç", + "disable": "Canlı Ses Çözümlemeyi Kapat" + }, + "snapshot": { + "takeSnapshot": "Anlık Ekran Görüntüsünü İndir", + "noVideoSource": "Anlık görüntü için kullanılabilir bir video kaynağı bulunamadı.", + "captureFailed": "Anlık görüntü yakalanamadı.", + "downloadStarted": "Anlık görüntü indirme işlemi başlatıldı." + }, + "noCameras": { + "title": "Hiç Kamera Yapılandırılmamış", + "description": "Frigate’e bir kamera bağlayarak başlayın.", + "buttonText": "Kamera Ekle", + "restricted": { + "title": "Kullanılabilir Kamera Yok", + "description": "Bu gruptaki kameraları görüntüleme izniniz yok." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/recording.json new file mode 100644 index 0000000..c113d36 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Filtre", + "export": "Dışa Aktar", + "filters": "Filtreler", + "toast": { + "error": { + "noValidTimeSelected": "Geçerli bir zaman aralığı seçilmedi", + "endTimeMustAfterStartTime": "Bitiş zamanı başlangıç zamanında sonra olmalıdır" + } + }, + "calendar": "Takvim" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/search.json new file mode 100644 index 0000000..17ba988 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/search.json @@ -0,0 +1,74 @@ +{ + "button": { + "save": "Aramayı kaydet", + "filterActive": "Fitreler açık", + "filterInformation": "Filtrele", + "clear": "Aramayı temizle", + "delete": "Kayıtlı aramayı sil" + }, + "trackedObjectId": "Takip Edilen Nesne ID", + "filter": { + "label": { + "min_score": "Min. Skor", + "recognized_license_plate": "Tanınan Plaka", + "search_type": "Arama Türü", + "has_snapshot": "Fotoğrafı var", + "cameras": "Kameralar", + "max_score": "Maks. Skor", + "labels": "Etiketler", + "time_range": "Zaman Aralığı", + "before": "Önce", + "zones": "Alanlar", + "after": "Sonra", + "has_clip": "Klibi var", + "min_speed": "Min. Hız", + "sub_labels": "Alt Etiketler", + "max_speed": "Maks. Hız" + }, + "searchType": { + "description": "Açıklama", + "thumbnail": "Küçük resim" + }, + "tips": { + "title": "Metin filtreleri nasıl kullanılır", + "desc": { + "text": "Filtreler arama sonuçlarınızı daraltmanıza yardımcı olur. Giriş alanındaki kullanımları şöyledir:", + "step": "
    • Bir filtre adı yazın ve iki nokta üst üste (:) ile bitirin (örn. \"kameralar:\").
    • Önerilerden bir değer seçin veya kendiniz yazın.
    • Birden fazla filtreyi aralarına boşluk koyarak art arda ekleyip kullanın.
    • Tarih filtreleri (before: ve after:) {{DateFormat}} biçimini kullanır.
    • Zaman aralığı filtresi {{exampleTime}} biçimini kullanır.
    • Filtreleri kaldırmak için yanlarındaki 'x'e tıklayın.
    ", + "example": "Örnek(anahtar kelimeler ingilizce olmalıdır): cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM ", + "exampleLabel": "Örnek:", + "step1": "Bir filtre anahtarını iki nokta üst üste ile beraber yazın (örn. belli kameraları seçmek için \"cameras:\").", + "step2": "Önerilen bir değer seçin veya kendiniz girin.", + "step3": "Birden fazla filtreyi aralarında boşluk bırakarak kullanabilirsiniz.", + "step4": "Tarih filtreleri (before: ve after:) {{DateFormat}} formatını kullanır.", + "step5": "Zaman aralığı filtreleri {{exampleTime}} formatını kullanır.", + "step6": "Filtreleri kaldırmak için yanlarındaki çarpıya basın." + } + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "'Önce' tarihi 'sonra' tarihinden sonra olmalıdır.", + "maxScoreMustBeGreaterOrEqualMinScore": "Maksimum skor, minimum skora eşit veya daha fazla olmalıdır.", + "minScoreMustBeLessOrEqualMaxScore": "Minimum skor, maksimum skora eşit veya daha az olmalıdır.", + "afterDatebeEarlierBefore": "'Sonra' tarihi 'Önce' tarihinden önce olmalıdır.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Maksimum hız, minimum hıza eşit veya daha fazla olmalıdır.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Minimum hız, maksimum hıza eşit veya daha az olmalıdır." + } + }, + "header": { + "noFilters": "Filtreler", + "activeFilters": "Aktif Filtreler", + "currentFilterType": "Değerleri Filtrele" + } + }, + "placeholder": { + "search": "Ara…" + }, + "similaritySearch": { + "active": "Benzerlik araması aktif", + "title": "Benzerlik Araması", + "clear": "Benzerlik aramasını temizle" + }, + "searchFor": "{{inputValue}} için Arat", + "search": "Arama", + "savedSearches": "Kayıtlı Aramalar" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/settings.json new file mode 100644 index 0000000..52ddc60 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/settings.json @@ -0,0 +1,1215 @@ +{ + "documentTitle": { + "default": "Ayarlar - Frigate", + "classification": "Sınıflandırma Ayarları - Frigate", + "camera": "Kamera Ayarları - Frigate", + "masksAndZones": "Maske ve Alan Düzenleyici - Frigate", + "authentication": "Kimlik Doğrulama Ayarları - Frigate", + "motionTuner": "Hareket Algılama Ayarları - Frigate", + "frigatePlus": "Frigate+ Ayarları - Frigate", + "object": "Hata Ayıklama - Frigate", + "general": "Kullanıcı Arayüzü Ayarları – Frigate", + "notifications": "Bildirim Ayarları - Frigate", + "enrichments": "Zenginleştirme Ayarları - Frigate", + "cameraManagement": "Kameraları Yönet - Frigate", + "cameraReview": "Kamera İnceleme Ayarları - Frigate" + }, + "menu": { + "masksAndZones": "Maskeler / Alanlar", + "users": "Kullanıcılar", + "frigateplus": "Frigate+", + "ui": "Arayüz", + "notifications": "Bildirimler", + "motionTuner": "Hareket Algılama", + "classification": "Sınıflandırma", + "debug": "Hata Ayıklama", + "cameras": "Kamera Ayarları", + "enrichments": "Zenginleştirmeler", + "triggers": "Tetikler", + "cameraManagement": "Yönet", + "cameraReview": "İncele", + "roles": "Roller" + }, + "general": { + "title": "Kullanıcı Arayüzü Ayarları", + "liveDashboard": { + "automaticLiveView": { + "label": "Otomatik Canlı Görünüm", + "desc": "Aktivite tespit edildiğinde otomatik olarak kameranın canlı akışına geç. Bu seçeneği devre dışı bırakmak canlı görüntü panelinde dakikada bir güncellenen sabit resim gösterilmesine sebep olur." + }, + "playAlertVideos": { + "label": "Alarm Videolarını Oynat", + "desc": "Varsayılan olarak canlı görüntü panelinde gösterilen son alarmlar ufak videolar olarak oynatılır. Bu tarayıcı/cihazda video yerine sabit resim göstermek için bu seçeneği kapatın." + }, + "title": "Canlı Görüntü Paneli", + "displayCameraNames": { + "label": "Kamera Adlarını Her Zaman Göster", + "desc": "Çok kameralı canlı izleme panelinde, kamera adlarını her zaman bir etiket içinde göster." + }, + "liveFallbackTimeout": { + "label": "Canlı Oynatıcı Yedekleme Zaman Aşımı", + "desc": "Bir kameranın yüksek kaliteli canlı akışı kullanılamadığında, belirtilen saniye kadar sonra düşük bant genişliği moduna geç. Varsayılan: 3." + } + }, + "storedLayouts": { + "desc": "Kamera grubundaki kameraların düzenini kameraları sürükleyerek ve büyüterek/küçülterek değiştirebilirsiniz. Düzen bilgisi tarayıcınızda depolanır.", + "clearAll": "Tüm Düzenleri Temizle", + "title": "Kayıtlı Düzenler" + }, + "cameraGroupStreaming": { + "title": "Kamera Grubu Yayın Ayarları", + "desc": "Kamera gruplarının ilgili yayın ayarları tarayıcınızda depolanır.", + "clearAll": "Tüm Yayın Ayarlarını Temizle" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Varsayılan Oynatma Hızı", + "desc": "Kayıt oynatılırken kullanılan varsayılan oynatma hızı." + }, + "title": "Kayıt Görüntüleyicisi" + }, + "calendar": { + "firstWeekday": { + "sunday": "Pazar", + "desc": "Arayüzdeki takvimde gösterilecek haftanın ilk günü.", + "label": "Haftanın ilk günü", + "monday": "Pazartesi" + }, + "title": "Takvim" + }, + "toast": { + "success": { + "clearStreamingSettings": "Tüm kamera grupları için yayın ayarları temizlendi.", + "clearStoredLayout": "{{cameraName}} için kayıtlı düzenler temizlendi" + }, + "error": { + "clearStreamingSettingsFailed": "Yayın ayarları temizlenemedi: {{errorMessage}}", + "clearStoredLayoutFailed": "Kayıtlı düzen temizlenemedi: {{errorMessage}}" + } + } + }, + "classification": { + "title": "Sınıflandırma Ayarları", + "semanticSearch": { + "title": "Anlamsal Arama", + "readTheDocumentation": "Dökümantasyonu Oku", + "reindexNow": { + "confirmButton": "Yeniden Dizinle", + "label": "Şimdi Yeniden Dizinle", + "desc": "Yeniden dizinleme bütün takip edilen nesneler için gömüleri tekrar oluşturur. Bu işlem arka planda çalışacak olsa da nesne sayısına göre işlemcinizi tamamen kullanabilir ve tamamlanması biraz zaman alabilir.", + "alreadyInProgress": "Yeniden dizinleme zaten devam ediyor.", + "confirmTitle": "Yenden Dizinlemeyi onayla", + "success": "Yeniden dizinleme başladı.", + "confirmDesc": "Takip edilen bütün objelerin gömülerini yeniden dizinlemek istediğinze emin misiniz? Bu işlem arka planda çalışacak fakat işlemcinizi tamamen kullanabilir ve tamamlanması biraz zaman alabilir. İlerlemeyi Keşfet sayfasından takip edebilirsiniz.", + "error": "Yeniden dizinlemeye başlanamadı: {{errorMessage}}" + }, + "modelSize": { + "label": "Model boyutu", + "large": { + "title": "büyük", + "desc": "Büyük modeli kullandığınızda tam boyutlu Jina modeli kullanılacaktır ve uygunsa otomatik olarak grafik işlemcisinde çalıştırılacaktır." + }, + "small": { + "desc": "Küçük modeli kullandığınızda modelin kuantize edilmiş bir sürümü kullanılır. Bu model daha az RAM kullanır, işlemcilerde daha hızlı çalışır ve gömü kalitesinde neredeyse hiç kalite farkı yoktur.", + "title": "küçük" + }, + "desc": "Anlamsal arama için kullanılan dil modelinin büyüklüğü." + }, + "desc": "Frigate'daki Anlamsal Arama özelliği inceleme öğelerinizde takip edilen nesneleri bizzat nesnenin resmi ile aratarak ya da otomatik olarak veya kullanıcı tarafından yazılmış bir metin açıklaması içinde aratarak bulmanıza imkan sağlar." + }, + "faceRecognition": { + "modelSize": { + "large": { + "desc": "Büyük modeli kullandığınızda tam boyutlu ArcFace yüz gömü modeli kullanılacaktır ve uygunsa otomatik olarak grafik işlemcisinde çalıştırılacaktır.", + "title": "büyük" + }, + "small": { + "title": "küçük", + "desc": "Küçük modeli kullandığınızda çoğu işlemcide verimli bir şekilde çalışan bir FaceNet yüz gömme modeli kullanılır." + }, + "label": "Model Boyutu", + "desc": "Yüz tanıma için kullanılan modelin boyutu." + }, + "title": "Yüz Tanıma", + "desc": "Yüz tanıma, tanınan insanlara isim vermenize olanak tanır ve bu yüzler tanındığında Frigate, kişinin adını alt etiket olarak ekler. Bu bilgi; kullanıcı arayüzü, filtreler ve bildirimlerde gösterilir.", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "toast": { + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}", + "success": "Sınıflandırma ayarları kaydedildi. Değişikliklerinizi uygulamak için Frigate'i yeniden başlatın." + }, + "licensePlateRecognition": { + "desc": "Frigate araç plakalarını tanıyabilir ve algılanan karakterleri otomatik olarak recognized_license_plate alanına veya belirli bir plaka için tanımladığınız bir takma ismi alt etiket olarak ilgili aracın tanımlanan nesnesine ekleyebilir. Bu sistem, garajınıza giren veya caddeden geçen araçların plakalarını okumak için kullanılabilir.", + "title": "Plaka Tanıma", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "birdClassification": { + "title": "Kuş Sınıflandırma", + "desc": "Kuş Sınıflandırma özelliği, bilinen kuş türlerini kuantize edilmiş bir Tensorflow modeli kullanarak teşhis etmenizi sağlar. Model bir kuş türünü teşhis ettiğinde, Frigate, bu türün adını alt etiket olarak ekler. Bu bilgi; kullanıcı arayüzü, filtreler ve bildirimlerde gösterilir." + }, + "restart_required": "Yeniden Başlatma Gerekli (Sınıflandırma ayarları değiştirildi)" + }, + "cameraSetting": { + "camera": "Kamera", + "noCamera": "Kamera Yok" + }, + "dialog": { + "unsavedChanges": { + "title": "Kaydedilmemiş değişiklikleriniz var.", + "desc": "Devam etmeden önce değişiklikleri kaydetmek ister misiniz?" + } + }, + "camera": { + "title": "Kamera Ayarları", + "review": { + "title": "İncele", + "alerts": "Alarmlar ", + "detections": "Tespitler ", + "desc": "Bu kamera için uyarıları ve algılamaları geçici olarak etkinleştirin/devre dışı bırakın. Frigate yeniden başlatıldığında yapılandırmanızdaki tercihlere geri dönülür. Devre dışı bırakıldığında, yeni inceleme öğeleri oluşturulamaz. " + }, + "reviewClassification": { + "readTheDocumentation": "Dökümantasyonu Oku", + "selectAlertsZones": "Alarmlar için alanları seçin", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} kamerasındaki kategorize edilmemiş bütün {{detectionsLabels}} nesneleri hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "notSelectDetections": "{{cameraName}} kamerasındaki {{zone}} alanı içinde algılanan ve Alarm olarak kategorize edilmeyen bütün {{detectionsLabels}} nesneleri hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "text": "{{cameraName}} kamerasındaki {{zone}} alanındaki kategorize edilmemiş bütün {{detectionsLabels}} nesneleri Algılama olarak sınıflandırılacaktır." + }, + "zoneObjectAlertsTips": "{{cameraName}} kamerasındaki {{zone}} alanında algılanan bütün {{alertsLabels}} nesneleri Alarm olarak sınıflandırılacaktır.", + "desc": "Frigate tespit edilen İnceleme öğelerini Alarmlar ve Tespitler olarak kategorize eder. Varsayılan olarak bütün kişi ve araba nesneleri Alarm olarak sınıflandırılır. İnceleme öğelerinizin sınıflandırmasını tespit sınırlandırma sayfasında gerekli alanlar seçerek sınırlandırabilirsiniz.", + "objectDetectionsTips": "{{cameraName}} kamerasında kategorize edilmemiş bütün {{detectionsLabels}} nesneleri, hangi alanda olduklarına bakılmaksızın Algılama olarak sınıflandırılacaktır.", + "selectDetectionsZones": "Algılamalar için alanları seçin", + "title": "Tespit Sınıflandırmaları", + "noDefinedZones": "Bu kamera için tanımlanmış alan yok.", + "objectAlertsTips": "{{cameraName}} kamerasındaki bütün {{alertsLabels}} nesneleri Alarm olarak sınıflandırılacaktır.", + "limitDetections": "Algılamaları belirli alanlara sınırla", + "toast": { + "success": "İnceleme Sınıflandırma ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + }, + "unsavedChanges": "{{camera}} için Kaydedilmemiş Sınıflandırma Ayarları" + }, + "streams": { + "desc": "Frigate yeniden başlataılana kadar bir kamerayı devre dışı bırakın. Bir kameranın devre dışı bırakılması, Frigate'in bu kamerayı işlemesini tamamen durdurur. Algılama, kayıt ve hata ayıklama özellikleri kullanılamaz.
    Not: Bu eylem, go2rtc'deki yeniden akışları devre dışı bırakmaz.", + "title": "Akışlar" + }, + "object_descriptions": { + "title": "Üretken AI Nesne Açıklamaları", + "desc": "Bu kamera için Üretken Yapay Zeka kullanarak nesne açıklamaları oluşturmayı geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kamerada takip edilen nesneler için yapay zekadan nesne açıklamaları talep edilmeyecektir." + }, + "review_descriptions": { + "title": "Üretken AI İnceleme Öğesi Açıklamaları", + "desc": "Bu kamera için Üretken Yapay Zeka kullanarak inceleme öğelerinin açıklamalarını oluşturmayı geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameraya bağlı inceleme öğeleri için yapay zekadan açıklama metni talep edilmeyecektir." + }, + "addCamera": "Yeni Kamera Ekle", + "editCamera": "Kamerayı Düzenle:", + "selectCamera": "Kamera Seç", + "backToSettings": "Kamera Ayarlarına Dön", + "cameraConfig": { + "add": "Kamera Ekle", + "edit": "Kamerayı Düzenle", + "description": "Kameranızın ayarlarını, kameraların akışları ve roller de dahil olacak şekilde yapılandırın.", + "name": "Kamera İsmi", + "nameRequired": "Kamera adı gereklidir", + "nameInvalid": "Kamera adı yalnızca harf, rakam, alt çizgi veya tire içerebilir", + "namePlaceholder": "örn: onkapi", + "enabled": "Açık", + "ffmpeg": { + "inputs": "Kamera Girdi Akışları", + "path": "Akış Yolu", + "pathRequired": "Akış yolu gereklidir", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "En az bir rol gereklidir", + "rolesUnique": "Her rol (ses, tespit, kayıt) yalnızca bir adet yayına atanabilir. Her rol aynı akışı kullanabilir, lakin bir rol birden fazla akışa atanamaz.", + "addInput": "Girdi Akışı Ekle", + "removeInput": "Girdi Akışını Kaldır", + "inputsRequired": "En az bir girdi akışı gereklidir" + }, + "toast": { + "success": "Kamera {{cameraName}} başarıyla kaydedildi" + }, + "nameLength": "Kamera ismi 24 karakterden kısa olmalıdır." + } + }, + "masksAndZones": { + "filter": { + "all": "Bütün Maskeler ve Alanlar" + }, + "toast": { + "success": { + "copyCoordinates": "{{polyName}} için koordinatlar panoya kopyalandı." + }, + "error": { + "copyCoordinatesFailed": "Koordinatlar panoya kopyalanamadı." + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "Alan adı en az 2 karakter olmalıdır.", + "hasIllegalCharacter": "Alan adı geçersiz karakterler içeriyor.", + "mustNotBeSameWithCamera": "Alan adı kamera adıyla aynı olmamalıdır.", + "alreadyExists": "Bu kamera için bu ada sahip bir alan zaten mevcut.", + "mustNotContainPeriod": "Alan adı nokta içermemelidir.", + "mustHaveAtLeastOneLetter": "Bölge adı en az bir harf içermelidir." + } + }, + "distance": { + "error": { + "text": "Mesafe 0.1'den büyük veya eşit olmalıdır.", + "mustBeFilled": "Hız tahmini özelliğini kullanabilmek için bütün mesafe alanları doldurulmalıdır." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Oyalanma süresi 0'dan büyük veya eşit olmalıdır." + } + }, + "polygonDrawing": { + "snapPoints": { + "false": "Noktaları hizalama", + "true": "Noktaları hizala" + }, + "removeLastPoint": "Son noktayı kaldır", + "reset": { + "label": "Bütün noktaları temizle" + }, + "delete": { + "desc": "{{type}} {{name}}'i silmek istediğinizden emin misiniz?", + "title": "Silmeyi Onayla", + "success": "{{name}} silindi." + }, + "error": { + "mustBeFinished": "Kaydetmeden önce çokgen çizimi bitirilmelidir." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Eylemsizlik sıfırın üzerinde olmalıdır." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Hız eşiği 0.1'e eşit veya daha yüksek olmalıdır." + } + } + }, + "zones": { + "label": "Alanlar", + "documentTitle": "Alanı Düzenle - Frigate", + "desc": { + "documentation": "Dökümantasyon", + "title": "Alanlar, görüntüde belirli bir alanını tanımlamanıza olanak tanır. Böylece bir nesnenin belirli bir alanda olup olmadığını tespit edebilirsiniz." + }, + "add": "Alan Ekle", + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "name": { + "inputPlaceHolder": "Bir isim girin…", + "title": "İsim", + "tips": "Ad en az 2 karakter olmalı, en az bir harf içermeli ve bu kameradaki bir kamera adıyla veya başka bir bölge adıyla aynı olmamalıdır." + }, + "inertia": { + "title": "Eylemsizlik", + "desc": "Bir nesnenin alanda kabul edilmesi için kaç kare boyunca alanda kalması gerektiğini belirtir. Varsayılan: 3" + }, + "objects": { + "title": "Nesneler", + "desc": "Bu alana uygulanan nesnelerin listesi." + }, + "speedEstimation": { + "title": "Hız Tahmini", + "desc": "Bu alandaki nesneler için hız tahminini etkinleştirin. Alan tam olarak 4 noktaya belirlenmiş olmalıdır.", + "docs": "Dökümantasyonu oku", + "lineADistance": "Çizgi A mesafesi ({{unit}})", + "lineBDistance": "Çizgi B mesafesi ({{unit}})", + "lineCDistance": "Çizgi C mesafesi ({{unit}})", + "lineDDistance": "Çizgi D mesafesi ({{unit}})" + }, + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın.", + "edit": "Alanı Düzenle", + "loiteringTime": { + "title": "Oyalanma Süresi", + "desc": "Nesnenin etkinleşmesi için alanda kalması gereken minimum süreyi saniye cinsinden ayarlar. Varsayılan: 0" + }, + "speedThreshold": { + "desc": "Nesnelerin bu alanda kabul edilmesi için minimum hızı belirtir.", + "toast": { + "error": { + "pointLengthError": "Bu alan için hız tahmini devre dışı bırakıldı. Hız tahmini olan alanlar tam olarak 4 noktaya sahip olmak zorundadır.", + "loiteringTimeError": "Oyalanma süreleri 0'dan büyük olan alanlar hız tahmini ile birlikte kullanılmamalıdır." + } + }, + "title": "Hız Alt Sınırı ({{unit}})" + }, + "toast": { + "success": "Bölge ({{zoneName}}) kaydedildi." + }, + "allObjects": "Bütün Nesneler" + }, + "motionMasks": { + "add": "Yeni Hareket Maskesi", + "context": { + "documentation": "Dökümantasyonu okuyun", + "title": "Hareket maskeleri, istenmeyen hareketli nesnelerin (örneğin: ağaç dalları, kamera yayınına gömülü tarih saat, vb.) algılamayı tetiklemesini önlemek için kullanılır. Hareket maskeleri çok dikkatli kullanılmalıdır, zira gereğinden fazla maskeleme nesnelerin tespitini zorlaştıracaktır." + }, + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın.", + "polygonAreaTooLarge": { + "title": "Bu hareket maskesi, kamera görüntüsünün %{{polygonArea}}'sını kaplıyor. Büyük hareket maskeleri kullanmanız önerilmez.", + "documentation": "Dökümantasyonu oku", + "tips": "Hareket maskeleri nesnelerin algılanmasını kesin olarak engellemez. Bunun yerine tespit sınıflandırma sayfasında gerekli alan kısıtlaması ayarlamalısınız." + }, + "toast": { + "success": { + "title": "{{polygonName}} kaydedildi.", + "noName": "Hareket Maskesi kaydedildi." + } + }, + "desc": { + "title": "Hareket maskeleri, istenmeyen hareketlerin algılamayı tetiklemesini önlemek için kullanılır. Gereğinden fazla maskeleme nesnelerin tespitini zorlaştıracaktır.", + "documentation": "Dökümantasyon" + }, + "label": "Hareket Maskesi", + "edit": "Hareket Maskesini Düzenle", + "documentTitle": "Hareket Maskesini Düzenle - Frigate" + }, + "objectMasks": { + "desc": { + "documentation": "Dökümantasyon", + "title": "Nesne filtresi maskeleri, belirli bir nesne türü için konum bazında yanlış pozitifleri filtrelemek için kullanılır." + }, + "point_one": "{{count}} nokta", + "point_other": "{{count}} nokta", + "context": "Nesne filtresi maskeleri, belirli bir nesne türü için konum bazında yanlış pozitifleri filtrelemek için kullanılır.", + "objects": { + "allObjectTypes": "Bütün nesne türleri", + "desc": "Bu nesne maskesinin uygulanacağı nesne türü.", + "title": "Nesneler" + }, + "add": "Nesne Maskesi Ekle", + "edit": "Nesne Maskesini Düzenle", + "toast": { + "success": { + "noName": "Nesne Maskesi kaydedildi.", + "title": "{{polygonName}} kaydedildi." + } + }, + "documentTitle": "Nesne Maskesini Düzenle - Frigate", + "label": "Nesne Maskeleri", + "clickDrawPolygon": "Görüntü üzerinde bir çokgen çizmek için tıklayın." + }, + "restart_required": "Yeniden Başlatma Gerekli (maskeler/alanlar değiştirildi)", + "motionMaskLabel": "Hareket Maskesi {{number}}", + "objectMaskLabel": "Nesne maskesi {{number}} ({{label}})" + }, + "motionDetectionTuner": { + "title": "Hareket Algılama Ayarlayıcı", + "Threshold": { + "desc": "Eşik değeri, bir pikselin parlaklığındaki ne kadar değişikliğin hareket olarak kabul edileceğini belirler. Varsayılan: 30", + "title": "Eşik" + }, + "contourArea": { + "desc": "Kontur alanı değeri, hangi değişen piksel gruplarının hareket olarak nitelendirileceğine karar vermek için kullanılır. Varsayılan: 10", + "title": "Kontur Alanı" + }, + "toast": { + "success": "Hareket ayarları kaydedildi." + }, + "desc": { + "title": "Frigate, kamera görüntüsünde nesne tespiti ile kontrol etmeye değer bir şey olup olmadığını görmek için ilk olarak hareket algılamayı kullanır.", + "documentation": "Hareket Algılama İnce Ayar Kılavuzunu Okuyun" + }, + "improveContrast": { + "desc": "Daha karanlık sahneler için kontrastı iyileştirin. Varsayılan: AÇIK", + "title": "Kontrastı İyileştir" + }, + "unsavedChanges": "Kaydedilmemiş Hareket Alıglama ayarları ({{camera}})" + }, + "debug": { + "title": "Hata Ayıklama", + "boundingBoxes": { + "colors": { + "label": "Nesne Çerçeve Renkleri", + "info": "
  • Başlangıçta, her nesne etiketi için farklı renkler atanacaktır
  • Koyu mavi ince bir çizgi, nesnenin şu anda algılanamadığını gösterir
  • Gri ince bir çizgi, nesnenin sabit olduğunun algılandığını gösterir
  • Kalın bir çizgi, nesnenin otomatik izlemeye (eğer açıksa) tabi olduğunu gösterir
  • " + }, + "title": "Çerçeveler", + "desc": "İzlenen nesnelerin etrafında çerçeve göster" + }, + "timestamp": { + "title": "Zaman Damgası", + "desc": "Görüntüye bir zaman damgası ekle" + }, + "motion": { + "title": "Hareket kutuları", + "desc": "Hareketin algılandığı alanların etrafında çerçeve göster", + "tips": "

    Hareket Kutuları


    Hareketin algılandığı alanlar kırmızı çerçeve ile gösterilecektir.

    " + }, + "regions": { + "title": "Tespit Bölgeleri", + "desc": "Nesne algılayıcıya gönderilen tespit alanlarını göster", + "tips": "

    Bölge Kutuları


    Nesne dedektörüne gönderilen tespit alanları görüntüde parlak yeşil renk çerçeve ile gösterilir.

    " + }, + "objectShapeFilterDrawing": { + "title": "Nesne Şekil Filtresi Çizimi", + "desc": "Alan ve oran ayrıntılarını görüntülemek için görüntü üzerinde bir dikdörtgen çizin", + "score": "Puan", + "ratio": "Oran", + "area": "Alan", + "document": "Dökümantasyonu oku ", + "tips": "Alanını ve oranını göstermek için kamera görüntüsü üzerinde bir dikdörtgen çizmek için bu seçeneği etkinleştirin. Bu değerler daha sonra yapılandırmanızda nesne şekil filtresi parametrelerini ayarlamak için kullanılabilir." + }, + "debugging": "Hata Ayıklama", + "detectorDesc": "Frigate, kamera video akışınızdaki nesneleri algılamak için algılayıcılar ({{detectors}}) kullanır.", + "noObjects": "Nesne Yok", + "mask": { + "title": "Hareket maskeleri", + "desc": "Tanımlanmış hareket maskelerinin sınırlarını göster" + }, + "zones": { + "title": "Alanlar", + "desc": "Tanımlanmış alanların sınırlarını göster" + }, + "objectList": "Nesne Listesi", + "desc": "Hata ayıklama görünümü, izlenen nesnelerin ve istatistiklerinin gerçek zamanlı bir görünümünü gösterir. Nesne listesi algılanan nesnelerin zaman gecikmeli bir özetini gösterir.", + "paths": { + "title": "Hareket İzi", + "desc": "Takip edilen nesnenin hareket izi üzerindeki önemli noktaları göster", + "tips": "

    Hareket İzi


    Çizgiler ve daireler, takip edilen nesnenin yaşam döngüsü boyunca hareket ettiği önemli noktaları gösterir.

    " + }, + "openCameraWebUI": "{{camera}}'nın Web Arayüzünü Aç", + "audio": { + "title": "Ses", + "noAudioDetections": "Ses tespiti yok", + "score": "skor", + "currentRMS": "Şu Anki RMS", + "currentdbFS": "Şu Anki dbFS" + } + }, + "users": { + "title": "Kullanıcılar", + "management": { + "title": "Kullanıcı Yönetimi", + "desc": "Bu Frigate kurulumundaki kullanıcı hesaplarını yönetin." + }, + "addUser": "Kullanıcı Ekle", + "toast": { + "success": { + "deleteUser": "{{user}} kullanıcısı başarıyla silindi", + "roleUpdated": "{{user}} için rol güncellendi", + "createUser": "{{user}} kullanıcısı başarıyla oluşturuldu", + "updatePassword": "Parola başarıyla güncellendi." + }, + "error": { + "setPasswordFailed": "Parola kaydedilemedi: {{errorMessage}}", + "createUserFailed": "Kullanıcı oluşturulamadı: {{errorMessage}}", + "deleteUserFailed": "Kullanıcı silinemedi: {{errorMessage}}", + "roleUpdateFailed": "Rol güncellenemedi: {{errorMessage}}" + } + }, + "table": { + "username": "Kullanıcı Adı", + "actions": "Eylemler", + "noUsers": "Kullanıcı bulunamadı.", + "changeRole": "Kullanıcı rolünü değiştir", + "deleteUser": "Kullanıcıyı sil", + "role": "Rol", + "password": "Parola" + }, + "dialog": { + "form": { + "user": { + "placeholder": "Kullanıcı adı girin", + "title": "Kullanıcı Adı", + "desc": "Yalnızca harfler, sayılar, noktalar ve alt çizgiler kullanabilirsiniz." + }, + "password": { + "title": "Parola", + "placeholder": "Parola girin", + "confirm": { + "title": "Parolayı Onayla", + "placeholder": "Parolayı Onayla" + }, + "strength": { + "title": "Parola kuvveti: ", + "weak": "Zayıf", + "medium": "Orta", + "strong": "Güçlü", + "veryStrong": "Çok Güçlü" + }, + "notMatch": "Parolalar eşleşmiyor", + "match": "Parolalar eşleşiyor", + "show": "Şifreyi göster", + "hide": "Şifreyi gizle", + "requirements": { + "title": "Şifre gereksinimleri:", + "length": "En az 8 karakter", + "uppercase": "En az bir büyük harf", + "digit": "En az bir rakam", + "special": "En az bir özel karakter (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "placeholder": "Yeni parola girin", + "confirm": { + "placeholder": "Yeni parolayı tekrar girin" + }, + "title": "Yeni Parola" + }, + "usernameIsRequired": "Kullanıcı adı gereklidir", + "passwordIsRequired": "Parola gereklidir", + "currentPassword": { + "title": "Mevcut Şifre", + "placeholder": "Mevcut şifrenizi girin" + } + }, + "createUser": { + "title": "Yeni Kullanıcı Oluştur", + "desc": "Yeni bir kullanıcı hesabı ekleyin ve Frigate'deki erişim düzeylerini sınırlandırmak için bir rol belirtin.", + "usernameOnlyInclude": "Kullanıcı adı yalnızca harfler, sayılar, nokta veya alt çizgi (_) içerebilir", + "confirmPassword": "Lütfen parolanızı onaylayın" + }, + "deleteUser": { + "title": "Kullanıcıyı Sil", + "warn": "{{username}}'i silmek istediğinizden emin misiniz?", + "desc": "Bu işlem geri alınamaz. Bu, kullanıcı hesabını kalıcı olarak silecek ve tüm ilişkili verileri kaldıracaktır." + }, + "passwordSetting": { + "updatePassword": "{{username}} için Parola Belirle", + "setPassword": "Parola Belirle", + "desc": "Bu hesabı güvenli hale getirmek güçlü bir parola belirleyin.", + "cannotBeEmpty": "Parola boş olamaz", + "doNotMatch": "Parolalar eşleşmiyor", + "currentPasswordRequired": "Mevcut şifre gerekli", + "incorrectCurrentPassword": "Mevcut şifre yanlış", + "passwordVerificationFailed": "Şifre doğrulanamadı", + "multiDeviceWarning": "Oturum açtığınız diğer tüm cihazlarda {{refresh_time}} içinde yeniden oturum açmanız gerekecektir. Ayrıca, JWT gizli anahtarınızı döndürerek tüm kullanıcıların hemen yeniden kimlik doğrulaması yapmasını da sağlayabilirsiniz." + }, + "changeRole": { + "title": "Kullanıcı Rolünü Değiştir", + "desc": "{{username}} için izinleri güncelle", + "roleInfo": { + "adminDesc": "Tüm özelliklere tam erişim.", + "intro": "Bu kullanıcı için bir rol seçin:", + "admin": "Yönetici", + "viewer": "Görüntüleyici", + "viewerDesc": "Yalnızca Canlı, İncele, Keşfet ve Dışa Aktar'a girebilir.", + "customDesc": "Belirli kamera erişimine sahip özel rol." + }, + "select": "Bir rol seçin" + } + }, + "updatePassword": "Parola Belirle" + }, + "notification": { + "title": "Bildirimler", + "notificationSettings": { + "title": "Bildirim Ayarları", + "documentation": "Dökümantasyonu Oku", + "desc": "Frigate, tarayıcıdan veya web uygulaması (PWA) olarak kullanıyor olmanız fark etmeksizin, tarayıcınızın bildirimler özelliği aracılığıyla bildirimler gönderebilir." + }, + "notificationUnavailable": { + "title": "Bildirimler Kullanılamıyor", + "documentation": "Dökümantasyonu Oku", + "desc": "Web push bildirimleri güvenli bağlantı (https://…) gerektirir. Bu tarayıcınızın bir sınırlandırmasıdır. Bildirimleri kullanmak için Frigate arayüzüne HTTPS ile erişin." + }, + "globalSettings": { + "title": "Genel Ayarlar", + "desc": "Kayıtlı tüm cihazlarda belirli kameralar için bildirimleri geçici olarak askıya alın." + }, + "email": { + "title": "E-posta", + "desc": "Geçerli bir e-posta adresi gereklidir ve push hizmetiyle ilgili herhangi bir sorun olması durumunda sizi bilgilendirmek için kullanılacaktır.", + "placeholder": "örn. ornek@eposta.com" + }, + "cameras": { + "desc": "Bildirimlerin etkinleştirileceği kameraları seçin.", + "title": "Kameralar", + "noCameras": "Kullanılabilir kamera yok" + }, + "deviceSpecific": "Cihaza Özel Ayarlar", + "suspended": "Bildirimler askıya alındı {{time}}", + "suspendTime": { + "1hour": "1 saat süreyle askıya al", + "12hours": "12 saat süreyle askıya al", + "24hours": "24 saat süreyle askıya al", + "30minutes": "30 dakika süreyle askıya al", + "untilRestart": "Yeniden başlatılana kadar askıya al", + "10minutes": "10 dakika süreyle askıya al", + "5minutes": "5 dakika süreyle askıya al", + "suspend": "Askıya Al" + }, + "toast": { + "success": { + "registered": "Bildirimlere başarıyla kaydolundu. Herhangi bir bildirimin (test bildirimi dahil) gönderilebilmesi için Frigate'in yeniden başlatılması gereklidir.", + "settingSaved": "Bildirim ayarları kaydedildi." + }, + "error": { + "registerFailed": "Bildirimlere kaydolunurken hata oluştu." + } + }, + "registerDevice": "Bu Cihazı Kaydet", + "sendTestNotification": "Bir test bildirimi gönder", + "cancelSuspension": "Askıya Almayı İptal Et", + "unregisterDevice": "Bu Cihazın Kaydını Sil", + "active": "Bildirimler Aktif", + "unsavedChanges": "Kaydedilmemiş bildirim ayar değişiklikleri", + "unsavedRegistrations": "Kaydedilmemiş bildirim ayar değişiklikleri" + }, + "frigatePlus": { + "title": "Frigate+ Ayarları", + "apiKey": { + "title": "Frigate+ API Anahtarı", + "validated": "Frigate+ API anahtarınız doğrulandı", + "plusLink": "Frigate+ hakkında daha fazla bilgi edinin", + "notValidated": "Frigate+ API anahtarı bulunamadı veya doğrulanamadı", + "desc": "Frigate+ API anahtarı, Frigate+ hizmetiyle entegrasyonu sağlar." + }, + "snapshotConfig": { + "title": "Fotoğraf Yapılandırması", + "documentation": "Dökümantasyonu oku", + "table": { + "camera": "Kamera", + "snapshots": "Fotoğraflar", + "cleanCopySnapshots": "clean_copy Fotoğraflar" + }, + "desc": "Frigate+'a göndermek için yapılandırmanızda hem fotoğrafların hem de clean_copy fotoğraflarının etkinleştirilmesi gerekir.", + "cleanCopyWarning": "Bazı kameralarda fotoğraflar etkin ancak temiz kopya özelliği devre dışı. Bu kameralardan Frigate+'a görüntü gönderebilmek için fotoğraf(snapshots) yapılandırmanızda clean_copy'yi etkinleştirmeniz gerekiyor." + }, + "modelInfo": { + "error": "Model bilgileri yüklenemedi", + "loadingAvailableModels": "Kullanılabilir modeller yükleniyor…", + "loading": "Model bilgileri yükleniyor…", + "modelType": "Model Türü", + "dimensions": "Boyutlar", + "availableModels": "Kullanılabilir Modeller", + "modelSelect": "Frigate+'daki kullanılabilir modelleriniz buradan seçilebilir. Yalnızca mevcut algılayıcı yapılandırmanızla uyumlu modellerin seçilebileceğini unutmayın.", + "baseModel": "Temel Model", + "title": "Model Bilgileri", + "trainDate": "Eğitim Tarihi", + "supportedDetectors": "Desteklenen Algılayıcılar", + "cameras": "Kameralar", + "plusModelType": { + "userModel": "İnce Ayarlı", + "baseModel": "Baz Model" + } + }, + "toast": { + "success": "Frigate+ ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + }, + "restart_required": "Yeniden Başlatma Gerekli (Frigate+ modeli değiştirildi)", + "unsavedChanges": "Kaydedilmemiş Frigate+ ayar değişiklikleri" + }, + "enrichments": { + "birdClassification": { + "title": "Kuş Sınıflandırma", + "desc": "Kuş sınıflandırma, kuantize edilmiş bir Tensorflow modeli aracılığıyla bilinen kuş türlerini tespit eder. Bilinen bir kuş türü tespit edildiğinde, bu türün bilinen adı bir alt etiket olarak ilgili öğeye iliştirilir. Bu bilgi; arayüzde ve bildirimlerde gösterilir ve filtre olarak kullanılabilir." + }, + "unsavedChanges": "Değişitirilen zenginleştirme ayarları kaydedilmedi", + "semanticSearch": { + "reindexNow": { + "desc": "Yeniden dizinleme işlemi, bütün takip edilen nesneler için gömüleri tekrar üretecektir. Bu işlem arka planda gerçekleşir, fakat nesne sayısına bağlı olarak çok işlemci kullanabilir veya tamamlanması makul bir süre alabilir.", + "label": "Şimdi Yeniden Dizinle", + "confirmTitle": "Yeniden Dizinlemeyi Onayla", + "confirmDesc": "Bütün takip edilen nesnelerin gömülerini yeniden dizinlemek istediğinize emin misiniz? Bu işlem arka planda gerçekleşecektir fakat nesne sayısına bağlı olarak çok işlemci kullanabilir ve biraz süre alabilir. İlerlemesini Keşfet sayfasından görüntüleyebilirsiniz.", + "confirmButton": "Yeniden Dizinle", + "success": "Yeniden dizinleme başarıyla başlatıldı.", + "alreadyInProgress": "Yeniden dizinleme zaten sürüyor.", + "error": "Yeniden dizinleme başlatılamadı: {{errorMessage}}" + }, + "title": "Anlamsal Arama", + "desc": "Anlamsal arama, takip edilen nesneleri; ya görselin kendisini kullanarak ya da görsel öğelere ait açıklamalar üzerinden (kullanıcı tarafından yazılmış ya da otomatik oluşturulmuş) metinle arama yaparak bulmanıza olanak tanır.", + "readTheDocumentation": "Dökümantasyonu Oku", + "modelSize": { + "label": "Model Boyutu", + "desc": "Anlamsal aramadaki gömüler için kullanılan modelin büyüklüğü.", + "small": { + "title": "küçük", + "desc": "Modelin küçük sürümü, gömme kalitesinde fark edilmesi zor bir değişikliğe karşılık daha az RAM kullanan ve CPU’da daha hızlı çalışan kuantize bir model kullanır." + }, + "large": { + "title": "büyük", + "desc": "Modelin büyük sürümü tam boyutlu Jina modelini kullanır ve uygunsa GPU'da çalışacaktır." + } + } + }, + "title": "Zenginleştirme Ayarları", + "faceRecognition": { + "title": "Yüz Tanıma", + "desc": "Yüz tanıma, tespit edilen insanların yüzleri tanındığında onlara isim atamanıza olanak sağlar ve bu isim bilgisi alt etiket olarak iliştirilir. Bu bilgi; arayüzde ve bildirimlerde gösterilir ve filtre olarak kullanılabilir.", + "readTheDocumentation": "Dökümantasyonu Oku", + "modelSize": { + "label": "Model Boyutu", + "desc": "Yüz tanıma için kullanılan modelin büyüklüğü.", + "small": { + "title": "küçük", + "desc": "Modelin küçük sürümü olarak çoğu işlemcide verimli çalışabilen bir FaceNet yüz gömü modeli kullanılır." + }, + "large": { + "title": "büyük", + "desc": "Modelin büyük sürümü olarak ArcFace yüz gömü modeli kullanılır ve uygunsa GPU'da çalışır." + } + } + }, + "licensePlateRecognition": { + "title": "Plaka Tanıma", + "desc": "Frigate tespit edilen araçların tescil plakalarındaki karakterleri okuyabilir ve ilgili araba türündeki nesnenin recognized_license_plate bölümüne, varsa bilindik bir adını da sub_label bölümüne ekleyebilir. Bu özelliği bir yoldan geçen yahut evin önüne park eden araçların plakalarını okumak için kullanabilirsiniz.", + "readTheDocumentation": "Dökümantasyonu Oku" + }, + "restart_required": "Yeniden başlatma gerekli (zenginleştirme ayarları değişti)", + "toast": { + "success": "Zenginleştirme ayarları kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın.", + "error": "Yapılandırma değişiklikleri kaydedilemedi: {{errorMessage}}" + } + }, + "triggers": { + "dialog": { + "form": { + "name": { + "error": { + "invalidCharacters": "Alan yalnızca harf, rakam, alt çizgi ve tire içerebilir.", + "minLength": "Alan en az 2 karakter uzunluğunda olmalıdır.", + "alreadyExists": "Bu kamerada aynı isimle bir tetik zaten mevcut." + }, + "title": "İsim", + "placeholder": "Bu tetikleyiciye ad verin", + "description": "Bu tetikleyiciyi tanımlamak için benzersiz bir ad veya açıklama girin" + }, + "enabled": { + "description": "Bu tetiği açın veya kapatın" + }, + "type": { + "title": "Tetik Türü", + "placeholder": "Tetik türünü seçin", + "description": "Benzer izlenen nesne açıklaması algılandığında tetiklenir", + "thumbnail": "Benzer izlenen nesne küçük resmi algılandığında tetiklenir" + }, + "content": { + "title": "İçerik", + "imagePlaceholder": "Bir küçük resim seçin", + "textPlaceholder": "Metin içeriği girin", + "imageDesc": "Yalnızca en son 100 küçük resim görüntülenir. İstediğiniz küçük resmi bulamazsanız, lütfen Keşfet bölümündeki önceki nesneleri inceleyin ve oradaki menüden bir tetikleyici ayarlayın.", + "textDesc": "Benzer bir takip edilen nesne açıklaması algılandığında bu eylemi tetiklemek için metin girin.", + "error": { + "required": "İçerik gereklidir." + } + }, + "threshold": { + "title": "Tetik Eşiği", + "error": { + "min": "Tetik eşiği 0 ile 1 arasında olmalıdır", + "max": "Tetik eşiği 0 ile 1 arasında olmalıdır" + }, + "desc": "Bu tetikleyici için benzerlik eşiğini ayarlayın. Daha yüksek bir eşik, tetiği tetiklemek için daha yakın bir eşleşme gerektiği anlamına gelir." + }, + "actions": { + "title": "Eylemler", + "desc": "Varsayılan olarak, Frigate tüm tetikleyiciler için bir MQTT mesajı gönderir. Alt etiketler, tetikleyici adını nesne etiketine ekler. Nitelikler, izlenen nesne meta verilerinde ayrı olarak depolanan aranabilir meta verilerdir.", + "error": { + "min": "En az bir eylem seçilmelidir." + } + } + }, + "createTrigger": { + "title": "Tetik Oluştur", + "desc": "{{camera}} kamerası için tetik oluşturun" + }, + "editTrigger": { + "title": "Tetiği Düzenle", + "desc": "{{camera}} kamerasındaki tetiğin ayarlarını düzenleyin" + }, + "deleteTrigger": { + "title": "Tetiği Sil", + "desc": "{{triggerName}} isimli tetiği silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + } + }, + "documentTitle": "Tetikler", + "management": { + "title": "Tetikleyiciler", + "desc": "{{camera}} için tetikleri yönetin. Seçtiğiniz takip edilen nesneye benzer küçük resimlerde tetiklemek için küçük resmi kullanın veya belirlediğiniz metne benzer açıklamalar çıkması durumunda tetiklemek için ise açıklama seçeneğini kullanın." + }, + "addTrigger": "Tetik Ekle", + "table": { + "name": "İsim", + "type": "Tetik Türü", + "content": "İçerik", + "threshold": "Tetik Eşiği", + "actions": "Eylemler", + "noTriggers": "Bu kamera için hiç bir tetik ayarlanmadı.", + "edit": "Düzenle", + "deleteTrigger": "Tetiği Sil", + "lastTriggered": "En son tetikleme" + }, + "type": { + "thumbnail": "Küçük Resim", + "description": "Açıklama" + }, + "actions": { + "alert": "Alarm Olarak İşaretle", + "notification": "Bildirim Gönder", + "sub_label": "Alt Etiket Ekle", + "attribute": "Özellik Ekle" + }, + "toast": { + "success": { + "createTrigger": "Tetik {{name}} başarıyla oluşturuldu.", + "updateTrigger": "Tetik {{name}} başarıyla güncellendi.", + "deleteTrigger": "Tetik {{name}} başarıyla silindi." + }, + "error": { + "createTriggerFailed": "Tetik oluşturulamadı: {{errorMessage}}", + "updateTriggerFailed": "Tetik güncellenemedi: {{errorMessage}}", + "deleteTriggerFailed": "Tetik silinemedi: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Anlamsal Arama devre dışı bırakıldı", + "desc": "Tetikleyicileri kullanmak için Anlamsal Arama'nın etkinleştirilmesi gerekir." + }, + "wizard": { + "title": "Tetikleyici Oluştur", + "step1": { + "description": "Tetikleyiciniz için temel ayarları yapılandırın." + }, + "step2": { + "description": "Bu eylemi tetikleyecek içeriği ayarlayın." + }, + "step3": { + "description": "Bu tetikleyici için eşik değerini ve eylemleri yapılandırın." + }, + "steps": { + "nameAndType": "Ad ve Tür", + "configureData": "Verileri Yapılandır", + "thresholdAndActions": "Eşik ve Eylemler" + } + } + }, + "cameraWizard": { + "title": "Kamera Ekle", + "description": "Aşağıdaki adımları izleyerek Frigate kurulumunuza yeni bir kamera ekleyin.", + "steps": { + "nameAndConnection": "Ad & Bağlantı", + "probeOrSnapshot": "Probe veya Anlık Görüntü", + "streamConfiguration": "Akış Yapılandırması", + "validationAndTesting": "Doğrulama ve Test" + }, + "save": { + "success": "Yeni kamera {{cameraName}} başarıyla kaydedildi.", + "failure": "{{cameraName}} kaydedilirken hata oluştu." + }, + "testResultLabels": { + "resolution": "Çözünürlük", + "video": "Video", + "audio": "Ses", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Lütfen geçerli bir akış URL'si sağlayın", + "testFailed": "Akış testi başarısız oldu: {{error}}" + }, + "step1": { + "description": "Kamera bilgilerinizi girin ve kamerayı taramayı (probe) ya da markayı manuel olarak seçmeyi tercih edin.", + "cameraName": "Kamera Adı", + "cameraNamePlaceholder": "ör. front_door veya Arka Bahçe Genel Görünümü", + "host": "Ana Makine / IP Adresi", + "port": "Port", + "username": "Kullanıcı adı", + "usernamePlaceholder": "İsteğe bağlı", + "password": "Şifre", + "passwordPlaceholder": "İsteğe bağlı", + "selectTransport": "İletişim protokolünü seçin", + "cameraBrand": "Kamera Markası", + "selectBrand": "URL şablonu için kamera markasını seçin", + "customUrl": "Özel Akış URL’si", + "brandInformation": "Marka Bilgileri", + "brandUrlFormat": "RTSP URL formatı şu şekilde olan kameralar için: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://kullanıcıadı:şifre@host:port/path", + "connectionSettings": "Bağlantı Ayarları", + "detectionMethod": "Akış Algılama Yöntemi", + "onvifPort": "ONVIF Portu", + "probeMode": "Kamerayı tara", + "manualMode": "Manuel seçim", + "detectionMethodDescription": "Kamera akış URL’lerini bulmak için kamerayı ONVIF ile tarayın (destekleniyorsa) veya ön tanımlı URL’leri kullanmak için kamera markasını manuel olarak seçin. Özel bir RTSP URL’si girmek için manuel yöntemi seçin ve “Diğer”i işaretleyin.", + "onvifPortDescription": "ONVIF'i destekleyen kameralarda bu genellikle 80 veya 8080'dir.", + "useDigestAuth": "Digest kimlik doğrulamasını kullan", + "errors": { + "nameRequired": "Kamera adı gerekli", + "nameLength": "Kamera adı 64 karakter veya daha az olmalıdır", + "invalidCharacters": "Kamera adı geçersiz karakterler içeriyor", + "nameExists": "Kamera adı zaten mevcut", + "customUrlRtspRequired": "Özel URL'ler \"rtsp://\" ile başlamalıdır. RTSP olmayan kamera akışları için manuel yapılandırma gereklidir.", + "brandOrCustomUrlRequired": "Bir kamera markası seçip host/IP adresi girin ya da özel bir URL kullanmak için ‘Diğer’ seçeneğini tercih edin" + }, + "useDigestAuthDescription": "ONVIF için HTTP digest kimlik doğrulamasını kullanın. Bazı kameralar, standart yönetici kullanıcısı yerine özel bir ONVIF kullanıcı adı/şifresi gerektirebilir." + }, + "step2": { + "description": "Mevcut akışları bulmak için kamerayı tarayın veya seçtiğiniz algılama yöntemine göre manuel ayarları yapılandırın.", + "testSuccess": "Bağlantı testi başarılı!", + "testFailed": "Bağlantı testi başarısız oldu. Lütfen tüm alanları kontrol edip tekrar deneyin.", + "testFailedTitle": "Test Başarısız", + "streamDetails": "Akış Ayrıntıları", + "probing": "Kamera taranıyor...", + "retry": "Yeniden dene", + "testing": { + "probingMetadata": "Kamera meta verileri inceleniyor...", + "fetchingSnapshot": "Kamera anlık görüntüsü alınıyor..." + }, + "probeFailed": "Kamerayı tarama başarısız oldu: {{error}}", + "probingDevice": "Cihaz taranıyor…", + "probeSuccessful": "Tarama başarılı", + "probeError": "Tarama hatası", + "probeNoSuccess": "Tarama başarısız", + "deviceInfo": "Cihaz Bilgileri", + "manufacturer": "Üretici", + "model": "Modeli", + "firmware": "Donanım yazılımı", + "profiles": "Profiller", + "ptzSupport": "PTZ Desteği", + "autotrackingSupport": "Otomatik Takip Desteği", + "presets": "Ön ayarlar", + "rtspCandidates": "RTSP Yayınları", + "rtspCandidatesDescription": "Kamera taramasından aşağıdaki RTSP URL'leri bulundu. Akış meta verilerini görüntülemek için bağlantıyı test edin.", + "noRtspCandidates": "Kameradan RTSP URL'si bulunamadı. Kimlik bilgileriniz yanlış olabilir veya kamera ONVIF'i veya RTSP URL'lerini almak için kullanılan yöntemi desteklemiyor olabilir. Geri dönün ve RTSP URL'sini manuel olarak girin.", + "candidateStreamTitle": "Yayın {{number}}", + "useCandidate": "Kullan", + "uriCopy": "Kopyala", + "uriCopied": "URI panoya kopyalandı", + "testConnection": "Bağlantıyı Test Et", + "toggleUriView": "Tam URI görünümünü değiştirmek için tıklayın", + "connected": "Bağlandı", + "notConnected": "Bağlı Değil", + "errors": { + "hostRequired": "Host/IP adresi gereklidir" + } + }, + "step3": { + "description": "Akış rollerini yapılandırın ve kameranız için ek akışlar ekleyin.", + "streamsTitle": "Kamera Yayınları", + "addStream": "Yayın Ekle", + "addAnotherStream": "Başka Bir Yayın Ekle", + "streamTitle": "Yayın {{number}}", + "streamUrl": "Yayın URL'si", + "streamUrlPlaceholder": "rtsp://kullanıcıadı:şifre@host:port/path", + "selectStream": "Bir yayın seçin", + "searchCandidates": "Yayınları arayın...", + "noStreamFound": "Yayın bulunamadı", + "url": "URL", + "resolution": "Çözünürlük", + "selectResolution": "Çözünürlüğü seçin", + "quality": "Kalite", + "selectQuality": "Kaliteyi seçin", + "roles": "Roller", + "roleLabels": { + "detect": "Nesne Algılama", + "record": "Kayıt", + "audio": "Ses" + }, + "testStream": "Bağlantıyı Test Et", + "testSuccess": "Yayın testi başarılı!", + "testFailed": "Yayın testi başarısız oldu", + "testFailedTitle": "Test Başarısız", + "connected": "Bağlı", + "notConnected": "Bağlı Değil", + "featuresTitle": "Özellikler", + "go2rtc": "Kameraya olan bağlantıları azaltın", + "detectRoleWarning": "Devam edebilmek için en az bir akışın \"algılama\" rolüne sahip olması gerekir.", + "rolesPopover": { + "title": "Yayın Rolleri", + "detect": "Nesne tespiti için ana besleme.", + "record": "Yapılandırma ayarlarına göre video akışının bölümlerini kaydeder.", + "audio": "Ses tabanlı algılama için besleme." + }, + "featuresPopover": { + "title": "Yayın Özellikleri", + "description": "Kameranıza olan bağlantıları azaltmak için go2rtc yeniden akışını kullanın." + } + }, + "step4": { + "disconnectStream": "Bağlantıyı kes", + "estimatedBandwidth": "Tahmini Bant Genişliği", + "roles": "Roller", + "ffmpegModule": "Yayın uyumluluk modunu kullan", + "ffmpegModuleDescription": "Yayın birkaç denemeden sonra yüklenmezse, bunu etkinleştirmeyi deneyin. Etkinleştirildiğinde, Frigate go2rtc ile ffmpeg modülünü kullanacaktır. Bu, bazı kamera yayınları ile daha iyi uyumluluk sağlayabilir.", + "none": "Hiçbiri", + "error": "Hata", + "description": "Yeni kameranızı kaydetmeden önce son doğrulama ve analiz. Kaydetmeden önce her akışı bağlayın.", + "validationTitle": "Yayın Doğrulaması", + "connectAllStreams": "Tüm Yayınları Bağla", + "reconnectionSuccess": "Yeniden bağlantı başarılı.", + "reconnectionPartial": "Bazı yayınlara yeniden bağlanılamadı.", + "streamUnavailable": "Yayın önizlemesi kullanılamıyor", + "reload": "Yeniden yükle", + "connecting": "Bağlanıyor...", + "streamTitle": "Yayın {{number}}", + "valid": "Geçerli", + "failed": "Başarısız", + "notTested": "Test edilmedi", + "connectStream": "Bağlan", + "connectingStream": "Bağlanıyor", + "streamValidated": "{{number}} yayını başarıyla doğrulandı", + "streamValidationFailed": "Yayın {{number}} doğrulaması başarısız oldu", + "saveAndApply": "Yeni Kamerayı Kaydet", + "saveError": "Geçersiz yapılandırma. Lütfen ayarlarınızı kontrol edin.", + "issues": { + "title": "Yayın Doğrulaması", + "videoCodecGood": "Video kodeği {{codec}}.", + "audioCodecGood": "Ses kodeği {{codec}}.", + "resolutionHigh": "{{resolution}} çözünürlüğü kaynak kullanımının artmasına neden olabilir.", + "resolutionLow": "{{resolution}} çözünürlüğü, küçük nesnelerin güvenilir bir şekilde algılanması için çok düşük olabilir.", + "noAudioWarning": "Bu yayın için ses algılanmadı, kayıtlarda ses bulunmayacak.", + "audioCodecRecordError": "Kayıtlarda sesi desteklemek için AAC ses kodeği gereklidir.", + "audioCodecRequired": "Ses algılamayı desteklemek için bir ses akışı gereklidir.", + "restreamingWarning": "Kayıt akışı için kameraya olan bağlantıları azaltmak CPU kullanımını bir miktar artırabilir.", + "brands": { + "reolink-rtsp": "Reolink RTSP önerilmez. Kameranın donanım yazılımı ayarlarında HTTP'yi etkinleştirin ve sihirbazı yeniden başlatın." + }, + "dahua": { + "substreamWarning": "Alt akış 1 düşük çözünürlüğe kilitlenmiştir. Birçok Dahua / Amcrest / EmpireTech kamera, kamera ayarlarında etkinleştirilmesi gereken ek alt akışları destekler. Mevcutsa, bu akışları kontrol edip kullanmanız önerilir." + }, + "hikvision": { + "substreamWarning": "Alt akış 1 düşük çözünürlüğe kilitlendi. Birçok Hikvision kamera, kamera ayarlarında etkinleştirilmesi gereken ek alt akışları destekler. Mevcutsa, bu akışları kontrol edip kullanmanız önerilir." + } + } + } + }, + "cameraManagement": { + "title": "Kameraları Yönet", + "addCamera": "Yeni Kamera Ekle", + "editCamera": "Kamerayı Düzenle:", + "selectCamera": "Bir Kamera Seçin", + "backToSettings": "Kamera Ayarlarına Dön", + "streams": { + "title": "Kameraları Etkinleştir / Devre Dışı Bırak", + "desc": "Frigate yeniden başlatılana kadar bir kamerayı geçici olarak devre dışı bırakın. Bir kamerayı devre dışı bırakmak, Frigate'in bu kameranın akışlarını işlemesini tamamen durdurur. Algılama, kayıt ve hata ayıklama kullanılamaz.
    Not: Bu, go2rtc yeniden akışlarını devre dışı bırakmaz." + }, + "cameraConfig": { + "add": "Kamera Ekle", + "edit": "Kamerayı Düzenle", + "description": "Yayınlar ve roller dahil olmak üzere kamera ayarlarını yapılandırın.", + "name": "Kamera Adı", + "nameRequired": "Kamera adı gerekli", + "nameLength": "Kamera adı 64 karakterden az olmalıdır.", + "namePlaceholder": "örneğin, ön_kapı veya Arka Bahçe Genel Bakışı", + "enabled": "Etkinleştirilmiş", + "ffmpeg": { + "inputs": "Giriş Yayınları", + "path": "Yayın Yolu", + "pathRequired": "Yayın yolu gereklidir", + "pathPlaceholder": "rtsp://...", + "roles": "Roller", + "rolesRequired": "En az bir rol gereklidir", + "rolesUnique": "Her rol (ses, algılama, kayıt) yalnızca bir akışa atanabilir", + "addInput": "Giriş Yayını Ekle", + "removeInput": "Giriş Yayınını Kaldır", + "inputsRequired": "En az bir giriş yayını gereklidir" + }, + "go2rtcStreams": "go2rtc Yayınları", + "streamUrls": "Yayın URL'leri", + "addUrl": "URL ekle", + "addGo2rtcStream": "go2rtc Yayını Ekle", + "toast": { + "success": "Kamera {{cameraName}} başarıyla kaydedildi" + } + } + }, + "cameraReview": { + "title": "Kamera İnceleme Ayarları", + "object_descriptions": { + "title": "Üretken Yapay Zeka Nesne Açıklamaları", + "desc": "Bu kamera için Yapay Zeka Nesne Tanımlamalarını geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameradaki izlenen nesneler için Yapay Zeka tarafından oluşturulan tanımlar istenmeyecektir." + }, + "review_descriptions": { + "title": "Üretken Yapay Zeka İnceleme Açıklamaları", + "desc": "Bu kamera için Yapay Zeka Üretici İnceleme açıklamalarını geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, bu kameradaki inceleme öğeleri için Yapay Zeka tarafından oluşturulan açıklamalar istenmeyecektir." + }, + "review": { + "title": "İncele", + "desc": "Frigate yeniden başlatılana kadar bu kamera için uyarıları ve algılamaları geçici olarak etkinleştirin/devre dışı bırakın. Devre dışı bırakıldığında, yeni inceleme öğeleri oluşturulmaz. ", + "alerts": "Uyarılar ", + "detections": "Tespitler " + }, + "reviewClassification": { + "title": "Sınıflandırmayı İncele", + "desc": "Frigate, inceleme öğelerini Uyarılar ve Algılamalar olarak kategorilere ayırır. Varsayılan olarak, tüm kişi ve araba nesneleri Uyarı olarak kabul edilir. İnceleme öğelerinizin kategorilendirmesini, bunlar için gerekli bölgeleri yapılandırarak iyileştirebilirsiniz.", + "noDefinedZones": "Bu kamera için herhangi bir bölge tanımlanmamıştır.", + "objectAlertsTips": "{{cameraName}} üzerindeki tüm {{alertsLabels}} nesneleri Uyarılar olarak gösterilecektir.", + "zoneObjectAlertsTips": "{{cameraName}} üzerinde, {{zone}} bölgesinde tespit edilen tüm {{alertsLabels}} nesneleri Uyarılar olarak gösterilecektir.", + "objectDetectionsTips": "{{cameraName}} üzerinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, hangi bölgede olursa olsun Tespitler olarak gösterilecektir.", + "zoneObjectDetectionsTips": { + "text": "{{cameraName}} üzerindeki {{zone}} bölgesinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, Tespitler olarak gösterilecektir.", + "notSelectDetections": "{{cameraName}} üzerinde {{zone}} bölgesinde tespit edilen ve Uyarı olarak kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, hangi bölgede olurlarsa olsunlar Tespitler olarak gösterilecektir.", + "regardlessOfZoneObjectDetectionsTips": "{{cameraName}} üzerinde kategorize edilmemiş tüm {{detectionsLabels}} nesneleri, bulundukları bölgeden bağımsız olarak Tespitler (Detections) olarak gösterilecektir." + }, + "unsavedChanges": "{{camera}} için Kaydedilmemiş İnceleme Sınıflandırması ayarları", + "selectAlertsZones": "Uyarılar için bölgeleri seçin", + "selectDetectionsZones": "Tespitler için bölgeleri seçin", + "limitDetections": "Tespitleri belirli bölgelerle sınırlayın", + "toast": { + "success": "Sınıflandırma yapılandırması kaydedildi. Değişiklikleri uygulamak için Frigate'i yeniden başlatın." + } + } + }, + "roles": { + "management": { + "title": "İzleyici Rol Yönetimi", + "desc": "Bu Frigate örneği için özel görüntüleyici rollerini ve kamera erişim izinlerini yönetin." + }, + "addRole": "Rol Ekle", + "table": { + "role": "Rol", + "cameras": "Kameralar", + "actions": "Eylemler", + "noRoles": "Özel rol bulunamadı.", + "editCameras": "Kameraları Düzenle", + "deleteRole": "Rolü Sil" + }, + "toast": { + "success": { + "createRole": "{{role}} rolü başarıyla oluşturuldu", + "updateCameras": "{{role}} rolü için kameralar güncellendi", + "deleteRole": "{{role}} rolü başarıyla silindi", + "userRolesUpdated_one": "Bu role atanan {{count}} kullanıcı, tüm kameralara erişimi olan 'görüntüleyici' olarak güncellendi.", + "userRolesUpdated_other": "Bu role atanan {{count}} kullanıcı, tüm kameralara erişimi olan 'görüntüleyici' olarak güncellendi." + }, + "error": { + "createRoleFailed": "Rol oluşturulamadı: {{errorMessage}}", + "updateCamerasFailed": "Kameralar güncellenemedi: {{errorMessage}}", + "deleteRoleFailed": "Rol silinemedi: {{errorMessage}}", + "userUpdateFailed": "Kullanıcı rolleri güncellenemedi: {{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "Yeni Rol Oluştur", + "desc": "Yeni bir rol ekleyin ve kamera erişim izinlerini belirtin." + }, + "editCameras": { + "title": "Rol Kameralarını Düzenle", + "desc": "{{role}} rolü için kamera erişimini güncelleyin." + }, + "deleteRole": { + "title": "Rolü Sil", + "desc": "Bu işlem geri alınamaz. Bu işlem, rolü kalıcı olarak silecek ve bu role sahip tüm kullanıcıları 'izleyici' rolüne atayacaktır. Bu rol, izleyiciye tüm kameralara erişim sağlayacaktır.", + "warn": "{{role}} rolünü silmek istediğinizden emin misiniz?", + "deleting": "Siliniyor..." + }, + "form": { + "role": { + "title": "Rol Adı", + "placeholder": "Rol adını girin", + "desc": "Sadece harf, rakam, nokta ve alt çizgi kullanılabilir.", + "roleIsRequired": "Rol adı gereklidir", + "roleOnlyInclude": "Rol adı yalnızca harf, sayı veya _ içerebilir", + "roleExists": "Bu isimde bir rol zaten mevcut." + }, + "cameras": { + "title": "Kameralar", + "desc": "Bu rolün erişebileceği kameraları seçin. En az bir kamera gereklidir.", + "required": "En az bir kamera seçilmelidir." + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/tr/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/tr/views/system.json new file mode 100644 index 0000000..2cc2ef0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/tr/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "logs": { + "frigate": "Frigate Günlükleri - Frigate", + "go2rtc": "Go2RTC Günlükleri - Frigate", + "nginx": "Nginx Günlükleri - Frigate" + }, + "general": "Genel İstatistikler - Frigate", + "storage": "Depolama İstatistikleri - Frigate", + "cameras": "Kamera İstatistikleri - Frigate", + "enrichments": "Zenginleştirme İstatistikleri - Frigate" + }, + "metrics": "Sistem metrikleri", + "general": { + "hardwareInfo": { + "gpuDecoder": "GPU Kod Çözücü", + "gpuInfo": { + "nvidiaSMIOutput": { + "name": "İsim: {{name}}", + "driver": "Sürücü: {{driver}}", + "cudaComputerCapability": "CUDA Hesaplama Yeteneği: {{cuda_compute}}", + "vbios": "VBios Bilgisi: {{vbios}}", + "title": "Nvidia SMI Çıktısı" + }, + "toast": { + "success": "GPU bilgisi panoya kopyalandı" + }, + "vainfoOutput": { + "processError": "İşlem Hatası:", + "returnCode": "Dönüt Kodu: {{code}}", + "processOutput": "İşlem Çıktısı:", + "title": "Vainfo çıktısı" + }, + "closeInfo": { + "label": "GPU bilgisini kapat" + }, + "copyInfo": { + "label": "GPU bilgisini kopyala" + } + }, + "gpuUsage": "GPU Kullanımı", + "gpuMemory": "GPU Belleği", + "gpuEncoder": "GPU Kodlayıcı", + "title": "Donanım Bilgisi", + "npuUsage": "NPU Kullanımı", + "npuMemory": "NPU Bellek Kullanımı", + "intelGpuWarning": { + "title": "Intel GPU İstatistik Uyarısı", + "message": "GPU istatistikleri kullanılamıyor", + "description": "Bu, Intel’in GPU istatistik raporlama araçlarında (intel_gpu_top) bilinen bir hatadır; araç çalışmayı bozarak, donanımsal hızlandırma ve nesne tespiti (i)GPU üzerinde doğru şekilde çalışıyor olsa bile, GPU kullanımını tekrar tekrar %0 olarak döndürür. Bu bir Frigate hatası değildir. Sorunu geçici olarak düzeltmek ve GPU’nun doğru çalıştığını doğrulamak için host sistemini yeniden başlatabilirsiniz. Bu durum performansı etkilemez." + } + }, + "otherProcesses": { + "title": "Diğer İşlemler", + "processCpuUsage": "İşlem CPU Kullanımı", + "processMemoryUsage": "İşlem Bellek Kullanımı" + }, + "detector": { + "title": "Algılayıcılar", + "inferenceSpeed": "Algılayıcı Çıkarım Hızı", + "memoryUsage": "Algılayıcı Bellek Kullanımı", + "cpuUsage": "Algılayıcı İşlemci Kullanımı", + "temperature": "Algılayıcı Sıcaklığı", + "cpuUsageInformation": "Tespit modellerine giriş ve çıkış verilerini hazırlarken kullanılan işlemci yoğunluğu. Bu değer, grafik işlemci veya benzeri bir hızlandırıcı kullanılsa bile çıkarım yükünü ölçmek için kullanılmamalıdır." + }, + "title": "Genel" + }, + "storage": { + "title": "Depolama", + "overview": "Genel", + "recordings": { + "title": "Kayıtlar", + "earliestRecording": "Mevcut en erken kayıt:", + "tips": "Burada gösterilen değer, Frigate’in veritabanına göre kayıtların diskinizde kullandığı toplam alanı ifade eder. Frigate, diskinizdeki tüm dosyaların alan kullanımını takip etmez." + }, + "cameraStorage": { + "title": "Kamera Depolaması", + "camera": "Kamera", + "unused": { + "tips": "Eğer diskinizde Frigate'in kayıtları dışında dosyalar varsa bu değer diskinizdeki boş alanı doğru olarak göstermeyebilir. Frigate kendi kayıtları dışındaki dosyaların disk kullanımını takip etmez.", + "title": "Kullanılmayan" + }, + "percentageOfTotalUsed": "Toplam Yüzde", + "storageUsed": "Depolama", + "bandwidth": "Saatlik Veri Kullanımı", + "unusedStorageInformation": "Kullanılmayan Depolama Bilgisi" + }, + "shm": { + "warning": "Şu anki {{total}}MB'lik SHM boyutu yetersiz. Bu boyutu en az {{min_shm}}MB'a çıkartın.", + "title": "Ayrılan SHM (paylaşımlı bellek)" + } + }, + "cameras": { + "info": { + "streamDataFromFFPROBE": "Yayın bilgisi ffprobe ile edinilmiştir.", + "video": "Video:", + "codec": "Kodlama:", + "fps": "Kare Hızı:", + "resolution": "Çözünürlük:", + "unknown": "Bilinmeyen", + "stream": "Yayın {{idx}}", + "tips": { + "title": "Kamera Detayları" + }, + "fetching": "Kamera Bilgileri Alınıyor", + "cameraProbeInfo": "{{camera}} Kamera Detayları", + "error": "Hata: {{error}}", + "audio": "Ses:", + "aspectRatio": "en boy oranı" + }, + "framesAndDetections": "Kare / Tespit", + "label": { + "camera": "kamera", + "detect": "tespit", + "ffmpeg": "FFmpeg", + "capture": "kayıt", + "skipped": "atlanan", + "overallDetectionsPerSecond": "toplam tespit/sn", + "overallFramesPerSecond": "toplam kare/sn", + "cameraFramesPerSecond": "{{camName}} kare/sn", + "overallSkippedDetectionsPerSecond": "toplam atlanan tespit/sn", + "cameraCapture": "{{camName}} kayıt", + "cameraDetect": "{{camName}} tespit", + "cameraDetectionsPerSecond": "{{camName}} tespit/sn", + "cameraSkippedDetectionsPerSecond": "{{camName}} atlanan tespit/sn", + "cameraFfmpeg": "{{camName}} FFmpeg" + }, + "toast": { + "success": { + "copyToClipboard": "Detaylar panoya kopyalandı." + }, + "error": { + "unableToProbeCamera": "Kamera detayları alınamadı: {{errorMessage}}" + } + }, + "title": "Kameralar", + "overview": "Genel" + }, + "lastRefreshed": "Son güncelleme: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} FFmpeg'te yüksek miktarda CPU kullanıyor (%{{ffmpegAvg}})", + "reindexingEmbeddings": "Gömüler yeniden dizinleniyor (%{{processed}} tamamlandı)", + "detectHighCpuUsage": "{{camera}} tespitte yüksek miktarda CPU kullanıyor (%{{detectAvg}})", + "healthy": "Sistem sağlıklı", + "detectIsVerySlow": "{{detect}} çok yavaş çalışıyor ({{speed}} ms)", + "cameraIsOffline": "{{camera}} çevrimdışı", + "detectIsSlow": "{{detect}} yavaş çalışıyor ({{speed}} ms)", + "shmTooLow": "Ayrılan /dev/shm belleği (şu anda {{total}} MB), en az {{min}} MB'a çıkartılmalıdır." + }, + "enrichments": { + "embeddings": { + "image_embedding_speed": "Resim Gömü Hızı", + "text_embedding_speed": "Metin Gömü Hızı", + "plate_recognition_speed": "Plaka Tanıma Hızı", + "face_embedding_speed": "Yüz Gömü Hızı", + "image_embedding": "Resim Gömüleme", + "text_embedding": "Metin Gömülüeme", + "face_recognition": "Yüz Tanıma", + "plate_recognition": "Plaka Tanıma", + "face_recognition_speed": "Yüz Tanıma Hızı", + "yolov9_plate_detection_speed": "YOLOv9 Plaka Tanıma Hızı", + "yolov9_plate_detection": "YOLOv9 Plaka Tanıma", + "review_description": "İnceleme Açıklaması", + "review_description_speed": "İnceleme Açıklama Hızı", + "review_description_events_per_second": "İnceleme Açıklaması", + "object_description": "Nesne Açıklaması", + "object_description_speed": "Nesne Açıklama Hızı", + "object_description_events_per_second": "Nesne Açıklaması" + }, + "infPerSecond": "Saniye Başına Çıkarım", + "title": "Zenginleştirmeler", + "averageInf": "Ortalama Çıkarım Süresi" + }, + "logs": { + "download": { + "label": "Günlükleri İndir" + }, + "type": { + "message": "Mesaj", + "tag": "Etiket", + "timestamp": "Zaman Damgası", + "label": "Tür" + }, + "copy": { + "error": "Günlükler panoya kopyalanamadı", + "label": "Panoya Kopyala", + "success": "Günlükler panoya kopyalandı" + }, + "tips": "Günlükler sunucudan yansıtılıyor", + "toast": { + "error": { + "whileStreamingLogs": "Günlükler yansıtılırken hata: {{errorMessage}}", + "fetchingLogsFailed": "Günlükler alınırken hata: {{errorMessage}}" + } + } + }, + "title": "Sistem" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/audio.json b/sam2-cpu/frigate-dev/web/public/locales/uk/audio.json new file mode 100644 index 0000000..773d5e3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/audio.json @@ -0,0 +1,503 @@ +{ + "child_singing": "Дитячий спів", + "breathing": "Дихання", + "cough": "Кашель", + "throat_clearing": "Прозорий очищення", + "mantra": "Мантра", + "synthetic_singing": "Синтетичний спів", + "whimper_dog": "Собаче скиглення", + "cat": "Кіт", + "cowbell": "Коров'ячий здвіночок", + "whispering": "Шепіт", + "run": "Біг", + "choir": "Хор", + "chewing": "Жування", + "pets": "Домашні улюбленці", + "dog": "Собака", + "bark": "Лай", + "meow": "Котяче нявчання", + "horse": "Кінь", + "moo": "Мичання", + "goat": "Коза", + "sheep": "Вівця", + "chicken": "Курка", + "speech": "Мо́влення", + "idling": "Холостий хід", + "railroad_car": "Залізничний вагон", + "alarm": "Сигналізація", + "fire_alarm": "Пожежна сигналізація", + "flute": "Флейта", + "musical_instrument": "Музичний інструмент", + "buzz": "Дзижчання", + "fly": "Муха", + "vocal_music": "Вокальна музика", + "motorcycle": "Мотоцикл", + "rustling_leaves": "Шелест листя", + "crackle": "Потріскування", + "thunder": "Грім", + "rock_and_roll": "Рок-н-рол", + "theme_music": "Тематична музика", + "exciting_music": "Енергійна музика", + "water": "Вода", + "violin": "Скрипка", + "tubular_bells": "Трубчасті дзвони", + "christmas_music": "Різдвяна музика", + "house_music": "Хауз", + "fire": "Вогонь", + "tapping": "Постукування", + "scratching": "Скретчінг", + "drum_kit": "Ударна установка", + "engine": "Двигун", + "light_engine": "Легкий двигун", + "swing_music": "Свінг", + "opera": "Опера", + "electronic_dance_music": "Електронна танцювальна музика", + "dance_music": "Танцювальна музика", + "thunderstorm": "Гроза", + "waves": "Хвилі", + "trombone": "Тромбон", + "music_of_asia": "Азіатська музика", + "tools": "Iнструменти", + "wind_chime": "Музика вітру", + "singing_bowl": "Співоча чаша", + "boat": "Човен", + "sailboat": "Вітрильник", + "rowboat": "Весловий човен", + "power_windows": "Електросклопідйомники", + "cutlery": "Столові прибори", + "mechanical_fan": "Механічний вентилятор", + "traffic_noise": "Дорожній шум", + "aircraft_engine": "Двигун повітряного судна", + "dental_drill's_drill": "Стоматологічна бормашина", + "door": "Двері", + "accelerating": "Прискорення", + "siren": "Сирена", + "typewriter": "Друкарська машинка", + "computer_keyboard": "Комп'ютерна клавіатура", + "smoke_detector": "Датчик диму", + "hammer": "Молот", + "gunshot": "Постріл", + "machine_gun": "Автомат", + "fireworks": "Феєрверки", + "firecracker": "Петарда", + "heartbeat": "Серцебиття", + "heart_murmur": "Серцевий шум", + "footsteps": "Кроки", + "burping": "Відрижка", + "hiccup": "Ікання", + "fart": "Пукання", + "finger_snapping": "Клацати пальцями", + "applause": "Оплески", + "chatter": "Балаканина", + "animal": "Тварина", + "yip": "Гавкання", + "babbling": "Бурмотіння", + "yell": "Кричати", + "bow_wow": "Гав гав", + "growling": "Ревіння", + "purr": "Муркотіти", + "hiss": "Шипіння", + "clip_clop": "Цокання", + "neigh": "іржання", + "oink": "Рохкання", + "bleat": "Мекання", + "cluck": "Кудкудакання", + "cock_a_doodle_doo": "Кукурікання", + "honk": "Гелготання", + "roar": "Гуркіт", + "bird": "Птах", + "chirp": "Цвірінькання", + "pigeon": "Голуб", + "coo": "Воркування", + "crow": "Ворона", + "caw": "Каркання", + "owl": "Сова", + "hoot": "Ухання", + "flapping_wings": "Ляскання крил", + "dogs": "Собаки", + "rats": "Щури", + "mouse": "Миш", + "patter": "Шерех", + "insect": "Комара", + "cricket": "Цвіркун", + "mosquito": "Комар", + "frog": "Жаба", + "croak": "Квакання", + "snake": "Змія", + "rattle": "Тріск", + "whale_vocalization": "Співання кита", + "music": "Музика", + "guitar": "Гітара", + "electric_guitar": "Електрогітара", + "bass_guitar": "Бас-гітара", + "acoustic_guitar": "Акустична гітара", + "strum": "Звук струн", + "keyboard": "Клавіатура", + "piano": "Піаніно", + "electric_piano": "Електропіаніно", + "organ": "Орган", + "electronic_organ": "Електроорган", + "synthesizer": "Синтезатор", + "percussion": "Ударні інструменти", + "drum": "Барабан", + "snare_drum": "Малий барабан", + "drum_roll": "Барабанний дріб", + "bass_drum": "Бас-барабан", + "tambourine": "Бубон", + "gong": "Гонг", + "glockenspiel": "Дзвіночки", + "orchestra": "Оркестр", + "double_bass": "Контрабас", + "wind_instrument": "Духовий інструмент", + "saxophone": "Саксофон", + "clarinet": "Кларнет", + "harp": "Арфа", + "bell": "Дзвін", + "church_bell": "Церковний дзвін", + "jingle_bell": "Бубонець", + "bicycle_bell": "Велосипедний дзвінок", + "tuning_fork": "Камертон", + "chime": "Дзвіночок", + "harmonica": "Губна гармоніка", + "accordion": "Акордеон", + "bagpipes": "Волинка", + "theremin": "Терменвокс", + "pop_music": "Поп-музика", + "hip_hop_music": "Хіп-хоп музика", + "beatboxing": "Бітбоксинг", + "rock_music": "Рок-музика", + "punk_rock": "Панк-рок", + "psychedelic_rock": "Психоделічний рок", + "rhythm_and_blues": "Ритм-н-блюз", + "country": "Кантрі", + "funk": "Фанк", + "folk_music": "Фолк-музика", + "jazz": "Джаз", + "disco": "Диско", + "classical_music": "Класична музика", + "electronic_music": "Електронна музика", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм-н-бейс", + "electronica": "Електроніка", + "ambient_music": "Ембієнт", + "trance_music": "Транс музика", + "music_of_latin_america": "Латиноамериканська музика", + "flamenco": "Фламенко", + "blues": "Блюз", + "music_for_children": "Дитяча музика", + "a_capella": "А капела", + "music_of_africa": "Африканська музика", + "afrobeat": "Афробіт", + "christian_music": "Християнська музика", + "gospel_music": "Госпел", + "carnatic_music": "Карнатична музика", + "ska": "Ска-музика", + "traditional_music": "Традиційна музика", + "independent_music": "Iнді музика", + "song": "Пісня", + "background_music": "Фонова музика", + "jingle": "Джингл", + "soundtrack_music": "Саундтрек", + "lullaby": "Колискова", + "video_game_music": "Музика з відеоігор", + "wedding_music": "Весільна музика", + "happy_music": "Весела музика", + "sad_music": "Сумна музика", + "tender_music": "Ніжна музика", + "angry_music": "Сердита музика", + "scary_music": "Моторошна музика", + "wind": "Вітер", + "wind_noise": "Шум вітру", + "rain": "Дощ", + "raindrop": "Краплі дощу", + "rain_on_surface": "Дощ на поверхні", + "stream": "Потік", + "waterfall": "Водоспад", + "ocean": "Океан", + "steam": "Пар", + "gurgling": "Дзюрчання", + "vehicle": "Транспорт", + "motorboat": "Моторний човен", + "ship": "Корабель", + "motor_vehicle": "Моторний транспорт", + "car": "Автомобіль", + "toot": "Гудок", + "car_alarm": "Автосигналізація", + "skidding": "Занос", + "tire_squeal": "Вереск шін", + "car_passing_by": "Проїжджаюча машина", + "race_car": "Гоночний автомобіль", + "truck": "Вантажівка", + "air_brake": "Пневматичне гальмо", + "air_horn": "Пневматичні гудок", + "reversing_beeps": "Сигнал заднього ходу", + "ice_cream_truck": "Вантажівки з морозивом", + "bus": "Автобус", + "emergency_vehicle": "Транспорт екстрених служб", + "police_car": "Поліцейський автомобіль", + "ambulance": "Швидка допомога", + "fire_engine": "Пожежна машина", + "rail_transport": "Рейковий транспорт", + "train": "Поїзд", + "train_whistle": "Свист поїзда", + "train_horn": "Гудок поїзда", + "train_wheels_squealing": "Вереск колес поїзда", + "subway": "Метро", + "aircraft": "Повітряне судно", + "jet_engine": "Реактивний двигун", + "propeller": "Пропелер", + "helicopter": "Вертоліт", + "fixed-wing_aircraft": "Літак з нерухомим крилом", + "bicycle": "Велосипед", + "skateboard": "Скейтборд", + "lawn_mower": "Газонокосарка", + "chainsaw": "Ланцюгова пила", + "medium_engine": "Середні двигун", + "heavy_engine": "Важкий двигун", + "engine_knocking": "Детонація у двигуні", + "engine_starting": "Запуск двигуна", + "doorbell": "Дверний дзвінок", + "ding-dong": "Дін-дон", + "sliding_door": "Розсувні двері", + "slam": "Бавовна", + "knock": "Стукіт", + "tap": "Невеличкий стук", + "squeak": "Скрип", + "cupboard_open_or_close": "Відкриття або закриття шафи", + "drawer_open_or_close": "Відкриття або закриття коробки", + "dishes": "Тарілки", + "chopping": "Нарізування", + "frying": "Смаження", + "microwave_oven": "Мікрохвильова піч", + "blender": "Блендер", + "water_tap": "Водопровідний кран", + "sink": "Раковина", + "bathtub": "Ванна", + "hair_dryer": "Фен", + "toilet_flush": "Злив унітазу", + "toothbrush": "Зубна щітка", + "electric_toothbrush": "Електрична зубна щітка", + "vacuum_cleaner": "Пилосос", + "zipper": "Блискавки на одязі", + "keys_jangling": "Брязкання ключів", + "coin": "Монета", + "scissors": "Ножиці", + "electric_shaver": "Електробритва", + "shuffling_cards": "Тасуванні карт", + "typing": "Друкування", + "writing": "Написання", + "telephone": "Телефон", + "telephone_bell_ringing": "Телефонний дзвінок", + "ringtone": "Рінгтон", + "telephone_dialing": "Набір телефонного номеру", + "dial_tone": "Телефонний гудок", + "busy_signal": "Сигнал зайнято", + "alarm_clock": "Будильник", + "civil_defense_siren": "Сирени громадянської оборони", + "buzzer": "Зумер", + "foghorn": "Туманний горн", + "whistle": "Свисток", + "steam_whistle": "Парової свисток", + "mechanisms": "Механізми", + "ratchet": "Тріскачка", + "clock": "Годинник", + "tick": "Тік", + "tick-tock": "Тік-так", + "gears": "Шестерні", + "pulleys": "Шківи", + "sewing_machine": "Швейна машинка", + "air_conditioning": "Кондиціонер", + "cash_register": "Каса", + "printer": "Принтер", + "camera": "Камера", + "single-lens_reflex_camera": "Дзеркальна камера", + "jackhammer": "Відбійний молоток", + "sawing": "Розпилювання", + "filing": "Звучання напилку", + "sanding": "Шліфування", + "power_tool": "Електроінструмент", + "drill": "Дриль", + "explosion": "Вибух", + "fusillade": "Збройна черга", + "artillery_fire": "Артилерійський вогонь", + "cap_gun": "Iграшковий пістолет", + "burst": "Черга пострілів", + "eruption": "Виверження", + "boom": "Бум", + "wood": "Деревина", + "chop": "Рубання", + "splinter": "Тріска", + "crack": "Тріщина", + "glass": "Скло", + "chink": "Дзенькіт", + "shatter": "Розбиття", + "silence": "Тиша", + "sound_effect": "Звуковий ефект", + "environmental_noise": "Шум навколишнього середовища", + "static": "Статичний шум", + "white_noise": "Білий шум", + "pink_noise": "Рожевий шум", + "television": "Телебачення", + "radio": "Радіо", + "field_recording": "Польова запись", + "scream": "Крик", + "laughter": "Сміх", + "bellow": "Рев", + "singing": "Спів", + "whoop": "Вигук", + "snicker": "хіхікання", + "crying": "Плач", + "sigh": "Зітхання", + "yodeling": "Співати йодлем", + "chant": "Скандування", + "grunt": "Гарчання", + "wheeze": "Хрипіти", + "gasp": "Aхнути", + "snort": "Пирхання", + "sniff": "Понюхати", + "shuffle": "Перетасувати", + "biting": "Кусання", + "gargling": "Полоскання", + "stomach_rumble": "Шлунок бурчати", + "hands": "Руки", + "clapping": "Плескання", + "cheering": "Аплодувати", + "crowd": "Натовп", + "children_playing": "Діти граються", + "howl": "Виття", + "rapping": "Стукiт", + "humming": "Гудіння", + "caterwaul": "Нявкання", + "livestock": "Тваринництво", + "cattle": "Велика рогата худоба", + "pig": "Свиня", + "fowl": "Птиця", + "turkey": "Iндичка", + "gobble": "Гелґотіння", + "duck": "Качка", + "quack": "Крякання", + "goose": "Гусак", + "wild_animals": "Дикі тварини", + "roaring_cats": "Ревіння котів", + "squawk": "Пташиний крик", + "plucked_string_instrument": "Щипковий струнний інструмент", + "steel_guitar": "Слайд-гітара", + "banjo": "Банджо", + "sitar": "Ситара", + "mandolin": "Мандоліна", + "zither": "Цитра", + "ukulele": "Укулеле", + "hammond_organ": "Орган Хаммонда", + "sampler": "Семплер", + "harpsichord": "Клавесин", + "drum_machine": "Драм-машина", + "rimshot": "Удар по ободу", + "timpani": "Тимпані", + "tabla": "Табла", + "cymbal": "Тарілка", + "hi_hat": "Хай-хет", + "wood_block": "Дерев'яний брусок", + "maraca": "Маракас", + "mallet_percussion": "Малет-перкусія", + "marimba": "Маримба", + "vibraphone": "Вібрафон", + "steelpan": "Стілпен", + "brass_instrument": "Мідний духовий інструмент", + "french_horn": "Валторна", + "trumpet": "Труба", + "bowed_string_instrument": "Струнно-смичкови інструмент", + "string_section": "Струнна секція", + "pizzicato": "Піцикато", + "cello": "Віолончель", + "didgeridoo": "Діджеріду", + "heavy_metal": "Хеві-метал", + "grunge": "Гранж", + "progressive_rock": "Прогресивний рок", + "soul_music": "Соул", + "reggae": "Реггі", + "bluegrass": "Блюграс", + "middle_eastern_music": "Близькосхідна музика", + "salsa_music": "Музика сальси", + "new-age_music": "Музика нью-ейдж", + "music_of_bollywood": "Музика Боллівуду", + "groan": "стогнати", + "whistling": "Свист", + "snoring": "Хропіння", + "pant": "Задихатися", + "sneeze": "Чхати", + "sodeling": "Соделінг", + "chird": "Дитина", + "change_ringing": "Змінити дзвінок", + "shofar": "Шофар", + "liquid": "Рідина", + "splash": "Сплеск", + "slosh": "Сльоз", + "squish": "Хлюпати", + "drip": "Крапельне", + "pour": "Для", + "trickle": "Струмінь", + "gush": "Гуш", + "fill": "Заповнити", + "spray": "Спрей", + "pump": "Насос", + "stir": "Перемішати", + "boiling": "Кипіння", + "sonar": "Сонар", + "arrow": "Стрілка", + "whoosh": "Свисти", + "thump": "Тупіт", + "thunk": "Тюнк", + "electronic_tuner": "Електронний тюнер", + "effects_unit": "Блок ефектів", + "chorus_effect": "Ефект хорусу", + "basketball_bounce": "Відскок баскетбольного м'яча", + "bang": "Вибухи", + "slap": "Ляпас", + "whack": "Вдарити", + "smash": "Розгром", + "breaking": "Розбиттям", + "bouncing": "Підстрибування", + "whip": "Батіг", + "flap": "Клаптик", + "scratch": "Подряпина", + "scrape": "Скрейп", + "rub": "Розтирання", + "roll": "Рулон", + "crushing": "Дроблення", + "crumpling": "Зминання", + "tearing": "Розривання", + "beep": "Звуковий сигнал", + "ping": "Пінг", + "ding": "Дін", + "clang": "Брязкіт", + "squeal": "Вереск", + "creak": "Скрипи", + "rustle": "Шелест", + "whir": "Гудінням", + "clatter": "Брязкіти", + "sizzle": "Шипінням", + "clicking": "Клацання", + "clickety_clack": "Клацання-Клак", + "rumble": "Гуркіті", + "plop": "Плюх", + "hum": "Гум", + "zing": "Зінг", + "boing": "Боїнг", + "crunch": "Хрускіт", + "sine_wave": "Синусоїда", + "harmonic": "Гармоніка", + "chirp_tone": "Чирп-тон", + "pulse": "Пульс", + "inside": "Всередині", + "outside": "Зовні", + "reverberation": "Реверберація", + "echo": "Відлуння", + "noise": "Шум", + "mains_hum": "Гуміння рук", + "distortion": "Спотворення", + "sidetone": "Побічний тон", + "cacophony": "Какофонія", + "throbbing": "Пульсуючий", + "vibration": "Вібрація" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/common.json b/sam2-cpu/frigate-dev/web/public/locales/uk/common.json new file mode 100644 index 0000000..b6692ba --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/common.json @@ -0,0 +1,305 @@ +{ + "time": { + "year_one": "{{time}}рік", + "year_few": "{{time}}роки", + "year_many": "{{time}}років", + "month_one": "{{time}}місяць", + "month_few": "{{time}}місяця", + "month_many": "{{time}} місяців", + "justNow": "Зараз", + "today": "Сьогодні", + "last7": "Останні 7 днів", + "last14": "Останній 14 днів", + "last30": "Останній 30 днів", + "thisWeek": "Цей тиждень", + "lastWeek": "Останній тиждень", + "thisMonth": "У цьому місяці", + "lastMonth": "Останній місяць", + "5minutes": "5 хвилин", + "10minutes": "10 хвилин", + "30minutes": "30 хвилин", + "1hour": "1 година", + "12hours": "12 годин", + "24hours": "24 години", + "pm": "вечора", + "am": "ранку", + "yr": "{{time}} рік", + "hour_one": "{{time}} година", + "hour_few": "{{time}} години", + "hour_many": "{{time}} годин", + "minute_one": "{{time}} хвилина", + "minute_few": "{{time}} хвилини", + "minute_many": "{{time}} хвилин", + "day_one": "{{time}} день", + "day_few": "{{time}} дні", + "day_many": "{{time}} днів", + "second_one": "{{time}}секунда", + "second_few": "{{time}}секунди", + "second_many": "{{time}}секунд", + "ago": "{{timeAgo}} тому", + "yesterday": "Учора", + "mo": "{{time}}місяць", + "d": "{{time}}д", + "h": "{{time}}г", + "m": "{{time}}хв", + "s": "{{time}}сек", + "untilForTime": "До {{time}}", + "untilForRestart": "Доки Frigate не перезавантажиться.", + "untilRestart": "До перезавантаження", + "formattedTimestamp": { + "12hour": "MMM d, h:mm:ss aaa", + "24hour": "MMM d, HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "MMM d, h:mm aaa", + "24hour": "MMM d, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "MMM d yyyy, h:mm aaa", + "24hour": "MMM d yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "MMM d", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "MM-dd-yy-HH-mm-ss" + }, + "formattedTimestamp2": { + "12hour": "dd.MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampMonthDayYear": { + "24hour": "MMM d, yyyy", + "12hour": "MMM d, yyyy" + }, + "inProgress": "У процесі", + "invalidStartTime": "Недійсний час початку", + "invalidEndTime": "Недійсний час завершення" + }, + "button": { + "exitFullscreen": "Вийти з повноекранного режиму", + "on": "ВКЛ", + "yes": "Так", + "copy": "Копіювати", + "close": "Закрити", + "saving": "Збереження…", + "history": "Історія", + "cancel": "Відмінити", + "fullscreen": "Повноекранний режим", + "back": "Назад", + "pictureInPicture": "Картинка в картинці", + "cameraAudio": "Аудіо камери", + "copyCoordinates": "Копіювати координати", + "edit": "Редагувати", + "no": "Ні", + "twoWayTalk": "Двосторонній зв'язок", + "off": "ВИКЛ", + "delete": "Видалити", + "apply": "Застосувати", + "reset": "Скинути", + "done": "Готово", + "enabled": "Увімкнено", + "enable": "Увімкнути", + "disabled": "Вимкнено", + "disable": "Вимкнути", + "save": "Зберегти", + "download": "Завантажити", + "info": "Інфо", + "suspended": "Призупинено", + "play": "Грати", + "unselect": "Прибрати виділення", + "export": "Експортувати", + "deleteNow": "Видалити негайно", + "next": "Наступне", + "unsuspended": "Відновити дію", + "continue": "Продовжити" + }, + "menu": { + "language": { + "da": "Датська", + "uk": "Українська", + "ro": "Румунська", + "es": "Іспанська", + "zhCN": "Спрощена китайська", + "hi": "Хінді", + "fr": "Французька", + "ar": "Арабська", + "pt": "Португальська", + "de": "Німецька", + "ja": "Японська", + "tr": "Турецька", + "ru": "російська", + "it": "Італійська", + "nl": "Голландська", + "sv": "Швецька", + "cs": "Чешська", + "nb": "Норвежский букмол", + "ko": "Корейська", + "vi": "В'єтнамська", + "fa": "Персидська", + "pl": "Польська", + "he": "Иврит", + "el": "Грецька", + "hu": "Венгерська", + "fi": "Фінська", + "sk": "Словацька", + "withSystem": { + "label": "Використовувати системну мову" + }, + "en": "Англійська", + "yue": "粵語 (Кантонська)", + "th": "ไทย (Тайська)", + "ca": "Català (Каталанська)", + "ptBR": "Português brasileiro (Бразильська португальська)", + "sr": "Српски (Сербська)", + "sl": "Slovenščina (Словенська)", + "lt": "Lietuvių (Литовська)", + "bg": "Български (Болгарська)", + "gl": "Galego (Галісійська)", + "id": "Bahasa Indonesia (Індонезійська)", + "ur": "اردو (Урду)" + }, + "system": "Система", + "systemMetrics": "Системна метріка", + "configuration": "Конфігурація", + "systemLogs": "Системні логи", + "settings": "Налаштування", + "configurationEditor": "Редактор конфігурації", + "languages": "Мови", + "theme": { + "nord": "Північ", + "red": "Червоний", + "contrast": "Висока контрастність", + "default": "Типовий", + "label": "Тема", + "blue": "Синій", + "green": "Зелений", + "highcontrast": "Висока контрастність" + }, + "help": "Допомогти", + "documentation": { + "title": "Документація", + "label": "Frigate документація" + }, + "restart": "Перезапустити Frigate", + "live": { + "title": "Пряма трансляція", + "allCameras": "Всi камери", + "cameras": { + "title": "Камери", + "count_one": "{{count}} Камера", + "count_few": "{{count}} Камери", + "count_many": "{{count}} Камер" + } + }, + "review": "Перегляд", + "explore": "Вивчити", + "export": "Експорт", + "uiPlayground": "UI iгровий майданчик", + "faceLibrary": "Бібліотека обличчя", + "user": { + "title": "Користувач", + "account": "Акаунт", + "current": "Поточний користувач: {{user}}", + "anonymous": "анонімний", + "logout": "Вихід", + "setPassword": "Встановити пароль" + }, + "darkMode": { + "label": "Темний режим", + "light": "Світло", + "dark": "Темний", + "withSystem": { + "label": "Використовуйте налаштування системи для світлого або темного режиму" + } + }, + "appearance": "Зовнішність", + "withSystem": "Система", + "classification": "Класифікація" + }, + "unit": { + "speed": { + "mph": "миль/г", + "kph": "км/г" + }, + "length": { + "feet": "ноги", + "meters": "метрів" + }, + "data": { + "kbps": "кБ/с", + "mbps": "МБ/с", + "gbps": "ГБ/с", + "kbph": "кБ/годину", + "mbph": "МБ/годину", + "gbph": "ГБ/годину" + } + }, + "label": { + "back": "Повернутись", + "hide": "Приховати {{item}}", + "show": "Показати {{item}}", + "ID": "ID", + "none": "Жоден", + "all": "Усі" + }, + "toast": { + "save": { + "title": "Зберегти", + "error": { + "title": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}", + "noMessage": "Не вдалося зберегти зміни налаштування" + } + }, + "copyUrlToClipboard": "Скопійовано URL до буфера обміну." + }, + "role": { + "title": "Роль", + "desc": "Адміністратори мають повний доступ до всіх функцій інтерфейсу Fregate. Глядачі обмежуються переглядом камер, оглядовими елементами та історичними кадрами в інтерфейсі користувача.", + "admin": "Адміністратор", + "viewer": "Глядач" + }, + "pagination": { + "previous": { + "title": "Попередній", + "label": "Повернутись до попередньої сторінки" + }, + "next": { + "title": "Наступний", + "label": "Перехід до наступної сторінки" + }, + "more": "Більше сторінок", + "label": "Пагінація" + }, + "accessDenied": { + "documentTitle": "Доступ заборонений - Frigate", + "title": "Доступ заборонений", + "desc": "У вас немає дозволу на перегляд цієї сторінки." + }, + "notFound": { + "documentTitle": "Не знайдено - Frigate", + "desc": "Сторінка не знайдена", + "title": "404" + }, + "selectItem": "Вибрати {{item}}", + "readTheDocumentation": "Прочитати документацію", + "information": { + "pixels": "{{area}}пикс" + }, + "list": { + "two": "{{0}} і {{1}}", + "many": "{{items}}, і {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Необов'язково", + "internalID": "Внутрішній ідентифікатор, який Frigate використовує в конфігурації та базі даних" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/auth.json new file mode 100644 index 0000000..4c9f7e2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "errors": { + "webUnknownError": "Невідома помилка. Перевірте журнали консолi.", + "usernameRequired": "Ви повинні ввести ім'я користувача", + "passwordRequired": "Необхідно ввести пароль", + "rateLimit": "Перевищення кількості спроб. Спробуйте пізніше.", + "loginFailed": "Спроба входу зазнала невдачі", + "unknownError": "Невідома помилка. Перевірте журнали." + }, + "user": "Iм'я користувача", + "password": "Пароль", + "login": "Логiн", + "firstTimeLogin": "Намагаєтеся вперше увійти? Облікові дані надруковані в журналах Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/camera.json new file mode 100644 index 0000000..0836510 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "name": { + "placeholder": "Введіть назву…", + "errorMessage": { + "exists": "Назва групи камер вже існує.", + "mustLeastCharacters": "Назва групи камер має містити щонайменше 2 символи.", + "nameMustNotPeriod": "Назва групи камер не повинна містити крапку.", + "invalid": "Недійсна назва групи камер." + }, + "label": "Ім'я" + }, + "camera": { + "setting": { + "streamMethod": { + "method": { + "noStreaming": { + "label": "Без потокового передавання", + "desc": "Зображення з камер оновлюватимуться лише раз на хвилину, і пряма трансляція не відбуватиметься." + }, + "continuousStreaming": { + "desc": { + "warning": "Безперервна потокова передача може призвести до високого використання пропускної здатності та проблем із продуктивністю. Використовуйте обережно.", + "title": "Зображення з камери завжди транслюватиметься в реальному часі, коли воно відображається на панелі приладів, навіть якщо жодної активності не виявлено." + }, + "label": "Безперервна потокова передача" + }, + "smartStreaming": { + "label": "Розумне потокове передавання (рекомендовано)", + "desc": "Інтелектуальна потокова передача оновлює зображення з камери раз на хвилину за відсутності видимої активності, щоб заощадити смугу пропускання і ресурси. При виявленні активності зображення плавно перемикається на пряму трансляцію." + } + }, + "label": "Метод потокової передачі", + "placeholder": "Виберіть спосіб потокової передачі" + }, + "audioIsUnavailable": "Аудіо недоступне для цієї трансляції", + "title": "{{cameraName}} Налаштування потокової передачі", + "audio": { + "tips": { + "document": "Прочитайте документацію ", + "title": "Аудіо має виводитися з вашої камери та бути налаштованим у go2rtc для цього потоку." + } + }, + "label": "Налаштування потокової передачі з камери", + "desc": "Змініть параметри прямої трансляції для панелі керування цієї групи камер. Ці налаштування залежать від пристрою/браузера.", + "audioIsAvailable": "Для цього потоку доступне аудіо", + "compatibilityMode": { + "label": "Режим сумісності", + "desc": "Увімкніть цю опцію, лише якщо пряма трансляція вашої камери відображає кольорові артефакти та має діагональну лінію з правого боку зображення." + }, + "stream": "Потік", + "placeholder": "Виберіть потік" + }, + "birdseye": "Бердсай" + }, + "edit": "Редагувати групу камер", + "delete": { + "label": "Видалити групу камер", + "confirm": { + "title": "Підтвердити видалення", + "desc": "Ви впевнені, що хочете видалити групу камер? {{name}}?" + } + }, + "success": "Групу камер ({{name}}) збережено.", + "label": "Групи камер", + "add": "Додати групу камер", + "cameras": { + "label": "Камери", + "desc": "Виберіть камери для цієї групи." + }, + "icon": "Значок" + }, + "debug": { + "zones": "Зони", + "mask": "Маска", + "motion": "Рух", + "regions": "Регiони", + "options": { + "label": "Налаштування", + "title": "Опції", + "showOptions": "Показати параметри", + "hideOptions": "Приховати параметри" + }, + "boundingBox": "Обмежувальна рамка", + "timestamp": "Позначка часу" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/dialog.json new file mode 100644 index 0000000..7ede790 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/dialog.json @@ -0,0 +1,124 @@ +{ + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Чи є цей об'єкт {{label}}?", + "ask_full": "Чи є цей об'єкт {{untranslatedLabel}}{{translatedLabel}}?", + "label": "Підтвердіть цей ярлик для Frigate Plus", + "ask_an": "Це об'єкт – {{label}}?" + }, + "state": { + "submitted": "Поданi" + } + }, + "submitToPlus": { + "label": "Надіслати да Frigate+", + "desc": "Об'єкти в місцях, які ви хочете уникнути, не є помилковими спрацьовуваннями. Подання їх як помилкових спрацьовувань заплутає модель." + } + }, + "video": { + "viewInHistory": "Перегляд у історії" + } + }, + "streaming": { + "label": "Потік", + "restreaming": { + "desc": { + "readTheDocumentation": "Прочитати документацію", + "title": "Налаштуйте go2rtc для додаткових параметрів перегляду в реальному часі та аудіо для цієї камери." + }, + "disabled": "Перезавантаження не ввімкнено для цієї камери." + }, + "showStats": { + "label": "Показати статистику потоку", + "desc": "Позначте цей пункт, щоб показувати статистику потоку як накладання на канал камери." + }, + "debugView": "Режим зневаджування" + }, + "search": { + "saveSearch": { + "label": "Зберегти пошук", + "button": { + "save": { + "label": "Зберегти цей пошук" + } + }, + "desc": "Вкажіть назву для цього збереженого пошуку.", + "placeholder": "Введіть назву для пошуку", + "overwrite": "{{searchName}} вже існує. Збереження перезапише існуюче значення.", + "success": "Пошук ({{searchName}}) збережено." + } + }, + "export": { + "toast": { + "error": { + "failed": "Не вдалося розпочати експорт: {{error}}", + "endTimeMustAfterStartTime": "Час закінчення повинен бути після часу початку", + "noVaildTimeSelected": "Не вибрано допустимий діапазон часу" + }, + "success": "Експорт успішно розпочато. Перегляньте файл на сторінці експорту.", + "view": "Переглянути" + }, + "fromTimeline": { + "saveExport": "Зберегти експорт", + "previewExport": "Попередній перегляд експорту" + }, + "time": { + "fromTimeline": "Вибір шкали часу", + "custom": "Користувацький", + "start": { + "title": "Час початку", + "label": "Виберіть час початку" + }, + "end": { + "title": "Час закінчення", + "label": "Вибрати час закінчення" + }, + "lastHour_one": "Остання {{count}} година", + "lastHour_few": "Останні {{count}} години", + "lastHour_many": "Останні {{count}} годин" + }, + "name": { + "placeholder": "Введіть назву для експорту" + }, + "select": "Вибрати", + "export": "Експорт", + "selectOrExport": "Выбiр або експорт" + }, + "recording": { + "button": { + "export": "Експорт", + "markAsReviewed": "Позначити як переглянуте", + "deleteNow": "Вилучити зараз", + "markAsUnreviewed": "Позначити як непереглянуте" + }, + "confirmDelete": { + "title": "Підтвердити вилучення", + "desc": { + "selected": "Ви впевнені, що хочете видалити все записане відео, пов'язане з цим пунктом огляду?

    Утримуйте клавішу Shift, щоб обійти це діалогове вікно в майбутньому." + }, + "toast": { + "error": "Не вдалося видалити: {{error}}", + "success": "Відеозаписи, пов’язані з вибраними елементами огляду, успішно видалено." + } + } + }, + "restart": { + "title": "Ви впевнені, що хочете перезапустити Frigate?", + "button": "Перезавантажувати", + "restarting": { + "title": "Frigate перезапускається", + "content": "Цю сторінку буде перезавантажено за {{countdown}} секунд.", + "button": "Примусово перезавантажити" + } + }, + "imagePicker": { + "selectImage": "Вибір мініатюри відстежуваного об'єкта", + "search": { + "placeholder": "Пошук за міткою або підміткою..." + }, + "noImages": "Для цієї камери не знайдено мініатюр", + "unknownLabel": "Збережене зображення тригера" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/filter.json new file mode 100644 index 0000000..5a29434 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Фiльтр", + "explore": { + "settings": { + "defaultView": { + "desc": "Якщо фільтри не вибрано, відобразити резюме останніх відстежуваних об'єктів за міткою, або показати нефільтровану сітку.", + "title": "Вид за замовчуванням", + "summary": "Підсумок", + "unfilteredGrid": "Нефільтровані сiтка" + }, + "searchSource": { + "desc": "Виберіть, чи слід шукати мініатюри або описи відстежуваних об'єктів.", + "label": "Пошук джерела", + "options": { + "thumbnailImage": "Зображення мініатюри", + "description": "Опис" + } + }, + "title": "Налаштування", + "gridColumns": { + "title": "Колонки сітки", + "desc": "Виберіть кількість стовпчиків у вигляді сітки." + } + }, + "date": { + "selectDateBy": { + "label": "Виберіть дату для фільтрування" + } + } + }, + "labels": { + "count_one": "{{count}} Етикетка", + "all": { + "title": "Всi етикетки", + "short": "Етикетки" + }, + "label": "Етикетки", + "count_other": "{{count}} Етикетки" + }, + "cameras": { + "all": { + "short": "Камери", + "title": "Всi камери" + }, + "label": "Фільтр камери" + }, + "timeRange": "Часовий діапазон", + "features": { + "hasSnapshot": "Має своє уявлення", + "hasVideoClip": "Має відеокліп", + "label": "Особливості", + "submittedToFrigatePlus": { + "label": "Подані до Frigate+", + "tips": "Спочатку потрібно відфільтрувати об'єкти, які відслідковуються.

    Відстежувані об'єкти без знімка не можуть бути передані Frigate+." + } + }, + "sort": { + "dateAsc": "Дата (за зростанням)", + "dateDesc": "Дата (по спадаючій)", + "label": "Сортування", + "scoreAsc": "Оцiнка об'єкту (За зростанням)", + "scoreDesc": "Оцiнка об'єкту (За спаданням)", + "speedAsc": "Розрахункова швидкість (За зростанням)", + "speedDesc": "Розрахункова швидкість (За спаданням)", + "relevance": "Актуальність" + }, + "trackedObjectDelete": { + "desc": "Видалення цих {{objectLength}} відстежуваних об'єктів видаляє знімок, будь-які збережені вкладення та пов'язані з ними записи життєвого циклу об'єкта. Записані кадри цих відстежуваних об'єктів у перегляді історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?

    Утримуйте клавішу Shift, щоб обійти це діалогове вікно в майбутньому.", + "title": "Підтвердіть видалення", + "toast": { + "success": "Відстежені об'єкти успішно видалені.", + "error": "Не вдалося видалити відстежувані об'єкти: {{errorMessage}}" + } + }, + "zones": { + "label": "Зони", + "all": { + "title": "Всi зони", + "short": "Зони" + } + }, + "dates": { + "all": { + "title": "Всi дати", + "short": "Дати" + }, + "selectPreset": "Виберіть пресет…" + }, + "more": "Кілька фільтрів", + "reset": { + "label": "Відновити типові значення фільтрів" + }, + "subLabels": { + "label": "Суб-мiтка", + "all": "Всi суб-мiтки" + }, + "score": "Рахунок", + "estimatedSpeed": "Розрахункова швидкість ({{unit}})", + "review": { + "showReviewed": "Показувати переглянуті" + }, + "motion": { + "showMotionOnly": "Показати тiльки рух" + }, + "logSettings": { + "label": "Фільтр рівня журналу", + "filterBySeverity": "Фільтрувати журнали за ступенем тяжкості", + "loading": { + "title": "Завантаження", + "desc": "Коли панель журналу прокручується внизу, нові журнали автоматично транслюються після додавання." + }, + "disableLogStreaming": "Вимикати журнал стрімінгу", + "allLogs": "Всi журнали" + }, + "zoneMask": { + "filterBy": "Фільтрувати за маскою зони" + }, + "recognizedLicensePlates": { + "title": "Розпізнано номерні знаки", + "loadFailed": "Не вдалося завантажити розпізнані номерні знаки.", + "loading": "Завантаження визнаних номерів…", + "placeholder": "Введіть для пошуку номерні знаки…", + "noLicensePlatesFound": "Номерних знаків не знайдено.", + "selectPlatesFromList": "Виберіть одну або кілька пластин зі списку.", + "selectAll": "Вибрати все", + "clearAll": "Очистити все" + }, + "classes": { + "label": "Заняття", + "all": { + "title": "Усі класи" + }, + "count_one": "Клас {{count}}", + "count_other": "{{count}} Класи" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/icons.json new file mode 100644 index 0000000..bb5ab6e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Оберіть іконку", + "search": { + "placeholder": "Пошук значка…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/input.json new file mode 100644 index 0000000..e7b818d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Завантажити відео", + "toast": { + "success": "Розпочата завантаження відео." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/uk/components/player.json new file mode 100644 index 0000000..746eba6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Не знайдено жодного запису", + "noPreviewFoundFor": "Попередній перегляд для {{cameraName}} не знайдені", + "livePlayerRequiredIOSVersion": "Для цього типу потокового передавання потрібна iOS 17.1 або пізніша версія.", + "stats": { + "droppedFrames": { + "short": { + "title": "Пропущене", + "value": "{{droppedFrames}} кадрiв" + }, + "title": "Пропущене кадрiв:" + }, + "droppedFrameRate": "Частота пропущенив кадрiв:", + "streamType": { + "short": "Тип", + "title": "Тип потоку:" + }, + "bandwidth": { + "title": "Пропускна здатність:", + "short": "Пропускна здатність" + }, + "latency": { + "title": "Затримка:", + "short": { + "title": "Затримка", + "value": "{{seconds}} сек" + }, + "value": "{{seconds}} секунд" + }, + "totalFrames": "Всього кадрiв:", + "decodedFrames": "Декодовані кадри:" + }, + "noPreviewFound": "Попередній перегляд не знайдені", + "submitFrigatePlus": { + "title": "Відправити це зображення Frigate+?", + "submit": "Посилати" + }, + "streamOffline": { + "title": "Струм офлайн", + "desc": "Потік detect камера {{cameraName}} не отримувала ніяких кадрів, перевіряйте журнали помилок" + }, + "cameraDisabled": "Камера вимкнена", + "toast": { + "success": { + "submittedFrigatePlus": "Зображення успішно завантажено до Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Не вдалося надіслати фрейм Frigate+" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/objects.json b/sam2-cpu/frigate-dev/web/public/locales/uk/objects.json new file mode 100644 index 0000000..881ac79 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/objects.json @@ -0,0 +1,120 @@ +{ + "dog": "Собака", + "cat": "Кіт", + "horse": "Кінь", + "sheep": "Вівця", + "bark": "Лай", + "goat": "Коза", + "mouse": "Миш", + "skateboard": "Скейтборд", + "bird": "Птах", + "boat": "Човен", + "car": "Автомобіль", + "bus": "Автобус", + "motorcycle": "Мотоцикл", + "train": "Поїзд", + "bicycle": "Велосипед", + "keyboard": "Клавіатура", + "door": "Двері", + "sink": "Раковина", + "animal": "Тварина", + "vehicle": "Транспорт", + "blender": "Блендер", + "hair_dryer": "Фен", + "toothbrush": "Зубна щітка", + "scissors": "Ножиці", + "clock": "Годинник", + "toilet": "Вбиральня", + "spoon": "Ложка", + "tennis_racket": "Тенісна ракетка", + "potted_plant": "Кімнатна рослина", + "bbq_grill": "Гриль та барбекю", + "person": "Людина", + "airplane": "Літак", + "traffic_light": "Світлофор", + "fire_hydrant": "Пожежний гідрант", + "parking_meter": "Паркоматів", + "bench": "Лавка", + "cow": "Корова", + "elephant": "Слон", + "bear": "Ведмідь", + "zebra": "Зебра", + "giraffe": "Жираф", + "hat": "Шапка", + "backpack": "Рюкзак", + "street_sign": "Дорожній знак", + "stop_sign": "Знак зупинки", + "umbrella": "Парасолька", + "shoe": "Взуття", + "eye_glasses": "Окуляри", + "handbag": "Гаманець", + "tie": "Краватка", + "suitcase": "Валiза", + "frisbee": "Фрiсбi", + "skis": "Лижи", + "snowboard": "Сноуборд", + "sports_ball": "Спортивні м'яч", + "kite": "Повітряний змій", + "baseball_bat": "Бейсбольна біта", + "baseball_glove": "Рукавичка ловця", + "surfboard": "Дошка для серфінгу", + "bottle": "Пляшка", + "plate": "Тарiлка", + "wine_glass": "Винний келих", + "cup": "Чашка", + "fork": "Виделка", + "knife": "Нiж", + "bowl": "Миска", + "banana": "Банан", + "apple": "Яблуко", + "sandwich": "Сендвіч", + "orange": "Апельсин", + "broccoli": "Броколі", + "carrot": "Морква", + "hot_dog": "Хот-дог", + "pizza": "Пiца", + "donut": "Пампушка", + "cake": "Торт", + "chair": "Стілець", + "couch": "Диван", + "bed": "Лiжко", + "mirror": "Люстерко", + "dining_table": "Обідній стіл", + "window": "Вiкно", + "desk": "Стiл", + "tv": "Телевізор", + "laptop": "Лептоп", + "remote": "Пульт дистанційного керування", + "cell_phone": "Мобільний телефон", + "microwave": "Мікрохвильовка", + "oven": "Пiч", + "toaster": "Тостер", + "refrigerator": "Холодильник", + "book": "Книжка", + "vase": "Ваза", + "teddy_bear": "Плюшевий ведмедик", + "hair_brush": "Гребінець", + "squirrel": "Білка", + "deer": "Олень", + "fox": "Лисиця", + "rabbit": "Кролик", + "raccoon": "Єнот", + "robot_lawnmower": "Роботизована газонокосарка", + "waste_bin": "Відро для сміття", + "on_demand": "На вимогу", + "face": "Обличчя", + "license_plate": "Номерний знак", + "package": "Посилка", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "Fed Ex", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator'", + "postnl": "PostNL'", + "nzpost": "NZPost'", + "postnord": "PostNord'", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/classificationModel.json new file mode 100644 index 0000000..b88ec48 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/classificationModel.json @@ -0,0 +1,190 @@ +{ + "documentTitle": "Моделі класифікації - Frigate", + "button": { + "deleteClassificationAttempts": "Видалити зображення класифікації", + "renameCategory": "Перейменувати клас", + "deleteCategory": "Видалити клас", + "deleteImages": "Видалити зображення", + "trainModel": "Модель поїзда", + "addClassification": "Додати класифікацію", + "deleteModels": "Видалити моделі", + "editModel": "Редагувати модель" + }, + "toast": { + "success": { + "deletedCategory": "Видалений клас", + "deletedImage": "Видалені зображення", + "categorizedImage": "Зображення успішно класифіковано", + "trainedModel": "Успішно навчена модель.", + "trainingModel": "Успішно розпочато навчання моделі.", + "deletedModel_one": "Успішно видалено модель {{count}}", + "deletedModel_few": "Успішно видалено моделей {{count}}", + "deletedModel_many": "Успішно видалено моделі {{count}}", + "updatedModel": "Конфігурацію моделі успішно оновлено", + "renamedCategory": "Клас успішно перейменовано на {{name}}" + }, + "error": { + "deleteImageFailed": "Не вдалося видалити: {{errorMessage}}", + "deleteCategoryFailed": "Не вдалося видалити клас: {{errorMessage}}", + "categorizeFailed": "Не вдалося класифікувати зображення: {{errorMessage}}", + "trainingFailed": "Навчання моделі не вдалося. Перегляньте журнали Frigate для отримання детальної інформації.", + "deleteModelFailed": "Не вдалося видалити модель: {{errorMessage}}", + "updateModelFailed": "Не вдалося оновити модель: {{errorMessage}}", + "renameCategoryFailed": "Не вдалося перейменувати клас: {{errorMessage}}", + "trainingFailedToStart": "Не вдалося розпочати навчання моделі: {{errorMessage}}" + } + }, + "deleteCategory": { + "title": "Видалити клас", + "desc": "Ви впевнені, що хочете видалити клас {{name}}? Це назавжди видалить усі пов'язані зображення та вимагатиме повторного навчання моделі.", + "minClassesTitle": "Не вдається видалити клас", + "minClassesDesc": "Модель класифікації повинна мати щонайменше 2 класи. Додайте ще один клас, перш ніж видаляти цей." + }, + "deleteDatasetImages": { + "title": "Видалити зображення набору даних", + "desc_one": "Ви впевнені, що хочете видалити {{count}} зображень з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} зображенні з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} зображенні з {{dataset}}? Цю дію неможливо скасувати, вона вимагатиме повторного навчання моделі." + }, + "deleteTrainImages": { + "title": "Видалити зображення поїздів", + "desc_one": "Ви впевнені, що хочете видалити {{count}} зображень? Цю дію не можна скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} зображенні? Цю дію не можна скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} зображенні? Цю дію не можна скасувати." + }, + "renameCategory": { + "title": "Перейменувати клас", + "desc": "Введіть нову назву для {{name}}. Вам потрібно буде перенавчити модель, щоб зміна назви набула чинності." + }, + "description": { + "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." + }, + "train": { + "title": "Нещодавні класифікації", + "titleShort": "Нещодавні", + "aria": "Виберіть останні класифікації" + }, + "categories": "Заняття", + "createCategory": { + "new": "Створити новий клас" + }, + "categorizeImageAs": "Класифікувати зображення як:", + "categorizeImage": "Класифікувати зображення", + "noModels": { + "object": { + "title": "Без моделей класифікації об'єктів", + "description": "Створіть власну модель для класифікації виявлених об'єктів.", + "buttonText": "Створення об'єктної моделі" + }, + "state": { + "title": "Без моделей класифікації штатів", + "description": "Створіть власну модель для моніторингу та класифікації змін стану в певних областях камери.", + "buttonText": "Створити модель стану" + } + }, + "wizard": { + "title": "Створити нову класифікацію", + "steps": { + "nameAndDefine": "Назва та визначення", + "stateArea": "Площа штату", + "chooseExamples": "Виберіть приклади" + }, + "step1": { + "description": "Моделі станів відстежують зміни в зонах дії фіксованих камер (наприклад, відкриття/закриття дверей). Моделі об'єктів додають класифікації до виявлених об'єктів (наприклад, відомі тварини, кур'єри тощо).", + "name": "Ім'я", + "namePlaceholder": "Введіть назву моделі...", + "type": "Тип", + "typeState": "Штат", + "typeObject": "Об'єкт", + "objectLabel": "Мітка об'єкта", + "objectLabelPlaceholder": "Виберіть тип об'єкта...", + "classificationType": "Тип класифікації", + "classificationTypeTip": "Дізнайтеся про типи класифікації", + "classificationTypeDesc": "Підмітки додають додатковий текст до мітки об’єкта (наприклад, «Особа: UPS»). Атрибути – це метадані для пошуку, що зберігаються окремо в метаданих об’єкта.", + "classificationSubLabel": "Підмітка", + "classificationAttribute": "Атрибут", + "classes": "Заняття", + "classesTip": "Дізнайтеся про заняття", + "classesStateDesc": "Визначте різні стани, в яких може перебувати зона вашої камери. Наприклад: «відкрито» та «закрито» для гаражних воріт.", + "classesObjectDesc": "Визначте різні категорії для класифікації виявлених об'єктів. Наприклад: «доставник», «мешканець», «незнайомець» для класифікації осіб.", + "classPlaceholder": "Введіть назву класу...", + "errors": { + "nameRequired": "Назва моделі обов'язкова", + "nameLength": "Назва моделі має містити не більше 64 символів", + "nameOnlyNumbers": "Назва моделі не може містити лише цифри", + "classRequired": "Потрібно хоча б 1 заняття", + "classesUnique": "Назви класів мають бути унікальними", + "stateRequiresTwoClasses": "Моделі станів вимагають щонайменше 2 класів", + "objectLabelRequired": "Будь ласка, виберіть мітку об'єкта", + "objectTypeRequired": "Будь ласка, виберіть тип класифікації" + }, + "states": "Штати" + }, + "step2": { + "description": "Виберіть камери та визначте область для моніторингу для кожної камери. Модель класифікуватиме стан цих областей.", + "cameras": "Камери", + "selectCamera": "Виберіть Камеру", + "noCameras": "Натисніть +, щоб додати камери", + "selectCameraPrompt": "Виберіть камеру зі списку, щоб визначити її зону спостереження" + }, + "step3": { + "selectImagesPrompt": "Виберіть усі зображення з: {{className}}", + "selectImagesDescription": "Натисніть на зображення, щоб вибрати їх. Натисніть «Продовжити», коли закінчите з цим уроком.", + "generating": { + "title": "Створення зразків зображень", + "description": "Фрегат отримує типові зображення з ваших записів. Це може зайняти деякий час..." + }, + "training": { + "title": "Модель навчання", + "description": "Ваша модель навчається у фоновому режимі. Закрийте це діалогове вікно, і ваша модель почне працювати, щойно навчання буде завершено." + }, + "retryGenerate": "Генерація повторних спроб", + "noImages": "Немає згенерованих зразків зображень", + "classifying": "Класифікація та навчання...", + "trainingStarted": "Навчання розпочалося успішно", + "errors": { + "noCameras": "Немає налаштованих камер", + "noObjectLabel": "Мітку об'єкта не вибрано", + "generateFailed": "Не вдалося створити приклади: {{error}}", + "generationFailed": "Помилка генерації. Будь ласка, спробуйте ще раз.", + "classifyFailed": "Не вдалося класифікувати зображення: {{error}}" + }, + "generateSuccess": "Зразки зображень успішно створено", + "allImagesRequired_one": "Будь ласка, класифікуйте всі зображення. Залишилося {{count}} зображення.", + "allImagesRequired_few": "Будь ласка, класифікуйте всі зображення. Залишилося зображень: {{count}}.", + "allImagesRequired_many": "Будь ласка, класифікуйте всі зображення. Залишилося зображень: {{count}}.", + "modelCreated": "Модель успішно створено. Використовуйте режим перегляду «Нещодавні класифікації», щоб додати зображення для відсутніх станів, а потім навчіть модель.", + "missingStatesWarning": { + "title": "Приклади відсутніх станів", + "description": "Для найкращих результатів рекомендується вибрати приклади для всіх станів. Ви можете продовжити, не вибираючи всі стани, але модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель." + } + } + }, + "deleteModel": { + "title": "Видалити модель класифікації", + "single": "Ви впевнені, що хочете видалити {{name}}? Це назавжди видалить усі пов’язані дані, включаючи зображення та дані навчання. Цю дію не можна скасувати.", + "desc_one": "Ви впевнені, що хочете видалити {{count}} модель? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} моделей? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} моделі? Це назавжди видалить усі пов’язані дані, включаючи зображення та навчальні дані. Цю дію не можна скасувати." + }, + "menu": { + "objects": "Об'єкти", + "states": "Стани" + }, + "details": { + "scoreInfo": "Оцінка представляє середню достовірність класифікації для всіх виявлень цього об'єкта." + }, + "edit": { + "title": "Редагувати модель класифікації", + "descriptionState": "Відредагуйте класи для цієї моделі класифікації штатів. Зміни вимагатимуть перенавчання моделі.", + "descriptionObject": "Відредагуйте тип об'єкта та тип класифікації для цієї моделі класифікації об'єктів.", + "stateClassesInfo": "Примітка: Зміна класів станів вимагає перенавчання моделі з використанням оновлених класів." + }, + "tooltip": { + "trainingInProgress": "Модель зараз тренується", + "noNewImages": "Немає нових зображень для навчання. Спочатку класифікуйте більше зображень у наборі даних.", + "modelNotReady": "Модель не готова до навчання", + "noChanges": "З моменту останнього навчання в наборі даних не було змін." + }, + "none": "Жоден" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/configEditor.json new file mode 100644 index 0000000..0e3ef13 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "saveAndRestart": "Зберегти та перезавантажити", + "toast": { + "error": { + "savingError": "Помилка збереження конфігурації" + }, + "success": { + "copyToClipboard": "Налаштування було скопійовано до буфера обміну даними." + } + }, + "documentTitle": "Редактор конфігурації - Frigate", + "copyConfig": "Скопіювати конфігурацію", + "saveOnly": "Тільки зберегти", + "configEditor": "Налаштування редактора", + "confirm": "Вийти без збереження?", + "safeConfigEditor": "Редактор конфігурації (безпечний режим)", + "safeModeDescription": "Фрегат перебуває в безпечному режимі через помилку перевірки конфігурації." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/events.json new file mode 100644 index 0000000..3cceebd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/events.json @@ -0,0 +1,63 @@ +{ + "camera": "Камера", + "selected_one": "{{count}} відібраний", + "selected_other": "{{count}} відібраний", + "alerts": "Оповіщення", + "detections": "Виявлень", + "motion": { + "label": "Рух", + "only": "Тiльки рух" + }, + "allCameras": "Всi камери", + "empty": { + "alert": "Немає попереджень для перегляду", + "detection": "Немає ніяких ознак", + "motion": "Даних про рух не знайдено" + }, + "timeline": "Хронологія", + "timeline.aria": "Вибрати хронiку", + "events": { + "label": "Події", + "aria": "Выбрати події", + "noFoundForTimePeriod": "За цей період подій не знайдено." + }, + "documentTitle": "Перегляд подiя - Frigate", + "recordings": { + "documentTitle": "Записи - Frigate" + }, + "calendarFilter": { + "last24Hours": "Останні 24 години" + }, + "markAsReviewed": "Прибрати позначку про необхідність огляду", + "markTheseItemsAsReviewed": "Позначити ці елементи як переглянуті", + "newReviewItems": { + "label": "Переглянути нові елементи огляду", + "button": "Нові матеріали для перегляду" + }, + "detected": "виявлено", + "suspiciousActivity": "Підозріла активність", + "threateningActivity": "Загрозлива діяльність", + "detail": { + "noDataFound": "Немає детальних даних для перегляду", + "aria": "Перемикання детального перегляду", + "trackedObject_one": "{{count}} об'єкт", + "trackedObject_other": "{{count}} об'єкти", + "noObjectDetailData": "Детальні дані про об'єкт недоступні.", + "label": "Деталь", + "settings": "Налаштування детального перегляду", + "alwaysExpandActive": { + "title": "Завжди розгортати активне", + "desc": "Завжди розгортайте деталі об'єкта активного елемента огляду, якщо вони доступні." + } + }, + "objectTrack": { + "trackedPoint": "Відстежувана Точка", + "clickToSeek": "Натисніть, щоб перейти до цього часу" + }, + "zoomIn": "Збільшити масштаб", + "zoomOut": "Зменшити масштаб", + "normalActivity": "Звичайний", + "needsReview": "Потребує перегляду", + "securityConcern": "Проблема безпеки", + "select_all": "Усі" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/explore.json new file mode 100644 index 0000000..db7715f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/explore.json @@ -0,0 +1,295 @@ +{ + "exploreIsUnavailable": { + "downloadingModels": { + "tips": { + "documentation": "Прочитати документацію", + "context": "Можливо, вам варто переіндексувати вбудовування відстежуваних об'єктів після завантаження моделей." + }, + "setup": { + "visionModel": "Модель зору", + "visionModelFeatureExtractor": "Екстрактор ознак моделі зору", + "textModel": "Текстова модель", + "textTokenizer": "Токенізатор тексту" + }, + "error": "Сталася помилка. Перевірте журнали Frigate.", + "context": "Frigate завантажує необхідні моделі вбудовування для підтримки функції семантичного пошуку. Це може тривати кілька хвилин залежно від швидкості вашого мережевого з’єднання." + }, + "title": "Огляд недоступний", + "embeddingsReindexing": { + "context": "Функцію «Дослідити» можна використовувати після завершення переіндексації вбудовування відстежуваних об’єктів.", + "startingUp": "Запуск…", + "estimatedTime": "Орієнтовний час, що залишився:", + "finishingShortly": "Закінчується незабаром", + "step": { + "thumbnailsEmbedded": "Вбудовані мініатюри: ", + "descriptionsEmbedded": "Вбудовані описи: ", + "trackedObjectsProcessed": "Оброблено відстежуваних об'єктів: " + } + } + }, + "documentTitle": "Пошук подія - Frigate", + "searchResult": { + "tooltip": "Збігається з {{type}} на рівні {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "error": "Не вдалося видалити відстежуваний об'єкт: {{errorMessage}}", + "success": "Відстежуваний об'єкт успішно видалено." + } + }, + "previousTrackedObject": "Попередній відстежуваний об'єкт", + "nextTrackedObject": "Наступний відстежуваний об'єкт" + }, + "trackedObjectsCount_one": "{{count}} відстежуваний об'єкт ", + "trackedObjectsCount_few": "{{count}} відстежувані об'єкти ", + "trackedObjectsCount_many": "{{count}} відстежувані об'єктів ", + "objectLifecycle": { + "title": "Життєвий цикл об'єкта", + "createObjectMask": "Створити маску об'єкта", + "annotationSettings": { + "title": "Налаштування анотацій", + "offset": { + "desc": "Ці дані надходять із сигналу виявлення вашої камери, але накладаються на зображення із сигналу запису. Малоймовірно, що два потоки ідеально синхронізовані. В результаті, обмежувальна рамка та відеоматеріал не будуть ідеально вирівняні. Однак, для налаштування цього можна скористатися полем annotation_offset.", + "tips": "ПОРАДА: Уявіть, що є кліп події, в якому людина йде зліва направо. Якщо рамка часової шкали події постійно знаходиться ліворуч від людини, то значення слід зменшити. Аналогічно, якщо людина йде зліва направо, а обмежувальна рамка постійно знаходиться попереду неї, тоді значення слід збільшити.", + "label": "Зсув анотації", + "documentation": "Прочитайте документацію ", + "millisecondsToOffset": "Мілісекунди для зміщення виявлених анотацій. За замовчуванням: 0", + "toast": { + "success": "Зміщення анотації для {{camera}} збережено у файлі конфігурації. Перезапустіть Frigate, щоб застосувати зміни." + } + }, + "showAllZones": { + "title": "Показати всі зони", + "desc": "Завжди показувати зони на кадрах, де об'єкти увійшли в зону." + } + }, + "scrollViewTips": "Прокрутіть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.", + "lifecycleItemDesc": { + "attribute": { + "other": "{{label}} визнаний як {{attribute}}", + "faceOrLicense_plate": "{{attribute}} виявлено для {{label}}" + }, + "header": { + "zones": "Зони", + "ratio": "Співвідношення", + "area": "Площа" + }, + "visible": "{{label}} виявлено", + "entered_zone": "{{label}} увійшов {{zones}}", + "active": "{{label}} став активним", + "stationary": "{{label}} став нерухомим", + "gone": "{{label}} ліворуч", + "heard": "{{label}} чув", + "external": "{{label}} виявлено" + }, + "noImageFound": "Для цієї позначки часу не знайдено зображення.", + "adjustAnnotationSettings": "Налаштування параметрів анотацій", + "autoTrackingTips": "Положення обмежувальних рамок будуть неточними для камер з автоматичним відстеженням.", + "carousel": { + "previous": "Попередній слайд", + "next": "Наступний слайд" + }, + "count": "{{first}} з {{second}}", + "trackedPoint": "Відстежувана точка" + }, + "details": { + "label": "Мітка", + "editLPR": { + "title": "Редагувати номерний знак", + "desc": "Введіть нове значення номерного знака для цього {{label}}", + "descNoLabel": "Введіть нове значення номерного знака для цього відстежуваного об'єкта" + }, + "item": { + "toast": { + "success": { + "updatedLPR": "Номерний знак успішно оновлено.", + "updatedSublabel": "Підмітку успішно оновлено.", + "regenerate": "Новий опис було запрошено від {{provider}}. Залежно від швидкості вашого провайдера, його перегенерація може зайняти деякий час.", + "audioTranscription": "Запит на аудіотранскрипцію успішно надіслано. Залежно від швидкості вашого сервера Frigate, транскрипція може тривати деякий час." + }, + "error": { + "regenerate": "Не вдалося звернутися до {{provider}} для отримання нового опису: {{errorMessage}}", + "updatedSublabelFailed": "Не вдалося оновити підмітку: {{errorMessage}}", + "updatedLPRFailed": "Не вдалося оновити номерний знак: {{errorMessage}}", + "audioTranscription": "Не вдалося надіслати запит на транскрипцію аудіо: {{errorMessage}}" + } + }, + "button": { + "share": "Поділитися цим оглядом", + "viewInExplore": "Переглянути в розділі «Огляд»" + }, + "tips": { + "hasMissingObjects": "Змініть конфігурацію, якщо хочете, щоб Frigate зберігав відстежувані об'єкти для таких міток: {{objects}}", + "mismatch_one": "{{count}} Виявлено та включено до цього елемента огляду недоступний об’єкт. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені.", + "mismatch_few": "{{count}} Було виявлено та включено до цього елемента огляду недоступні об’єкти. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені.", + "mismatch_many": "{{count}} Були виявлені та включені до цього елементи огляду недоступні об’єкти. Ці об’єкти або не кваліфікувалися як сповіщення чи виявлення, або вже були очищені/видалені." + }, + "title": "Огляд деталей товару", + "desc": "Переглянути деталі товару" + }, + "editSubLabel": { + "title": "Редагувати підмітку", + "desc": "Введіть нову підмітку для цього {{label}}", + "descNoLabel": "Введіть нову підмітку для цього відстежуваного об'єкта" + }, + "snapshotScore": { + "label": "Оцінка моментального результату" + }, + "topScore": { + "label": "Найкращий результат", + "info": "Найвищий бал – це найвищий середній бал для відстежуваного об’єкта, тому він може відрізнятися від балу, що відображається на мініатюрі результатів пошуку." + }, + "timestamp": "Позначка часу", + "recognizedLicensePlate": "Розпізнаний номерний знак", + "zones": "Зони", + "description": { + "aiTips": "Фрегат не запитуватиме опис у вашого постачальника генеративного штучного інтелекту, доки не завершиться життєвий цикл відстежуваного об'єкта.", + "placeholder": "Опис відстежуваного об'єкта", + "label": "Опис" + }, + "regenerateFromThumbnails": "Згенерувати з мініатюр", + "tips": { + "descriptionSaved": "Опис успішно збережено", + "saveDescriptionFailed": "Не вдалося оновити опис: {{errorMessage}}" + }, + "objects": "Об'єкти", + "estimatedSpeed": "Орієнтовна швидкість", + "camera": "Камера", + "button": { + "findSimilar": "Знайти схожі", + "regenerate": { + "title": "Регенерувати", + "label": "Згенерувати опис відстежуваного об'єкта повторно" + } + }, + "expandRegenerationMenu": "Розгорнути меню регенерації", + "regenerateFromSnapshot": "Відновити зі знімка", + "score": { + "label": "Оцінка" + } + }, + "dialog": { + "confirmDelete": { + "title": "Підтвердити видалення", + "desc": "Видалення цього відстежуваного об'єкта призведе до видалення знімка, усіх збережених вбудованих даних та усіх пов'язаних записів деталей відстеження. Записані кадри цього відстежуваного об'єкта в режимі перегляду історії НЕ будуть видалені.

    Ви впевнені, що хочете продовжити?" + } + }, + "itemMenu": { + "findSimilar": { + "label": "Знайти схожі", + "aria": "Знайти схожі відстежувані об'єкти" + }, + "viewInHistory": { + "label": "Переглянути в історії", + "aria": "Переглянути в історії" + }, + "downloadVideo": { + "aria": "Завантажити Відео", + "label": "Завантажити Відео" + }, + "submitToPlus": { + "aria": "Надіслати до Frigate Plus", + "label": "Надіслати до Frigate+" + }, + "downloadSnapshot": { + "label": "Завантажити знімок", + "aria": "Завантажити знімок" + }, + "viewObjectLifecycle": { + "label": "Переглянути життєвий цикл об'єкта", + "aria": "Показати життєвий цикл об'єкта" + }, + "deleteTrackedObject": { + "label": "Видалити цей відстежуваний об'єкт" + }, + "addTrigger": { + "label": "Додати тригер", + "aria": "Додати тригер для цього відстежуваного об'єкта" + }, + "audioTranscription": { + "label": "Транскрибувати", + "aria": "Запит на аудіотранскрипцію" + }, + "viewTrackingDetails": { + "label": "Переглянути деталі відстеження", + "aria": "Показати деталі відстеження" + }, + "showObjectDetails": { + "label": "Показати шлях до об'єкта" + }, + "hideObjectDetails": { + "label": "Приховати шлях до об'єкта" + }, + "downloadCleanSnapshot": { + "label": "Завантажити чистий знімок", + "aria": "Завантажити чистий знімок" + } + }, + "noTrackedObjects": "Відстежуваних об'єктів не знайдено", + "fetchingTrackedObjectsFailed": "Помилка отримання відстежуваних об'єктів: {{errorMessage}}", + "generativeAI": "Генеративний ШІ", + "trackedObjectDetails": "Деталі відстежуваного об'єкта", + "type": { + "details": "деталі", + "snapshot": "знімок", + "video": "відео", + "object_lifecycle": "життєвий цикл об'єкта", + "thumbnail": "мініатюра", + "tracking_details": "деталі відстеження" + }, + "exploreMore": "Дослідіть більше об'єктів {{label}}", + "aiAnalysis": { + "title": "Аналіз ШІ" + }, + "concerns": { + "label": "Проблеми" + }, + "trackingDetails": { + "title": "Деталі відстеження", + "noImageFound": "Для цієї позначки часу не знайдено зображення.", + "createObjectMask": "Створити маску об'єкта", + "adjustAnnotationSettings": "Налаштування параметрів анотацій", + "scrollViewTips": "Натисніть, щоб переглянути важливі моменти життєвого циклу цього об'єкта.", + "autoTrackingTips": "Положення обмежувальних рамок будуть неточними для камер з автоматичним відстеженням.", + "count": "{{first}} з {{second}}", + "trackedPoint": "Відстежувана точка", + "lifecycleItemDesc": { + "visible": "Виявлено {{label}}", + "entered_zone": "{{label}} увійшов до {{zones}}", + "active": "{{label}} став активним", + "stationary": "{{label}} став нерухомим", + "attribute": { + "faceOrLicense_plate": "Виявлено атрибут {{attribute}} для {{label}}", + "other": "{{label}} розпізнано як {{attribute}}" + }, + "gone": "{{label}} залишилося", + "heard": "{{label}} почув(ла)", + "external": "Виявлено {{label}}", + "header": { + "zones": "Зони", + "ratio": "Співвідношення", + "area": "Площа", + "score": "Рахунок" + } + }, + "annotationSettings": { + "title": "Налаштування анотацій", + "showAllZones": { + "title": "Показати всі зони", + "desc": "Завжди показувати зони на кадрах, де об'єкти увійшли в зону." + }, + "offset": { + "label": "Зсув анотації", + "desc": "Ці дані надходять із каналу виявлення вашої камери, але накладаються на зображення з каналу запису. Малоймовірно, що ці два потоки будуть ідеально синхронізовані. Як результат, обмежувальна рамка та відеоматеріал не будуть ідеально збігатися. Ви можете використовувати це налаштування, щоб змістити анотації вперед або назад у часі, щоб краще узгодити їх із записаним відеоматеріалом.", + "millisecondsToOffset": "Мілісекунди для зміщення виявлених анотацій. За замовчуванням: 0", + "tips": "Зменште значення, якщо відтворення відео відбувається попереду блоків та точок шляху, і збільште значення, якщо відтворення відео відбувається позаду них. Це значення може бути від’ємним.", + "toast": { + "success": "Зміщення анотації для {{camera}} було збережено у файлі конфігурації." + } + } + }, + "carousel": { + "previous": "Попередній слайд", + "next": "Наступний слайд" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/exports.json new file mode 100644 index 0000000..6b4108f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "Експорта - Frigate", + "search": "Пошук", + "noExports": "Не знайдено експортованих файлів", + "deleteExport": "Видалити експортування", + "deleteExport.desc": "Ви справді бажаєте вилучити {{exportName}}?", + "editExport": { + "title": "Перейменувати експорт", + "desc": "Введіть нову назву для цього експорту.", + "saveExport": "Зберегти експорт" + }, + "toast": { + "error": { + "renameExportFailed": "Не вдалося перейменувати експорт: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Поділитися експортом", + "downloadVideo": "Завантажити відео", + "editName": "Редагувати ім'я", + "deleteExport": "Видалити експорт" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/faceLibrary.json new file mode 100644 index 0000000..1170e3e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/faceLibrary.json @@ -0,0 +1,103 @@ +{ + "selectItem": "Вибрати {{item}}", + "documentTitle": "Бібліотека обличчя - Frigate", + "readTheDocs": "Прочитати документацію", + "deleteFaceLibrary": { + "desc": "Ви впевнені, що хочете видалити колекцію {{name}}? Це назавжди видалить усі пов’язані з нею обличчя.", + "title": "Видалити ім'я" + }, + "toast": { + "error": { + "renameFaceFailed": "Не вдалося перейменувати обличчя: {{errorMessage}}", + "updateFaceScoreFailed": "Не вдалося оновити оцінку обличчя: {{errorMessage}}", + "deleteFaceFailed": "Не вдалося видалити: {{errorMessage}}", + "uploadingImageFailed": "Не вдалося завантажити зображення: {{errorMessage}}", + "addFaceLibraryFailed": "Не вдалося встановити ім'я обличчя: {{errorMessage}}", + "deleteNameFailed": "Не вдалося видалити ім'я: {{errorMessage}}", + "trainFailed": "Не вдалося тренуватися: {{errorMessage}}" + }, + "success": { + "updatedFaceScore": "Оцінку обличчя успішно оновлено до {{name}} ({{score}}).", + "deletedName_one": "{{count}} Обличчя успішно видалено.", + "deletedName_few": "{{count}} Обличчі успішно видалено.", + "deletedName_many": "{{count}} Облич. успішно видалено.", + "uploadedImage": "Зображення успішно завантажено.", + "addFaceLibrary": "{{name}} успішно додано до Бібліотеки облич!", + "renamedFace": "Обличчя успішно перейменовано на {{name}}", + "trainedFace": "Успішно натреноване обличчя.", + "deletedFace_one": "Успішно видалено {{count}} обличчя.", + "deletedFace_few": "Успішно видалено {{count}} обличчі.", + "deletedFace_many": "Успішно видалено {{count}} облич." + } + }, + "details": { + "scoreInfo": "Оцінка підмітки – це зважена оцінка для всіх розпізнаних ознак достовірності обличчя, тому вона може відрізнятися від оцінки, показаної на знімку.", + "subLabelScore": "Оцінка підмітки", + "person": "Людина", + "face": "Деталі обличчя", + "faceDesc": "Деталі відстежуваного об'єкта, який створив це обличчя", + "timestamp": "Позначка часу", + "unknown": "Невідомо" + }, + "steps": { + "uploadFace": "Завантажити зображення обличчя", + "nextSteps": "Наступні кроки", + "faceName": "Введіть ім'я обличчя", + "description": { + "uploadFace": "Завантажте зображення обличчя {{name}}, на якому його/її обличчя зображено спереду. Зображення не потрібно обрізати, щоб воно мало лише обличчя." + } + }, + "selectFace": "Виберіть обличчя", + "renameFace": { + "title": "Перейменувати обличчя", + "desc": "Введіть нову назву для {{name}}" + }, + "button": { + "deleteFaceAttempts": "Видалити обличчі", + "renameFace": "Перейменувати обличчя", + "uploadImage": "Завантажити зображення", + "deleteFace": "Видалити обличчя", + "reprocessFace": "Переобробка обличчя", + "addFace": "Додати обличчя" + }, + "imageEntry": { + "maxSize": "Максимальний розмір: {{size}} МБ", + "validation": { + "selectImage": "Будь ласка, виберіть файл зображення." + }, + "dropActive": "Скинь зображення сюди…", + "dropInstructions": "Перетягніть або вставте зображення сюди, або клацніть, щоб вибрати" + }, + "trainFaceAs": "Тренуйте обличчя як:", + "trainFace": "Обличчя поїзда", + "description": { + "addFace": "Додайте нову колекцію до Бібліотеки облич, завантаживши своє перше зображення.", + "placeholder": "Введіть назву для цієї колекції", + "invalidName": "Недійсне ім'я. Ім'я може містити лише літери, цифри, пробіли, апострофи, символи підкреслення та дефіси." + }, + "uploadFaceImage": { + "title": "Завантажити зображення обличчя", + "desc": "Завантажте зображення для сканування облич та додайте його для {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "Створити колекцію", + "desc": "Створити нову колекцію", + "new": "Створити нове обличчя", + "nextSteps": "Щоб створити міцну основу:
  • Використовуйте вкладку «Недавні розпізнавання», щоб вибрати та навчити систему розпізнавати зображення для кожної виявленої особи.
  • Для досягнення найкращих результатів зосередьтеся на прямих зображеннях; уникайте навчання зображень, на яких обличчя зняті під кутом.
  • " + }, + "train": { + "title": "Нещодавні визнання", + "aria": "Виберіть нещодавні визнання", + "empty": "Немає останніх спроб розпізнавання обличчя", + "titleShort": "Нещодавні" + }, + "collections": "Колекції", + "deleteFaceAttempts": { + "title": "Видалити обличчі", + "desc_one": "Ви впевнені, що хочете видалити {{count}} обличчя? Цю дію неможливо скасувати.", + "desc_few": "Ви впевнені, що хочете видалити {{count}} обличчі? Цю дію неможливо скасувати.", + "desc_many": "Ви впевнені, що хочете видалити {{count}} облич? Цю дію неможливо скасувати." + }, + "nofaces": "Немає облич", + "pixels": "{{area}}пикс" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/live.json new file mode 100644 index 0000000..0b8b405 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/live.json @@ -0,0 +1,189 @@ +{ + "manualRecording": { + "started": "Почав ручний запис на вимогу.", + "showStats": { + "desc": "Позначте цей пункт, щоб показувати статистику потоку як накладання на канал камери.", + "label": "Показати статистику" + }, + "failedToEnd": "Не вдалося завершити запис вручну на вимогу.", + "playInBackground": { + "label": "Грати у фоновому режимі", + "desc": "Увімкніть цей параметр, щоб продовжувати потокове передавання, коли програвач приховано." + }, + "tips": "Завантажте миттєвий знімок або запустіть ручну подію на основі налаштувань збереження запису цієї камери.", + "title": "На-вимогу", + "debugView": "Режим зневаджування", + "start": "Почати запис за запитом", + "failedToStart": "Не вдалося запустити ручний запис на вимогу.", + "end": "Завершення запису на вимогу", + "ended": "Запис на вимогу припинився.", + "recordDisabledTips": "Оскільки запис вимкнено або обмежено в конфігурації цієї камери, буде збережено лише знімок." + }, + "snapshots": { + "enable": "Увімкнути знімки", + "disable": "Вимкнути знімки" + }, + "stream": { + "twoWayTalk": { + "tips": "Ваш пристрій повинен підтримувати функцію, а WebRTC повинен бути налаштований для двосторонньої розмови.", + "tips.documentation": "Прочитати документацію ", + "available": "Двостороння розмова доступна для цього потоку", + "unavailable": "Двостороння розмова недоступна для цього потоку" + }, + "playInBackground": { + "tips": "Увімкніть цей параметр, щоб продовжувати потокове передавання, коли програвач приховано.", + "label": "Грати у фоновому режимі" + }, + "title": "Потiк", + "audio": { + "tips": { + "documentation": "Прочитати документацію ", + "title": "Звук повинен бути виведений з камери і налаштований в go2rtc для цього потоку." + }, + "available": "Звук доступний для цього потоку", + "unavailable": "Аудіо недоступне для цього потоку" + }, + "lowBandwidth": { + "resetStream": "Скинути потік", + "tips": "Режим перегляду в реальному часі перемикається в економічний режим через помилки буферизації або потоку." + }, + "debug": { + "picker": "Вибір потоку недоступний у режимі налагодження. У режимі налагодження завжди використовується потік, якому призначено роль виявлення." + } + }, + "muteCameras": { + "disable": "Увімкнути звук на всі камери", + "enable": "Вимкнути всі камери" + }, + "ptz": { + "move": { + "clickMove": { + "label": "Клацніть у кадрі, щоб відцентрувати камеру", + "enable": "Увімкнути клацання для переміщення", + "disable": "Вимкнути клацання для переміщення" + }, + "up": { + "label": "Перемістити PTZ камеру вгору" + }, + "left": { + "label": "Переміщення камери PTZ вліво" + }, + "down": { + "label": "Переміщення PTZ камери вниз" + }, + "right": { + "label": "Переміщення PTZ камери вправо" + } + }, + "zoom": { + "in": { + "label": "Наближати PTZ камеру" + }, + "out": { + "label": "Зменшити PTZ камеру" + } + }, + "presets": "Попередни установки PTZ камери", + "frame": { + "center": { + "label": "Клацніть у кадрі, щоб відцентрувати камеру PTZ" + } + }, + "focus": { + "in": { + "label": "Фокус PTZ-камери" + }, + "out": { + "label": "Вихід PTZ-камери для фокусування" + } + } + }, + "editLayout": { + "exitEdit": "Вийти з редагування", + "label": "Редагувати макет", + "group": { + "label": "Редагувати групу камер" + } + }, + "documentTitle": "Пряма трансляція - Frigate", + "documentTitle.withCamera": "{{camera}} - Пряма трансляція - Frigate", + "lowBandwidthMode": "Економічний режим", + "twoWayTalk": { + "enable": "Увімкнути двосторонню розмову", + "disable": "Вимкнути двосторонню розмову" + }, + "cameraAudio": { + "enable": "Увімкнути звук камери", + "disable": "Вимкнути звук камери" + }, + "camera": { + "enable": "Увімкнути камеру", + "disable": "Вимкнути камеру" + }, + "detect": { + "enable": "Увімкнути виявлення", + "disable": "Вимкнути виявлення" + }, + "recording": { + "enable": "Увімкнути запис", + "disable": "Вимкнути запис" + }, + "audioDetect": { + "enable": "Увімкнути виявлення звуку", + "disable": "Вимкнути виявлення звуку" + }, + "autotracking": { + "disable": "Вимкнути автотрекінг", + "enable": "Увімкнути автотрекінг" + }, + "streamStats": { + "enable": "Показати статистику потоку", + "disable": "Сховати статистику потоку" + }, + "streamingSettings": "Параметри потокового передавання", + "notifications": "Повідомлення", + "audio": "Аудіо", + "suspend": { + "forTime": "Призупинити до: " + }, + "cameraSettings": { + "title": "{{camera}} Налаштування", + "cameraEnabled": "Камера включена", + "objectDetection": "Виявлення об'єктів", + "recording": "Записування", + "snapshots": "Знімки", + "audioDetection": "Виявлення звуку", + "autotracking": "Автотрекiнг", + "transcription": "Аудіотранскрипція" + }, + "history": { + "label": "Показати історичні кадри" + }, + "effectiveRetainMode": { + "modes": { + "all": "Всi", + "motion": "Рух", + "active_objects": "Активні об'єкти" + }, + "notAllTips": "Ваш {{source}} конфігурацію збереження записів встановлено на режим: {{effectiveRetainMode}}, тому цей запис на вимогу збереже лише сегменти з {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Увімкнути транскрипцію аудіо в реальному часі", + "disable": "Вимкнути транскрипцію аудіо в реальному часі" + }, + "noCameras": { + "title": "Немає налаштованих камер", + "description": "Почніть з підключення камери до Frigate.", + "buttonText": "Додати камеру", + "restricted": { + "title": "Немає Доступних Камер", + "description": "У вас немає дозволу на перегляд будь-яких камер у цій групі." + } + }, + "snapshot": { + "takeSnapshot": "Завантажити миттєвий знімок", + "noVideoSource": "Немає доступного джерела відео для знімка.", + "captureFailed": "Не вдалося зробити знімок.", + "downloadStarted": "Розпочато завантаження знімка." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/recording.json new file mode 100644 index 0000000..a1ae10e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "Експорт", + "toast": { + "error": { + "noValidTimeSelected": "Вибраний діапазон часу не є коректним", + "endTimeMustAfterStartTime": "Час закінчення повинен бути пізніше часу початку" + } + }, + "calendar": "Календар", + "filter": "Фiльтр", + "filters": "Фiльтри" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/search.json new file mode 100644 index 0000000..0d8657e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Пошук", + "filter": { + "tips": { + "desc": { + "step1": "Введіть назву ключа фільтра, а потім двокрапку (наприклад, «камери:»).", + "step2": "Виберіть значення з пропозицій або введіть своє.", + "step3": "Використовуйте кілька фільтрів, додаючи їх один за одним з проміжком між ними.", + "step5": "Фільтр діапазону часу використовує формат {{exampleTime}}.", + "step6": "Видалити фільтри, натиснувши 'x' поруч з ними.", + "exampleLabel": "Наприклад:", + "step4": "Фільтри дат (до: і після:) використовують формат {{DateFormat}}.", + "text": "Фільтри допомагають звузити результати пошуку. Ось як використовувати їх у полі вводу:" + }, + "title": "Як використовувати текстові фільтри" + }, + "header": { + "currentFilterType": "Фільтрувати значення", + "activeFilters": "Активнi фiльтри", + "noFilters": "Фiлтри" + }, + "label": { + "zones": "Зони", + "sub_labels": "Суб-меткi", + "search_type": "Тип пошуку", + "cameras": "Камери", + "labels": "Етикеткi", + "time_range": "Часовий діапазон", + "before": "Перед", + "after": "Пiсля", + "min_score": "Мінімальний бал", + "max_score": "Найвищий бал", + "min_speed": "Мінімальна швидкість", + "max_speed": "Максимальна швидкість", + "recognized_license_plate": "Розпізнаний номерний знак", + "has_clip": "Має клiп", + "has_snapshot": "Має знiмок" + }, + "searchType": { + "thumbnail": "Мініатюра", + "description": "Опис" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "Дата 'до' повинна бути пізнішою, ніж дата 'після'.", + "afterDatebeEarlierBefore": "Дата 'після' повинна бути раніше, ніж дата 'до'.", + "minScoreMustBeLessOrEqualMaxScore": "Значення 'min_score' має бути меншим або дорівнювати 'max_score'.", + "maxScoreMustBeGreaterOrEqualMinScore": "Значення 'max_score' має бути більшим або дорівнювати 'min_score'.", + "minSpeedMustBeLessOrEqualMaxSpeed": "Значення 'min_speed' має бути меншим або дорівнювати 'max_speed'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "Значення 'max_speed' має бути більшим або дорівнювати 'min_speed'." + } + } + }, + "similaritySearch": { + "title": "Пошук подібності", + "active": "Активнi пошук подібності", + "clear": "Очистити пошук подібності" + }, + "placeholder": { + "search": "Пошук…" + }, + "button": { + "save": "Зберігати пошук", + "delete": "Видалити збережений пошук", + "filterInformation": "Фільтрувати інформацію", + "clear": "Очистити пошук", + "filterActive": "Активни фільтри" + }, + "savedSearches": "Збережені пошуки", + "searchFor": "Пошук да {{inputValue}}", + "trackedObjectId": "Iдентифікатори об'єктів" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/settings.json new file mode 100644 index 0000000..811444b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/settings.json @@ -0,0 +1,1313 @@ +{ + "notification": { + "notificationSettings": { + "documentation": "Прочитати документацію", + "desc": "Frigate може надсилати push-сповіщення на ваш пристрій, коли він працює у браузері або встановлений як PWA.", + "title": "Налаштування сповіщень" + }, + "notificationUnavailable": { + "desc": "Веб-повідомлення вимагають безпечного контексту (https://…). Це обмеження браузера. Безпечний доступ до фрегатів для використання сповіщень.", + "documentation": "Прочитати документацію", + "title": "Сповіщення недоступні" + }, + "globalSettings": { + "desc": "Тимчасово призупинити сповіщення для певних камер на всіх зареєстрованих пристроях.", + "title": "Глобальні налаштування" + }, + "email": { + "title": "Електронна пошта", + "placeholder": "наприклад example@email.com", + "desc": "Потрібна дійсна електронна адреса, яка буде використана для сповіщення вас про будь-які проблеми з push-сервісом." + }, + "cameras": { + "title": "Камери", + "noCameras": "Немає доступних камер", + "desc": "Виберіть, для яких камер увімкнути сповіщення." + }, + "suspendTime": { + "5minutes": "Призупинити на 5 хвилин", + "30minutes": "Призупинити на 30 хвилин", + "1hour": "Призупинити на 1 годину", + "12hours": "Призупинити на 12 годин", + "24hours": "Призупинити на 24 години", + "untilRestart": "Призупинити до перезапуску", + "10minutes": "Призупинити на 10 хвилин", + "suspend": "Призупинити" + }, + "toast": { + "success": { + "registered": "Успішно зареєстровано для отримання сповіщень. Перед надсиланням будь-яких сповіщень (включно з тестовим сповіщенням) потрібен перезапуск Frigate.", + "settingSaved": "Налаштування сповіщень збережено." + }, + "error": { + "registerFailed": "Не вдалося зберегти реєстрацію сповіщення." + } + }, + "title": "Сповіщення", + "suspended": "Сповіщення призупинено {{time}}", + "deviceSpecific": "Налаштування, специфічні для пристрою", + "registerDevice": "Зареєструйте цей пристрій", + "unregisterDevice": "Скасувати реєстрацію цього пристрою", + "sendTestNotification": "Надіслати тестове сповіщення", + "active": "Активні сповіщення", + "cancelSuspension": "Скасувати призупинення", + "unsavedRegistrations": "Незбережені реєстрації сповіщень", + "unsavedChanges": "Незбережені зміни сповіщень" + }, + "camera": { + "streams": { + "title": "Потоки", + "desc": "Тимчасово вимкніть камеру до перезавантаження Frigate. Вимкнення камери повністю зупиняє обробку потоків цієї камери Frigate. Виявлення, запис і налагодження будуть недоступні.
    Примітка. Це не вимикає повторні потоки go2rtc." + }, + "reviewClassification": { + "readTheDocumentation": "Прочитати документацію", + "objectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "zoneObjectDetectionsTips": { + "regardlessOfZoneObjectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "text": "Усі об’єкти {{detectionsLabels}}, що не належать до категорії {{zone}} на {{cameraName}}, будуть відображатися як Виявлення.", + "notSelectDetections": "Усі об’єкти {{detectionsLabels}}, виявлені в {{zone}} на {{cameraName}}, які не віднесені до категорії «Сповіщення», будуть відображатися як Виявлення незалежно від того, в якій зоні вони знаходяться." + }, + "objectAlertsTips": "Усі об’єкти {{alertsLabels}} на {{cameraName}} будуть відображатися як сповіщення.", + "toast": { + "success": "Конфігурацію класифікації перегляду збережено. Перезапустіть Frigate, щоб застосувати зміни." + }, + "title": "Класифікація оглядів", + "desc": "Frigate класифікує елементи огляду як сповіщення та виявлення. За замовчуванням усі об’єкти люди та автомобілі вважаються сповіщеннями. Ви можете уточнити категоризацію елементів огляду, налаштувавши для них обов'язкові зони.", + "noDefinedZones": "Для цієї камери не визначено жодної зони.", + "selectDetectionsZones": "Виберіть зони для виявлення", + "limitDetections": "Обмеження виявлення певними зонами", + "zoneObjectAlertsTips": "Усі об’єкти {{alertsLabels}}, виявлені в {{zone}} на {{cameraName}}, будуть відображатися як сповіщення.", + "selectAlertsZones": "Виберіть зони для сповіщень", + "unsavedChanges": "Незбережені налаштування класифікації відгуків для {{camera}}" + }, + "review": { + "alerts": "Сповіщення ", + "detections": "Виявлення ", + "title": "Огляд", + "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. " + }, + "title": "Налаштування камери", + "object_descriptions": { + "title": "Генеративні описи об'єктів штучного інтелекту", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери. Якщо вимкнено, для елементів огляду на цій камері не запитуватимуться згенеровані штучним інтелектом описи." + }, + "addCamera": "Додати нову камеру", + "editCamera": "Редагувати камеру:", + "selectCamera": "Виберіть камеру", + "backToSettings": "Назад до налаштувань камери", + "cameraConfig": { + "add": "Додати камеру", + "edit": "Редагувати камеру", + "description": "Налаштуйте параметри камери, включаючи потокові входи та ролі.", + "name": "Назва камери", + "nameRequired": "Потрібно вказати назву камери", + "nameInvalid": "Назва камери повинна містити лише літери, цифри, символи підкреслення або дефіси", + "namePlaceholder": "наприклад, вхідні_двері", + "enabled": "Увімкнено", + "ffmpeg": { + "inputs": "Вхідні потоки", + "path": "Шлях потоку", + "pathRequired": "Шлях потоку обов'язковий", + "pathPlaceholder": "'rtsp://...", + "roles": "Ролі", + "rolesRequired": "Потрібна хоча б одна роль", + "rolesUnique": "Кожна роль (аудіо, виявлення, запис) може бути призначена лише одному потоку", + "addInput": "Додати вхідний потік", + "removeInput": "Вилучити вхідний потік", + "inputsRequired": "Потрібен принаймні один вхідний потік" + }, + "toast": { + "success": "Камеру {{cameraName}} успішно збережено" + }, + "nameLength": "Назва камери має містити менше 24 символів." + } + }, + "masksAndZones": { + "motionMasks": { + "polygonAreaTooLarge": { + "documentation": "Прочитати документацію", + "tips": "Маски руху не запобігають виявленню об'єктів. Натомість слід використовувати обов'язкову зону.", + "title": "Маска руху покриває {{polygonArea}}% кадру камери. Великі маски руху не рекомендуються." + }, + "context": { + "documentation": "Прочитати документацію", + "title": "Маски руху використовуються для запобігання виявлення небажаних типів руху (наприклад: гілки дерева, часові мітки камери). Слід використовувати маски рухудуже економно, надмірне маскування ускладнить відстеження об'єктів." + }, + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "add": "Нова маска руху", + "edit": "Редагувати маску руху", + "toast": { + "success": { + "title": "{{polygonName}} збережено.", + "noName": "Маску руху збережено." + } + }, + "label": "Маска руху", + "documentTitle": "Редагувати маску руху – Фрегат", + "desc": { + "title": "Маски руху використовуються для запобігання спрацьовуванню виявлення небажаних типів руху. Надмірне маскування ускладнить відстеження об'єктів.", + "documentation": "Документація" + }, + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів" + }, + "zones": { + "label": "Зони", + "name": { + "inputPlaceHolder": "Введіть назву…", + "title": "Ім'я", + "tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони на цій камері." + }, + "desc": { + "title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.", + "documentation": "Документація" + }, + "allObjects": "Усі об'єкти", + "speedEstimation": { + "title": "Оцінка швидкості", + "desc": "Увімкнути оцінку швидкості для об'єктів у цій зоні. Зона повинна мати рівно 4 точки.", + "docs": "Прочитайте документацію", + "lineADistance": "Відстань лінії A ({{unit}})", + "lineBDistance": "Відстань лінії B ({{unit}})", + "lineCDistance": "Відстань лінії C ({{unit}})", + "lineDDistance": "Відстань лінії D ({{unit}})" + }, + "speedThreshold": { + "title": "Поріг швидкості ({{unit}})", + "desc": "Визначає мінімальну швидкість для об'єктів, які слід враховувати в цій зоні.", + "toast": { + "error": { + "pointLengthError": "Оцінку швидкості для цієї зони вимкнено. Зони з оцінкою швидкості повинні мати рівно 4 бали.", + "loiteringTimeError": "Зони з часом байдикування більше 0 не слід використовувати з оцінкою швидкості." + } + } + }, + "documentTitle": "Зона редагування – Фрегат", + "add": "Додати зону", + "edit": "Редагувати зону", + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів", + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "inertia": { + "title": "Інерція", + "desc": "Визначає, скільки кадрів об'єкт має перебувати в зоні, перш ніж його буде враховано в ній. За замовчуванням: 3" + }, + "loiteringTime": { + "title": "Час тиняння", + "desc": "Встановлює мінімальний час у секундах, протягом якого об'єкт має перебувати в зоні для активації. За замовчуванням: 0" + }, + "objects": { + "title": "Об'єкти", + "desc": "Список об'єктів, що належать до цієї зони." + }, + "toast": { + "success": "Зону ({{zoneName}}) збережено." + } + }, + "objectMasks": { + "desc": { + "title": "Маски фільтрів об'єктів використовуються для фільтрації хибнопозитивних результатів для заданого типу об'єкта на основі його розташування.", + "documentation": "Документація" + }, + "documentTitle": "Редагувати маску об'єкта - Фрегат", + "add": "Додати маску об'єкта", + "edit": "Редагувати маску об'єкта", + "context": "Маски фільтрів об'єктів використовуються для фільтрації хибнопозитивних результатів для заданого типу об'єкта на основі його розташування.", + "point_one": "{{count}} бал", + "point_few": "{{count}} бали", + "point_many": "{{count}} балів", + "clickDrawPolygon": "Клацніть, щоб намалювати багатокутник на зображенні.", + "objects": { + "title": "Об'єкти", + "desc": "Тип об'єкта, що застосовується до цієї маски об'єкта.", + "allObjectTypes": "Усі типи об'єктів" + }, + "toast": { + "success": { + "title": "{{polygonName}} збережено.", + "noName": "Маску об'єкта збережено." + } + }, + "label": "Маски об'єктів" + }, + "restart_required": "Потрібно перезавантажити (маски/зони змінено)", + "toast": { + "success": { + "copyCoordinates": "Координати для {{polyName}} скопійовано в буфер обміну." + }, + "error": { + "copyCoordinatesFailed": "Не вдалося скопіювати координати в буфер обміну." + } + }, + "form": { + "zoneName": { + "error": { + "alreadyExists": "Для цієї камери вже існує зона з такою назвою.", + "mustNotContainPeriod": "Назва зони не повинна містити крапок.", + "mustNotBeSameWithCamera": "Назва зони не повинна збігатися з назвою камери.", + "mustBeAtLeastTwoCharacters": "Назва зони має містити щонайменше 2 символи.", + "hasIllegalCharacter": "Назва зони містить недопустимі символи.", + "mustHaveAtLeastOneLetter": "Назва зони повинна містити щонайменше одну літеру." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Ви впевнені, що хочете видалити {{type}} {{name}}?", + "title": "Підтвердити видалення", + "success": "{{name}} було видалено." + }, + "removeLastPoint": "Видалити останню точку", + "reset": { + "label": "Очистити всі бали" + }, + "snapPoints": { + "true": "Точки прив'язки", + "false": "Не зачіпайте точки" + }, + "error": { + "mustBeFinished": "Малювання полігону має бути завершене перед збереженням." + } + }, + "distance": { + "error": { + "text": "Відстань має бути більшою або рівною 0,1.", + "mustBeFilled": "Для використання оцінки швидкості необхідно заповнити всі поля відстані." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Інерція повинна бути вище 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Час байдикування має бути більшим або рівним 0." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Поріг швидкості має бути більшим або рівним 0,1." + } + } + }, + "filter": { + "all": "Усі маски та зони" + }, + "motionMaskLabel": "Маска руху {{number}}", + "objectMaskLabel": "Маска об'єкта {{number}} ({{label}})" + }, + "debug": { + "zones": { + "title": "Зони", + "desc": "Показати контур будь-яких визначених зон" + }, + "objectShapeFilterDrawing": { + "document": "Прочитати документацію ", + "tips": "Увімкніть цю опцію, щоб намалювати прямокутник на зображенні з камери, щоб показати його площу та співвідношення. Ці значення потім можна використовувати для встановлення параметрів фільтра форми об'єкта у вашій конфігурації.", + "title": "Фільтр форми об'єкта, малюнок", + "desc": "Намалюйте прямокутник на зображенні, щоб переглянути деталі площі та співвідношення", + "score": "Рахунок", + "area": "Площа", + "ratio": "Співвідношення" + }, + "regions": { + "tips": "

    Регіональні рамки


    Яскраво-зелені рамки будуть накладені на області інтересу в кадрі, які надсилаються на детектор об'єктів.

    ", + "desc": "Показати рамку області інтересу, що надсилається на детектор об'єктів", + "title": "Регіони" + }, + "boundingBoxes": { + "colors": { + "info": "
  • Під час запуску кожній мітці об’єкта будуть призначені різні кольори
  • Темно-синя тонка лінія вказує на те, що об’єкт не виявлено в цей момент часу
  • Тонка сіра лінія вказує на те, що об’єкт виявлено як нерухомий
  • Товста лінія вказує на те, що об’єкт є об’єктом автоматичного відстеження (якщо його ввімкнено)
  • ", + "label": "Кольори рамки обмежування об'єкта" + }, + "title": "Обмежувальні рамки", + "desc": "Показувати обмежувальні рамки навколо відстежуваних об'єктів" + }, + "title": "Налагодження", + "detectorDesc": "Фрегат використовує ваші детектори ({{detectors}}) для виявлення об'єктів у відеопотоці вашої камери.", + "desc": "У режимі налагодження відображається відстежувані об’єкти та їхня статистика в режимі реального часу. Список об’єктів показує зведену інформацію про виявлені об’єкти із затримкою в часі.", + "debugging": "Налагоджуємо", + "timestamp": { + "title": "Позначка часу", + "desc": "Накладання позначки часу на зображення" + }, + "mask": { + "title": "Маски руху", + "desc": "Показати полігони маски руху" + }, + "motion": { + "title": "Коробки руху", + "desc": "Показувати рамки навколо областей, де виявлено рух", + "tips": "

    Поля руху


    Червоні поля будуть накладені на області кадру, де наразі виявляється рух

    " + }, + "objectList": "Список об'єктів", + "noObjects": "Без об'єктів", + "paths": { + "title": "Шляхи", + "desc": "Показувати важливі точки шляху відстежуваного об'єкта", + "tips": "

    Шляхи


    Лінії та кола позначатимуть важливі точки, які відстежуваний об'єкт переміщував протягом свого життєвого циклу.

    " + }, + "audio": { + "title": "Аудіо", + "noAudioDetections": "Немає виявлення звуку", + "score": "рахунок", + "currentRMS": "Поточне середньоквадратичне значення", + "currentdbFS": "Поточний dbFS" + }, + "openCameraWebUI": "Відкрийте веб-інтерфейс {{camera}}" + }, + "classification": { + "licensePlateRecognition": { + "readTheDocumentation": "Прочитати документацію", + "title": "Розпізнавання номерних знаків", + "desc": "Фрегат може розпізнавати номерні знаки транспортних засобів та автоматично додавати виявлені символи до поля recognized_license_plate або відоме ім'я як sub_label до об'єктів типу автомобіль. Поширеним випадком використання може бути зчитування номерних знаків автомобілів, що заїжджають на під'їзну доріжку, або автомобілів, що проїжджають повз вулицю." + }, + "faceRecognition": { + "readTheDocumentation": "Прочитати документацію", + "title": "Розпізнавання обличчя", + "desc": "Розпізнавання обличчя дозволяє присвоювати людям імена, і коли їхнє обличчя розпізнається, Frigate призначає ім'я людини як підмітку. Ця інформація міститься в інтерфейсі користувача, фільтрах, а також у сповіщеннях.", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для розпізнавання обличчя.", + "small": { + "title": "маленький", + "desc": "Використання small використовує модель вбудовування облич FaceNet, яка ефективно працює на більшості процесорів." + }, + "large": { + "title": "великий", + "desc": "Використання параметра large використовує модель вбудовування облич ArcFace та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "semanticSearch": { + "reindexNow": { + "alreadyInProgress": "Переіндексація вже триває.", + "error": "Не вдалося розпочати переіндексацію: {{errorMessage}}", + "confirmDesc": "Ви впевнені, що хочете переіндексувати всі вбудовані відстежувані об'єкти? Цей процес працюватиме у фоновому режимі, але може максимально навантажити ваш процесор і зайняти чимало часу. Ви можете спостерігати за прогресом на Ex.", + "confirmButton": "Переіндексувати", + "desc": "Переіндексація призведе до повторного створення вбудованих елементів для всіх відстежуваних об'єктів. Цей процес працює у фоновому режимі та може максимально навантажити ваш процесор і зайняти достатню кількість часу залежно від кількості відстежуваних об'єктів.", + "success": "Переіндексацію успішно розпочато.", + "label": "Переіндексувати зараз", + "confirmTitle": "Підтвердити переіндексацію" + }, + "title": "Семантичний пошук", + "desc": "Семантичний пошук у Frigate дозволяє знаходити відстежувані об'єкти у ваших оглядах, використовуючи або саме зображення, або текстовий опис, визначений користувачем, або автоматично згенерований.", + "readTheDocumentation": "Прочитайте документацію", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для вбудовування семантичного пошуку.", + "small": { + "title": "маленький", + "desc": "Використання малих працівників квантована версія моделі, яка використовує менше оперативної пам'яті та працює швидше на процесорі з дуже незначною різницею в якості вбудовування." + }, + "large": { + "title": "великий", + "desc": "Використання large використовує повну модель Jina та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "restart_required": "Потрібно перезавантажити (налаштування класифікації змінено)", + "toast": { + "success": "Налаштування класифікації збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "title": "Налаштування класифікації", + "birdClassification": { + "title": "Класифікація птахів", + "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі Tensorflow. Коли відомого птаха розпізнають, його загальна назва буде додана як sub_label. Ця інформація міститься в інтерфейсі користувача, фільтрах, а також у сповіщеннях." + }, + "unsavedChanges": "Незбережені зміни налаштувань класифікації" + }, + "frigatePlus": { + "modelInfo": { + "loading": "Завантаження інформації про модель…", + "loadingAvailableModels": "Завантаження доступних моделей…", + "plusModelType": { + "baseModel": "Базова модель", + "userModel": "Точно налаштований" + }, + "supportedDetectors": "Підтримувані детектори", + "error": "Не вдалося завантажити інформацію про модель", + "availableModels": "Доступні моделі", + "trainDate": "Дата тренування", + "baseModel": "Базова модель", + "modelSelect": "Тут можна вибрати доступні моделі на Frigate+. Зверніть увагу, що можна вибрати лише моделі, сумісні з вашою поточною конфігурацією детектора.", + "title": "Інформація про модель", + "modelType": "Тип моделі", + "cameras": "Камери" + }, + "snapshotConfig": { + "title": "Конфігурація знімків", + "desc": "Для надсилання до Frigate+ потрібно ввімкнути як знімки, так і знімки clean_copy у вашій конфігурації.", + "documentation": "Прочитайте документацію", + "table": { + "camera": "Камера", + "snapshots": "Знімки", + "cleanCopySnapshots": "clean_copy Знімки" + }, + "cleanCopyWarning": "На деяких камерах увімкнено знімки екрана, але вимкнено чисте копіювання. Щоб мати змогу надсилати зображення з цих камер до Frigate+, потрібно ввімкнути параметр clean_copy у конфігурації знімків екрана." + }, + "apiKey": { + "desc": "Ключ API Frigate+ забезпечує інтеграцію з сервісом Frigate+.", + "notValidated": "Ключ API Frigate+ не виявлено або не перевірено", + "title": "Ключ API Фрегат+", + "validated": "Ключ API Frigate+ виявлено та перевірено", + "plusLink": "Дізнайтеся більше про Фрегат+" + }, + "restart_required": "Потрібно перезавантаження (модель Frigate+ змінена)", + "toast": { + "success": "Налаштування Frigate+ збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "title": "Налаштування Фрегат+", + "unsavedChanges": "Незбережені зміни налаштувань Frigate+" + }, + "general": { + "calendar": { + "title": "Календар", + "firstWeekday": { + "label": "Перший день тижня", + "desc": "День, з якого починаються тижні календаря оглядів.", + "sunday": "Неділя", + "monday": "Понеділок" + } + }, + "title": "Налаштування інтерфейсу користувача", + "liveDashboard": { + "title": "Панель керування в прямому ефірі", + "automaticLiveView": { + "label": "Автоматичний перегляд у реальному часі", + "desc": "Автоматично перемикатися на режим реального часу з камери, коли виявляється активність. Якщо вимкнути цю опцію, статичні зображення з камери на панелі керування реальним часом оновлюватимуться лише раз на хвилину." + }, + "playAlertVideos": { + "label": "Відтворити відео зі сповіщеннями", + "desc": "За замовчуванням останні сповіщення на панелі керування Live відтворюються як невеликі відеозаписи, що циклічно відтворюються. Вимкніть цю опцію, щоб відображати лише статичне зображення останніх сповіщень на цьому пристрої/у браузері." + }, + "displayCameraNames": { + "label": "Завжди показувати назви камер", + "desc": "Завжди відображати назви камер у чіпі на панелі керування режимом живого перегляду з кількох камер." + }, + "liveFallbackTimeout": { + "label": "Час очікування резервного програвача в реальному часі", + "desc": "Коли високоякісна пряма трансляція з камери недоступна, повернутися до режиму низької пропускної здатності через певну кількість секунд. За замовчуванням: 3." + } + }, + "storedLayouts": { + "title": "Збережені макети", + "clearAll": "Очистити всі макети", + "desc": "Розташування камер у групі камер можна перетягувати/змінювати розмір. Позиції зберігаються в локальному сховищі вашого браузера." + }, + "cameraGroupStreaming": { + "title": "Налаштування потокової передачі групи камер", + "desc": "Налаштування потокової передачі для кожної групи камер зберігаються в локальному сховищі вашого браузера.", + "clearAll": "Очистити всі налаштування потокового передавання" + }, + "recordingsViewer": { + "title": "Переглядач записів", + "defaultPlaybackRate": { + "label": "Стандартна швидкість відтворення", + "desc": "Швидкість відтворення записів за замовчуванням." + } + }, + "toast": { + "success": { + "clearStoredLayout": "Очищено збережений макет для {{cameraName}}", + "clearStreamingSettings": "Очищено налаштування потокової передачі для всіх груп камер." + }, + "error": { + "clearStoredLayoutFailed": "Не вдалося очистити збережений макет: {{errorMessage}}", + "clearStreamingSettingsFailed": "Не вдалося очистити налаштування потокового передавання: {{errorMessage}}" + } + } + }, + "motionDetectionTuner": { + "improveContrast": { + "title": "Покращення контрастності", + "desc": "Покращення контрастності для темніших сцен. За замовчуванням: УВІМК." + }, + "desc": { + "documentation": "Прочитайте посібник з налаштування руху", + "title": "Фрегат використовує виявлення руху як першочергову перевірку, щоб побачити, чи відбувається щось у кадрі, що варто перевірити за допомогою виявлення об'єктів." + }, + "title": "Тюнер виявлення руху", + "Threshold": { + "title": "Поріг", + "desc": "Порогове значення визначає, наскільки велика зміна яскравості пікселя має вважатися рухом. За замовчуванням: 30" + }, + "toast": { + "success": "Налаштування руху збережено." + }, + "contourArea": { + "title": "Площа контуру", + "desc": "Значення площі контуру використовується для визначення того, які групи змінених пікселів кваліфікуються як рух. За замовчуванням: 10" + }, + "unsavedChanges": "Незбережені зміни в налаштуванні руху ({{camera}})" + }, + "documentTitle": { + "object": "Налагодження – Фрегат", + "notifications": "Налаштування сповіщень – Фрегат", + "default": "Налаштування - Фрегат", + "authentication": "Налаштування автентифікації – Фрегат", + "camera": "Налаштування камери – Фрегат", + "classification": "Налаштування класифікації – Фрегат", + "masksAndZones": "Редактор масок та зон – Фрегат", + "motionTuner": "Тюнер руху - Фрегат", + "general": "Основна Статус – Frigate", + "frigatePlus": "Налаштування Frigate+ – Frigate", + "enrichments": "Налаштуваннях збагачення – Frigate", + "cameraManagement": "Керування камерами - Frigate", + "cameraReview": "Налаштування перегляду камери - Frigate" + }, + "menu": { + "ui": "Інтерфейс користувача", + "classification": "Класифікація", + "cameras": "Налаштування камери", + "users": "Користувачі", + "masksAndZones": "Маски / Зони", + "motionTuner": "Тюнер руху", + "debug": "Налагодження", + "notifications": "Сповіщення", + "frigateplus": "Frigate+", + "enrichments": "Збагаченням", + "triggers": "Тригери", + "roles": "Ролі", + "cameraManagement": "Управління", + "cameraReview": "Огляду" + }, + "dialog": { + "unsavedChanges": { + "desc": "Ви хочете зберегти зміни, перш ніж продовжити?", + "title": "У вас є незбережені зміни." + } + }, + "cameraSetting": { + "camera": "Камера", + "noCamera": "Без камери" + }, + "users": { + "management": { + "title": "Керування користувачами", + "desc": "Керувати обліковими записами користувачів цього екземпляра Frigate." + }, + "addUser": "Додати користувача", + "updatePassword": "Оновити пароль", + "toast": { + "success": { + "deleteUser": "Користувач {{user}} успішно видалений", + "roleUpdated": "Роль оновлено для {{user}}", + "updatePassword": "Пароль успішно оновлено.", + "createUser": "Користувач {{user}} успішно створено" + }, + "error": { + "setPasswordFailed": "Не вдалося зберегти пароль: {{errorMessage}}", + "createUserFailed": "Не вдалося створити користувача: {{errorMessage}}", + "deleteUserFailed": "Не вдалося видалити користувача: {{errorMessage}}", + "roleUpdateFailed": "Не вдалося оновити роль: {{errorMessage}}" + } + }, + "table": { + "password": "Пароль", + "deleteUser": "Видалити користувача", + "username": "Ім'я користувача", + "actions": "Дії", + "noUsers": "Користувачів не знайдено.", + "role": "Роль", + "changeRole": "Змінити роль користувача" + }, + "dialog": { + "form": { + "user": { + "title": "Ім'я користувача", + "desc": "Дозволено використовувати лише літери, цифри, крапки та символи підкреслення.", + "placeholder": "Введіть ім'я користувача" + }, + "password": { + "strength": { + "medium": "Середній", + "strong": "Сильний", + "veryStrong": "Дуже сильний", + "title": "Надійність пароля: ", + "weak": "Слабкий" + }, + "match": "Паролі збігаються", + "title": "Пароль", + "notMatch": "Паролі не збігається", + "placeholder": "Введіть пароль", + "confirm": { + "title": "Підтвердьте пароль", + "placeholder": "Підтвердьте пароль" + }, + "show": "Показати пароль", + "hide": "Приховати пароль", + "requirements": { + "title": "Вимоги до пароля:", + "length": "Принаймні 8 символів", + "uppercase": "Принаймні одна велика літера", + "digit": "Принаймні одна цифра", + "special": "Принаймні один спеціальний символ (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "confirm": { + "placeholder": "Введіть новий пароль ще раз" + }, + "title": "Новий пароль", + "placeholder": "Введіть новий пароль" + }, + "usernameIsRequired": "Потрібне ім'я користувача", + "passwordIsRequired": "Потрібен пароль", + "currentPassword": { + "title": "Поточний пароль", + "placeholder": "Введіть свій поточний пароль" + } + }, + "changeRole": { + "roleInfo": { + "admin": "Адміністратор", + "intro": "Виберіть відповідну роль для цього користувача:", + "adminDesc": "Повний доступ до всіх функцій.", + "viewer": "Глядач", + "viewerDesc": "Обмежено лише активними інформаційними панелями, функціями «Огляд», «Дослідження» та «Експорт».", + "customDesc": "Особлива роль з доступом до певної камери." + }, + "title": "Змінити роль користувача", + "desc": "Оновити дозволи для {{username}}", + "select": "Виберіть роль" + }, + "createUser": { + "title": "Створити нового користувача", + "desc": "Додайте новий обліковий запис користувача та вкажіть роль для доступу до областей інтерфейсу Frigate.", + "usernameOnlyInclude": "Ім'я користувача може містити лише літери, цифри, . або _", + "confirmPassword": "Будь ласка, підтвердіть свій пароль" + }, + "deleteUser": { + "title": "Видалити користувача", + "desc": "Цю дію не можна скасувати. Це призведе до остаточного видалення облікового запису користувача та всіх пов’язаних з ним даних.", + "warn": "Ви впевнені, що хочете видалити {{username}}?" + }, + "passwordSetting": { + "updatePassword": "Оновити пароль для {{username}}", + "setPassword": "Встановити пароль", + "desc": "Створіть надійний пароль для захисту цього облікового запису.", + "cannotBeEmpty": "Пароль не може бути порожнім", + "doNotMatch": "Паролі не збігаються", + "currentPasswordRequired": "Потрібно ввести поточний пароль", + "incorrectCurrentPassword": "Поточний пароль неправильний", + "passwordVerificationFailed": "Не вдалося перевірити пароль", + "multiDeviceWarning": "На будь-яких інших пристроях, на яких ви ввійшли в систему, потрібно буде повторно ввійти протягом {{refresh_time}}.", + "multiDeviceAdmin": "Ви також можете змусити всіх користувачів негайно повторно автентифікуватися, змінивши свій JWT-секрет." + } + }, + "title": "Користувачі" + }, + "enrichments": { + "unsavedChanges": "Зміни в налаштуваннях незбережених збагачень", + "semanticSearch": { + "title": "Семантичний пошук", + "desc": "Семантичний пошук у Frigate дозволяє знаходити відстежувані об'єкти в елементах огляду, використовуючи або саме зображення, або текстовий опис, визначений користувачем, або автоматично згенерований опис.", + "readTheDocumentation": "Прочитайте документації", + "reindexNow": { + "label": "Переіндексувати зараз", + "confirmTitle": "Підтвердити переіндексацію", + "error": "Не вдалося розпочати переіндексацію: {{errorMessage}}", + "confirmDesc": "Ви впевнені, що хочете переіндексувати всі вбудовані відстежувані об'єкти? Цей процес працюватиме у фоновому режимі, але може максимально навантажити ваш процесор і зайняти чимало часу. Ви можете спостерігати за прогресом на Ex.", + "confirmButton": "Переіндексувати", + "alreadyInProgress": "Переіндексація вже триває.", + "success": "Переіндексацію успішно розпочато.", + "desc": "Переіндексація відновить вбудовування для всіх відстежуваних об'єктів. Цей процес працює у фоновому режимі та може максимально навантажувати ваш процесор і займати достатню кількість часу залежно від кількості відстежуваних об'єктів." + }, + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для вбудовування семантичного пошуку.", + "small": { + "title": "маленький", + "desc": "Використання small застосовує квантовану версію моделі, яка використовує менше оперативної пам'яті та працює швидше на процесорі з дуже незначною різницею в якості вбудовування." + }, + "large": { + "title": "великий", + "desc": "Використання large використовує повну модель Jina та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + } + }, + "faceRecognition": { + "title": "Розпізнавання обличчя", + "readTheDocumentation": "Прочитайте документація", + "modelSize": { + "label": "Розмір моделі", + "desc": "Розмір моделі, що використовується для розпізнавання обличчя.", + "small": { + "title": "маленький", + "desc": "Використання small використовує модель вбудовування облич FaceNet, яка ефективно працює на більшості процесорів." + }, + "large": { + "title": "великий", + "desc": "Використання параметра large використовує модель вбудовування облич ArcFace та автоматично запускатиметься на графічному процесорі, якщо це можливо." + } + }, + "desc": "Розпізнавання облич дозволяє присвоювати людям імена, і коли обличчя розпізнається, Frigate додає ім'я людини в якості підмітки. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях." + }, + "licensePlateRecognition": { + "title": "Розпізнавання номерних знаків", + "readTheDocumentation": "Прочитайте документації", + "desc": "Frigate може розпізнавати номерні знаки на автомобілях і автоматично додавати виявлені символи до поля розпізнаний_номер_автомобіля або відоме ім'я як підмітку до об'єктів, що мають тип \"автомобіль\". Поширеним випадком використання може бути зчитування номерних знаків автомобілів, що заїжджають на під'їзд, або автомобілів, що проїжджають вулицею." + }, + "restart_required": "Потрібно перезавантажити (налаштування збагачення змінено)", + "toast": { + "success": "Налаштування збагачення збережено. Перезапустіть Frigate, щоб застосувати зміни.", + "error": "Не вдалося зберегти зміни конфігурації: {{errorMessage}}" + }, + "birdClassification": { + "desc": "Класифікація птахів ідентифікує відомих птахів за допомогою квантованої моделі тензорного потоку. Коли відомого птаха розпізнано, його загальну назву буде додано як підмітку. Ця інформація відображається в інтерфейсі, фільтрах, а також у сповіщеннях.", + "title": "Класифікація птахів" + }, + "title": "Налаштуваннях Збагаченням" + }, + "triggers": { + "documentTitle": "Тригери", + "management": { + "title": "Тригери", + "desc": "Керуйте тригерами для {{camera}}. Використовуйте тип мініатюри для спрацьовування на схожих мініатюрах до вибраного об’єкта відстеження, а тип опису – для спрацьовування на схожих описах до вказаного вами тексту." + }, + "addTrigger": "Додати Тригер", + "table": { + "name": "Ім'я", + "type": "Тип", + "content": "Зміст", + "threshold": "Поріг", + "actions": "Дії", + "noTriggers": "Для цієї камери не налаштовано жодних тригерів.", + "edit": "Редагувати", + "deleteTrigger": "Видалити тригер", + "lastTriggered": "Остання активація" + }, + "type": { + "thumbnail": "Мініатюра", + "description": "Опис" + }, + "actions": { + "alert": "Позначити як сповіщення", + "notification": "Надіслати сповіщення", + "sub_label": "Додати підмітку", + "attribute": "Додати атрибут" + }, + "dialog": { + "createTrigger": { + "title": "Створити тригер", + "desc": "Створіть тригер для камери {{camera}}" + }, + "editTrigger": { + "title": "Редагувати тригер", + "desc": "Редагувати налаштування для тригера на камері {{camera}}" + }, + "deleteTrigger": { + "title": "Видалити тригер", + "desc": "Ви впевнені, що хочете видалити тригер {{triggerName}}? Цю дію не можна скасувати." + }, + "form": { + "name": { + "title": "Ім'я", + "placeholder": "Назвіть цей тригер", + "error": { + "minLength": "Поле має містити щонайменше 2 символи.", + "invalidCharacters": "Поле може містити лише літери, цифри, символи підкреслення та дефіси.", + "alreadyExists": "Тригер із такою назвою вже існує для цієї камери." + }, + "description": "Введіть унікальну назву або опис, щоб ідентифікувати цей тригер" + }, + "enabled": { + "description": "Увімкнути або вимкнути цей тригер" + }, + "type": { + "title": "Тип", + "placeholder": "Виберіть тип тригера", + "description": "Спрацьовує, коли виявляється схожий опис відстежуваного об'єкта", + "thumbnail": "Спрацьовує, коли виявляється мініатюра схожого відстежуваного об'єкта" + }, + "content": { + "title": "Зміст", + "imagePlaceholder": "Виберіть мініатюру", + "textPlaceholder": "Введіть текстовий вміст", + "imageDesc": "Відображаються лише 100 останніх мініатюр. Якщо ви не можете знайти потрібну мініатюру, перегляньте попередні об’єкти в розділі «Огляд» і налаштуйте тригер у меню.", + "textDesc": "Введіть текст, щоб запустити цю дію, коли буде виявлено схожий опис відстежуваного об’єкта.", + "error": { + "required": "Контент обов'язковий." + } + }, + "threshold": { + "title": "Поріг", + "error": { + "min": "Поріг має бути щонайменше 0", + "max": "Поріг має бути не більше 1" + }, + "desc": "Встановіть поріг подібності для цього тригера. Вищий поріг означає, що для спрацьовування тригера потрібна ближча відповідність." + }, + "actions": { + "title": "Дії", + "desc": "За замовчуванням Frigate надсилає повідомлення MQTT для всіх тригерів. Підмітки додають назву тригера до мітки об'єкта. Атрибути – це метадані, які можна шукати, що зберігаються окремо в метаданих відстежуваного об'єкта.", + "error": { + "min": "Потрібно вибрати принаймні одну дію." + } + }, + "friendly_name": { + "title": "Зрозуміле ім'я", + "placeholder": "Назвіть або опишіть цей тригер", + "description": "Зрозуміла назва або описовий текст (необов'язково) для цього тригера." + } + } + }, + "toast": { + "success": { + "createTrigger": "Тригер {{name}} успішно створено.", + "updateTrigger": "Тригер {{name}} успішно оновлено.", + "deleteTrigger": "Тригер {{name}} успішно видалено." + }, + "error": { + "createTriggerFailed": "Не вдалося створити тригер: {{errorMessage}}", + "updateTriggerFailed": "Не вдалося оновити тригер: {{errorMessage}}", + "deleteTriggerFailed": "Не вдалося видалити тригер: {{errorMessage}}" + } + }, + "semanticSearch": { + "title": "Семантичний пошук вимкнено", + "desc": "Для використання тригерів необхідно ввімкнути семантичний пошук." + }, + "wizard": { + "title": "Створити тригер", + "step1": { + "description": "Налаштуйте основні параметри для вашого тригера." + }, + "step2": { + "description": "Налаштуйте контент, який запускатиме цю дію." + }, + "step3": { + "description": "Налаштуйте поріг та дії для цього тригера." + }, + "steps": { + "nameAndType": "Ім'я та тип", + "configureData": "Налаштувати дані", + "thresholdAndActions": "Поріг та дії" + } + } + }, + "roles": { + "addRole": "Додати роль", + "table": { + "role": "Роль", + "cameras": "Камери", + "actions": "Дії", + "noRoles": "Не знайдено користувацьких ролей.", + "editCameras": "Редагувати камери", + "deleteRole": "Видалити роль" + }, + "toast": { + "success": { + "createRole": "Роль {{role}} успішно створена", + "updateCameras": "Камери оновлено для ролі {{role}}", + "deleteRole": "Роль {{role}} успішно видалено", + "userRolesUpdated_one": "{{count}} користувача, призначену цій ролі, оновлено до «глядача», який має доступ до всіх камер.", + "userRolesUpdated_few": "{{count}} Користувачі, яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер.", + "userRolesUpdated_many": "{{count}} Користувачів, яким призначено цю роль, оновлено до ролі «глядача», що має доступ до всіх камер." + }, + "error": { + "createRoleFailed": "Не вдалося створити роль: {{errorMessage}}", + "updateCamerasFailed": "Не вдалося оновити камери: {{errorMessage}}", + "deleteRoleFailed": "Не вдалося видалити роль: {{errorMessage}}", + "userUpdateFailed": "Не вдалося оновити ролі користувачів: {{errorMessage}}" + } + }, + "management": { + "title": "Керування ролями глядача", + "desc": "Керуйте ролями глядачів та їхніми дозволами на доступ до камери для цього екземпляра Frigate." + }, + "dialog": { + "createRole": { + "title": "Створити нову роль", + "desc": "Додайте нову роль і вкажіть дозволи доступу до камери." + }, + "editCameras": { + "title": "Редагувати рольові камери", + "desc": "Оновіть доступ до камери для цієї ролі {{role}}." + }, + "deleteRole": { + "title": "Видалити роль", + "desc": "Цю дію не можна скасувати. Це призведе до остаточного видалення ролі та призначення всім користувачам із цією роллю ролі «глядач», що надасть глядачеві доступ до всіх камер.", + "warn": "Ви впевнені, що хочете видалити {{role}}?", + "deleting": "Видалення..." + }, + "form": { + "role": { + "title": "Назва ролі", + "placeholder": "Введіть назву ролі", + "desc": "Дозволено використовувати лише літери, цифри, крапки та символи підкреслення.", + "roleIsRequired": "Потрібно вказати назву ролі", + "roleOnlyInclude": "Назва ролі може містити лише літери, цифри, символи *.* або *.*", + "roleExists": "Роль із такою назвою вже існує." + }, + "cameras": { + "title": "Камери", + "desc": "Виберіть камери, до яких ця роль має доступ. Потрібна принаймні одна камера.", + "required": "Потрібно вибрати принаймні одну камеру." + } + } + } + }, + "cameraWizard": { + "title": "Додати камеру", + "description": "Виконайте наведені нижче кроки, щоб додати нову камеру до вашої установки Frigate.", + "steps": { + "nameAndConnection": "Ім'я та з'єднання", + "streamConfiguration": "Конфігурація потоку", + "validationAndTesting": "Валідація та тестування", + "probeOrSnapshot": "Зонд або знімок" + }, + "save": { + "success": "Нову камеру успішно збережено {{cameraName}}.", + "failure": "Помилка збереження {{cameraName}}." + }, + "testResultLabels": { + "resolution": "Роздільна здатність", + "video": "Відео", + "audio": "Аудіо", + "fps": "FPS" + }, + "commonErrors": { + "noUrl": "Будь ласка, надайте дійсну URL-адресу потоку", + "testFailed": "Тест потоку не вдався: {{error}}" + }, + "step1": { + "description": "Введіть дані вашої камери та виберіть тестування камери або виберіть бренд вручну.", + "cameraName": "Назва камери", + "cameraNamePlaceholder": "наприклад, передні_двері або огляд заднього двору", + "host": "Хост/IP-адреса", + "port": "Порт", + "username": "Ім'я користувача", + "usernamePlaceholder": "Необов'язково", + "password": "Пароль", + "passwordPlaceholder": "Необов'язково", + "selectTransport": "Виберіть транспортний протокол", + "cameraBrand": "Бренд камери", + "selectBrand": "Виберіть марку камери для шаблону URL-адреси", + "customUrl": "URL-адреса користувацького потоку", + "brandInformation": "Інформація про бренд", + "brandUrlFormat": "Для камер з форматом RTSP URL, як: {{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "Тестове з'єднання", + "testSuccess": "Тестування з'єднання успішне!", + "testFailed": "Перевірка з’єднання не вдалася. Перевірте введені дані та повторіть спробу.", + "streamDetails": "Деталі трансляції", + "warnings": { + "noSnapshot": "Не вдалося отримати знімок із налаштованого потоку." + }, + "errors": { + "brandOrCustomUrlRequired": "Виберіть або марку камери з хостом/IP-адресою, або виберіть «Інше» з власною URL-адресою", + "nameRequired": "Потрібно вказати назву камери", + "nameLength": "Назва камери має містити не більше 64 символів", + "invalidCharacters": "Назва камери містить недійсні символи", + "nameExists": "Назва камери вже існує", + "brands": { + "reolink-rtsp": "Не рекомендується використовувати Reolink RTSP. Увімкніть HTTP у налаштуваннях прошивки камери та перезапустіть майстер." + }, + "customUrlRtspRequired": "Користувацькі URL-адреси мають починатися з \"rtsp://\". Для потоків з камер, що не підтримують RTSP, потрібне ручне налаштування." + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "Зондування метаданих камери...", + "fetchingSnapshot": "Отримання знімка камери..." + }, + "connectionSettings": "Налаштування підключення", + "detectionMethod": "Метод виявлення потоку", + "onvifPort": "Порт ONVIF", + "probeMode": "Зонд-камера", + "manualMode": "Ручний вибір", + "detectionMethodDescription": "Перевірте камеру з ONVIF (якщо підтримується) для пошуку URL-адресів потоку камери або вручну виберіть бренд камери, щоб використовувати попередньо визначені URL. Щоб введіти налаштований URL-адрес RTSP, виберіть ручний метод і вибрати \"Інший\".", + "onvifPortDescription": "Для камер, що підтримують ONVIF, це зазвичай 80 або 8080.", + "useDigestAuth": "Використовувати дайджест-автентифікацію", + "useDigestAuthDescription": "Використовуйте автентифікацію HTTP-дайджест для ONVIF. Деякі камери можуть вимагати спеціальне ім’я користувача/пароль ONVIF замість стандартного користувача-адміністратора." + }, + "step2": { + "description": "Перевірте камеру на наявність доступних потоків або налаштуйте ручні параметри на основі вибраного методу виявлення.", + "streamsTitle": "Потоки з камери", + "addStream": "Додати потік", + "addAnotherStream": "Додати ще один потік", + "streamTitle": "Потік {{number}}", + "streamUrl": "URL-адреса потоку", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "URL", + "resolution": "Роздільна здатність", + "selectResolution": "Виберіть роздільну здатність", + "quality": "Якість", + "selectQuality": "Виберіть якість", + "roles": "Ролі", + "roleLabels": { + "detect": "Виявлення об'єктів", + "record": "Запис", + "audio": "Аудіо" + }, + "testStream": "Тестове з'єднання", + "testSuccess": "Тестування з'єднання успішне!", + "testFailed": "Перевірка з’єднання не вдалася. Перевірте введені дані та повторіть спробу.", + "testFailedTitle": "Тест не вдався", + "connected": "Підключено", + "notConnected": "Не підключено", + "featuresTitle": "Особливості", + "go2rtc": "Зменште кількість підключень до камери", + "detectRoleWarning": "Для продовження принаймні один потік повинен мати роль \"виявлення\".", + "rolesPopover": { + "title": "Ролі потоку", + "detect": "Основний канал для виявлення об'єктів.", + "record": "Зберігає сегменти відеоканалу на основі налаштувань конфігурації.", + "audio": "Стрічка даних для виявлення на основі аудіо." + }, + "featuresPopover": { + "title": "Функції потоку", + "description": "Використовуйте ретрансляцію go2rtc, щоб зменшити кількість підключень до вашої камери." + }, + "streamDetails": "Деталі трансляції", + "probing": "Зондуюча камера...", + "retry": "Повторити спробу", + "testing": { + "probingMetadata": "Зондування метаданих камери...", + "fetchingSnapshot": "Отримання знімка камери..." + }, + "probeFailed": "Не вдалося дослідити камеру: {{error}}", + "probingDevice": "Зондуючий пристрій...", + "probeSuccessful": "Зонд успішно", + "probeError": "Помилка зонда", + "probeNoSuccess": "Зондування Невдало", + "deviceInfo": "Інформація про пристрій", + "manufacturer": "Виробник", + "model": "Модель", + "firmware": "Прошивка", + "profiles": "Профілі", + "ptzSupport": "Підтримка PTZ-камер", + "autotrackingSupport": "Підтримка автоматичного відстеження", + "presets": "Пресети", + "rtspCandidates": "Кандидати RTSP", + "rtspCandidatesDescription": "З камери було знайдено такі URL-адреси RTSP. Перевірте з’єднання, щоб переглянути метадані потоку.", + "noRtspCandidates": "Не знайдено URL-адрес RTSP з камери. Ваші облікові дані можуть бути неправильними, або камера може не підтримувати ONVIF чи метод, який використовується для отримання URL-адрес RTSP. Поверніться та введіть URL-адресу RTSP вручну.", + "candidateStreamTitle": "Кандидат {{number}}", + "useCandidate": "Використання", + "uriCopy": "Копіювати", + "uriCopied": "URI скопійовано в буфер обміну", + "testConnection": "Тестове з'єднання", + "toggleUriView": "Натисніть, щоб перемкнути повний вигляд URI", + "errors": { + "hostRequired": "Потрібно вказати хост/IP-адресу" + } + }, + "step3": { + "description": "Налаштуйте ролі потоків та додайте додаткові потоки для вашої камери.", + "validationTitle": "Перевірка потоку", + "connectAllStreams": "Підключити всі потоки", + "reconnectionSuccess": "Повторне підключення успішне.", + "reconnectionPartial": "Не вдалося відновити підключення до деяких потоків.", + "streamUnavailable": "Попередній перегляд трансляції недоступний", + "reload": "Перезавантажити", + "connecting": "Підключення...", + "streamTitle": "Потік {{number}}", + "valid": "Дійсний", + "failed": "Не вдалося", + "notTested": "Не тестувалося", + "connectStream": "Підключитися", + "connectingStream": "Підключення", + "disconnectStream": "Відключитися", + "estimatedBandwidth": "Орієнтовна пропускна здатність", + "roles": "Ролі", + "none": "Жоден", + "error": "Помилка", + "streamValidated": "Потік {{number}} успішно перевірено", + "streamValidationFailed": "Не вдалося перевірити потік {{number}}", + "saveAndApply": "Зберегти нову камеру", + "saveError": "Недійсна конфігурація. Перевірте свої налаштування.", + "issues": { + "title": "Перевірка потоку", + "videoCodecGood": "Відеокодек: {{codec}}.", + "audioCodecGood": "Аудіокодек: {{codec}}.", + "noAudioWarning": "Для цього потоку не виявлено аудіо, записи не матимуть аудіо.", + "audioCodecRecordError": "Для підтримки аудіо в записах потрібен аудіокодек AAC.", + "audioCodecRequired": "Для підтримки виявлення звуку потрібен аудіопотік.", + "restreamingWarning": "Зменшення кількості підключень до камери для потоку запису може дещо збільшити використання процесора.", + "dahua": { + "substreamWarning": "Підпотік 1 заблокований на низькій роздільній здатності. Багато камер Dahua / Amcrest / EmpireTech підтримують додаткові підпотоки, які потрібно ввімкнути в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "hikvision": { + "substreamWarning": "Підпотік 1 заблокований на низькій роздільній здатності. Багато камер Hikvision підтримують додаткові підпотоки, які потрібно ввімкнути в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "resolutionHigh": "Роздільна здатність {{resolution}} може призвести до збільшення використання ресурсів.", + "resolutionLow": "Роздільна здатність {{resolution}} може бути занадто низькою для надійного виявлення малих об'єктів." + }, + "ffmpegModule": "Використовувати режим сумісності з потоками", + "ffmpegModuleDescription": "Якщо потік не завантажується після кількох спроб, спробуйте ввімкнути цю функцію. Коли вона ввімкнена, Frigate використовуватиме модуль ffmpeg з go2rtc. Це може забезпечити кращу сумісність з деякими потоками камер.", + "streamsTitle": "Трансляції з камери", + "addStream": "Додати потік", + "addAnotherStream": "Додати ще один потік", + "streamUrl": "URL-адреса потоку", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "selectStream": "Виберіть потік", + "searchCandidates": "Пошук кандидатів...", + "noStreamFound": "Потік не знайдено", + "url": "URL", + "resolution": "Роздільна здатність", + "selectResolution": "Виберіть роздільну здатність", + "quality": "Якість", + "selectQuality": "Виберіть якість", + "roleLabels": { + "detect": "Виявлення об'єктів", + "record": "Запис", + "audio": "Аудіо" + }, + "testStream": "Тестове з'єднання", + "testSuccess": "Тестування трансляції успішне!", + "testFailed": "Тест потоку не вдався", + "testFailedTitle": "Тест не вдався", + "connected": "Підключено", + "notConnected": "Не підключено", + "featuresTitle": "Особливості", + "go2rtc": "Зменште кількість підключень до камери", + "detectRoleWarning": "Для продовження принаймні один потік повинен мати роль \"виявлення\".", + "rolesPopover": { + "title": "Ролі потоку", + "detect": "Основний канал для виявлення об'єктів.", + "record": "Зберігає сегменти відеоканалу на основі налаштувань конфігурації.", + "audio": "Стрічка даних для виявлення на основі аудіо." + }, + "featuresPopover": { + "title": "Функції потоку", + "description": "Використовуйте ретрансляцію go2rtc, щоб зменшити кількість підключень до вашої камери." + } + }, + "step4": { + "description": "Фінальна перевірка та аналіз перед збереженням нової камери. Підключіть кожен потік перед збереженням.", + "validationTitle": "Перевірка потоку", + "connectAllStreams": "Підключити всі потоки", + "reconnectionSuccess": "Повторне підключення успішне.", + "reconnectionPartial": "Не вдалося відновити підключення до деяких потоків.", + "streamUnavailable": "Попередній перегляд трансляції недоступний", + "reload": "Перезавантажити", + "connecting": "Підключення...", + "streamTitle": "Потік {{number}}", + "valid": "Дійсний", + "failed": "Не вдалося", + "notTested": "Не тестувалося", + "connectStream": "Підключитися", + "connectingStream": "Підключення", + "disconnectStream": "Відключитися", + "estimatedBandwidth": "Орієнтовна пропускна здатність", + "roles": "Ролі", + "ffmpegModule": "Використовувати режим сумісності з потоками", + "ffmpegModuleDescription": "Якщо потік не завантажується після кількох спроб, спробуйте ввімкнути цю функцію. Коли вона ввімкнена, Frigate використовуватиме модуль ffmpeg з go2rtc. Це може забезпечити кращу сумісність з деякими потоками камер.", + "none": "Жоден", + "error": "Помилка", + "streamValidated": "Потік {{number}} успішно перевірено", + "streamValidationFailed": "Не вдалося перевірити потік {{number}}", + "saveAndApply": "Зберегти нову камеру", + "saveError": "Недійсна конфігурація. Перевірте свої налаштування.", + "issues": { + "title": "Перевірка потоку", + "videoCodecGood": "Відеокодек є {{codec}}.", + "audioCodecGood": "Аудіокодек є {{codec}}.", + "resolutionHigh": "Роздільна здатність {{resolution}} може призвести до збільшення використання ресурсів.", + "resolutionLow": "Роздільна здатність {{resolution}} може бути занадто низькою для надійного виявлення малих об'єктів.", + "noAudioWarning": "Для цього потоку не виявлено аудіо, записи не матимуть аудіо.", + "audioCodecRecordError": "Для підтримки аудіо в записах потрібен аудіокодек AAC.", + "audioCodecRequired": "Для підтримки виявлення звуку потрібен аудіопотік.", + "restreamingWarning": "Зменшення кількості підключень до камери для потоку запису може дещо збільшити використання процесора.", + "brands": { + "reolink-rtsp": "Не рекомендується використовувати Reolink RTSP. Увімкніть HTTP у налаштуваннях прошивки камери та перезапустіть майстер.", + "reolink-http": "Для кращої сумісності HTTP-потоки Reolink повинні використовувати FFmpeg. Увімкніть для цього потоку опцію «Використовувати режим сумісності потоків»." + }, + "dahua": { + "substreamWarning": "Підпотік 1 заперечений до низького розділу. Багато камери Dahua / Amcrest / EmpireTech підтримують додаткові підтоки, які потрібно включити в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + }, + "hikvision": { + "substreamWarning": "Підпотік 1 заперечений до низького розділу. Багато камер Hikvision підтримують додаткові підтоки, які повинні бути включені в налаштуваннях камери. Рекомендується перевірити та використовувати ці потоки, якщо вони доступні." + } + } + } + }, + "cameraManagement": { + "title": "Керування камерами", + "addCamera": "Додати нову камеру", + "editCamera": "Редагувати камеру:", + "selectCamera": "Виберіть камеру", + "backToSettings": "Назад до налаштувань камери", + "streams": { + "title": "Увімкнути/вимкнути камери", + "desc": "Тимчасово вимкніть камеру до перезапуску Frigate. Вимкнення камери повністю зупиняє обробку потоків цієї камери в Frigate. Функції виявлення, запису та налагодження будуть недоступні.
    Примітка: це не вимикає ретрансляції " + }, + "cameraConfig": { + "add": "Додати камеру", + "edit": "Редагувати камеру", + "description": "Налаштуйте параметри камери, включаючи потокові входи та ролі.", + "name": "Назва камери", + "nameRequired": "Потрібно вказати назву камери", + "nameLength": "Назва камери має містити менше 64 символів.", + "namePlaceholder": "наприклад, передні_двері або огляд заднього двору", + "enabled": "Увімкнено", + "ffmpeg": { + "inputs": "Вхідні потоки", + "path": "Шлях потоку", + "pathRequired": "Шлях потоку обов'язковий", + "pathPlaceholder": "rtsp://...", + "roles": "Ролі", + "rolesRequired": "Потрібна хоча б одна роль", + "rolesUnique": "Кожна роль (аудіо, виявлення, запис) може бути призначена лише одному потоку", + "addInput": "Додати вхідний потік", + "removeInput": "Вилучити вхідний потік", + "inputsRequired": "Потрібен принаймні один вхідний потік" + }, + "go2rtcStreams": "go2rtc Стріми", + "streamUrls": "URL-адреси потоків", + "addUrl": "Додати URL-адресу", + "addGo2rtcStream": "Додати потік go2rtc", + "toast": { + "success": "Камеру {{cameraName}} успішно збережено" + } + } + }, + "cameraReview": { + "title": "Налаштування перегляду камери", + "object_descriptions": { + "title": "Генеративні описи об'єктів штучного інтелекту", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи об'єктів ШІ для цієї камери. Якщо вимкнено, згенеровані ШІ описи не запитуватимуться для об'єктів, що відстежуються на цій камері." + }, + "review_descriptions": { + "title": "Описи генеративного ШІ-огляду", + "desc": "Тимчасово ввімкнути/вимкнути генеративні описи огляду за допомогою штучного інтелекту для цієї камери. Якщо вимкнено, для елементів огляду на цій камері не запитуватимуться згенеровані штучним інтелектом описи." + }, + "review": { + "title": "Огляду", + "desc": "Тимчасово ввімкнути/вимкнути сповіщення та виявлення для цієї камери до перезавантаження Frigate. Якщо вимкнено, нові елементи огляду не створюватимуться. ", + "alerts": "Сповіщення ", + "detections": "Виявлення " + }, + "reviewClassification": { + "title": "Класифікація оглядів", + "desc": "Frigate класифікує об'єкти перевірки як сповіщення та виявлення. За замовчуванням усі об'єкти людина та автомобіль вважаються сповіщеннями. Ви можете уточнити класифікацію об'єктів перевірки, налаштувавши для них необхідні зони.", + "noDefinedZones": "Для цієї камери не визначено жодної зони.", + "objectAlertsTips": "Усі об’єкти {{alertsLabels}} на {{cameraName}} будуть відображатися як сповіщення.", + "zoneObjectAlertsTips": "Усі об’єкти {{alertsLabels}}, виявлені в {{zone}} на {{cameraName}}, будуть відображатися як сповіщення.", + "objectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться.", + "zoneObjectDetectionsTips": { + "text": "Усі об’єкти {{detectionsLabels}}, що не належать до категорії {{zone}} на {{cameraName}}, будуть відображатися як Виявлення.", + "notSelectDetections": "Усі об’єкти {{detectionsLabels}}, виявлені в {{zone}} на {{cameraName}}, які не віднесені до категорії «Сповіщення», будуть відображатися як Виявлення незалежно від того, в якій зоні вони знаходяться.", + "regardlessOfZoneObjectDetectionsTips": "Усі об’єкти {{detectionsLabels}}, які не класифіковані на {{cameraName}}, будуть відображатися як виявлені, незалежно від того, в якій зоні вони знаходяться." + }, + "unsavedChanges": "Незбережені налаштування класифікації рецензій для {{camera}}", + "selectAlertsZones": "Виберіть зони для сповіщень", + "selectDetectionsZones": "Виберіть зони для виявлення", + "limitDetections": "Обмеження виявлення певними зонами", + "toast": { + "success": "Конфігурацію класифікації перегляду збережено. Перезапустіть Frigate, щоб застосувати зміни." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/uk/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/uk/views/system.json new file mode 100644 index 0000000..d76a729 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/uk/views/system.json @@ -0,0 +1,199 @@ +{ + "cameras": { + "label": { + "ffmpeg": "FFmpeg'", + "cameraSkippedDetectionsPerSecond": "{{camName}} пропущених виявлень на секунду", + "cameraDetect": "{{camName}} виявлення", + "cameraFfmpeg": "{{camName}} FFmpeg'", + "overallDetectionsPerSecond": "загальна кiлькiсть виявлень за секунду", + "cameraDetectionsPerSecond": "{{camName}} виявлень на секунду", + "overallFramesPerSecond": "загальна кiлкiсть кадрiв на секунду", + "overallSkippedDetectionsPerSecond": "загальна кiлкiсть пропущених виявлень за секунду", + "cameraCapture": "{{camName}} захоплення", + "cameraFramesPerSecond": "{{camName}} кадрiв на секунду", + "skipped": "пропущено", + "capture": "захоплення", + "camera": "камера", + "detect": "виявити" + }, + "toast": { + "success": { + "copyToClipboard": "Тестові дані копіюються в буфер обміну." + }, + "error": { + "unableToProbeCamera": "Не вдалося дослідити камеру: {{errorMessage}}" + } + }, + "title": "Камери", + "info": { + "fetching": "Отримання даних з камери", + "cameraProbeInfo": "{{camera}} Інформація про зонд камери", + "streamDataFromFFPROBE": "Дані потоку отримуються за допомогою ffprobe.", + "stream": "Потік {{idx}}", + "video": "Відео:", + "codec": "Кодек:", + "resolution": "Роздільна здатність:", + "fps": "FPS:", + "unknown": "Невідомо", + "audio": "Аудіо:", + "error": "Помилка: {{error}}", + "tips": { + "title": "Інформація про зонд камери" + }, + "aspectRatio": "співвідношення сторін" + }, + "overview": "Огляд", + "framesAndDetections": "Кадри / Виявлення" + }, + "enrichments": { + "embeddings": { + "plate_recognition": "Розпiзнавання номерiв", + "image_embedding_speed": "Швидкість вбудовування зображень", + "face_embedding_speed": "Швидкість вбудовування облич", + "face_recognition_speed": "Швидкість розпізнавання обличчя", + "plate_recognition_speed": "Швидкість розпізнавання номерних знаків", + "text_embedding_speed": "Швидкість вбудовування тексту", + "image_embedding": "Вбудовування зображень", + "text_embedding": "Вбудовування тексту", + "face_recognition": "Розпізнавання обличчя", + "yolov9_plate_detection_speed": "Швидкість виявлення номерних знаків YOLOv9", + "yolov9_plate_detection": "Виявлення пластин YOLOv9", + "review_description": "Опис огляду", + "review_description_speed": "Огляд Опис Швидкість", + "review_description_events_per_second": "Опис огляду", + "object_description": "Опис об'єкта", + "object_description_speed": "Опис об'єкта Швидкість", + "object_description_events_per_second": "Опис об'єкта" + }, + "title": "Збагаченням", + "infPerSecond": "Висновки за секунду", + "averageInf": "Середній час висновування" + }, + "general": { + "title": "Загальна", + "hardwareInfo": { + "npuUsage": "Використання нейронного процесора", + "npuMemory": "Пам'ять NPU", + "title": "Інформація про обладнання", + "gpuUsage": "Використання графічного процесора", + "gpuMemory": "Пам'ять графічного процесора", + "gpuEncoder": "GPU-кодер", + "gpuDecoder": "Декодер графічного процесора", + "gpuInfo": { + "vainfoOutput": { + "title": "Вихід Vainfo", + "returnCode": "Код повернення: {{code}}", + "processOutput": "Вихід процесу:", + "processError": "Помилка процесу:" + }, + "nvidiaSMIOutput": { + "title": "Вихід Nvidia SMI", + "name": "Ім'я: {{name}}", + "driver": "Водій: {{driver}}", + "cudaComputerCapability": "Можливості обчислень CUDA: {{cuda_compute}}", + "vbios": "Інформація про VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Закрити інформацію про графічний процесор" + }, + "copyInfo": { + "label": "Копіювати інформацію про графічний процесор" + }, + "toast": { + "success": "Інформацію про графічний процесор скопійовано в буфер обміну" + } + }, + "intelGpuWarning": { + "title": "Попередження щодо статистики графічного процесора Intel", + "message": "Статистика графічного процесора недоступна", + "description": "Це відома помилка в інструментах звітності статистики графічного процесора Intel (intel_gpu_top), яка неодноразово повертає використання графічного процесора на рівні 0%, навіть у випадках, коли апаратне прискорення та виявлення об'єктів працюють належним чином на (i)GPU. Це не помилка Frigate. Ви можете перезавантажити хост, щоб тимчасово виправити проблему та переконатися, що графічний процесор працює правильно. Це не впливає на продуктивність." + } + }, + "otherProcesses": { + "processMemoryUsage": "Використання пам'яті процесу", + "processCpuUsage": "Використання процесора процесу", + "title": "Інші процеси" + }, + "detector": { + "temperature": "Температура детектора", + "title": "Детектори", + "inferenceSpeed": "Швидкість виведення детектора", + "cpuUsage": "Використання процесора детектора", + "memoryUsage": "Використання пам'яті детектора", + "cpuUsageInformation": "Процесор, що використовується для підготовки вхідних та вихідних даних до/з моделей виявлення. Це значення не вимірює використання логічного висновку, навіть якщо використовується графічний процесор або прискорювач." + } + }, + "storage": { + "cameraStorage": { + "unused": { + "tips": "Це значення може неточно відображати обсяг вільного простору, доступного для Frigate, якщо на вашому диску зберігаються інші файли, окрім записів Frigate. Frigate не відстежує використання пам’яті поза ним.", + "title": "Невикористаний" + }, + "title": "Зберігання камери", + "storageUsed": "Зберігання", + "camera": "Камера", + "unusedStorageInformation": "Інформація про невикористане сховище", + "percentageOfTotalUsed": "Відсоток від загальної кількості", + "bandwidth": "Пропускна здатність" + }, + "overview": "Огляд", + "recordings": { + "title": "Записи", + "tips": "Це значення відображає загальний обсяг пам’яті, що використовується записами в базі даних Frigate. Frigate не відстежує використання пам’яті для всіх файлів на вашому диску.", + "earliestRecording": "Найдавніший доступний запис:" + }, + "title": "Зберігання", + "shm": { + "title": "Розподіл спільної пам'яті (SHM)", + "warning": "Поточний розмір SHM, що становить {{total}} МБ, замалий. Збільште його принаймні до {{min_shm}} МБ.", + "readTheDocumentation": "Прочитайте документацію" + } + }, + "lastRefreshed": "Останнє оновлення: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} має високе використання процесора FFmpeg ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} має високе використання процесора для виявлення ({{detectAvg}}%)", + "healthy": "Система справна", + "reindexingEmbeddings": "Переіндексація вбудовування (виконано {{processed}}%)", + "cameraIsOffline": "{{camera}} не в мережі", + "detectIsSlow": "{{detect}} повільний ({{speed}} мс)", + "detectIsVerySlow": "{{detect}} дуже повільний ({{speed}} мс)", + "shmTooLow": "Розмір /dev/shm ({{total}} МБ) слід збільшити щонайменше до {{min}} МБ." + }, + "documentTitle": { + "cameras": "Статистика камер - Фрегат", + "storage": "Статистика сховища - Фрегат", + "general": "Основна Статус – Frigate", + "enrichments": "Статистика збагачені - Фрегат", + "logs": { + "frigate": "Фрегатні журнали - Фрегат", + "go2rtc": "Журнали Go2RTC - Фрегат", + "nginx": "Журнали Nginx - Фрегат" + } + }, + "title": "Система", + "metrics": "Системні показники", + "logs": { + "download": { + "label": "Завантажити журнали" + }, + "copy": { + "label": "Копіювати в буфер обміну", + "success": "Скопійовано журнали в буфер обміну", + "error": "Не вдалося скопіювати журнали в буфер обміну" + }, + "type": { + "label": "Тип", + "timestamp": "Позначка часу", + "tag": "Тег", + "message": "Повідомлення" + }, + "tips": "Журнали передаються потоком із сервера", + "toast": { + "error": { + "fetchingLogsFailed": "Помилка отримання журналів: {{errorMessage}}", + "whileStreamingLogs": "Помилка під час потокової передачі журналів: {{errorMessage}}" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/audio.json b/sam2-cpu/frigate-dev/web/public/locales/ur/audio.json new file mode 100644 index 0000000..b6b5322 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/audio.json @@ -0,0 +1,38 @@ +{ + "wheeze": "گھرگھراہٹ", + "snoring": "خراٹے", + "cough": "کھانسی", + "singing": "گانا", + "throat_clearing": "گلا صاف کرنا", + "sneeze": "چھینک", + "sniff": "سونگھنا", + "run": "دوڑو", + "applause": "تالیاں", + "chatter": "چہچہانا", + "crowd": "بھیڑ", + "animal": "جانور", + "children_playing": "بچے کھیل رہے ہیں", + "howl": "چیخنا", + "growling": "کراہنے والا", + "dog": "کتا", + "cat": "بلی", + "pets": "پالتو جانور", + "whimper_dog": "کتے کی آواز", + "bark": "بھونکنا", + "meow": "میانو", + "speech": "تقریر", + "babbling": "بڑبڑانا", + "yell": "چیخنا", + "bellow": "دَہاڑ", + "whoop": "اففف", + "whispering": "سرگوشی", + "laughter": "ہنسی", + "snicker": "منہ دبا کر ہنسنا", + "car": "گاڑی", + "bus": "بس", + "motorcycle": "موٹر سائیکل", + "train": "ٹرین", + "bicycle": "سائیکل", + "crying": "رونا", + "sigh": "آہیں" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/common.json b/sam2-cpu/frigate-dev/web/public/locales/ur/common.json new file mode 100644 index 0000000..37ff068 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/common.json @@ -0,0 +1,39 @@ +{ + "time": { + "untilForTime": "{{time}} تک", + "untilForRestart": "فریگیٹ کے دوبارہ شروع ہونے تک۔", + "untilRestart": "دوبارہ شروع ہونے تک", + "ago": "{{timeAgo}} پہلے", + "justNow": "ابھی ابھی", + "today": "آج", + "yesterday": "کل", + "last7": "پچھلے 7 دن", + "last14": "آخری 14 دن", + "last30": "آخری 30 دن", + "thisWeek": "اس ہفتے", + "lastWeek": "گزشتہ ہفتے", + "thisMonth": "اس مہینے", + "lastMonth": "پچھلے مہینے", + "5minutes": "5 منٹ", + "10minutes": "10 منٹ", + "30minutes": "30 منٹ", + "1hour": "1 گھنٹہ", + "12hours": "12 گھنٹے", + "24hours": "24 گھنٹے", + "pm": "pm", + "am": "am", + "yr": "{{time}}سال", + "mo": "{{time}} مہینہ", + "d": "{{time}} دن", + "h": "{{time}} گھنٹہ", + "year_one": "{{time}} سال", + "year_other": "{{time}} سال", + "day_one": "{{time}} دن", + "day_other": "{{time}} دن", + "month_one": "{{time}} مہینہ", + "month_other": "{{time}} مہینے", + "hour_one": "{{time}} گھنٹہ", + "hour_other": "{{time}} گھنٹے" + }, + "readTheDocumentation": "دستاویز پڑھیں" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/auth.json new file mode 100644 index 0000000..e7625b6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/auth.json @@ -0,0 +1,14 @@ +{ + "form": { + "user": "صارف نام", + "password": "پاسورڈ", + "login": "لاگ ان", + "errors": { + "usernameRequired": "صارف نام درکار ہے", + "passwordRequired": "پاس ورڈ درکار ہے", + "rateLimit": "شرح کی حد سے تجاوز کر گیا۔ بعد میں دوبارہ کوشش کریں۔", + "loginFailed": "لاگ ان ناکام ہو گیا", + "unknownError": "نامعلوم خرابی۔ لاگز چیک کریں۔" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/camera.json new file mode 100644 index 0000000..900341e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/camera.json @@ -0,0 +1,17 @@ +{ + "group": { + "delete": { + "confirm": { + "desc": "کیا آپ واقعی کیمرہ گروپ {{name}} کو حذف کرنا چاہتے ہیں؟", + "title": "حذف کی تصدیق کریں" + }, + "label": "کیمرہ گروپ کو حذف کریں" + }, + "label": "کیمرہ گروپس", + "add": "کیمرہ گروپ شامل کریں", + "edit": "کیمرہ گروپ میں ترمیم کریں", + "name": { + "label": "نام" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/dialog.json new file mode 100644 index 0000000..6367caf --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/dialog.json @@ -0,0 +1,24 @@ +{ + "restart": { + "title": "کیا آپ واقعی Frigate کو دوبارہ شروع کرنا چاہتے ہیں؟", + "button": "دوبارہ شروع کریں", + "restarting": { + "title": "فریگیٹ دوبارہ شروع ہو رہا ہے", + "content": "یہ صفحہ {{countdown}} سیکنڈ میں دوبارہ لوڈ ہو جائے گا۔", + "button": "ابھی دوبارہ لوڈ کرنے پر مجبور کریں" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "فریگیٹ+ کو جمع کروائیں", + "desc": "ایسے مقامات میں موجود آبجیکٹس جن سے آپ بچنا چاہتے ہیں، جھوٹے مثبت نہیں ہوتے۔ انہیں جھوٹے مثبت کے طور پر جمع کروانے سے ماڈل کنفیوز ہو جائے گا۔" + }, + "review": { + "question": { + "label": "فریگیٹ پلس کے لیے اس لیبل کی تصدیق کریں" + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/filter.json new file mode 100644 index 0000000..45cd366 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/filter.json @@ -0,0 +1,18 @@ +{ + "filter": "فلٹر", + "labels": { + "label": "لیبلز", + "all": { + "title": "تمام لیبلز", + "short": "لیبلز" + }, + "count_one": "{{count}} لیبل", + "count_other": "{{count}} لیبلز" + }, + "zones": { + "label": "زونز", + "all": { + "title": "تمام زونز" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/icons.json new file mode 100644 index 0000000..8c2d816 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "آئیکن منتخب کریں", + "search": { + "placeholder": "آئیکن تلاش کریں…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/input.json new file mode 100644 index 0000000..8b71c9b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "ویڈیو ڈاؤن لوڈ کریں", + "toast": { + "success": "آپ کی جائزے کی ویڈیو ڈاؤن لوڈ ہونا شروع ہو گئی ہے۔" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/ur/components/player.json new file mode 100644 index 0000000..b66f970 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/components/player.json @@ -0,0 +1,13 @@ +{ + "noRecordingsFoundForThisTime": "اس وقت کے لیے کوئی ریکارڈنگ نہیں ملی", + "noPreviewFound": "کوئی پیش نظارہ نہیں ملا", + "noPreviewFoundFor": "{{cameraName}} کے لیے کوئی پیش نظارہ نہیں ملا", + "submitFrigatePlus": { + "title": "اس فریم کو فریگیٹ+ میں جمع کرائیں؟", + "submit": "جمع کروائیں" + }, + "livePlayerRequiredIOSVersion": "اس لائیو اسٹریم کی قسم کے لیے iOS 17.1 یا اس سے جدید ورژن درکار ہے۔", + "streamOffline": { + "title": "آف لائن اسٹریم" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/objects.json b/sam2-cpu/frigate-dev/web/public/locales/ur/objects.json new file mode 100644 index 0000000..88a44f1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/objects.json @@ -0,0 +1,9 @@ +{ + "person": "شخص", + "bicycle": "سائیکل", + "car": "گاڑی", + "motorcycle": "موٹر سائیکل", + "airplane": "ہوائی جہاز", + "bus": "بس", + "train": "ٹرین" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/configEditor.json new file mode 100644 index 0000000..e32955e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/configEditor.json @@ -0,0 +1,16 @@ +{ + "toast": { + "error": { + "savingError": "کنفیگریشن محفوظ کرنے میں خرابی" + }, + "success": { + "copyToClipboard": "کنفیگ کلپ بورڈ پر کاپی ہو گیا۔" + } + }, + "documentTitle": "کنفیگریشن ایڈیٹر - Frigate", + "configEditor": "کنفیگریشن ایڈیٹر", + "copyConfig": "کنفیگریشن نقل کریں", + "saveAndRestart": "محفوظ کریں اور دوبارہ شروع کریں", + "saveOnly": "صرف محفوظ کریں", + "confirm": "محفوظ کیے بغیر باہر نکلیں؟" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/events.json new file mode 100644 index 0000000..517d7dd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/events.json @@ -0,0 +1,14 @@ +{ + "alerts": "انتباہات", + "detections": "کھوج", + "motion": { + "label": "حرکت", + "only": "صرف حرکت" + }, + "allCameras": "تمام کیمرے", + "empty": { + "alert": "جائزہ لینے کے لیے کوئی انتباہات نہیں ہیں", + "detection": "جائزہ لینے کے لیے کوئی ڈیٹیکشن موجود نہیں ہے", + "motion": "کوئی موشن ڈیٹا نہیں ملا" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/explore.json new file mode 100644 index 0000000..2657e80 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/explore.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "جائزہ لیں - فریگیٹ", + "generativeAI": "جنریٹو اے آئی", + "exploreIsUnavailable": { + "title": "جائزہ لینا دستیاب نہیں ہے", + "embeddingsReindexing": { + "context": "جب ٹریک کیے گئے آبجیکٹ ایمبیڈنگز کی دوبارہ انڈیکسنگ مکمل ہو جائےتو \"جائزہ لیں\" استعمال کیا جا سکتا ہے۔", + "startingUp": "شروع ہو رہا ہے…", + "estimatedTime": "متوقع باقی وقت:" + } + }, + "exploreMore": "مزید {{label}} اشیاء کو دریافت کریں" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/exports.json new file mode 100644 index 0000000..395ee16 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/exports.json @@ -0,0 +1,11 @@ +{ + "documentTitle": "برآمد - فریگیٹ", + "search": "تلاش", + "noExports": "کوئی برآمدات نہیں ملے", + "deleteExport": "برآمد کو حذف کریں", + "deleteExport.desc": "کیا آپ واقعی {{exportName}} کو حذف کرنا چاہتے ہیں؟", + "editExport": { + "title": "برآمد کا نام تبدیل کریں", + "desc": "اس برآمد کے لیے ایک نیا نام درج کریں۔" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/faceLibrary.json new file mode 100644 index 0000000..cfb7dce --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/faceLibrary.json @@ -0,0 +1,13 @@ +{ + "description": { + "addFace": "فیس لائبریری میں نئی کلیکشن شامل کرنے کا طریقہ بتائیں۔", + "placeholder": "اس مجموعہ کے لیے ایک نام درج کریں", + "invalidName": "غلط نام۔ ناموں میں صرف حروف، اعداد، فاصلے، اپوسٹروف، انڈر اسکور، اور ہائفن شامل ہو سکتے ہیں۔" + }, + "details": { + "face": "چہرے کی تفصیلات", + "person": "شخص", + "subLabelScore": "سب لیبل سکور", + "scoreInfo": "سب لیبل سکور تمام تسلیم شدہ چہرے کے اعتماد کے لیے وزنی سکور ہے، اس لیے یہ سنیپ شاٹ پر دکھائے گئے سکور سے مختلف ہو سکتا ہے۔" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/live.json new file mode 100644 index 0000000..cace611 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/live.json @@ -0,0 +1,13 @@ +{ + "documentTitle": "لائیو - فریگیٹ", + "documentTitle.withCamera": "{{camera}} -براہِ راست - فریگیٹ", + "lowBandwidthMode": "کم بینڈوتھ موڈ", + "twoWayTalk": { + "enable": "دو طرفہ گفتگو کو فعال کریں", + "disable": "دو طرفہ گفتگو کو غیر فعال کریں" + }, + "cameraAudio": { + "enable": "کیمرہ آڈیو فعال کریں", + "disable": "کیمرہ آڈیو کو غیر فعال کریں" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/recording.json new file mode 100644 index 0000000..5ec3474 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "فلٹر", + "export": "برآمد", + "calendar": "کیلنڈر", + "filters": "فلٹرز", + "toast": { + "error": { + "noValidTimeSelected": "درست وقت کی حد منتخب نہیں کی گئی", + "endTimeMustAfterStartTime": "اختتامی وقت آغاز کے وقت کے بعد ہونا چاہیے" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/search.json new file mode 100644 index 0000000..5e73fa4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/search.json @@ -0,0 +1,11 @@ +{ + "search": "تلاش", + "savedSearches": "محفوظ شدہ تلاشیں", + "searchFor": "{{inputValue}} تلاش کریں", + "button": { + "clear": "تلاش صاف کریں", + "save": "تلاش کو محفوظ کریں", + "delete": "محفوظ کردہ تلاش کو حذف کریں", + "filterInformation": "معلومات کو فلٹر کریں" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/settings.json new file mode 100644 index 0000000..d7172ab --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/settings.json @@ -0,0 +1,11 @@ +{ + "documentTitle": { + "default": "ترتیبات - فریگیٹ", + "authentication": "تصدیق کی ترتیبات - فریگیٹ", + "camera": "کیمرے کی ترتیبات - فریگیٹ", + "masksAndZones": "ماسک اور زون ایڈیٹر - فریگیٹ", + "motionTuner": "موشن ٹونر - فریگیٹ", + "object": "ڈی بگ - فریگیٹ", + "enrichments": "افزودگی کی ترتیبات - فریگیٹ" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/ur/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/ur/views/system.json new file mode 100644 index 0000000..3cc08e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/ur/views/system.json @@ -0,0 +1,13 @@ +{ + "documentTitle": { + "cameras": "کیمروں کے اعدادوشمار - فریگیٹ", + "storage": "اسٹوریج کے اعدادوشمار - فریگیٹ", + "general": "عمومی اعدادوشمار - فریگیٹ", + "enrichments": "افزودگی کے اعدادوشمار - فریگیٹ", + "logs": { + "frigate": "فریگیٹ لاگز - فریگیٹ", + "go2rtc": "Go2RTC لاگز - فریگیٹ", + "nginx": "Nginx لاگز - فریگیٹ" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/audio.json b/sam2-cpu/frigate-dev/web/public/locales/vi/audio.json new file mode 100644 index 0000000..07561cc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/audio.json @@ -0,0 +1,429 @@ +{ + "babbling": "Nói líu lo", + "bellow": "Gầm rú", + "whoop": "Hò reo", + "whispering": "Thì thầm", + "laughter": "Tiếng cười", + "snicker": "Cười khúc khích", + "sigh": "Thở dài", + "singing": "Hát", + "choir": "Dàn hợp xướng", + "yodeling": "Hát ngân nga", + "chant": "Hát đồng ca", + "mantra": "Thần chú", + "synthetic_singing": "Giọng hát tổng hợp", + "rapping": "Rap", + "humming": "Ngân nga", + "groan": "Rên rỉ", + "grunt": "Gằn giọng", + "whistling": "Huýt sáo", + "breathing": "Hít thở", + "wheeze": "Thở khò khè", + "gasp": "Thở hổn hển", + "pant": "Thở gấp", + "snort": "Khịt mũi", + "cough": "Ho", + "throat_clearing": "Hắng giọng", + "sneeze": "Hắt hơi", + "sniff": "Hít mũi", + "crying": "Khóc", + "yell": "La hét", + "snoring": "Ngáy", + "speech": "Giọng nói", + "child_singing": "Trẻ con hát", + "run": "Chạy", + "shuffle": "Kéo lê chân", + "footsteps": "Tiếng bước chân", + "chewing": "Nhai", + "biting": "Cắn", + "gargling": "Súc miệng", + "stomach_rumble": "Bụng sôi", + "burping": "Ợ", + "hiccup": "Nấc cụt", + "fart": "Đánh rắm", + "artillery_fire": "Tiếng pháo kích", + "cap_gun": "Tiếng súng giấy", + "fireworks": "Tiếng pháo hoa", + "firecracker": "Tiếng pháo nổ", + "burst": "Tiếng nổ bung", + "eruption": "Tiếng phun trào", + "boom": "Tiếng bùm", + "wood": "Tiếng gỗ", + "chop": "Tiếng chặt", + "splinter": "Tiếng gỗ vỡ", + "crack": "Tiếng nứt", + "glass": "Tiếng thủy tinh", + "chink": "Tiếng leng keng", + "shatter": "Tiếng vỡ vụn", + "silence": "Sự im lặng", + "sound_effect": "Hiệu ứng âm thanh", + "environmental_noise": "Tiếng ồn môi trường", + "static": "Tiếng nhiễu", + "white_noise": "Tiếng trắng", + "pink_noise": "Tiếng hồng", + "television": "Tiếng tivi", + "hands": "Tay", + "finger_snapping": "Búng tay", + "clapping": "Vỗ tay", + "heartbeat": "Nhịp tim", + "heart_murmur": "Tiếng thổi tim", + "cheering": "Cổ vũ", + "applause": "Tràng pháo tay", + "chatter": "Nói chuyện rì rầm", + "crowd": "Đám đông", + "children_playing": "Trẻ con chơi", + "animal": "Động vật", + "pets": "Thú cưng", + "dog": "Chó", + "bark": "Sủa", + "yip": "Sủa nhỏ", + "howl": "Hú", + "bow_wow": "Gâu gâu", + "growling": "Gầm gừ", + "whimper_dog": "Rên rỉ (chó)", + "livestock": "Gia súc", + "cat": "Mèo", + "purr": "Rù rì", + "meow": "Meo meo", + "hiss": "Phì phì", + "caterwaul": "Tiếng mèo gào", + "horse": "Ngựa", + "clip_clop": "Lộc cộc", + "neigh": "Hí (ngựa)", + "cattle": "Bò", + "moo": "Bò rống", + "cowbell": "Chuông bò", + "pig": "Heo", + "oink": "Ụt ịt", + "goat": "Dê", + "bleat": "Kêu be be", + "sheep": "Cừu", + "fowl": "Gia cầm", + "chicken": "Gà", + "cluck": "Cục tác", + "cock_a_doodle_doo": "Gáy (gà trống)", + "turkey": "Gà tây", + "gobble": "Gù gù (gà tây)", + "duck": "Vịt", + "quack": "Quạc quạc", + "goose": "Ngỗng", + "roar": "Gầm rú", + "bird": "Chim", + "chirp": "Hót líu lo", + "squawk": "Kêu the thé", + "honk": "Kêu vang (ngỗng)", + "wild_animals": "Động vật hoang dã", + "roaring_cats": "Mèo lớn gầm", + "pigeon": "Bồ câu", + "coo": "Cục cu", + "crow": "Quạ", + "caw": "Kêu quạ quạ", + "owl": "Cú mèo", + "hoot": "Kêu tu hú", + "flapping_wings": "Vỗ cánh", + "dogs": "Nhiều con chó", + "rats": "Chuột cống", + "mouse": "Chuột nhắt", + "patter": "Lách cách (bước chân nhỏ)", + "insect": "Côn trùng", + "cricket": "Dế", + "mosquito": "Muỗi", + "fly": "Ruồi", + "buzz": "Vo ve", + "frog": "Ếch", + "croak": "Ếch kêu", + "snake": "Rắn", + "rattle": "Lắc lư / lách cách", + "whale_vocalization": "Tiếng cá voi", + "music": "Âm nhạc", + "musical_instrument": "Nhạc cụ", + "plucked_string_instrument": "Nhạc cụ dây gảy", + "guitar": "Đàn guitar", + "electric_guitar": "Đàn guitar điện", + "bass_guitar": "Đàn guitar bass", + "acoustic_guitar": "Đàn guitar acoustic", + "steel_guitar": "Đàn steel guitar", + "tapping": "Kỹ thuật tapping", + "strum": "Gảy đàn", + "banjo": "Đàn banjo", + "sitar": "Đàn sitar", + "mandolin": "Đàn mandolin", + "zither": "Đàn tranh", + "ukulele": "Đàn ukulele", + "keyboard": "Bàn phím nhạc", + "piano": "Đàn piano", + "electric_piano": "đàn piano điện", + "organ": "Đàn organ", + "electronic_organ": "Đàn organ điện tử", + "hammond_organ": "Đàn organ Hammond", + "synthesizer": "Bộ tổng hợp âm", + "sampler": "Thiết bị lấy mẫu âm thanh", + "harpsichord": "Đàn harpsichord", + "percussion": "Bộ gõ", + "drum_kit": "Bộ trống", + "drum_machine": "Máy trống", + "drum": "Tiếng trống", + "snare_drum": "Trống snare", + "rimshot": "Gõ vành trống", + "drum_roll": "Cuộn trống", + "bass_drum": "Trống bass", + "timpani": "Trống timpani", + "tabla": "Trống tabla", + "cymbal": "Tiếng chũm chọe", + "hi_hat": "Tiếng hi-hat", + "wood_block": "Khối gỗ gõ", + "tambourine": "Trống lắc", + "maraca": "Tiếng lắc maraca", + "gong": "Tiếng chiêng", + "tubular_bells": "Chuông ống", + "mallet_percussion": "Nhạc cụ gõ bằng dùi", + "marimba": "Đàn marimba", + "glockenspiel": "Chuông gõ glockenspiel", + "vibraphone": "Đàn vibraphone", + "steelpan": "Trống thép", + "orchestra": "Dàn nhạc giao hưởng", + "brass_instrument": "Nhạc cụ đồng", + "french_horn": "Kèn Pháp", + "trumpet": "Kèn trumpet", + "trombone": "Kèn trombone", + "bowed_string_instrument": "Nhạc cụ dây kéo", + "string_section": "Dàn dây", + "violin": "Đàn violin", + "pizzicato": "Gảy dây pizzicato", + "cello": "Đàn cello", + "double_bass": "Đàn contrabass", + "wind_instrument": "Nhạc cụ hơi", + "flute": "Tiếng sáo", + "saxophone": "Kèn saxophone", + "clarinet": "Kèn clarinet", + "harp": "Đàn harp", + "bell": "Chuông", + "church_bell": "Chuông nhà thờ", + "jingle_bell": "Chuông leng keng", + "bicycle_bell": "Chuông xe đạp", + "tuning_fork": "Âm thoa", + "chime": "Tiếng chuỗi chuông", + "wind_chime": "Tiếng chuông gió", + "harmonica": "Tiếng kèn harmonica", + "accordion": "Tiếng đàn accordion", + "bagpipes": "Tiếng kèn túi", + "didgeridoo": "Tiếng kèn didgeridoo", + "theremin": "Tiếng nhạc cụ theremin", + "singing_bowl": "Tiếng chuông xoay Tây Tạng", + "scratching": "Scratch nhạc (xoay đĩa)", + "pop_music": "Nhạc pop", + "hip_hop_music": "Nhạc hip hop", + "beatboxing": "Beatbox", + "rock_music": "Nhạc rock", + "heavy_metal": "Nhạc heavy metal", + "punk_rock": "Nhạc punk rock", + "grunge": "Nhạc grunge", + "progressive_rock": "Nhạc rock tiến bộ", + "rock_and_roll": "Nhạc rock and roll", + "psychedelic_rock": "Nhạc rock ảo giác", + "rhythm_and_blues": "Nhạc R&B", + "soul_music": "Nhạc soul", + "reggae": "Nhạc reggae", + "country": "Nhạc đồng quê", + "swing_music": "Nhạc swing", + "bluegrass": "Nhạc bluegrass", + "funk": "Nhạc funk", + "folk_music": "Nhạc dân gian", + "middle_eastern_music": "Nhạc Trung Đông", + "jazz": "Nhạc jazz", + "disco": "Nhạc disco", + "classical_music": "Nhạc cổ điển", + "opera": "Nhạc opera", + "electronic_music": "Nhạc điện tử", + "house_music": "Nhạc house", + "techno": "Nhạc techno", + "dubstep": "Nhạc dubstep", + "drum_and_bass": "Nhạc trống và bass", + "electronica": "Nhạc electronica", + "electronic_dance_music": "Nhạc nhảy điện tử", + "ambient_music": "Nhạc nền", + "trance_music": "Nhạc trance", + "music_of_latin_america": "Nhạc Mỹ Latinh", + "salsa_music": "Nhạc salsa", + "flamenco": "Nhạc flamenco", + "blues": "Nhạc blues", + "music_for_children": "Nhạc thiếu nhi", + "new-age_music": "Nhạc thời đại mới", + "vocal_music": "Nhạc thanh nhạc", + "a_capella": "Nhạc a cappella", + "music_of_africa": "Nhạc châu Phi", + "afrobeat": "Nhạc afrobeat", + "christian_music": "Nhạc Cơ Đốc", + "gospel_music": "Nhạc phúc âm", + "music_of_asia": "Nhạc châu Á", + "carnatic_music": "Nhạc Carnatic", + "music_of_bollywood": "Nhạc Bollywood", + "ska": "Nhạc ska", + "traditional_music": "Nhạc truyền thống", + "independent_music": "Nhạc indie", + "song": "Bài hát", + "background_music": "Nhạc nền", + "theme_music": "Nhạc chủ đề", + "jingle": "Nhạc quảng cáo", + "soundtrack_music": "Nhạc phim", + "lullaby": "Tiếng ru", + "video_game_music": "Nhạc trò chơi", + "christmas_music": "Nhạc Giáng Sinh", + "dance_music": "Nhạc khiêu vũ", + "wedding_music": "Nhạc đám cưới", + "happy_music": "Nhạc vui", + "sad_music": "Nhạc buồn", + "tender_music": "Nhạc nhẹ nhàng", + "exciting_music": "Nhạc sôi động", + "angry_music": "Nhạc tức giận", + "scary_music": "Nhạc rùng rợn", + "wind": "Tiếng gió", + "rustling_leaves": "Tiếng lá xào xạc", + "wind_noise": "Tiếng gió rít", + "thunderstorm": "Tiếng giông bão", + "water": "Tiếng nước", + "thunder": "Tiếng sấm", + "rain": "Tiếng mưa", + "raindrop": "Tiếng giọt mưa", + "rain_on_surface": "Tiếng mưa rơi", + "stream": "Tiếng suối", + "waterfall": "Tiếng thác nước", + "ocean": "Tiếng biển", + "waves": "Tiếng sóng", + "steam": "Tiếng hơi nước", + "gurgling": "Tiếng róc rách", + "fire": "Tiếng lửa", + "crackle": "Tiếng tí tách", + "vehicle": "Tiếng phương tiện", + "boat": "Tiếng thuyền", + "sailboat": "Tiếng thuyền buồm", + "rowboat": "Tiếng chèo thuyền", + "motorboat": "Tiếng xuồng máy", + "ship": "Tiếng tàu", + "motor_vehicle": "Tiếng xe cơ giới", + "car": "Tiếng xe ô tô", + "toot": "Tiếng bấm còi", + "car_alarm": "Tiếng báo động ô tô", + "power_windows": "Tiếng cửa kính xe", + "skidding": "Tiếng trượt bánh", + "tire_squeal": "Tiếng lốp rít", + "car_passing_by": "Tiếng xe chạy qua", + "race_car": "Tiếng xe đua", + "truck": "Tiếng xe tải", + "ice_cream_truck": "Tiếng xe kem", + "air_brake": "Tiếng phanh hơi", + "air_horn": "Tiếng còi hơi", + "reversing_beeps": "Tiếng kêu lùi xe", + "bus": "Tiếng xe buýt", + "emergency_vehicle": "Tiếng xe khẩn cấp", + "police_car": "Tiếng xe cảnh sát", + "ambulance": "Tiếng xe cứu thương", + "fire_engine": "Tiếng xe cứu hỏa", + "motorcycle": "Tiếng xe máy", + "traffic_noise": "Tiếng giao thông", + "rail_transport": "Tiếng đường sắt", + "train_horn": "Tiếng còi tàu hỏa", + "railroad_car": "Tiếng toa tàu", + "train": "Tiếng tàu hỏa", + "train_whistle": "Tiếng còi tàu", + "train_wheels_squealing": "Tiếng bánh tàu rít", + "subway": "Tiếng tàu điện ngầm", + "aircraft": "Tiếng máy bay", + "aircraft_engine": "Tiếng động cơ máy bay", + "jet_engine": "Tiếng động cơ phản lực", + "propeller": "Tiếng cánh quạt", + "helicopter": "Tiếng trực thăng", + "fixed-wing_aircraft": "Tiếng máy bay cánh cố định", + "bicycle": "Tiếng xe đạp", + "skateboard": "Tiếng ván trượt", + "engine": "Tiếng động cơ", + "light_engine": "Tiếng động cơ nhẹ", + "dental_drill's_drill": "Tiếng khoan nha khoa", + "lawn_mower": "Tiếng máy cắt cỏ", + "chainsaw": "Tiếng cưa máy", + "medium_engine": "Tiếng động cơ vừa", + "heavy_engine": "Tiếng động cơ nặng", + "engine_knocking": "Tiếng gõ máy", + "engine_starting": "Tiếng khởi động động cơ", + "ding-dong": "Tiếng ding-dong", + "idling": "Tiếng nổ không tải", + "accelerating": "Tiếng tăng tốc", + "door": "Tiếng cửa", + "doorbell": "Tiếng chuông cửa", + "sliding_door": "Tiếng cửa trượt", + "slam": "Tiếng đóng sầm", + "knock": "Tiếng gõ cửa", + "tap": "Tiếng gõ nhẹ", + "squeak": "Tiếng kêu cót két", + "cupboard_open_or_close": "Tiếng mở/đóng tủ", + "drawer_open_or_close": "Tiếng mở/đóng ngăn kéo", + "dishes": "Tiếng bát đĩa", + "cutlery": "Tiếng dao nĩa", + "chopping": "Tiếng băm chặt", + "frying": "Tiếng chiên xào", + "microwave_oven": "Tiếng lò vi sóng", + "blender": "Tiếng máy xay", + "water_tap": "Tiếng vòi nước", + "sink": "Tiếng bồn rửa", + "bathtub": "Tiếng bồn tắm", + "coin": "Tiếng đồng xu", + "hair_dryer": "Tiếng máy sấy tóc", + "toilet_flush": "Tiếng xả nước", + "toothbrush": "Tiếng bàn chải", + "electric_toothbrush": "Tiếng bàn chải điện", + "vacuum_cleaner": "Tiếng máy hút bụi", + "zipper": "Tiếng dây kéo", + "keys_jangling": "Tiếng chìa khóa leng keng", + "scissors": "Tiếng kéo cắt", + "electric_shaver": "Tiếng máy cạo râu", + "shuffling_cards": "Tiếng xào bài", + "typing": "Tiếng gõ phím", + "typewriter": "Tiếng máy đánh chữ", + "computer_keyboard": "Tiếng bàn phím", + "writing": "Tiếng viết", + "alarm": "Tiếng báo động", + "telephone": "Tiếng điện thoại", + "telephone_bell_ringing": "Tiếng chuông điện thoại", + "ringtone": "Tiếng nhạc chuông", + "telephone_dialing": "Tiếng quay số", + "dial_tone": "Tiếng âm quay số", + "busy_signal": "Tiếng tín hiệu bận", + "alarm_clock": "Tiếng đồng hồ báo thức", + "siren": "Tiếng còi báo động", + "civil_defense_siren": "Tiếng còi phòng không", + "buzzer": "Tiếng chuông báo", + "smoke_detector": "Tiếng báo khói", + "fire_alarm": "Tiếng báo cháy", + "foghorn": "Tiếng còi sương", + "whistle": "Tiếng còi", + "steam_whistle": "Tiếng còi hơi", + "mechanisms": "Tiếng cơ khí", + "ratchet": "Tiếng cơ cấu bánh cóc", + "clock": "Tiếng đồng hồ", + "tick": "Tiếng tích", + "tick-tock": "Tiếng tích tắc", + "gears": "Tiếng bánh răng", + "pulleys": "Tiếng ròng rọc", + "sewing_machine": "Tiếng máy may", + "camera": "Tiếng máy ảnh", + "single-lens_reflex_camera": "Tiếng máy ảnh DSLR", + "mechanical_fan": "Tiếng quạt máy", + "air_conditioning": "Tiếng máy lạnh", + "cash_register": "Tiếng máy tính tiền", + "printer": "Tiếng máy in", + "tools": "Tiếng dụng cụ", + "hammer": "Tiếng búa", + "jackhammer": "Tiếng khoan bê tông", + "sawing": "Tiếng cưa", + "filing": "Tiếng giũa", + "sanding": "Tiếng chà nhám", + "power_tool": "Tiếng dụng cụ điện", + "drill": "Tiếng máy khoan", + "explosion": "Tiếng nổ", + "gunshot": "Tiếng súng", + "machine_gun": "Tiếng súng máy", + "fusillade": "Tiếng loạt súng", + "radio": "Tiếng radio", + "field_recording": "Ghi âm hiện trường", + "scream": "Tiếng hét" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/common.json b/sam2-cpu/frigate-dev/web/public/locales/vi/common.json new file mode 100644 index 0000000..dea1157 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/common.json @@ -0,0 +1,300 @@ +{ + "time": { + "untilRestart": "Đến khi khởi động lại", + "untilForTime": "Cho đến khi {{time}}", + "untilForRestart": "Cho đến khi Frigate khởi động lại.", + "ago": "{{timeAgo}} trước", + "formattedTimestamp": { + "12hour": "d MMM, h:mm:ss aaa", + "24hour": "d MMM, HH:mm:ss" + }, + "year_other": "{{time}} năm", + "month_other": "{{time}} tháng", + "day_other": "{{time}} ngày", + "hour_other": "{{time}} giờ", + "minute_other": "{{time}} phút", + "second_other": "{{time}} giây", + "justNow": "Vừa xong", + "today": "Hôm nay", + "yesterday": "Hôm qua", + "last7": "7 ngày qua", + "last14": "14 ngày qua", + "last30": "30 ngày qua", + "thisWeek": "Tuần này", + "lastWeek": "Tuần trước", + "thisMonth": "Tháng này", + "lastMonth": "Tháng trước", + "5minutes": "5 phút", + "10minutes": "10 phút", + "30minutes": "30 phút", + "1hour": "1 giờ", + "12hours": "12 giờ", + "24hours": "24 giờ", + "pm": "pm", + "am": "am", + "mo": "{{time}} tháng", + "d": "{{time}} ngày", + "m": "{{time}} phút", + "s": "{{time}} giây", + "formattedTimestamp2": { + "12hour": "dd/MM h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "thời gian 12 giờ (không giây)", + "24hour": "thời gian 24 giờ (không giây)" + }, + "formattedTimestampWithYear": { + "12hour": "thời gian 12 giờ kèm năm", + "24hour": "thời gian 24 giờ kèm năm" + }, + "formattedTimestampOnlyMonthAndDay": "chỉ tháng và ngày", + "yr": "{{time}} năm", + "h": "{{time}} giờ", + "formattedTimestampMonthDayYear": { + "12hour": "d MMM, yyyy", + "24hour": "d MMM, yyyy" + }, + "formattedTimestampHourMinute": { + "12hour": "h:mm aaa", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "h:mm:ss aaa", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "d MMM, h:mm aaa", + "24hour": "d MMM, HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa", + "24hour": "d MMM yyyy, HH:mm" + }, + "formattedTimestampMonthDay": "d MMM", + "formattedTimestampFilename": { + "12hour": "dd-MM-yy-h-mm-ss-a", + "24hour": "dd-MM-yy-HH-mm-s" + }, + "inProgress": "Đang tiến hành", + "invalidStartTime": "Thời gian bắt đầu không hợp lệ", + "invalidEndTime": "Thời gian kết thúc không hợp lệ" + }, + "menu": { + "systemLogs": "Nhật ký hệ thống", + "user": { + "account": "Tài khoản", + "anonymous": "Ẩn danh", + "logout": "Đăng xuất", + "setPassword": "Đặt mật khẩu", + "current": "Người dùng hiện tại: {{user}}", + "title": "Người dùng" + }, + "language": { + "en": "English (Tiếng Anh)", + "es": "Español (Tiếng Tây Ban Nha)", + "zhCN": "简体中文 (Tiếng Trung Giản Thể)", + "ar": "العربية (Tiếng Ả Rập)", + "hi": "हिन्दी (Tiếng Hindi)", + "fr": "Français (Tiếng Pháp)", + "pt": "Português (Tiếng Bồ Đào Nha)", + "ru": "Русский (Tiếng Nga)", + "de": "Deutsch (Tiếng Đức)", + "ja": "日本語 (Tiếng Nhật)", + "tr": "Türkçe (Tiếng Thổ Nhĩ Kỳ)", + "it": "Italiano (Tiếng Ý)", + "nl": "Nederlands (Tiếng Hà Lan)", + "sv": "Svenska (Tiếng Thụy Điển)", + "cs": "Čeština (Tiếng Séc)", + "nb": "Norsk Bokmål (Tiếng Na Uy)", + "ko": "한국어 (Tiếng Hàn)", + "pl": "Polski (Tiếng Ba Lan)", + "vi": "Tiếng Việt (Tiếng Việt)", + "fa": "فارسی (Tiếng Ba Tư)", + "uk": "Українська (Tiếng Ukraina)", + "he": "עברית (Tiếng Do Thái)", + "el": "Ελληνικά (Tiếng Hy Lạp)", + "ro": "Română (Tiếng Romania)", + "hu": "Magyar (Tiếng Hungary)", + "fi": "Suomi (Tiếng Phần Lan)", + "da": "Dansk (Tiếng Đan Mạch)", + "sk": "Slovenčina (Tiếng Slovakia)", + "withSystem": { + "label": "Theo hệ thống" + }, + "yue": "粵語 (Tiếng Quảng Đông)", + "ca": "Català (Tiếng Catalan)", + "th": "ไทย (Tiếng Thái)", + "ptBR": "Português brasileiro (Tiếng Bồ Đào Nha Brazil)", + "sr": "Српски (Tiếng Serbian)", + "sl": "Slovenščina (Tiếng Slovenian)", + "lt": "Lietuvių (Tiếng Lithuanian)", + "bg": "Български (Tiếng Bulgarian)", + "gl": "Galego (Tiếng Galician)", + "id": "Bahasa Indonesia (Tiếng Indonesian)", + "ur": "اردو (Tiếng Urdu)" + }, + "system": "Hệ thống", + "systemMetrics": "Thông số hệ thống", + "configuration": "Cấu hình", + "settings": "Cài đặt", + "configurationEditor": "Trình chỉnh sửa cấu hình", + "languages": "Ngôn ngữ", + "appearance": "Giao diện", + "darkMode": { + "label": "Chế độ tối", + "light": "Sáng", + "dark": "Tối", + "withSystem": { + "label": "Theo hệ thống" + } + }, + "withSystem": "Hệ thống", + "theme": { + "label": "Giao diện", + "red": "Đỏ", + "contrast": "tương phản", + "blue": "Xanh dương", + "green": "Xanh lá", + "nord": "Nord", + "default": "Mặc định", + "highcontrast": "Độ tương phản cao" + }, + "help": "Trợ giúp", + "documentation": { + "title": "Tài liệu", + "label": "Hướng dẫn" + }, + "restart": "Khởi động lại", + "live": { + "title": "Trực tiếp", + "allCameras": "Tất cả Camera", + "cameras": { + "title": "Camera", + "count_other": "{{count}} Camera" + } + }, + "review": "Xem lại", + "explore": "Khám phá", + "export": "Xuất", + "uiPlayground": "UI Playground", + "faceLibrary": "Thư viện khuôn mặt", + "classification": "Phân loại" + }, + "unit": { + "speed": { + "mph": "mph (dặm/giờ)", + "kph": "km/h (kilômét/giờ)" + }, + "length": { + "meters": "mét (m)", + "feet": "feet (ft)" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/giờ", + "mbph": "MB/giờ", + "gbph": "GB/giờ" + } + }, + "label": { + "back": "Quay lại", + "hide": "Ẩn {{item}}", + "show": "Hiển thị {{item}}", + "ID": "ID", + "none": "Không có", + "all": "Tất cả" + }, + "button": { + "apply": "Áp dụng", + "reset": "Đặt lại", + "done": "Xong", + "enabled": "Đã bật", + "enable": "Bật", + "disabled": "Đã tắt", + "disable": "Tắt", + "save": "Lưu", + "cancel": "Hủy", + "close": "Đóng", + "copy": "Sao chép", + "back": "Quay lại", + "history": "Lịch sử", + "fullscreen": "Toàn màn hình", + "on": "Bật", + "exitFullscreen": "Thoát toàn màn hình", + "pictureInPicture": "Hình trong hình", + "twoWayTalk": "Đàm thoại hai chiều", + "cameraAudio": "Âm thanh Camera", + "off": "Tắt", + "edit": "Chỉnh sửa", + "copyCoordinates": "Sao chép tọa độ", + "delete": "Xóa", + "yes": "Có", + "no": "Không", + "download": "Tải xuống", + "info": "Thông tin", + "suspended": "Đã tạm dừng", + "unsuspended": "Khôi phục", + "play": "Phát", + "unselect": "Bỏ chọn", + "export": "Xuất", + "deleteNow": "Xóa ngay", + "next": "Tiếp theo", + "saving": "Đang lưu…", + "continue": "Tiếp tục" + }, + "toast": { + "copyUrlToClipboard": "Đã sao chép liên kết.", + "save": { + "title": "Lưu thành công", + "error": { + "noMessage": "Không thể lưu thay đổi cấu hình", + "title": "Lỗi khi lưu thay đổi cấu hình: {{errorMessage}}" + } + } + }, + "role": { + "title": "Vai trò", + "admin": "Quản trị viên", + "viewer": "Người xem", + "desc": "Quản trị viên có toàn quyền truy cập tất cả các tính năng trong giao diện Frigate. Người xem chỉ được phép xem camera, mục đã ghi lại và các đoạn video lịch sử trong giao diện." + }, + "pagination": { + "label": "Trang", + "previous": { + "title": "Trước đó", + "label": "Trước" + }, + "next": { + "title": "Kế tiếp", + "label": "Tiếp" + }, + "more": "Xem thêm" + }, + "accessDenied": { + "documentTitle": "Từ chối truy cập", + "title": "Truy cập bị từ chối", + "desc": "Bạn không có quyền truy cập vào trang này." + }, + "notFound": { + "documentTitle": "Không tìm thấy", + "title": "Không tìm thấy", + "desc": "Trang bạn đang tìm không tồn tại" + }, + "selectItem": "Chọn mục {{item}}", + "readTheDocumentation": "Đọc tài liệu", + "list": { + "two": "{{0}} và {{1}}", + "many": "{{items}}, và {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "Không bắt buộc", + "internalID": "Internal ID Frigate sử dụng trong cấu hình và cơ sở dữ liệu" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/auth.json new file mode 100644 index 0000000..bc664d5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "Tên người dùng", + "password": "Mật khẩu", + "login": "Đăng nhập", + "errors": { + "usernameRequired": "Tên người dùng là bắt buộc", + "passwordRequired": "Mật khẩu là bắt buộc", + "rateLimit": "Đã vượt quá giới hạn tốc độ. Hãy thử lại sau.", + "loginFailed": "Đăng nhập không thành công", + "unknownError": "Lỗi không xác định. Kiểm tra nhật ký.", + "webUnknownError": "Lỗi không xác định. Kiểm tra nhật ký bảng điều khiển." + }, + "firstTimeLogin": "Lần đầu đăng nhập? Thông tin đăng nhập được in trong nhật ký (log) của Frigate." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/camera.json new file mode 100644 index 0000000..e67824e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "delete": { + "label": "Xóa nhóm Camera", + "confirm": { + "title": "Xác nhận xóa", + "desc": "Bạn có chắc chắn muốn xóa nhóm camera {{name}} không?" + } + }, + "label": "Các nhóm Camera", + "add": "Thêm nhóm Camera", + "camera": { + "setting": { + "stream": "Trực tiếp", + "audio": { + "tips": { + "title": "Âm thanh phải được xuất từ camera của bạn và được định cấu hình trong go2rtc cho luồng này.", + "document": "Đọc tài liệu " + } + }, + "desc": "Thay đổi các tùy chọn truyền phát trực tiếp cho bảng điều khiển của nhóm camera này.Các cài đặt này dành riêng cho thiết bị/trình duyệt..", + "streamMethod": { + "method": { + "noStreaming": { + "desc": "Hình ảnh camera sẽ chỉ cập nhật mỗi phút một lần và không có truyền phát nào xảy ra.", + "label": "Không truyền phát" + }, + "continuousStreaming": { + "desc": { + "title": "Hình ảnh camera sẽ luôn là luồng trực tiếp khi hiển thị trên bảng điều khiển, ngay cả khi không có hoạt động nào được phát hiện.", + "warning": "Truyền phát liên tục có thể gây ra sử dụng băng thông cao và các vấn đề về hiệu suất. Sử dụng một cách thận trọng." + }, + "label": "Truyền phát liên tục" + }, + "smartStreaming": { + "label": "Truyền phát Thông minh (khuyến nghị)", + "desc": "Truyền phát thông minh sẽ cập nhật hình ảnh camera của bạn mỗi phút một lần khi không có hoạt động nào được phát hiện để tiết kiệm băng thông và tài nguyên. Khi phát hiện hoạt động, hình ảnh sẽ chuyển đổi liền mạch sang luồng trực tiếp." + } + }, + "placeholder": "Chọn phương thức truyền phát", + "label": "Phương thức truyền phát" + }, + "placeholder": "Chọn phát trực tiếp", + "compatibilityMode": { + "label": "Chế độ tương thích", + "desc": "Chỉ bật tùy chọn này nếu luồng trực tiếp của camera của bạn hiển thị các hiện vật màu và có một đường chéo ở phía bên phải của hình ảnh." + }, + "title": "Cài đặt trực tiếp {{cameraName}}", + "audioIsAvailable": "Âm thanh có sẵn cho luồng này", + "audioIsUnavailable": "Âm thanh không có sẵn cho luồng này", + "label": "Cài đặt trực tiếp Camera" + }, + "birdseye": "Toàn cảnh" + }, + "name": { + "label": "Tên", + "errorMessage": { + "mustLeastCharacters": "Tên nhóm Camera phải có ít nhất 2 ký tự.", + "nameMustNotPeriod": "Tên nhóm camera không được chứa dấu chấm.", + "exists": "Tên nhóm Camera đã tồn tại.", + "invalid": "Tên nhóm camera không hợp lệ." + }, + "placeholder": "Nhập tên…" + }, + "icon": "Biểu tượng", + "success": "Nhóm camera ({{name}}) đã được lưu.", + "cameras": { + "desc": "Chọn camera cho nhóm này.", + "label": "Camera" + }, + "edit": "Sửa nhóm Camera" + }, + "debug": { + "boundingBox": "Hộp giới hạn", + "options": { + "hideOptions": "Ẩn tùy chọn", + "label": "Cài đặt", + "title": "Tùy chọn", + "showOptions": "Hiện thị tùy chọn" + }, + "timestamp": "Dấu thời gian", + "zones": "Khu vực", + "mask": "Mặt nạ", + "motion": "Chuyển động", + "regions": "Vùng" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/dialog.json new file mode 100644 index 0000000..b8b2895 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/dialog.json @@ -0,0 +1,122 @@ +{ + "restart": { + "title": "Bạn có chắc chắn muốn khởi động lại Frigate không?", + "button": "Khởi động lại", + "restarting": { + "title": "Đang khởi động lại Frigate", + "content": "Trang này sẽ tải lại sau {{countdown}} giây.", + "button": "Tải lại ngay" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "ask_a": "Đối tượng này có phải là một {{label}} không?", + "ask_an": "Đối tượng này có phải là một {{label}} không?", + "label": "Xác nhận nhãn này cho Frigate Plus", + "ask_full": "Đối tượng này có phải là một {{untranslatedLabel}} ({{translatedLabel}}) không?" + }, + "state": { + "submitted": "Đã gửi" + } + }, + "submitToPlus": { + "label": "Gửi lên Frigate+", + "desc": "Đối tượng xuất hiện ở các khu vực bạn muốn tránh không được xem là phát hiện sai. Gửi chúng lên dưới dạng phát hiện sai có thể làm mô hình bị nhầm lẫn." + } + }, + "video": { + "viewInHistory": "Xem lại trong Lịch sử" + } + }, + "export": { + "time": { + "fromTimeline": "Chọn từ Dòng thời gian", + "custom": "Tuỳ chọn", + "start": { + "title": "Thời gian bắt đầu", + "label": "Chọn thời gian bắt đầu" + }, + "end": { + "title": "Thời gian kết thúc", + "label": "Chọn thời gian kết thúc" + }, + "lastHour_other": "{{count}} giờ trước" + }, + "name": { + "placeholder": "Đặt tên cho bản xuất" + }, + "select": "Chọn", + "export": "Xuất", + "selectOrExport": "Chọn hay xuất", + "toast": { + "error": { + "endTimeMustAfterStartTime": "Thời gian kết thúc phải sau thời gian bắt đầu", + "noVaildTimeSelected": "Chưa chọn khoảng thời gian hợp lệ", + "failed": "Không thể bắt đầu xuất: {{error}}" + }, + "success": "Đã bắt đầu xuất dữ liệu thành công. Xem tệp trên trang xuất dữ liệu.", + "view": "Xem" + }, + "fromTimeline": { + "saveExport": "Lưu bản xuất", + "previewExport": "Xem trước bản xuất" + } + }, + "streaming": { + "debugView": "Chế độ xem Gỡ lỗi", + "label": "Luồng", + "restreaming": { + "disabled": "Tính năng phát lại luồng không được bật cho camera này.", + "desc": { + "title": "Thiết lập go2rtc để có thêm tùy chọn xem trực tiếp và âm thanh cho camera này.", + "readTheDocumentation": "Đọc tài liệu" + } + }, + "showStats": { + "label": "Hiển thị số liệu thống kê luồng", + "desc": "Bật tùy chọn này để hiển thị số liệu thống kê luồng dưới dạng lớp phủ trên nguồn cấp dữ liệu camera." + } + }, + "recording": { + "confirmDelete": { + "title": "Xác nhận xóa", + "toast": { + "success": "Đoạn video liên quan đến các mục đánh giá đã chọn đã được xóa thành công.", + "error": "Không thể xóa: {{error}}" + }, + "desc": { + "selected": "Bạn có chắc chắn muốn xóa tất cả video đã ghi liên quan đến mục đánh giá này không?

    Giữ phím Shift để bỏ qua hộp thoại này trong tương lai." + } + }, + "button": { + "deleteNow": "Xóa ngay", + "export": "Xuất", + "markAsReviewed": "Đánh dấu là đã xem xét", + "markAsUnreviewed": "Đánh dấu là chưa xem xét" + } + }, + "search": { + "saveSearch": { + "success": "Tìm kiếm ({{searchName}}) đã được lưu.", + "button": { + "save": { + "label": "Lưu tìm kiếm này" + } + }, + "label": "Lưu tìm kiếm", + "desc": "Cung cấp tên cho tìm kiếm đã lưu này.", + "placeholder": "Nhập tên cho tìm kiếm của bạn", + "overwrite": "{{searchName}} đã tồn tại. Lưu sẽ ghi đè lên giá trị hiện có." + } + }, + "imagePicker": { + "selectImage": "Chọn hình thu nhỏ của đối tượng cần theo dõi", + "search": { + "placeholder": "Tìm theo nhãn hoặc nhãn phụ..." + }, + "noImages": "Không tìm thấy hình thu nhỏ cho camera này", + "unknownLabel": "Ảnh kích hoạt đã lưu" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/filter.json new file mode 100644 index 0000000..3678ba1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/filter.json @@ -0,0 +1,136 @@ +{ + "filter": "Lọc", + "labels": { + "label": "Nhãn", + "all": { + "title": "Tất cả các nhãn", + "short": "Nhãn" + }, + "count_one": "{{count}} nhãn", + "count_other": "{{count}} nhãn" + }, + "features": { + "submittedToFrigatePlus": { + "tips": "Trước tiên, bạn phải lọc các đối tượng được theo dõi có ảnh chụp nhanh.

    Không thể gửi các đối tượng được theo dõi không có ảnh chụp nhanh tới Frigate+.", + "label": "Đã gửi tới Frigate+" + }, + "label": "Tính năng", + "hasSnapshot": "Có ảnh chụp nhanh", + "hasVideoClip": "Có video clip" + }, + "sort": { + "scoreDesc": "Điểm Đối tượng (Giảm dần)", + "speedAsc": "Tốc độ Ước tính (Tăng dần)", + "relevance": "Mức độ liên quan", + "label": "Sắp xếp", + "dateAsc": "Ngày (Tăng dần)", + "dateDesc": "Ngày (Giảm dần)", + "scoreAsc": "Điểm Đối tượng (Tăng dần)", + "speedDesc": "Tốc độ Ước tính (Giảm dần)" + }, + "cameras": { + "label": "Bộ lọc Camera", + "all": { + "title": "Tất cả Camera", + "short": "Camera" + } + }, + "review": { + "showReviewed": "Hiển thị đã xem xét" + }, + "motion": { + "showMotionOnly": "Chỉ Hiển thị Chuyển động" + }, + "explore": { + "settings": { + "title": "Cài đặt", + "defaultView": { + "title": "Chế độ xem Mặc định", + "desc": "Khi không có bộ lọc nào được chọn, hiển thị bản tóm tắt các đối tượng được theo dõi gần đây nhất cho mỗi nhãn, hoặc hiển thị lưới không được lọc.", + "summary": "Tóm tắt", + "unfilteredGrid": "Lưới không được lọc" + }, + "gridColumns": { + "title": "Cột Lưới", + "desc": "Chọn số lượng cột trong chế độ xem lưới." + }, + "searchSource": { + "label": "Nguồn Tìm kiếm", + "desc": "Chọn tìm kiếm hình thu nhỏ hay mô tả của các đối tượng được theo dõi của bạn.", + "options": { + "thumbnailImage": "Hình thu nhỏ", + "description": "Mô tả" + } + } + }, + "date": { + "selectDateBy": { + "label": "Chọn một ngày để lọc theo" + } + } + }, + "logSettings": { + "allLogs": "Tất cả nhật ký", + "label": "Lọc cấp độ nhật ký", + "filterBySeverity": "Lọc nhật ký theo mức độ nghiêm trọng", + "loading": { + "title": "Đang tải", + "desc": "Khi bảng nhật ký được cuộn xuống dưới cùng, nhật ký mới sẽ tự động truyền trực tuyến khi chúng được thêm vào." + }, + "disableLogStreaming": "Tắt truyền phát nhật ký" + }, + "trackedObjectDelete": { + "title": "Xác nhận Xóa", + "toast": { + "success": "Đã xóa thành công các đối tượng được theo dõi.", + "error": "Không thể xóa các đối tượng được theo dõi: {{errorMessage}}" + }, + "desc": "Việc xóa {{objectLength}} đối tượng được theo dõi này sẽ xóa ảnh chụp nhanh, mọi nội dung nhúng đã lưu và mọi mục nhập vòng đời đối tượng liên quan. Đoạn ghi hình đã ghi của các đối tượng được theo dõi này trong chế độ xem Lịch sử sẽ KHÔNG bị xóa.

    Bạn có chắc chắn muốn tiếp tục không?

    Giữ phím Shift để bỏ qua hộp thoại này trong tương lai." + }, + "recognizedLicensePlates": { + "selectPlatesFromList": "Chọn một hoặc nhiều biển số từ danh sách.", + "title": "Biển số xe được Nhận dạng", + "loadFailed": "Không thể tải biển số xe được nhận dạng.", + "loading": "Đang tải biển số xe được nhận dạng…", + "placeholder": "Nhập để tìm kiếm biển số xe…", + "noLicensePlatesFound": "Không tìm thấy biển số xe nào.", + "selectAll": "Chọn tất cả", + "clearAll": "Xóa tất cả" + }, + "more": "Thêm Bộ lọc", + "reset": { + "label": "Đặt lại bộ lọc về giá trị mặc định" + }, + "timeRange": "Phạm vi Thời gian", + "subLabels": { + "label": "Nhãn phụ", + "all": "Tất cả Nhãn phụ" + }, + "score": "Điểm", + "estimatedSpeed": "Tốc độ Ước tính ({{unit}})", + "dates": { + "selectPreset": "Chọn thiết lập sẵn…", + "all": { + "title": "Tất cả Ngày", + "short": "Ngày" + } + }, + "zoneMask": { + "filterBy": "Lọc theo mặt nạ khu vực" + }, + "zones": { + "label": "Khu vực", + "all": { + "title": "Tất cả Khu vực", + "short": "Khu vực" + } + }, + "classes": { + "label": "Các nhãn nhận diện", + "all": { + "title": "Tất cả nhãn nhận diện" + }, + "count_one": "{{count}} Nhãn nhận diện", + "count_other": "{{count}} Các nhãn nhận diện" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/icons.json new file mode 100644 index 0000000..666736e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "Chọn một biểu tượng", + "search": { + "placeholder": "Tìm kiếm một biểu tượng…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/input.json new file mode 100644 index 0000000..c12a614 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "Tải xuống video", + "toast": { + "success": "Video trong mục xem lại của bạn đã bắt đầu tải xuống." + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/vi/components/player.json new file mode 100644 index 0000000..3ce29ac --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "Không tìm thấy bản ghi nào cho thời điểm này", + "noPreviewFound": "Không tìm thấy bản xem trước", + "noPreviewFoundFor": "Không tìm thấy bản xem trước cho {{cameraName}}", + "stats": { + "droppedFrameRate": "Tỷ lệ khung hình bị rớt:", + "droppedFrames": { + "short": { + "title": "Bị rớt", + "value": "{{droppedFrames}} khung hình" + }, + "title": "Khung hình bị rớt:" + }, + "decodedFrames": "Khung hình đã giải mã:", + "latency": { + "short": { + "value": "{{seconds}} giây", + "title": "Độ trễ" + }, + "title": "Độ trễ:", + "value": "{{seconds}} giây" + }, + "totalFrames": "Tổng số khung hình:", + "streamType": { + "title": "Loại luồng:", + "short": "Loại" + }, + "bandwidth": { + "title": "Băng thông:", + "short": "Băng thông" + } + }, + "toast": { + "success": { + "submittedFrigatePlus": "Đã gửi thành công khung hình tới Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "Không gửi được khung hình tới Frigate+" + } + }, + "submitFrigatePlus": { + "title": "Gửi khung hình này tới Frigate+?", + "submit": "Gửi" + }, + "livePlayerRequiredIOSVersion": "Yêu cầu iOS 17.1 trở lên cho loại luồng trực tiếp này.", + "streamOffline": { + "title": "Luồng ngoại tuyến", + "desc": "Không nhận được khung hình nào trên luồng detect của {{cameraName}}, hãy kiểm tra nhật ký lỗi" + }, + "cameraDisabled": "Camera đã bị tắt" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/objects.json b/sam2-cpu/frigate-dev/web/public/locales/vi/objects.json new file mode 100644 index 0000000..d7168ee --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/objects.json @@ -0,0 +1,120 @@ +{ + "mouse": "Chuột nhắt", + "keyboard": "Bàn phím nhạc", + "blender": "Tiếng máy xay", + "sink": "Tiếng bồn rửa", + "animal": "Động vật", + "dog": "Chó", + "bark": "Sủa", + "cat": "Mèo", + "horse": "Ngựa", + "goat": "Dê", + "sheep": "Cừu", + "bird": "Chim", + "vehicle": "Tiếng phương tiện", + "boat": "Tiếng thuyền", + "car": "Tiếng xe ô tô", + "bus": "Tiếng xe buýt", + "motorcycle": "Tiếng xe máy", + "train": "Tiếng tàu hỏa", + "bicycle": "Tiếng xe đạp", + "skateboard": "Tiếng ván trượt", + "door": "Tiếng cửa", + "hair_dryer": "Tiếng máy sấy tóc", + "toothbrush": "Tiếng bàn chải", + "scissors": "Tiếng kéo cắt", + "clock": "Tiếng đồng hồ", + "person": "Người", + "airplane": "Máy bay", + "zebra": "Ngựa vằn", + "tennis_racket": "Vợt tennis", + "plate": "Đĩa", + "wine_glass": "Ly rượu vang", + "cup": "Cốc", + "fork": "Nĩa", + "knife": "Dao", + "spoon": "Thìa", + "bowl": "Bát", + "banana": "Chuối", + "apple": "Táo", + "sandwich": "Bánh mì kẹp", + "orange": "Cam", + "broccoli": "Bông cải xanh", + "carrot": "Cà rốt", + "hot_dog": "Xúc xích", + "pizza": "Pizza", + "donut": "Bánh rán", + "chair": "Ghế", + "couch": "Ghế sofa", + "potted_plant": "Cây trồng trong chậu", + "bed": "Giường", + "mirror": "Gương", + "window": "Cửa sổ", + "desk": "Bàn làm việc", + "toilet": "Nhà vệ sinh", + "tv": "Ti vi", + "microwave": "Lò vi sóng", + "oven": "Lò nướng", + "toaster": "Máy nướng bánh mì", + "refrigerator": "Tủ lạnh", + "book": "Sách", + "face": "Mặt", + "license_plate": "Biển số xe", + "package": "Gói hàng", + "bbq_grill": "Vỉ nướng BBQ", + "amazon": "Amazon", + "usps": "USPS", + "ups": "UPS", + "fedex": "FedEx", + "dhl": "DHL", + "an_post": "An Post", + "purolator": "Purolator", + "postnl": "PostNL", + "nzpost": "NZ Post", + "postnord": "PostNord", + "gls": "GLS", + "dpd": "DPD", + "traffic_light": "Đèn giao thông", + "fire_hydrant": "Trụ cứu hỏa", + "street_sign": "Biển báo đường phố", + "stop_sign": "Biển báo dừng", + "parking_meter": "Đồng hồ đỗ xe", + "bench": "Ghế dài", + "cow": "Bò", + "elephant": "Voi", + "bear": "Gấu", + "giraffe": "Hươu cao cổ", + "hat": "Mũ", + "backpack": "Ba lô", + "umbrella": "Ô", + "shoe": "Giày", + "snowboard": "Ván trượt tuyết", + "eye_glasses": "Kính mắt", + "handbag": "Túi xách", + "tie": "Cà vạt", + "suitcase": "Va li", + "frisbee": "Đĩa ném", + "skis": "Ván trượt tuyết", + "sports_ball": "Bóng thể thao", + "kite": "Diều", + "baseball_bat": "Gậy bóng chày", + "baseball_glove": "Găng tay bóng chày", + "surfboard": "Ván lướt sóng", + "bottle": "Chai", + "cake": "Bánh ngọt", + "dining_table": "Bàn ăn", + "laptop": "Máy tính xách tay", + "remote": "Điều khiển từ xa", + "cell_phone": "Điện thoại di động", + "vase": "Bình hoa", + "teddy_bear": "Gấu bông", + "hair_brush": "Lược chải tóc", + "squirrel": "Sóc", + "deer": "Hươu", + "fox": "Cáo", + "rabbit": "Thỏ", + "raccoon": "Gấu mèo", + "robot_lawnmower": "Máy cắt cỏ robot", + "waste_bin": "Thùng rác", + "on_demand": "Theo yêu cầu" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/classificationModel.json new file mode 100644 index 0000000..5db2c59 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/classificationModel.json @@ -0,0 +1,59 @@ +{ + "documentTitle": "شمار بندی کے ماڈل", + "button": { + "deleteClassificationAttempts": "Xóa Hình Ảnh Phân Loại", + "renameCategory": "Đổi Tên Lớp", + "deleteCategory": "Xoá Lớp", + "deleteImages": "Xoá Hình Ảnh", + "trainModel": "Huấn Luyện Mô Hình", + "addClassification": "Thêm Phân Loại", + "deleteModels": "Xoá Mô Hình", + "editModel": "Chỉnh sửa mô hình" + }, + "toast": { + "success": { + "deletedCategory": "Lớp Đã Bị Xoá", + "deletedImage": "Hình ảnh đã bị xóa", + "deletedModel_other": "Đã xóa thành công {{count}} mô hình", + "categorizedImage": "Phân Loại Hình Ảnh Thành Công", + "trainedModel": "Đã huấn luyện mô hình thành công.", + "trainingModel": "Đã bắt đầu huấn luyện mô hình thành công.", + "updatedModel": "Đã cập nhật cấu hình mô hình thành công", + "renamedCategory": "Đã đổi tên lớp thành công thành {{name}}" + }, + "error": { + "deleteImageFailed": "Xóa không thành công: {{errorMessage}}", + "deleteCategoryFailed": "Xóa lớp không thành công: {{errorMessage}}", + "deleteModelFailed": "Xóa mô hình không thành công: {{errorMessage}}", + "categorizeFailed": "Phân loại hình ảnh không thành công: {{errorMessage}}", + "trainingFailed": "Huấn luyện mô hình thất bại. Vui lòng kiểm tra nhật ký của Frigate để biết chi tiết.", + "trainingFailedToStart": "Khởi động huấn luyện mô hình không thành công: {{errorMessage}}", + "updateModelFailed": "Cập nhật mô hình không thành công: {{errorMessage}}", + "renameCategoryFailed": "Không đổi tên được lớp: {{errorMessage}}" + } + }, + "details": { + "scoreInfo": "Điểm số cho biết mức độ tự tin trung bình mà hệ thống xác định được cho tất cả các lần phát hiện đối tượng này." + }, + "tooltip": { + "trainingInProgress": "Mô hình hiện đang được huấn luyện", + "noNewImages": "Không có hình ảnh mới để đào tạo. Trước tiên, hãy phân loại nhiều hình ảnh hơn trong tập dữ liệu.", + "noChanges": "Không có thay đổi nào đối với tập dữ liệu kể từ lần đào tạo cuối cùng.", + "modelNotReady": "Mô hình chưa sẵn sàng để huấn luyện" + }, + "deleteCategory": { + "title": "Xóa lớp", + "desc": "Bạn có chắc chắn muốn xóa lớp {{name}} không? Điều này sẽ xóa vĩnh viễn tất cả các hình ảnh liên quan và yêu cầu đào tạo lại mô hình.", + "minClassesTitle": "Không thể xóa lớp", + "minClassesDesc": "Một mô hình phân loại phải có ít nhất 2 lớp. Thêm một lớp khác trước khi xóa lớp này." + }, + "deleteModel": { + "title": "Xóa mô hình phân loại", + "single": "Bạn có chắc chắn muốn xóa {{name}} không? Thao tác này sẽ xóa vĩnh viễn tất cả dữ liệu liên quan bao gồm hình ảnh và dữ liệu đào tạo. Không thể hoàn tác hành động này.", + "desc_other": "Bạn có chắc chắn muốn xóa mô hình {{count}} không? Thao tác này sẽ xóa vĩnh viễn tất cả dữ liệu liên quan bao gồm hình ảnh và dữ liệu đào tạo. Không thể hoàn tác hành động này." + }, + "edit": { + "title": "Chỉnh sửa mô hình phân loại", + "descriptionState": "Chỉnh sửa các lớp cho mô hình phân loại trạng thái này. Những thay đổi sẽ yêu cầu đào tạo lại mô hình." + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/configEditor.json new file mode 100644 index 0000000..a2ffce4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "copyConfig": "Sao chép cấu hình", + "saveAndRestart": "Lưu & Khởi động lại", + "saveOnly": "Chỉ lưu", + "confirm": "Thoát mà không lưu?", + "toast": { + "error": { + "savingError": "Lỗi khi lưu cấu hình" + }, + "success": { + "copyToClipboard": "Đã sao chép cấu hình vào bộ nhớ tạm." + } + }, + "configEditor": "Trình chỉnh sửa cấu hình", + "documentTitle": "Trình chỉnh sửa - Frigate", + "safeConfigEditor": "Chỉnh sửa cấu hình (Chế độ an toàn)", + "safeModeDescription": "Frigate đang ở chế độ an toàn do lỗi kiểm tra cấu hình." +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/events.json new file mode 100644 index 0000000..94b2bc7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/events.json @@ -0,0 +1,62 @@ +{ + "camera": "Tiếng máy ảnh", + "alerts": "Cảnh báo", + "detections": "Phát hiện", + "motion": { + "only": "Chỉ chuyển động", + "label": "Chuyển động" + }, + "allCameras": "Tất cả Camera", + "detected": "Đã phát hiện", + "recordings": { + "documentTitle": "Bản ghi - Frigate" + }, + "events": { + "aria": "Chọn sự kiện", + "label": "Sự kiện", + "noFoundForTimePeriod": "Không tìm thấy sự kiện nào trong khoảng thời gian này." + }, + "timeline.aria": "Chọn dòng thời gian", + "selected_one": "{{count}} đã chọn", + "selected_other": "{{count}} đã chọn", + "empty": { + "alert": "Không có cảnh báo nào để xem xét", + "detection": "Không có phát hiện nào để xem xét", + "motion": "Không tìm thấy dữ liệu chuyển động" + }, + "timeline": "Dòng thời gian", + "documentTitle": "Xem lại - Frigate", + "calendarFilter": { + "last24Hours": "24 giờ qua" + }, + "newReviewItems": { + "label": "Xem các mục mới cần xem xét", + "button": "Các mục mới cần xem xét" + }, + "markAsReviewed": "Đánh dấu là đã xem xét", + "markTheseItemsAsReviewed": "Đánh dấu các mục này là đã xem xét", + "suspiciousActivity": "Hoạt động đáng ngờ", + "threateningActivity": "Hoạt động đe dọa", + "zoomIn": "Phóng To", + "zoomOut": "Thu nhỏ", + "detail": { + "label": "Chi tiết", + "noDataFound": "Không có dữ liệu chi tiết để xem xét", + "aria": "Chuyển đổi chế độ xem chi tiết", + "trackedObject_one": "{{count}} đối tượng", + "trackedObject_other": "{{count}} đối tượng", + "noObjectDetailData": "Không có dữ liệu chi tiết đối tượng nào khả dụng.", + "settings": "Cài đặt chế độ xem chi tiết", + "alwaysExpandActive": { + "title": "Luôn mở rộng mục đang hoạt động", + "desc": "Luôn mở rộng chi tiết đối tượng của mục đánh giá đang hoạt động khi có sẵn." + } + }, + "objectTrack": { + "trackedPoint": "Điểm theo dõi", + "clickToSeek": "Nhấn để tua đến thời điểm này" + }, + "normalActivity": "Bình thường", + "needsReview": "Cần xem xét", + "securityConcern": "Mối lo ngại về an ninh" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/explore.json new file mode 100644 index 0000000..7110009 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/explore.json @@ -0,0 +1,291 @@ +{ + "objectLifecycle": { + "lifecycleItemDesc": { + "header": { + "zones": "Vùng", + "ratio": "Tỷ lệ", + "area": "Diện tích" + }, + "gone": "{{label}} đã rời đi", + "heard": "{{label}} đã nghe thấy", + "external": "{{label}} được phát hiện", + "visible": "{{label}} được phát hiện", + "entered_zone": "{{label}} đã đi vào {{zones}}", + "active": "{{label}} trở nên hoạt động", + "stationary": "{{label}} trở nên đứng yên", + "attribute": { + "faceOrLicense_plate": "Đã phát hiện {{attribute}} cho {{label}}", + "other": "{{label}} được nhận dạng là {{attribute}}" + } + }, + "carousel": { + "previous": "Trang trước", + "next": "Trang tiếp theo" + }, + "annotationSettings": { + "showAllZones": { + "desc": "Luôn hiển thị các vùng trên các khung hình mà đối tượng đã đi vào.", + "title": "Hiển thị tất cả các Vùng" + }, + "offset": { + "millisecondsToOffset": "Số mili giây để lệch các chú thích phát hiện. Mặc định: 0", + "documentation": "Đọc tài liệu ", + "desc": "Dữ liệu này đến từ luồng phát hiện của camera nằm ở trên hình ảnh từ luồng ghi hình. Có khả năng hai luồng này không đồng bộ hoàn hảo. Do đó, hộp giới hạn và đoạn ghi hình sẽ không khớp nhau một cách hoàn hảo. Tuy nhiên, trường annotation_offset có thể được sử dụng để điều chỉnh điều này.", + "label": "Độ lệch Chú thích", + "tips": "MẸO: Hãy tưởng tượng có một clip sự kiện với một người đi từ trái sang phải. Nếu hộp giới hạn trên dòng thời gian sự kiện luôn ở bên trái của người đó thì giá trị nên được giảm xuống. Tương tự, nếu một người đi từ trái sang phải và hộp giới hạn luôn ở phía trước người đó thì giá trị nên được tăng lên.", + "toast": { + "success": "Độ lệch chú thích cho {{camera}} đã được lưu vào tệp cấu hình. Khởi động lại Frigate để áp dụng các thay đổi của bạn." + } + }, + "title": "Cài đặt Chú thích" + }, + "count": "{{first}} trên {{second}}", + "title": "Vòng đời Đối tượng", + "noImageFound": "Không tìm thấy hình ảnh cho dấu thời gian này.", + "createObjectMask": "Tạo Mặt nạ Đối tượng", + "adjustAnnotationSettings": "Điều chỉnh cài đặt chú thích", + "trackedPoint": "Điểm được theo dõi", + "scrollViewTips": "Cuộn để xem những khoảnh khắc quan trọng trong vòng đời của đối tượng này.", + "autoTrackingTips": "Vị trí hộp giới hạn sẽ không chính xác đối với camera quay tự động theo dõi." + }, + "details": { + "item": { + "title": "Chi tiết Mục Xem lại", + "desc": "Chi tiết mục xem lại", + "button": { + "share": "Chia sẻ mục xem lại này", + "viewInExplore": "Xem trong Khám phá" + }, + "toast": { + "error": { + "updatedSublabelFailed": "Không thể cập nhật nhãn phụ: {{errorMessage}}", + "updatedLPRFailed": "Không thể cập nhật biển số xe: {{errorMessage}}", + "regenerate": "Không thể gọi {{provider}} để lấy mô tả mới: {{errorMessage}}", + "audioTranscription": "Không thể yêu cầu phiên âm: {{errorMessage}}" + }, + "success": { + "regenerate": "Một mô tả mới đã được yêu cầu từ {{provider}}. Tùy thuộc vào tốc độ của nhà cung cấp của bạn, mô tả mới có thể mất một chút thời gian để tạo lại.", + "updatedLPR": "Cập nhật biển số xe thành công.", + "updatedSublabel": "Cập nhật nhãn phụ thành công.", + "audioTranscription": "Đã yêu cầu chuyển đổi âm thanh thành văn bản thành công. Tùy vào tốc độ của máy chủ Frigate, quá trình chuyển đổi có thể mất một khoảng thời gian để hoàn tất." + } + }, + "tips": { + "mismatch_other": "{{count}} đối tượng không khả dụng đã được phát hiện và bao gồm trong mục xem lại này. Những đối tượng đó hoặc không đủ điều kiện là một cảnh báo hoặc phát hiện hoặc đã được dọn dẹp/xóa.", + "hasMissingObjects": "Điều chỉnh cấu hình của bạn nếu bạn muốn Frigate lưu các đối tượng được theo dõi cho các nhãn sau: {{objects}}" + } + }, + "expandRegenerationMenu": "Mở rộng menu tạo lại", + "label": "Nhãn", + "editSubLabel": { + "title": "Chỉnh sửa nhãn phụ", + "desc": "Nhập một nhãn phụ mới cho {{label}} này", + "descNoLabel": "Nhập một nhãn phụ mới cho đối tượng được theo dõi này" + }, + "snapshotScore": { + "label": "Điểm Ảnh chụp nhanh" + }, + "topScore": { + "info": "Điểm cao nhất là điểm trung vị cao nhất cho đối tượng được theo dõi, vì vậy điểm này có thể khác với điểm được hiển thị trên ảnh thu nhỏ của kết quả tìm kiếm.", + "label": "Điểm Cao nhất" + }, + "estimatedSpeed": "Tốc độ ước tính", + "objects": "Đối tượng", + "timestamp": "Dấu thời gian", + "button": { + "findSimilar": "Tìm đối tượng tương tự", + "regenerate": { + "title": "Tạo lại", + "label": "Tạo lại mô tả đối tượng được theo dõi" + } + }, + "description": { + "label": "Mô tả", + "placeholder": "Mô tả của đối tượng được theo dõi", + "aiTips": "Frigate sẽ không yêu cầu mô tả từ nhà cung cấp AI tạo sinh của bạn cho đến khi vòng đời của đối tượng được theo dõi kết thúc." + }, + "tips": { + "descriptionSaved": "Lưu mô tả thành công", + "saveDescriptionFailed": "Không thể cập nhật mô tả: {{errorMessage}}" + }, + "camera": "Camera", + "recognizedLicensePlate": "Biển số xe được nhận dạng", + "regenerateFromSnapshot": "Tạo lại từ Ảnh chụp nhanh", + "regenerateFromThumbnails": "Tạo lại từ Ảnh thu nhỏ", + "zones": "Vùng", + "editLPR": { + "title": "Chỉnh sửa biển số xe", + "desc": "Nhập một giá trị biển số xe mới cho {{label}} này", + "descNoLabel": "Nhập một giá trị biển số xe mới cho đối tượng được theo dõi này" + }, + "score": { + "label": "Điểm tin cậy" + } + }, + "itemMenu": { + "viewObjectLifecycle": { + "label": "Xem vòng đời đối tượng", + "aria": "Hiển thị vòng đời đối tượng" + }, + "downloadSnapshot": { + "label": "Tải xuống ảnh chụp nhanh", + "aria": "Tải xuống ảnh chụp nhanh" + }, + "downloadVideo": { + "aria": "Tải xuống video", + "label": "Tải xuống video" + }, + "findSimilar": { + "label": "Tìm đối tượng tương tự", + "aria": "Tìm các đối tượng được theo dõi tương tự" + }, + "submitToPlus": { + "aria": "Gửi đến Frigate Plus", + "label": "Gửi đến Frigate+" + }, + "viewInHistory": { + "label": "Xem trong Lịch sử", + "aria": "Xem trong Lịch sử" + }, + "deleteTrackedObject": { + "label": "Xóa đối tượng được theo dõi này" + }, + "addTrigger": { + "label": "Thêm sự kiện kích hoạt", + "aria": "Thêm trình kích hoạt cho đối tượng được theo dõi này" + }, + "audioTranscription": { + "label": "Phiên âm", + "aria": "Yêu cầu phiên âm" + }, + "downloadCleanSnapshot": { + "label": "Tải xuống ảnh chụp nhanh", + "aria": "Tải xuống ảnh chụp nhanh" + }, + "viewTrackingDetails": { + "label": "Xem chi tiết theo dõi", + "aria": "Xem chi tiết theo dõi" + }, + "showObjectDetails": { + "label": "Hiển thị đường dẫn đối tượng" + }, + "hideObjectDetails": { + "label": "Ẩn đường dẫn đối tượng" + } + }, + "exploreIsUnavailable": { + "embeddingsReindexing": { + "step": { + "descriptionsEmbedded": "Mô tả đã được nhúng: ", + "trackedObjectsProcessed": "Đối tượng được theo dõi đã xử lý: ", + "thumbnailsEmbedded": "Ảnh thu nhỏ đã được nhúng: " + }, + "finishingShortly": "Sẽ hoàn thành trong giây lát", + "context": "Tính năng Khám phá có thể được sử dụng sau khi quá trình tái lập chỉ mục dữ liệu nhúng (embeddings) của đối tượng được theo dõi hoàn tất.", + "startingUp": "Đang khởi động…", + "estimatedTime": "Thời gian ước tính còn lại:" + }, + "downloadingModels": { + "context": "Frigate đang tải xuống các mô hình dữ liệu nhúng cần thiết để hỗ trợ tính năng Tìm kiếm theo Ngữ nghĩa. Quá trình này có thể mất vài phút tùy thuộc vào tốc độ kết nối mạng của bạn.", + "setup": { + "visionModel": "Mô hình thị giác", + "visionModelFeatureExtractor": "Trình trích xuất đặc trưng mô hình thị giác", + "textModel": "Mô hình văn bản", + "textTokenizer": "Trình mã hóa văn bản" + }, + "tips": { + "context": "Bạn có thể muốn tái lập chỉ mục dữ liệu nhúng của các đối tượng được theo dõi sau khi các mô hình được tải xuống.", + "documentation": "Đọc tài liệu" + }, + "error": "Đã xảy ra lỗi. Vui lòng kiểm tra nhật ký của Frigate." + }, + "title": "Tính năng Khám phá không khả dụng" + }, + "dialog": { + "confirmDelete": { + "desc": "Việc xóa đối tượng được theo dõi này sẽ xóa ảnh chụp nhanh, mọi phần nhúng đã lưu và mọi mục nhập chi tiết theo dõi được liên kết. Đoạn phim đã ghi của đối tượng được theo dõi này trong chế độ xem Lịch sử sẽ KHÔNG bị xóa.

    Bạn có chắc chắn muốn tiếp tục không?", + "title": "Xác nhận Xóa" + } + }, + "noTrackedObjects": "Không tìm thấy Đối tượng được theo dõi nào", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "Đã xóa đối tượng được theo dõi thành công.", + "error": "Không thể xóa đối tượng được theo dõi: {{errorMessage}}" + } + }, + "tooltip": "Khớp {{type}} ở mức {{confidence}}%", + "previousTrackedObject": "Đối tượng được theo dõi trước đó", + "nextTrackedObject": "Đối tượng được theo dõi tiếp theo" + }, + "exploreMore": "Khám phá thêm các đối tượng {{label}}", + "trackedObjectDetails": "Chi tiết Đối tượng được theo dõi", + "type": { + "details": "chi tiết", + "snapshot": "ảnh chụp nhanh", + "video": "video", + "object_lifecycle": "vòng đời đối tượng", + "thumbnail": "Ảnh thu nhỏ", + "tracking_details": "chi tiết theo dõi" + }, + "fetchingTrackedObjectsFailed": "Lỗi khi tìm nạp các đối tượng được theo dõi: {{errorMessage}}", + "documentTitle": "Khám phá - Frigate", + "generativeAI": "AI Tạo sinh", + "trackedObjectsCount_other": "{{count}} đối tượng được theo dõi ", + "aiAnalysis": { + "title": "Phân tích bằng AI" + }, + "concerns": { + "label": "Mối lo ngại" + }, + "trackingDetails": { + "title": "Chi tiết theo dõi", + "noImageFound": "Không tìm thấy hình ảnh cho mốc thời gian này.", + "createObjectMask": "Tạo mặt nạ đối tượng", + "adjustAnnotationSettings": "Điều chỉnh cài đặt chú thích", + "scrollViewTips": "Nhấn để xem những khoảnh khắc quan trọng trong vòng đời của đối tượng này.", + "autoTrackingTips": "Vị trí khung bao sẽ không chính xác đối với các camera tự động theo dõi (autotracking).", + "count": "{{first}} của {{second}}", + "trackedPoint": "Điểm theo dõi", + "lifecycleItemDesc": { + "visible": "Đã phát hiện được {{label}}", + "entered_zone": "{{label}} đã vào {{zones}}", + "active": "{{label}} đã hoạt động", + "stationary": "{{label}} đã đứng yên", + "attribute": { + "faceOrLicense_plate": "Đã phát hiện {{attribute}} đối với {{label}}", + "other": "{{label}} được nhận diện là {{attribute}}" + }, + "gone": "{{label}} đã rời đi", + "heard": "Đã nghe thấy {{label}}", + "external": "{{label}} đã được nhận diện", + "header": { + "zones": "Vùng", + "ratio": "Tỷ lệ", + "area": "Khu vực", + "score": "Điểm" + } + }, + "annotationSettings": { + "title": "Cài đặt chú thích", + "showAllZones": { + "title": "Hiện tất cả các vùng", + "desc": "Luôn hiển thị các vùng trên khung hình khi có đối tượng đi vào vùng đó." + }, + "offset": { + "label": "Độ lệch chú thích", + "desc": "Dữ liệu này lấy từ luồng phát hiện (detect feed) của camera bạn, nhưng được hiển thị chồng lên hình ảnh từ luồng ghi hình (record feed). Hai luồng này thường không đồng bộ hoàn hảo với nhau. Do đó, khung bao (bounding box) và đoạn video có thể không khớp chính xác. Bạn có thể sử dụng cài đặt này để điều chỉnh thời gian hiển thị chú thích (annotation) lùi hoặc tiến để đồng bộ tốt hơn với video đã ghi.", + "millisecondsToOffset": "Số mili giây để điều chỉnh thời gian hiển thị chú thích phát hiện. Mặc định: 0", + "tips": "Giảm giá trị nếu quá trình phát lại video ở phía trước các hộp và điểm đường dẫn, đồng thời tăng giá trị nếu quá trình phát lại video ở phía sau chúng. Giá trị này có thể âm.", + "toast": { + "success": "Độ lệch chú thích cho {{camera}} đã được lưu vào tệp cấu hình." + } + } + }, + "carousel": { + "previous": "Trang trình bày trước", + "next": "Trang trình bày tiếp theo" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/exports.json new file mode 100644 index 0000000..95b3b87 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "Tìm kiếm", + "documentTitle": "Xuất tệp - Frigate", + "noExports": "Không tìm thấy tệp xuất nào", + "deleteExport": "Xóa tệp xuất", + "deleteExport.desc": "Bạn có chắc chắn muốn xóa {{exportName}} không?", + "editExport": { + "title": "Đổi tên tệp xuất", + "desc": "Nhập tên mới cho tệp xuất này.", + "saveExport": "Lưu tệp xuất" + }, + "toast": { + "error": { + "renameExportFailed": "Đổi tên tệp xuất thất bại: {{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "Chia sẻ bản xuất", + "downloadVideo": "Tải video", + "editName": "Chỉnh sửa tên", + "deleteExport": "Xóa bản xuất" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/faceLibrary.json new file mode 100644 index 0000000..cef8b9d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/faceLibrary.json @@ -0,0 +1,96 @@ +{ + "selectItem": "Chọn mục {{item}}", + "description": { + "addFace": "Thêm một bộ sưu tập mới vào Thư viện Khuôn Mặt bằng cách tải lên hình ảnh đầu tiên của bạn.", + "invalidName": "Tên không hợp lệ. Tên chỉ được phép chứa chữ cái, số, khoảng trắng, dấu nháy đơn, dấu gạch dưới và dấu gạch ngang.", + "placeholder": "Nhập tên cho bộ sưu tập này" + }, + "details": { + "person": "Người", + "unknown": "Không xác định", + "subLabelScore": "Điểm nhãn phụ", + "scoreInfo": "Điểm nhãn phụ là điểm số có trọng số cho tất cả các độ tin cậy của khuôn mặt được nhận dạng, vì vậy điểm này có thể khác với điểm hiển thị trên ảnh chụp nhanh.", + "timestamp": "Dấu thời gian", + "face": "Chi tiết khuôn mặt", + "faceDesc": "Chi tiết của đối tượng được theo dõi đã tạo ra khuôn mặt này" + }, + "renameFace": { + "title": "Đổi tên khuôn mặt", + "desc": "Nhập tên mới cho {{name}}" + }, + "button": { + "renameFace": "Đổi tên khuôn mặt", + "deleteFaceAttempts": "Xóa khuôn mặt", + "addFace": "Thêm khuôn mặt", + "deleteFace": "Xóa khuôn mặt", + "uploadImage": "Tải lên hình ảnh", + "reprocessFace": "Xử lý lại khuôn mặt" + }, + "imageEntry": { + "dropActive": "Thả hình ảnh vào đây…", + "validation": { + "selectImage": "Vui lòng chọn một tệp hình ảnh." + }, + "dropInstructions": "Kéo và thả hình ảnh vào đây, hoặc nhấp để chọn", + "maxSize": "Kích thước tối đa: {{size}}MB" + }, + "toast": { + "success": { + "uploadedImage": "Tải lên hình ảnh thành công.", + "trainedFace": "Huấn luyện khuôn mặt thành công.", + "updatedFaceScore": "Đã cập nhật thành công điểm khuôn mặt thành {{name}} ({{score}}).", + "addFaceLibrary": "{{name}} đã được thêm thành công vào Thư viện Khuôn mặt!", + "deletedFace_other": "Đã xóa thành công {{count}} khuôn mặt.", + "deletedName_other": "{{count}} khuôn mặt đã được xóa thành công.", + "renamedFace": "Đổi tên khuôn mặt thành {{name}} thành công" + }, + "error": { + "uploadingImageFailed": "Tải lên hình ảnh thất bại: {{errorMessage}}", + "addFaceLibraryFailed": "Đặt tên khuôn mặt thất bại: {{errorMessage}}", + "renameFaceFailed": "Đổi tên khuôn mặt thất bại: {{errorMessage}}", + "deleteFaceFailed": "Xóa thất bại: {{errorMessage}}", + "deleteNameFailed": "Xóa tên thất bại: {{errorMessage}}", + "trainFailed": "Huấn luyện thất bại: {{errorMessage}}", + "updateFaceScoreFailed": "Cập nhật điểm khuôn mặt thất bại: {{errorMessage}}" + } + }, + "collections": "Bộ sưu tập", + "steps": { + "description": { + "uploadFace": "Tải lên một hình ảnh của {{name}} cho thấy khuôn mặt của họ từ góc nhìn trực diện. Hình ảnh không cần phải được cắt chỉ lấy khuôn mặt." + }, + "faceName": "Nhập tên khuôn mặt", + "uploadFace": "Tải lên hình ảnh khuôn mặt", + "nextSteps": "Các bước tiếp theo" + }, + "deleteFaceLibrary": { + "title": "Xóa tên", + "desc": "Bạn có chắc chắn muốn xóa bộ sưu tập {{name}} không? Thao tác này sẽ xóa vĩnh viễn tất cả các khuôn mặt liên quan." + }, + "deleteFaceAttempts": { + "title": "Xóa khuôn mặt", + "desc_other": "Bạn có chắc chắn muốn xóa {{count}} khuôn mặt không? Hành động này không thể hoàn tác." + }, + "readTheDocs": "Đọc tài liệu", + "trainFaceAs": "Huấn luyện khuôn mặt với tên:", + "trainFace": "Huấn luyện khuôn mặt", + "nofaces": "Không có khuôn mặt nào", + "createFaceLibrary": { + "nextSteps": "Để xây dựng một nền tảng vững chắc:
  • Sử dụng tab Nhận dạng gần đây để chọn và huấn luyện trên hình ảnh cho mỗi người được phát hiện.
  • Tập trung vào hình ảnh chụp thẳng để có kết quả tốt nhất; tránh huấn luyện các hình ảnh chụp khuôn mặt ở một góc.
  • ", + "title": "Tạo bộ sưu tập", + "desc": "Tạo một bộ sưu tập mới", + "new": "Tạo khuôn mặt mới" + }, + "train": { + "title": "Nhận dạng gần đây", + "empty": "Không có nỗ lực nhận dạng khuôn mặt nào gần đây", + "aria": "Chọn các nhận dạng gần đây" + }, + "selectFace": "Chọn khuôn mặt", + "pixels": "{{area}}px", + "documentTitle": "Thư viện Khuôn mặt - Frigate", + "uploadFaceImage": { + "title": "Tải lên hình ảnh khuôn mặt", + "desc": "Tải lên một hình ảnh để quét khuôn mặt và bao gồm cho {{pageToggle}}" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/live.json new file mode 100644 index 0000000..c238a34 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "Trực tiếp - Frigate", + "documentTitle.withCamera": "{{camera}} - Trực tiếp - Frigate", + "lowBandwidthMode": "Chế độ băng thông thấp", + "twoWayTalk": { + "enable": "Bật đàm thoại hai chiều", + "disable": "Tắt đàm thoại hai chiều" + }, + "stream": { + "lowBandwidth": { + "tips": "Chế độ xem trực tiếp đang ở chế độ băng thông thấp do bộ đệm hoặc lỗi luồng.", + "resetStream": "Đặt lại luồng" + }, + "playInBackground": { + "label": "Phát trong nền", + "tips": "\"Bật tùy chọn này để tiếp tục phát khi trình phát bị ẩn." + }, + "title": "Luồng", + "audio": { + "tips": { + "title": "Âm thanh phải được phát ra từ camera của bạn và cấu hình trong go2rtc cho luồng này.", + "documentation": "Đọc tài liệu " + }, + "available": "Âm thanh khả dụng cho luồng này", + "unavailable": "Âm thanh không khả dụng cho luồng này" + }, + "twoWayTalk": { + "tips": "hiết bị của bạn phải hỗ trợ tính năng này và WebRTC phải được cấu hình để đàm thoại hai chiều.", + "tips.documentation": "Đọc tài liệu ", + "available": "Đàm thoại hai chiều khả dụng cho luồng này", + "unavailable": "Đàm thoại hai chiều không khả dụng cho luồng này" + }, + "debug": { + "picker": "Việc chọn luồng phát không khả dụng trong chế độ gỡ lỗi. Chế độ xem gỡ lỗi luôn sử dụng luồng được gán vai trò phát hiện (detect)." + } + }, + "editLayout": { + "group": { + "label": "Chỉnh sửa nhóm Camera" + }, + "label": "Chỉnh sửa bố cục", + "exitEdit": "Thoát chế độ chỉnh sửa" + }, + "ptz": { + "zoom": { + "out": { + "label": "Thu nhỏ camera PTZ" + }, + "in": { + "label": "Phóng to camera PTZ" + } + }, + "frame": { + "center": { + "label": "Nhấp vào khung hình để căn giữa camera PTZ" + } + }, + "move": { + "clickMove": { + "label": "Nhấp vào khung hình để căn giữa camera", + "enable": "Bật nhấp để di chuyển", + "disable": "Tắt nhấp để di chuyển" + }, + "left": { + "label": "Di chuyển camera PTZ sang trái" + }, + "up": { + "label": "Di chuyển camera PTZ lên" + }, + "down": { + "label": "Di chuyển camera PTZ xuống" + }, + "right": { + "label": "Di chuyển camera PTZ sang phải" + } + }, + "presets": "Các thiết lập sẵn cho camera PTZ", + "focus": { + "in": { + "label": "Lấy nét gần (camera PTZ)" + }, + "out": { + "label": "Lấy nét xa (camera PTZ)" + } + } + }, + "manualRecording": { + "playInBackground": { + "label": "Phát trong nền", + "desc": "Bật tùy chọn này để tiếp tục phát khi trình phát bị ẩn." + }, + "end": "Kết thúc ghi hình theo yêu cầu", + "failedToStart": "Không thể bắt đầu ghi hình theo yêu cầu.", + "started": "Đã bắt đầu ghi hình theo yêu cầu.", + "ended": "Đã kết thúc ghi hình theo yêu cầu.", + "title": "Theo yêu cầu", + "tips": "Tải xuống ảnh chụp nhanh tức thì hoặc bắt đầu sự kiện thủ công dựa trên cài đặt lưu giữ bản ghi của máy ảnh này.", + "showStats": { + "label": "Hiện thống kê", + "desc": "Bật tùy chọn này để hiển thị thống kê luồng trên khung hình." + }, + "debugView": "Chế độ gỡ lỗi", + "start": "Bắt đầu ghi hình theo yêu cầu", + "recordDisabledTips": "Vì ghi hình đã bị vô hiệu hóa hoặc bị giới hạn trong cấu hình của camera này, chỉ ảnh chụp sẽ được lưu.", + "failedToEnd": "Không thể kết thúc ghi hình theo yêu cầu." + }, + "cameraAudio": { + "enable": "Bật âm thanh Camera", + "disable": "Tắt âm thanh Camera" + }, + "camera": { + "enable": "Bật camera", + "disable": "Tắt camera" + }, + "muteCameras": { + "enable": "Tắt tiếng tất cả Camera", + "disable": "Bật tiếng tất cả Camera" + }, + "detect": { + "enable": "Bật phát hiện", + "disable": "Tắt phát hiện" + }, + "recording": { + "enable": "Bật ghi hình", + "disable": "Tắt ghi hình" + }, + "snapshots": { + "enable": "Bật ảnh chụp", + "disable": "Tắt ảnh chụp" + }, + "audioDetect": { + "enable": "Bật phát hiện âm thanh", + "disable": "Tắt phát hiện âm thanh" + }, + "autotracking": { + "enable": "Bật tự động theo dõi", + "disable": "Tắt tự động theo dõi" + }, + "streamStats": { + "enable": "Hiện thống kê luồng", + "disable": "Ẩn thống kê luồng" + }, + "streamingSettings": "Cài đặt truyền phát", + "notifications": "Thông báo", + "audio": "Âm thanh", + "suspend": { + "forTime": "Tạm dừng trong: " + }, + "cameraSettings": { + "title": "Cài đặt {{camera}}", + "cameraEnabled": "Camera đã bật", + "objectDetection": "Phát hiện đối tượng", + "recording": "Ghi hình", + "snapshots": "Ảnh chụp", + "audioDetection": "Phát hiện âm thanh", + "autotracking": "Tự động theo dõi", + "transcription": "Phiên âm" + }, + "history": { + "label": "Hiện cảnh quay lịch sử" + }, + "effectiveRetainMode": { + "modes": { + "all": "Tất cả", + "motion": "Chuyển động", + "active_objects": "Đối tượng hoạt động" + }, + "notAllTips": "Cấu hình giữ lại ghi hình {{source}} của bạn được đặt là mode: {{effectiveRetainMode}}, vì vậy lần ghi hình theo yêu cầu này chỉ giữ lại các đoạn có {{effectiveRetainModeName}}." + }, + "transcription": { + "enable": "Bật phiên âm trực tiếp", + "disable": "Tắt phiên âm trực tiếp" + }, + "snapshot": { + "takeSnapshot": "Tải xuống ảnh chụp nhanh ngay lập tức", + "noVideoSource": "Không có nguồn video để chụp ảnh nhanh.", + "captureFailed": "Chụp ảnh nhanh không thành công.", + "downloadStarted": "Bắt đầu tải xuống ảnh chụp nhanh." + }, + "noCameras": { + "title": "Không có camera nào được cấu hình", + "description": "Bắt đầu bằng cách kết nối một camera với Frigate.", + "buttonText": "Thêm Camera", + "restricted": { + "title": "Không có Camera nào khả dụng", + "description": "Bạn không có quyền xem bất kỳ camera nào trong nhóm này." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/recording.json new file mode 100644 index 0000000..de52f8b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "Lọc", + "export": "Xuất", + "calendar": "Lịch", + "filters": "Bộ lọc", + "toast": { + "error": { + "noValidTimeSelected": "Thời gian chọn không hợp lệ", + "endTimeMustAfterStartTime": "Thời gian kết thúc phải sau thời gian bắt đầu" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/search.json new file mode 100644 index 0000000..d95cd17 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "Tìm kiếm", + "savedSearches": "Tìm kiếm đã lưu", + "searchFor": "Tìm kiếm {{inputValue}}", + "button": { + "clear": "Xóa tìm kiếm", + "save": "Lưu tìm kiếm", + "delete": "Xóa tìm kiếm đã lưu", + "filterInformation": "Thông tin bộ lọc", + "filterActive": "Bộ lọc đang hoạt động" + }, + "filter": { + "tips": { + "desc": { + "step2": "Chọn một giá trị từ các gợi ý hoặc tự nhập.", + "step1": "Nhập tên khóa bộ lọc theo sau là dấu hai chấm (ví dụ: \"cameras:\").", + "step3": "Sử dụng nhiều bộ lọc bằng cách thêm chúng nối tiếp nhau, cách nhau bằng dấu cách.", + "step5": "Bộ lọc phạm vi thời gian sử dụng định dạng {{exampleTime}}.", + "exampleLabel": "Ví dụ:", + "step6": "Xóa bộ lọc bằng cách nhấp vào dấu 'x' bên cạnh chúng..", + "step4": "Bộ lọc ngày (before: và after:) sử dụng định dạng {{DateFormat}}.", + "text": "Bộ lọc giúp bạn thu hẹp kết quả tìm kiếm. Dưới đây là cách sử dụng chúng trong trường nhập liệu:" + }, + "title": "Cách sử dụng bộ lọc văn bản" + }, + "header": { + "activeFilters": "Bộ lọc đang hoạt động", + "currentFilterType": "Giá trị bộ lọc", + "noFilters": "Bộ lọc" + }, + "label": { + "has_clip": "Có clip", + "min_score": "Điểm tối thiểu", + "has_snapshot": "Có ảnh chụp nhanh", + "max_score": "Điểm tối đa", + "search_type": "Loại tìm kiếm", + "after": "Sau", + "cameras": "Camera", + "labels": "Nhãn", + "zones": "Khu vực", + "sub_labels": "Nhãn phụ", + "time_range": "Phạm vi thời gian", + "before": "Trước", + "min_speed": "Tốc độ tối thiểu", + "max_speed": "Tốc độ tối đa", + "recognized_license_plate": "Biển số xe được nhận dạng" + }, + "toast": { + "error": { + "afterDatebeEarlierBefore": "Ngày 'Sau' phải trước ngày 'Trước'.", + "beforeDateBeLaterAfter": "Ngày 'Trước' phải sau ngày 'Sau'.", + "minScoreMustBeLessOrEqualMaxScore": "'Điểm tối thiểu' phải nhỏ hơn hoặc bằng 'Điểm tối đa'.", + "maxScoreMustBeGreaterOrEqualMinScore": "'Điểm tối đa' phải lớn hơn hoặc bằng 'Điểm tối thiểu.", + "minSpeedMustBeLessOrEqualMaxSpeed": "'Tốc độ tối thiểu' phải nhỏ hơn hoặc bằng 'Tốc độ tối đa'.", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "'Tốc độ tối đa' phải lớn hơn hoặc bằng 'Tốc độ tối thiểu'." + } + }, + "searchType": { + "thumbnail": "Hình thu nhỏ", + "description": "Mô tả" + } + }, + "similaritySearch": { + "title": "Tìm kiếm tương đồng", + "clear": "Xóa tìm kiếm tương đồng", + "active": "Tìm kiếm tương đồng đang hoạt động" + }, + "placeholder": { + "search": "Tìm kiếm…" + }, + "trackedObjectId": "ID đối tượng được theo dõi" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/settings.json new file mode 100644 index 0000000..69b37b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/settings.json @@ -0,0 +1,776 @@ +{ + "documentTitle": { + "default": "Cài đặt - Frigate", + "authentication": "Cài đặt Xác thực - Frigate", + "camera": "Cài đặt Camera - Frigate", + "enrichments": "Cài đặt Làm giàu Dữ liệu - Frigate", + "notifications": "Cài đặt Thông báo - Frigate", + "masksAndZones": "Trình chỉnh sửa Mặt nạ và Vùng - Frigate", + "object": "Gỡ lỗi - Frigate", + "general": "Cài đặt giao diện – Frigate", + "frigatePlus": "Cài đặt Frigate+ - Frigate", + "motionTuner": "Bộ tinh chỉnh Chuyển động - Frigate", + "cameraManagement": "Quản Lý Camera - Frigate", + "cameraReview": "Cài Đặt Xem Lại Camera - Frigate" + }, + "notification": { + "toast": { + "error": { + "registerFailed": "Không thể lưu đăng ký thông báo." + }, + "success": { + "settingSaved": "Cài đặt thông báo đã được lưu.", + "registered": "Đã đăng ký nhận thông báo thành công. Cần khởi động lại Frigate trước khi có thể gửi bất kỳ thông báo nào (kể cả thông báo thử)." + } + }, + "unsavedChanges": "Các thay đổi Thông báo chưa được lưu", + "registerDevice": "Đăng ký Thiết bị này", + "unregisterDevice": "Hủy đăng ký Thiết bị này", + "unsavedRegistrations": "Các đăng ký Thông báo chưa được lưu", + "suspended": "Thông báo bị đình chỉ {{time}}", + "cancelSuspension": "Hủy tạm dừng", + "email": { + "desc": "Một email hợp lệ là bắt buộc và sẽ được sử dụng để thông báo cho bạn nếu có bất kỳ vấn đề nào với dịch vụ đẩy.", + "placeholder": "ví dụ: example@email.com", + "title": "Email" + }, + "cameras": { + "noCameras": "Không có camera nào", + "title": "Camera", + "desc": "Chọn camera để bật thông báo." + }, + "deviceSpecific": "Cài đặt dành riêng cho thiết bị", + "sendTestNotification": "Gửi một thông báo thử", + "active": "Thông báo đang hoạt động", + "suspendTime": { + "24hours": "Tạm dừng trong 24 giờ", + "untilRestart": "Tạm dừng cho đến khi khởi động lại", + "30minutes": "Tạm dừng trong 30 phút", + "1hour": "Tạm dừng trong 1 giờ", + "suspend": "Tạm dừng", + "12hours": "Tạm dừng trong 12 giờ", + "5minutes": "Tạm dừng trong 5 phút", + "10minutes": "Tạm dừng trong 10 phút" + }, + "notificationSettings": { + "desc": "Frigate có thể gửi thông báo đẩy tự nhiên đến thiết bị của bạn khi nó đang chạy trong trình duyệt hoặc được cài đặt dưới dạng PWA.", + "title": "Cài đặt Thông báo", + "documentation": "Đọc tài liệu" + }, + "notificationUnavailable": { + "desc": "Thông báo đẩy web yêu cầu một ngữ cảnh an toàn (https://…). Đây là một hạn chế của trình duyệt. Truy cập Frigate một cách an toàn để sử dụng thông báo.", + "title": "Thông báo không khả dụng", + "documentation": "Đọc tài liệu" + }, + "globalSettings": { + "desc": "Tạm thời đình chỉ thông báo cho các camera cụ thể trên tất cả các thiết bị đã đăng ký.", + "title": "Cài đặt Chung" + }, + "title": "Thông báo" + }, + "frigatePlus": { + "title": "Cài đặt Frigate+", + "apiKey": { + "title": "Khóa API Frigate+", + "validated": "Khóa API Frigate+ đã được phát hiện và xác thực", + "notValidated": "Khóa API Frigate+ không được phát hiện hoặc chưa được xác thực", + "desc": "Khóa API Frigate+ cho phép tích hợp với dịch vụ Frigate+.", + "plusLink": "Đọc thêm về Frigate+" + }, + "snapshotConfig": { + "table": { + "camera": "Máy quay", + "cleanCopySnapshots": "Ảnh chụp nhanh clean_copy", + "snapshots": "Ảnh chụp nhanh" + }, + "desc": "Việc gửi đến Frigate+ yêu cầu cả ảnh chụp nhanh và ảnh chụp nhanh clean_copy phải được bật trong cấu hình của bạn.", + "cleanCopyWarning": "Một số camera đã bật ảnh chụp nhanh nhưng đã tắt bản sao sạch. Bạn cần bật clean_copy trong cấu hình ảnh chụp nhanh của mình để có thể gửi hình ảnh từ các camera này đến Frigate+.", + "title": "Cấu hình Ảnh chụp nhanh", + "documentation": "Đọc tài liệu" + }, + "modelInfo": { + "error": "Không thể tải thông tin mô hình", + "plusModelType": { + "userModel": "Đã tinh chỉnh", + "baseModel": "Mô hình Cơ sở" + }, + "supportedDetectors": "Các bộ phát hiện được hỗ trợ", + "title": "Thông tin Mô hình", + "baseModel": "Mô hình Cơ sở", + "availableModels": "Các mô hình có sẵn", + "loadingAvailableModels": "Đang tải các mô hình có sẵn…", + "modelSelect": "Các mô hình có sẵn của bạn trên Frigate+ có thể được chọn ở đây. Lưu ý rằng chỉ những mô hình tương thích với cấu hình bộ phát hiện hiện tại của bạn mới có thể được chọn.", + "cameras": "Camera", + "loading": "Đang tải thông tin mô hình…", + "modelType": "Loại Mô hình", + "trainDate": "Ngày Huấn luyện" + }, + "unsavedChanges": "Các thay đổi cài đặt Frigate+ chưa được lưu", + "toast": { + "success": "Cài đặt Frigate+ đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi.", + "error": "Không thể lưu các thay đổi cấu hình: {{errorMessage}}" + }, + "restart_required": "Yêu cầu khởi động lại (mô hình Frigate+ đã thay đổi)" + }, + "camera": { + "title": "Cài đặt Camera", + "review": { + "alerts": "Cảnh báo ", + "detections": "Phát hiện ", + "title": "Xem lại", + "desc": "Tạm thời bật/tắt cảnh báo và phát hiện cho camera này cho đến khi Frigate khởi động lại. Khi bị vô hiệu hóa, sẽ không có mục xem lại mới nào được tạo ra. " + }, + "reviewClassification": { + "title": "Phân loại mục Xem lại", + "unsavedChanges": "Các thay đổi cài đặt Phân loại mục Xem lại chưa được lưu cho {{camera}}", + "readTheDocumentation": "Đọc tài liệu", + "objectDetectionsTips": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào.", + "desc": "Frigate phân loại các mục xem lại thành Cảnh báo và Phát hiện. Theo mặc định, tất cả các đối tượng ngườiô tô được coi là Cảnh báo. Bạn có thể tinh chỉnh việc phân loại các mục xem lại của mình bằng cách định cấu hình các vùng bắt buộc cho chúng.", + "zoneObjectDetectionsTips": { + "text": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trong vùng {{zone}} trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện.", + "notSelectDetections": "Tất cả các đối tượng {{detectionsLabels}} được phát hiện trong vùng {{zone}} trên {{cameraName}} không được phân loại là Cảnh báo sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào.", + "regardlessOfZoneObjectDetectionsTips": "Tất cả các đối tượng {{detectionsLabels}} không được phân loại trên {{cameraName}} sẽ được hiển thị dưới dạng Phát hiện bất kể chúng ở trong vùng nào." + }, + "toast": { + "success": "Cấu hình Phân loại mục Xem lại đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi." + }, + "limitDetections": "Giới hạn phát hiện trong các vùng cụ thể", + "selectDetectionsZones": "Chọn vùng cho Phát hiện", + "zoneObjectAlertsTips": "Tất cả các đối tượng {{alertsLabels}} được phát hiện trong vùng {{zone}} trên {{cameraName}} sẽ được hiển thị dưới dạng Cảnh báo.", + "noDefinedZones": "Không có vùng nào được xác định cho camera này.", + "objectAlertsTips": "Tất cả các đối tượng {{alertsLabels}} trên {{cameraName}} sẽ được hiển thị dưới dạng Cảnh báo.", + "selectAlertsZones": "Chọn vùng cho Cảnh báo" + }, + "streams": { + "title": "Luồng phát", + "desc": "Tạm thời vô hiệu hóa một camera cho đến khi Frigate khởi động lại. Vô hiệu hóa một camera sẽ dừng hoàn toàn quá trình xử lý các luồng của camera này của Frigate. Việc phát hiện, ghi hình và gỡ lỗi sẽ không khả dụng.
    Lưu ý: Điều này không vô hiệu hóa các luồng phát lại của go2rtc." + }, + "object_descriptions": { + "title": "Mô tả đối tượng bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả đối tượng bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các đối tượng được theo dõi trên camera này." + }, + "review_descriptions": { + "title": "Mô tả đánh giá bằng AI tạo sinh", + "desc": "Tạm thời bật/tắt mô tả xem lại bằng AI tạo sinh cho camera này. Khi tắt, mô tả do AI tạo sinh sẽ không được yêu cầu cho các mục xem lại trên camera này." + }, + "addCamera": "Thêm Camera mới", + "editCamera": "Chỉnh sửa Camera:", + "selectCamera": "Chọn Camera", + "backToSettings": "Quay lại cài đặt Camera", + "cameraConfig": { + "add": "Thêm Camera", + "edit": "Chỉnh sửa Camera", + "description": "Cấu hình Camera, bao gồm luồng đầu vào và vai trò.", + "name": "Tên Camera", + "nameRequired": "Yêu cầu nhập tên Camera", + "nameInvalid": "Tên Camera chỉ được chứa chữ cái, số, dấu gạch dưới hoặc dấu gạch ngang", + "namePlaceholder": "Ví dụ: front_door", + "enabled": "Bật", + "ffmpeg": { + "inputs": "Luồng đầu vào", + "path": "Đường dẫn luồng", + "pathRequired": "Yêu cầu nhập đường dẫn luồng", + "pathPlaceholder": "rtsp://...", + "roles": "Vai trò", + "rolesRequired": "Cần ít nhất một vai trò", + "rolesUnique": "Mỗi vai trò (âm thanh, phát hiện, ghi hình) chỉ có thể được gán cho một luồng duy nhất", + "addInput": "Thêm luồng đầu vào", + "removeInput": "Xóa luồng đầu vào", + "inputsRequired": "Cần ít nhất một luồng đầu vào" + }, + "toast": { + "success": "Camera {{cameraName}} đã được lưu thành công" + }, + "nameLength": "Tên của camera phải dưới 24 ký tự." + } + }, + "masksAndZones": { + "form": { + "zoneName": { + "error": { + "mustNotBeSameWithCamera": "Tên vùng không được trùng với tên camera.", + "mustBeAtLeastTwoCharacters": "Tên vùng phải có ít nhất 2 ký tự.", + "mustNotContainPeriod": "Tên vùng không được chứa dấu chấm.", + "alreadyExists": "Một vùng với tên này đã tồn tại cho camera này.", + "hasIllegalCharacter": "Tên vùng chứa các ký tự không hợp lệ." + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "Quán tính phải lớn hơn 0." + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "Thời gian lảng vảng phải lớn hơn hoặc bằng 0." + } + }, + "polygonDrawing": { + "delete": { + "desc": "Bạn có chắc muốn xóa {{type}} {{name}} không?", + "title": "Xác nhận Xóa", + "success": "{{name}} đã được xóa." + }, + "removeLastPoint": "Xóa điểm cuối", + "snapPoints": { + "true": "Bắt dính điểm", + "false": "Không bắt dính điểm" + }, + "reset": { + "label": "Xóa tất cả các điểm" + }, + "error": { + "mustBeFinished": "Việc vẽ đa giác phải được hoàn thành trước khi lưu." + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "Ngưỡng tốc độ phải lớn hơn hoặc bằng 0.1." + } + }, + "distance": { + "error": { + "text": "Khoảng cách phải lớn hơn hoặc bằng 0.1.", + "mustBeFilled": "Tất cả các trường khoảng cách phải được điền để sử dụng ước tính tốc độ." + } + } + }, + "objectMasks": { + "add": "Thêm Mặt nạ đối tượng", + "edit": "Chỉnh sửa Mặt nạ đối tượng", + "context": "Mặt nạ lọc đối tượng được sử dụng để lọc ra các kết quả dương tính giả cho một loại đối tượng nhất định dựa trên vị trí.", + "objects": { + "allObjectTypes": "Tất cả các loại đối tượng", + "title": "Đối tượng", + "desc": "Loại đối tượng áp dụng cho mặt nạ đối tượng này." + }, + "documentTitle": "Chỉnh sửa Mặt nạ đối tượng - Frigate", + "desc": { + "title": "Mặt nạ lọc đối tượng được sử dụng để lọc ra các kết quả dương tính giả cho một loại đối tượng nhất định dựa trên vị trí.", + "documentation": "Tài liệu" + }, + "point_other": "{{count}} điểm", + "toast": { + "success": { + "noName": "Mặt nạ đối tượng đã được lưu.", + "title": "{{polygonName}} đã được lưu." + } + }, + "label": "Mặt nạ đối tượng", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh." + }, + "zones": { + "speedEstimation": { + "docs": "Đọc tài liệu", + "desc": "Bật ước tính tốc độ cho các đối tượng trong vùng này. Vùng phải có chính xác 4 điểm.", + "title": "Ước tính Tốc độ", + "lineADistance": "Khoảng cách đường A ({{unit}})", + "lineBDistance": "Khoảng cách đường B ({{unit}})", + "lineCDistance": "Khoảng cách đường C ({{unit}})", + "lineDDistance": "Khoảng cách đường D ({{unit}})" + }, + "desc": { + "documentation": "Tài liệu", + "title": "Các vùng cho phép bạn xác định một khu vực cụ thể của khung hình để bạn có thể xác định xem một đối tượng có ở trong một khu vực cụ thể hay không." + }, + "add": "Thêm Vùng", + "allObjects": "Tất cả Đối tượng", + "speedThreshold": { + "toast": { + "error": { + "loiteringTimeError": "Các vùng có thời gian lảng vảng lớn hơn 0 không nên được sử dụng với ước tính tốc độ.", + "pointLengthError": "Ước tính tốc độ đã bị vô hiệu hóa cho vùng này. Các vùng có ước tính tốc độ phải có chính xác 4 điểm." + } + }, + "desc": "Chỉ định tốc độ tối thiểu để các đối tượng được coi là ở trong vùng này.", + "title": "Ngưỡng Tốc độ ({{unit}})" + }, + "loiteringTime": { + "title": "Thời gian lảng vảng", + "desc": "Đặt một khoảng thời gian tối thiểu tính bằng giây mà đối tượng phải ở trong vùng để kích hoạt nó. Mặc định: 0" + }, + "point_other": "{{count}} điểm", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh.", + "objects": { + "title": "Đối tượng", + "desc": "Danh sách các đối tượng áp dụng cho vùng này." + }, + "toast": { + "success": "Vùng ({{zoneName}}) đã được lưu." + }, + "name": { + "inputPlaceHolder": "Nhập tên…", + "tips": "Tên phải có ít nhất 2 ký tự, phải có ít nhất một chữ cái và không được là tên của camera hoặc vùng khác trên camera này.", + "title": "Tên" + }, + "edit": "Chỉnh sửa Vùng", + "label": "Vùng", + "documentTitle": "Chỉnh sửa Vùng - Frigate", + "inertia": { + "title": "Quán tính", + "desc": "Chỉ định số khung hình mà một đối tượng phải ở trong một vùng trước khi được coi là ở trong vùng. Mặc định: 3" + } + }, + "motionMasks": { + "documentTitle": "Chỉnh sửa Mặt nạ chuyển động - Frigate", + "desc": { + "documentation": "Tài liệu", + "title": "Mặt nạ chuyển động được sử dụng để ngăn các loại chuyển động không mong muốn kích hoạt phát hiện. Việc che quá nhiều sẽ khiến việc theo dõi đối tượng trở nên khó khăn hơn." + }, + "point_other": "{{count}} điểm", + "polygonAreaTooLarge": { + "documentation": "Đọc tài liệu", + "tips": "Mặt nạ chuyển động không ngăn chặn việc phát hiện đối tượng. Bạn nên sử dụng một vùng bắt buộc thay thế.", + "title": "Mặt nạ chuyển động đang che {{polygonArea}}% khung hình của camera. Không khuyến khích sử dụng mặt nạ chuyển động lớn." + }, + "add": "Mặt nạ chuyển động mới", + "edit": "Chỉnh sửa Mặt nạ chuyển động", + "context": { + "documentation": "Đọc tài liệu", + "title": "Mặt nạ chuyển động được sử dụng để ngăn chặn các loại chuyển động không mong muốn kích hoạt việc phát hiện (ví dụ: cành cây, dấu thời gian của camera). Mặt nạ chuyển động nên được sử dụng rất hạn chế, việc che quá nhiều sẽ làm cho việc theo dõi đối tượng trở nên khó khăn hơn." + }, + "label": "Mặt nạ chuyển động", + "clickDrawPolygon": "Nhấp để vẽ một đa giác trên hình ảnh.", + "toast": { + "success": { + "title": "{{polygonName}} đã được lưu.", + "noName": "Mặt nạ chuyển động đã được lưu." + } + } + }, + "toast": { + "success": { + "copyCoordinates": "Đã sao chép tọa độ của {{polyName}} vào clipboard." + }, + "error": { + "copyCoordinatesFailed": "Không thể sao chép tọa độ vào clipboard." + } + }, + "restart_required": "Yêu cầu khởi động lại (mặt nạ/vùng đã thay đổi)", + "motionMaskLabel": "Mặt nạ chuyển động {{number}}", + "objectMaskLabel": "Mặt nạ đối tượng {{number}} ({{label}})", + "filter": { + "all": "Tất cả Mặt nạ và Vùng" + } + }, + "motionDetectionTuner": { + "unsavedChanges": "Các thay đổi Tinh chỉnh Chuyển động chưa được lưu ({{camera}})", + "Threshold": { + "title": "Ngưỡng", + "desc": "Giá trị ngưỡng quy định mức độ thay đổi độ sáng của một pixel cần thiết để được coi là chuyển động. Mặc định: 30" + }, + "contourArea": { + "title": "Diện tích đường viền", + "desc": "Giá trị diện tích đường viền được sử dụng để quyết định nhóm pixel nào đã thay đổi đủ điều kiện là chuyển động. Mặc định: 10" + }, + "title": "Bộ tinh chỉnh Phát hiện Chuyển động", + "desc": { + "title": "Frigate sử dụng phát hiện chuyển động như một bước kiểm tra đầu tiên để xem có điều gì đang xảy ra trong khung hình đáng để kiểm tra bằng phát hiện đối tượng hay không.", + "documentation": "Đọc Hướng dẫn Tinh chỉnh Chuyển động" + }, + "improveContrast": { + "desc": "Cải thiện độ tương phản cho các cảnh tối hơn. Mặc định: BẬT", + "title": "Cải thiện độ tương phản" + }, + "toast": { + "success": "Cài đặt chuyển động đã được lưu." + } + }, + "debug": { + "objectShapeFilterDrawing": { + "title": "Vẽ bộ lọc hình dạng đối tượng", + "desc": "Vẽ một hình chữ nhật trên hình ảnh để xem chi tiết diện tích và tỷ lệ", + "document": "Đọc tài liệu ", + "score": "Điểm", + "tips": "Bật tùy chọn này để vẽ một hình chữ nhật trên hình ảnh của camera để hiển thị diện tích và tỷ lệ của nó. Các giá trị này sau đó có thể được sử dụng để đặt các tham số bộ lọc hình dạng đối tượng trong cấu hình của bạn.", + "ratio": "Tỷ lệ", + "area": "Diện tích" + }, + "detectorDesc": "Frigate sử dụng các bộ phát hiện của bạn ({{detectors}}) để phát hiện các đối tượng trong luồng video của camera.", + "boundingBoxes": { + "colors": { + "label": "Màu sắc Hộp giới hạn Đối tượng", + "info": "
  • Khi khởi động, các màu khác nhau sẽ được gán cho mỗi nhãn đối tượng
  • Một đường mỏng màu xanh đậm cho biết đối tượng không được phát hiện tại thời điểm hiện tại
  • Một đường mỏng màu xám cho biết đối tượng được phát hiện là đang đứng yên
  • Một đường dày cho biết đối tượng là chủ thể của việc theo dõi tự động (khi được bật)
  • " + }, + "desc": "Hiển thị các hộp giới hạn xung quanh các đối tượng được theo dõi", + "title": "Hộp giới hạn" + }, + "desc": "Chế độ xem gỡ lỗi hiển thị chế độ xem thời gian thực của các đối tượng được theo dõi và các thống kê của chúng. Danh sách đối tượng hiển thị một bản tóm tắt có độ trễ về các đối tượng được phát hiện.", + "debugging": "Gỡ lỗi", + "timestamp": { + "desc": "Chồng một dấu thời gian lên hình ảnh", + "title": "Dấu thời gian" + }, + "regions": { + "title": "Khu vực", + "tips": "

    Hộp khu vực


    Các hộp màu xanh lá cây sáng sẽ được chồng lên các khu vực quan tâm trong khung hình đang được gửi đến bộ phát hiện đối tượng.

    ", + "desc": "Hiển thị một hộp của khu vực quan tâm được gửi đến bộ phát hiện đối tượng" + }, + "zones": { + "title": "Vùng", + "desc": "Hiển thị đường viền của bất kỳ vùng nào đã được xác định" + }, + "mask": { + "title": "Mặt nạ chuyển động", + "desc": "Hiển thị các đa giác mặt nạ chuyển động" + }, + "title": "Gỡ lỗi", + "objectList": "Danh sách đối tượng", + "noObjects": "Không có đối tượng", + "motion": { + "desc": "Hiển thị các hộp xung quanh các khu vực phát hiện có chuyển động", + "tips": "

    Hộp chuyển động


    Các hộp màu đỏ sẽ được chồng lên các khu vực của khung hình nơi chuyển động đang được phát hiện

    ", + "title": "Hộp chuyển động" + }, + "paths": { + "title": "Đường dẫn", + "desc": "Hiển thị các điểm quan trọng trên đường đi của đối tượng được theo dõi", + "tips": "

    Đường đi


    Đường thẳng và vòng tròn sẽ hiển thị các điểm quan trọng mà đối tượng được theo dõi đã di chuyển trong suốt quá trình theo dõi.

    " + }, + "openCameraWebUI": "Đang mở giao diện Web của {{camera}}", + "audio": { + "title": "Âm thanh", + "noAudioDetections": "Không phát hiện âm thanh", + "score": "điểm", + "currentRMS": "RMS hiện tại", + "currentdbFS": "dbFS hiện tại" + } + }, + "users": { + "title": "Người dùng", + "management": { + "title": "Quản lý Người dùng", + "desc": "Quản lý các tài khoản người dùng của phiên bản Frigate này." + }, + "table": { + "noUsers": "Không tìm thấy người dùng nào.", + "username": "Tên người dùng", + "actions": "Hành động", + "role": "Vai trò", + "changeRole": "Thay đổi vai trò người dùng", + "password": "Mật khẩu", + "deleteUser": "Xóa người dùng" + }, + "dialog": { + "form": { + "password": { + "strength": { + "strong": "Mạnh", + "title": "Độ mạnh mật khẩu: ", + "medium": "Trung bình", + "veryStrong": "Rất mạnh", + "weak": "Yếu" + }, + "title": "Mật khẩu", + "placeholder": "Nhập mật khẩu", + "confirm": { + "title": "Xác nhận Mật khẩu", + "placeholder": "Xác nhận Mật khẩu" + }, + "notMatch": "Mật khẩu không trùng khớp", + "match": "Mật khẩu trùng khớp" + }, + "newPassword": { + "placeholder": "Nhập mật khẩu mới", + "confirm": { + "placeholder": "Nhập lại mật khẩu mới" + }, + "title": "Mật khẩu mới" + }, + "usernameIsRequired": "Tên người dùng là bắt buộc", + "user": { + "title": "Tên người dùng", + "desc": "Chỉ cho phép chữ cái, số, dấu chấm và dấu gạch dưới.", + "placeholder": "Nhập tên người dùng" + }, + "passwordIsRequired": "Mật khẩu là bắt buộc" + }, + "createUser": { + "desc": "Thêm một tài khoản người dùng mới và chỉ định một vai trò để truy cập vào các khu vực của giao diện người dùng Frigate.", + "usernameOnlyInclude": "Tên người dùng chỉ có thể bao gồm chữ cái, số, . hoặc _", + "title": "Tạo Người dùng Mới", + "confirmPassword": "Vui lòng xác nhận mật khẩu của bạn" + }, + "deleteUser": { + "desc": "Hành động này không thể được hoàn tác. Điều này sẽ xóa vĩnh viễn tài khoản người dùng và xóa tất cả dữ liệu liên quan.", + "warn": "Bạn có chắc muốn xóa {{username}} không?", + "title": "Xóa Người dùng" + }, + "passwordSetting": { + "setPassword": "Đặt Mật khẩu", + "updatePassword": "Cập nhật Mật khẩu cho {{username}}", + "cannotBeEmpty": "Mật khẩu không được để trống", + "desc": "Tạo một mật khẩu mạnh để bảo mật tài khoản này.", + "doNotMatch": "Mật khẩu không khớp" + }, + "changeRole": { + "title": "Thay đổi Vai trò Người dùng", + "roleInfo": { + "intro": "Chọn vai trò thích hợp cho người dùng này:", + "admin": "Quản trị viên", + "adminDesc": "Toàn quyền truy cập vào tất cả các tính năng.", + "viewer": "Người xem", + "viewerDesc": "Chỉ giới hạn ở các bảng điều khiển Trực tiếp, Xem lại, Khám phá và Xuất file." + }, + "select": "Chọn một vai trò", + "desc": "Cập nhật quyền cho {{username}}" + } + }, + "addUser": "Thêm Người dùng", + "updatePassword": "Cập nhật Mật khẩu", + "toast": { + "error": { + "setPasswordFailed": "Không thể lưu mật khẩu: {{errorMessage}}", + "createUserFailed": "Không thể tạo người dùng: {{errorMessage}}", + "deleteUserFailed": "Không thể xóa người dùng: {{errorMessage}}", + "roleUpdateFailed": "Không thể cập nhật vai trò: {{errorMessage}}" + }, + "success": { + "roleUpdated": "Vai trò đã được cập nhật cho {{user}}", + "createUser": "Người dùng {{user}} đã được tạo thành công", + "deleteUser": "Người dùng {{user}} đã được xóa thành công", + "updatePassword": "Mật khẩu đã được cập nhật thành công." + } + } + }, + "general": { + "calendar": { + "firstWeekday": { + "sunday": "Chủ nhật", + "monday": "Thứ hai", + "label": "Ngày đầu tuần", + "desc": "Ngày bắt đầu của các tuần trong lịch xem lại." + }, + "title": "Lịch" + }, + "storedLayouts": { + "desc": "Bố cục của các camera trong một nhóm camera có thể được kéo/thay đổi kích thước. Các vị trí được lưu trữ trong bộ nhớ cục bộ của trình duyệt của bạn.", + "title": "Bố cục đã lưu", + "clearAll": "Xóa tất cả Bố cục" + }, + "cameraGroupStreaming": { + "title": "Cài đặt Phát luồng Nhóm Camera", + "desc": "Cài đặt phát luồng cho mỗi nhóm camera được lưu trữ trong bộ nhớ cục bộ của trình duyệt của bạn.", + "clearAll": "Xóa tất cả Cài đặt Phát luồng" + }, + "toast": { + "success": { + "clearStoredLayout": "Đã xóa bố cục đã lưu cho {{cameraName}}", + "clearStreamingSettings": "Đã xóa cài đặt phát luồng cho tất cả các nhóm camera." + }, + "error": { + "clearStoredLayoutFailed": "Không thể xóa bố cục đã lưu: {{errorMessage}}", + "clearStreamingSettingsFailed": "Không thể xóa cài đặt phát luồng: {{errorMessage}}" + } + }, + "liveDashboard": { + "automaticLiveView": { + "desc": "Tự động chuyển sang chế độ xem trực tiếp của camera khi phát hiện hoạt động. Việc tắt tùy chọn này sẽ khiến hình ảnh tĩnh của camera trên bảng điều khiển Trực tiếp chỉ cập nhật mỗi phút một lần.", + "label": "Chế độ xem trực tiếp tự động" + }, + "playAlertVideos": { + "desc": "Theo mặc định, các cảnh báo gần đây trên bảng điều khiển Trực tiếp sẽ phát dưới dạng các video lặp lại nhỏ. Tắt tùy chọn này để chỉ hiển thị hình ảnh tĩnh của các cảnh báo gần đây trên thiết bị/trình duyệt này.", + "label": "Phát video cảnh báo" + }, + "title": "Bảng điều khiển trực tiếp", + "displayCameraNames": { + "label": "Luôn hiển thị tên camera", + "desc": "Luôn hiển thị tên camera trong một con chip trong bảng điều khiển xem trực tiếp nhiều camera." + }, + "liveFallbackTimeout": { + "label": "Hết thời gian chờ dự phòng của người chơi trực tiếp", + "desc": "Khi luồng trực tiếp chất lượng cao của camera không khả dụng, tự động chuyển sang chế độ băng thông thấp sau số giây này. Mặc định: 3." + } + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "label": "Tốc độ phát mặc định", + "desc": "Tốc độ phát mặc định cho việc xem lại các bản ghi." + }, + "title": "Trình xem Bản ghi" + }, + "title": "Cài đặt giao diện" + }, + "dialog": { + "unsavedChanges": { + "desc": "Bạn có muốn lưu các thay đổi trước khi tiếp tục không?", + "title": "Bạn có các thay đổi chưa được lưu." + } + }, + "enrichments": { + "title": "Cài đặt Làm giàu Dữ liệu", + "unsavedChanges": "Các thay đổi cài đặt Làm giàu Dữ liệu chưa được lưu", + "birdClassification": { + "title": "Phân loại Chim", + "desc": "Phân loại chim xác định các loài chim đã biết bằng mô hình Tensorflow lượng tử hóa. Khi một loài chim đã biết được nhận dạng, tên thông thường của nó sẽ được thêm vào dưới dạng nhãn phụ (sub_label). Thông tin này được bao gồm trong giao diện người dùng, bộ lọc, cũng như trong các thông báo." + }, + "semanticSearch": { + "title": "Tìm kiếm theo Ngữ nghĩa", + "desc": "Tìm kiếm theo Ngữ nghĩa trong Frigate cho phép bạn tìm các đối tượng được theo dõi trong các mục xem lại của mình bằng cách sử dụng chính hình ảnh, mô tả văn bản do người dùng xác định hoặc mô tả được tạo tự động.", + "reindexNow": { + "desc": "Việc tái lập chỉ mục sẽ tạo lại các nhúng (embeddings) cho tất cả các đối tượng được theo dõi. Quá trình này chạy ở chế độ nền và có thể làm CPU của bạn hoạt động tối đa và mất một khoảng thời gian đáng kể tùy thuộc vào số lượng đối tượng bạn đã theo dõi.", + "confirmTitle": "Xác nhận Tái lập chỉ mục", + "confirmDesc": "Bạn có chắc muốn tái lập chỉ mục cho tất cả các nhúng của đối tượng được theo dõi không? Quá trình này sẽ chạy ở chế độ nền nhưng có thể làm CPU của bạn hoạt động tối đa và mất một khoảng thời gian đáng kể. Bạn có thể theo dõi tiến trình trên trang Khám phá.", + "success": "Đã bắt đầu tái lập chỉ mục thành công.", + "alreadyInProgress": "Quá trình tái lập chỉ mục đang được tiến hành.", + "error": "Không thể bắt đầu tái lập chỉ mục: {{errorMessage}}", + "label": "Tái lập chỉ mục ngay", + "confirmButton": "Tái lập chỉ mục" + }, + "modelSize": { + "label": "Kích thước Mô hình", + "desc": "Kích thước của mô hình được sử dụng cho các nhúng tìm kiếm theo ngữ nghĩa.", + "small": { + "title": "nhỏ", + "desc": "Sử dụng mô hình nhỏ sẽ dùng phiên bản lượng tử hóa của mô hình, tiêu thụ ít RAM hơn và chạy nhanh hơn trên CPU với sự khác biệt không đáng kể về chất lượng nhúng." + }, + "large": { + "title": "lớn", + "desc": "Sử dụng mô hình lớn sẽ dùng mô hình Jina đầy đủ và sẽ tự động chạy trên GPU nếu có." + } + }, + "readTheDocumentation": "Đọc tài liệu" + }, + "faceRecognition": { + "title": "Nhận dạng Khuôn mặt", + "desc": "Nhận dạng khuôn mặt cho phép gán tên cho người và khi khuôn mặt của họ được nhận dạng, Frigate sẽ gán tên của người đó làm nhãn phụ. Thông tin này được bao gồm trong giao diện người dùng, bộ lọc, cũng như trong các thông báo.", + "readTheDocumentation": "Đọc tài liệu", + "modelSize": { + "label": "Kích thước Mô hình", + "desc": "Kích thước của mô hình được sử dụng để nhận dạng khuôn mặt.", + "small": { + "title": "nhỏ", + "desc": "Sử dụng mô hình nhỏ sẽ dùng mô hình nhúng khuôn mặt FaceNet, chạy hiệu quả trên hầu hết các CPU." + }, + "large": { + "title": "lớn", + "desc": "Sử dụng mô hình lớn sẽ dùng mô hình nhúng khuôn mặt ArcFace và sẽ tự động chạy trên GPU nếu có." + } + } + }, + "licensePlateRecognition": { + "title": "Nhận dạng Biển số xe", + "desc": "Frigate có thể nhận dạng biển số xe trên các phương tiện và tự động thêm các ký tự được phát hiện vào trường recognized_license_plate hoặc một tên đã biết làm nhãn phụ cho các đối tượng thuộc loại ô tô. Một trường hợp sử dụng phổ biến có thể là đọc biển số xe ô tô đi vào đường lái xe hoặc ô tô đi ngang qua trên đường phố.", + "readTheDocumentation": "Đọc tài liệu" + }, + "restart_required": "Yêu cầu khởi động lại (cài đặt Làm giàu Dữ liệu đã thay đổi)", + "toast": { + "success": "Cài đặt Làm giàu Dữ liệu đã được lưu. Khởi động lại Frigate để áp dụng các thay đổi của bạn.", + "error": "Không thể lưu các thay đổi cấu hình: {{errorMessage}}" + } + }, + "menu": { + "frigateplus": "Frigate+", + "ui": "Giao diện người dùng", + "masksAndZones": "Mặt nạ / Vùng", + "debug": "Gỡ lỗi", + "users": "Người dùng", + "notifications": "Thông báo", + "motionTuner": "Tinh chỉnh Chuyển động", + "cameras": "Cài đặt Camera", + "enrichments": "Làm giàu Dữ liệu", + "triggers": "Sự kiện kích hoạt", + "cameraManagement": "Quản lý", + "cameraReview": "Đánh giá", + "roles": "Vai trò" + }, + "cameraSetting": { + "camera": "Máy quay", + "noCamera": "Không có Camera" + }, + "triggers": { + "documentTitle": "Sự kiện kích hoạt", + "management": { + "title": "Sự kiện kích hoạt", + "desc": "Quản lý sự kiện kích hoạt cho {{camera}}. Sử dụng kiểu \"ảnh xem trước\" để kích hoạt dựa trên ảnh xem trước tương tự cho đối tượng cần theo dõi đã chọn, và kiểu \"mô tả\" để kích hoạt dựa trên những mô tả tương tự cho đoạn văn bản bạn đã chỉ định." + }, + "addTrigger": "Thêm sự kiện kích hoạt", + "table": { + "content": "Nội dung", + "threshold": "Ngưỡng", + "actions": "Hành động", + "noTriggers": "Không có sự kiện kích hoạt được cài đặt cho máy quay này.", + "type": "Kiểu", + "name": "Tên", + "deleteTrigger": "Xóa sự kiện kích hoạt", + "lastTriggered": "Lần kích hoạt gần nhất", + "edit": "Chỉnh sửa" + }, + "type": { + "description": "Mô tả", + "thumbnail": "Ảnh xem trước" + }, + "dialog": { + "form": { + "enabled": { + "description": "Kích hoạt hoặc vô hiệu hóa sự kiện kích hoạt này" + }, + "actions": { + "title": "Các hành động", + "desc": "Theo mặc định, Frigate kích hoạt thông báo MQTT cho tất cả các trình kích hoạt. Nhãn phụ thêm tên kích hoạt vào nhãn đối tượng. Thuộc tính là siêu dữ liệu có thể tìm kiếm được lưu trữ riêng biệt trong siêu dữ liệu đối tượng được theo dõi.", + "error": { + "min": "Phải chọn ít nhất một hành động." + } + }, + "name": { + "title": "Tên", + "placeholder": "Tên sự kiện kích hoạt", + "error": { + "minLength": "Trường phải dài ít nhất 2 ký tự.", + "invalidCharacters": "Trường chỉ có thể chứa các chữ cái, số, dấu gạch dưới và dấu gạch nối.", + "alreadyExists": "Một sự kiện kích hoạt trùng tên đã tồn tại cho máy quay này." + } + }, + "type": { + "title": "Kiểu", + "placeholder": "Chọn kiểu cho sự kiện kích hoạt" + }, + "content": { + "title": "Nội dung", + "imagePlaceholder": "Chọn một hình ảnh", + "textPlaceholder": "Nhập nội dung văn bản", + "imageDesc": "Chỉ 100 hình thu nhỏ gần đây nhất được hiển thị. Nếu bạn không thể tìm thấy hình thu nhỏ mong muốn, vui lòng xem lại các đối tượng trước đó trong Khám phá và thiết lập trình kích hoạt từ menu ở đó.", + "textDesc": "Nhập vẵn bản để kích hoạt hành động này khi một đối tượng theo dõi với mô tả tương tự được phát hiện.", + "error": { + "required": "Nội dung bắt buộc." + } + }, + "threshold": { + "title": "Ngưỡng", + "error": { + "min": "Ngưỡng phải ít nhất bằng 0", + "max": "Ngưỡng lớn nhất phải bé hơn 1" + } + } + }, + "createTrigger": { + "title": "Tạo sự kiện kích hoạt", + "desc": "Tạo sự kiện kích hoạt cho máy quay {{camera}}" + }, + "editTrigger": { + "title": "Chỉnh sửa Sự kiện kích hoạt", + "desc": "Chỉnh sửa cài đặt cho sự kiện kích hoạt trên máy quay {{camera}}" + }, + "deleteTrigger": { + "title": "Xóa Sự kiện kích hoạt", + "desc": "Bạn có chắc chắn muốn xóa sự kịn kích hoạt {{triggerName}}? Thao tác này không thể khôi phục được." + } + }, + "toast": { + "success": { + "createTrigger": "Sự kiện kích hoạt {{name}} đã được tạo thành công.", + "updateTrigger": "Sự kiện kích hoạt {{name}} đã được cập nhật thành công.", + "deleteTrigger": "Sự kiện kích hoạt {{name}} đã được xóa thành công." + }, + "error": { + "createTriggerFailed": "Tạo sự kiện kích hoạt thất bại: {{errorMessage}}", + "updateTriggerFailed": "Cập nhật sự kiện kích hoạt thất bại: {{errorMessage}}", + "deleteTriggerFailed": "Xóa sự kiện kích hoạt thất bại: {{errorMessage}}" + } + }, + "actions": { + "alert": "Gắn nhãn Cảnh báo", + "notification": "Gửi thông báo" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/vi/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/vi/views/system.json new file mode 100644 index 0000000..bdaffe7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/vi/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "storage": "Thống kê lưu trữ - Frigate", + "general": "Thống kê Chung - Frigate", + "enrichments": "Thống kê Làm giàu Dữ liệu - Frigate", + "logs": { + "frigate": "Nhật ký Frigate - Frigate", + "go2rtc": "Nhật ký Go2RTC - Frigate", + "nginx": "Nhật ký Nginx - Frigate" + }, + "cameras": "Thống kê Camera - Frigate" + }, + "general": { + "hardwareInfo": { + "npuUsage": "Mức sử dụng NPU", + "npuMemory": "Bộ nhớ NPU", + "gpuInfo": { + "vainfoOutput": { + "title": "Đầu ra Vainfo", + "returnCode": "Mã trả về: {{code}}", + "processOutput": "Đầu ra Tiến trình:", + "processError": "Lỗi Tiến trình:" + }, + "nvidiaSMIOutput": { + "title": "Đầu ra Nvidia SMI", + "name": "Tên: {{name}}", + "driver": "Trình điều khiển: {{driver}}", + "cudaComputerCapability": "Khả năng Tính toán CUDA: {{cuda_compute}}", + "vbios": "Thông tin VBios: {{vbios}}" + }, + "closeInfo": { + "label": "Đóng thông tin GPU" + }, + "copyInfo": { + "label": "Sao chép thông tin GPU" + }, + "toast": { + "success": "Đã sao chép thông tin GPU vào clipboard" + } + }, + "title": "Thông tin Phần cứng", + "gpuUsage": "Mức sử dụng GPU", + "gpuMemory": "Bộ nhớ GPU", + "gpuEncoder": "Bộ mã hóa GPU", + "gpuDecoder": "Bộ giải mã GPU", + "intelGpuWarning": { + "title": "Cảnh báo thống kê GPU Intel", + "message": "Không có số liệu thống kê GPU", + "description": "Đây là lỗi đã biết trong công cụ báo cáo thống kê GPU của Intel (intel_gpu_top), khi nó bị trục trặc và liên tục trả về mức sử dụng GPU là 0%, dù thực tế phần cứng tăng tốc và nhận diện đối tượng đang hoạt động đúng trên (i)GPU. Đây không phải lỗi của Frigate. Bạn có thể khởi động lại máy chủ để tạm thời khắc phục và xác nhận GPU vẫn hoạt động bình thường. Điều này không ảnh hưởng đến hiệu suất." + } + }, + "otherProcesses": { + "processCpuUsage": "Mức sử dụng CPU của Tiến trình", + "processMemoryUsage": "Mức sử dụng Bộ nhớ của Tiến trình", + "title": "Các Tiến trình Khác" + }, + "detector": { + "temperature": "Nhiệt độ Bộ phát hiện", + "memoryUsage": "Mức sử dụng Bộ nhớ của Bộ phát hiện", + "title": "Bộ phát hiện", + "inferenceSpeed": "Tốc độ Suy luận của Bộ phát hiện", + "cpuUsage": "Mức sử dụng CPU của Bộ phát hiện", + "cpuUsageInformation": "Dùng CPU để chuẩn bị đầu vào và ngõ ra dữ liệu dùng cho mẫu nhận dạng. Giá trị này không đo lường mức sử dụng suy luận, ngay cả khi sử dụng GPU hoặc bộ tăng tốc." + }, + "title": "Chung" + }, + "storage": { + "overview": "Tổng quan", + "cameraStorage": { + "title": "Lưu trữ Camera", + "camera": "Camera", + "unusedStorageInformation": "Thông tin Lưu trữ Chưa sử dụng", + "storageUsed": "Lưu trữ", + "percentageOfTotalUsed": "Tổng phần trăm", + "bandwidth": "Băng thông", + "unused": { + "title": "Chưa sử dụng", + "tips": "Giá trị này có thể không phản ánh chính xác dung lượng trống có sẵn cho Frigate nếu bạn có các tệp khác được lưu trữ trên ổ đĩa ngoài các bản ghi của Frigate. Frigate không theo dõi việc sử dụng dung lượng lưu trữ bên ngoài các bản ghi của nó." + } + }, + "title": "Lưu trữ", + "recordings": { + "title": "Bản ghi", + "tips": "Giá trị này thể hiện tổng dung lượng lưu trữ được sử dụng bởi các bản ghi trong cơ sở dữ liệu của Frigate. Frigate không theo dõi việc sử dụng dung lượng lưu trữ cho tất cả các tệp trên đĩa của bạn.", + "earliestRecording": "Bản ghi sớm nhất hiện có:" + }, + "shm": { + "title": "Sắp xếp bộ nhớ được chia sẻ (SHM)", + "warning": "Bộ nhớ chia sẻ hiện tại quá thấp {{total}}MB. Tăng lên tối thiểu là {{min_shm}}MB." + } + }, + "cameras": { + "label": { + "detect": "phát hiện", + "skipped": "bỏ qua", + "ffmpeg": "FFmpeg", + "capture": "ghi hình", + "overallDetectionsPerSecond": "tổng số phát hiện mỗi giây", + "cameraFramesPerSecond": "{{camName}} khung hình mỗi giây", + "cameraDetectionsPerSecond": "{{camName}} phát hiện mỗi giây", + "overallFramesPerSecond": "tổng số khung hình mỗi giây", + "camera": "camera", + "overallSkippedDetectionsPerSecond": "tổng số phát hiện bị bỏ qua mỗi giây", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} ghi hình", + "cameraDetect": "{{camName}} phát hiện", + "cameraSkippedDetectionsPerSecond": "{{camName}} phát hiện bị bỏ qua mỗi giây" + }, + "toast": { + "success": { + "copyToClipboard": "Đã sao chép dữ liệu thăm dò vào clipboard." + }, + "error": { + "unableToProbeCamera": "Không thể thăm dò camera: {{errorMessage}}" + } + }, + "info": { + "stream": "Luồng {{idx}}", + "streamDataFromFFPROBE": "Dữ liệu luồng được lấy bằng ffprobe.", + "video": "Video:", + "fetching": "Đang tìm nạp Dữ liệu Camera", + "codec": "Codec:", + "unknown": "Không xác định", + "audio": "Âm thanh:", + "error": "Lỗi: {{error}}", + "tips": { + "title": "Thông tin Thăm dò Camera" + }, + "resolution": "Độ phân giải:", + "fps": "FPS:", + "cameraProbeInfo": "Thông tin Thăm dò Camera {{camera}}", + "aspectRatio": "tỉ lệ khung hình" + }, + "overview": "Tổng quan", + "framesAndDetections": "Khung hình / Phát hiện", + "title": "Camera" + }, + "lastRefreshed": "Làm mới lần cuối: ", + "stats": { + "detectIsSlow": "{{detect}} đang chậm ({{speed}} ms)", + "detectIsVerySlow": "{{detect}} đang rất chậm ({{speed}} ms)", + "cameraIsOffline": "{{camera}} đang ngoại tuyến", + "ffmpegHighCpuUsage": "{{camera}} có mức sử dụng CPU FFmpeg cao ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} có mức sử dụng CPU phát hiện cao ({{detectAvg}}%)", + "healthy": "Hệ thống đang hoạt động tốt", + "reindexingEmbeddings": "Đang lập chỉ mục lại các embedding (hoàn thành {{processed}}%)", + "shmTooLow": "/dev/shm ({{total}} MB) cần được tăng lên tối thiểu {{min}} MB." + }, + "enrichments": { + "embeddings": { + "image_embedding": "Embedding Hình ảnh", + "text_embedding_speed": "Tốc độ Embedding Văn bản", + "face_embedding_speed": "Tốc độ Embedding Khuôn mặt", + "text_embedding": "Embedding Văn bản", + "face_recognition": "Nhận dạng Khuôn mặt", + "plate_recognition": "Nhận dạng Biển số", + "image_embedding_speed": "Tốc độ Embedding Hình ảnh", + "face_recognition_speed": "Tốc độ Nhận dạng Khuôn mặt", + "plate_recognition_speed": "Tốc độ Nhận dạng Biển số", + "yolov9_plate_detection_speed": "Tốc độ Phát hiện Biển số YOLOv9", + "yolov9_plate_detection": "Phát hiện Biển số YOLOv9", + "review_description": "Đánh giá mô tả", + "review_description_speed": "Đánh giá Mô tả Tốc độ", + "review_description_events_per_second": "Đánh giá mô tả", + "object_description": "Mô tả đối tượng", + "object_description_speed": "Đối tượng Mô tả Tốc độ", + "object_description_events_per_second": "Mô tả đối tượng" + }, + "title": "Làm giàu Dữ liệu", + "infPerSecond": "Suy luận Mỗi Giây", + "averageInf": "Thời gian suy luận trung bình" + }, + "title": "Hệ thống", + "metrics": "Số liệu hệ thống", + "logs": { + "download": { + "label": "Tải xuống Nhật ký" + }, + "copy": { + "label": "Sao chép vào Clipboard", + "success": "Đã sao chép nhật ký vào clipboard", + "error": "Không thể sao chép nhật ký vào clipboard" + }, + "type": { + "label": "Loại", + "timestamp": "Dấu thời gian", + "tag": "Thẻ", + "message": "Thông báo" + }, + "tips": "Nhật ký đang được truyền trực tiếp từ máy chủ", + "toast": { + "error": { + "fetchingLogsFailed": "Lỗi khi tìm nạp nhật ký: {{errorMessage}}", + "whileStreamingLogs": "Lỗi trong khi truyền trực tiếp nhật ký: {{errorMessage}}" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/audio.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/audio.json new file mode 100644 index 0000000..8d29100 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/audio.json @@ -0,0 +1,429 @@ +{ + "speech": "講話", + "babbling": "牙牙學語", + "yell": "大嗌", + "bellow": "咆哮", + "whoop": "歡呼聲", + "whispering": "細細聲", + "laughter": "笑聲", + "sigh": "歎氣聲", + "crying": "喊聲", + "yodeling": "山歌", + "choir": "合唱", + "snicker": "偷笑聲", + "mantra": "咒語", + "singing": "歌聲", + "chant": "唸經", + "breathing": "呼吸聲", + "child_singing": "兒童歌聲", + "rapping": "饒舌", + "humming": "哼歌", + "whistling": "口哨聲", + "synthetic_singing": "人造歌聲", + "groan": "呻吟聲", + "grunt": "哼聲", + "snort": "哼哼聲", + "throat_clearing": "清喉嚨", + "wheeze": "氣喘聲", + "snoring": "鼻鼾聲", + "gasp": "喘氣", + "pant": "急促喘氣", + "cough": "咳嗽", + "sneeze": "打乞嚏", + "shuffle": "拖步行", + "sniff": "嗅嗅聲", + "footsteps": "腳步聲", + "chewing": "咀嚼聲", + "biting": "咬嘢聲", + "gargling": "漱口聲", + "run": "跑步", + "stomach_rumble": "肚餓聲", + "burping": "打嗝聲", + "clapping": "掌聲", + "children_playing": "兒童玩耍聲", + "applause": "掌聲", + "heartbeat": "心跳", + "growling": "狗咆哮聲", + "bow_wow": "狗汪汪聲", + "caterwaul": "貓嚎叫", + "howl": "狗慘叫聲", + "livestock": "牲畜", + "clip_clop": "馬蹄聲", + "cattle": "牛", + "horse": "馬", + "heart_murmur": "心臟雜音", + "fart": "放屁", + "hands": "手", + "cheering": "歡呼聲", + "dog": "狗", + "bark": "樹皮", + "yip": "狗尖叫聲", + "chatter": "嘈雜聲", + "purr": "貓呼嚕聲", + "whimper_dog": "狗嗚咽聲", + "hiccup": "打嗝聲", + "finger_snapping": "彈手指聲", + "crowd": "人群聲", + "animal": "動物", + "pets": "寵物", + "cat": "貓", + "meow": "貓喵喵聲", + "hiss": "貓嘶嘶聲", + "neigh": "馬嘶聲", + "cowbell": "牛鈴", + "moo": "牛哞哞聲", + "gobble": "火雞叫聲", + "turkey": "火雞", + "chicken": "雞", + "cluck": "雞咯咯聲", + "fowl": "家禽", + "sheep": "羊", + "duck": "鴨子", + "goat": "山羊", + "pig": "豬", + "oink": "豬哼聲", + "bleat": "咩咩聲", + "cock_a_doodle_doo": "公雞叫聲", + "honk": "鵝叫聲", + "quack": "鴨叫聲", + "goose": "鵝", + "wild_animals": "野生動物", + "crow": "烏鴉", + "coo": "白鴿咕咕聲", + "pigeon": "白鴿", + "roaring_cats": "貓咆哮聲", + "roar": "咆哮聲", + "bird": "鳥", + "chirp": "鳥啾啾聲", + "squawk": "鳥嘎嘎聲", + "caw": "烏鴉呱呱聲", + "mouse": "滑鼠", + "owl": "貓頭鷹", + "rats": "大老鼠", + "hoot": "貓頭鷹咕咕聲", + "patter": "老鼠腳步聲", + "flapping_wings": "拍打翅膀聲", + "dogs": "狗", + "snake": "蛇", + "insect": "昆蟲", + "whale_vocalization": "鯨魚叫聲", + "rattle": "蛇叫聲", + "fly": "蒼蠅", + "croak": "青蛙呱呱聲", + "mosquito": "蚊", + "music": "音樂", + "frog": "青蛙", + "cricket": "蟋蟀", + "buzz": "嗡嗡聲", + "musical_instrument": "樂器", + "steel_guitar": "鋼弦結他", + "tapping": "拍擊", + "guitar": "結他", + "strum": "撥弦聲", + "electric_guitar": "電結他", + "plucked_string_instrument": "撥弦樂器", + "bass_guitar": "低音結他", + "banjo": "班祖琴", + "acoustic_guitar": "原聲結他", + "piano": "鋼琴", + "synthesizer": "合成器", + "keyboard": "鍵盤", + "organ": "風琴", + "sitar": "錫塔琴", + "mandolin": "曼陀鈴", + "zither": "齊特琴", + "ukulele": "烏克麗麗", + "sampler": "採樣器", + "hammond_organ": "哈蒙德風琴", + "electric_piano": "電子鋼琴", + "electronic_organ": "電子風琴", + "rimshot": "鼓邊敲擊", + "bass_drum": "低音鼓", + "drum_kit": "鼓套", + "drum_machine": "鼓機", + "drum": "鼓", + "snare_drum": "小鼓", + "timpani": "定音鼓", + "drum_roll": "鼓聲", + "harpsichord": "大鍵琴", + "percussion": "打擊樂器", + "trumpet": "小號", + "cymbal": "銅鈸", + "french_horn": "法國號", + "string_section": "弦樂組", + "saxophone": "色士風", + "marimba": "馬林巴琴", + "mallet_percussion": "鎚擊樂器", + "maraca": "沙槌", + "harp": "豎琴", + "orchestra": "管弦樂團", + "violin": "小提琴", + "gong": "鑼", + "flute": "長笛", + "bowed_string_instrument": "弓弦樂器", + "vibraphone": "顫音琴", + "tabla": "塔布拉鼓", + "trombone": "長號", + "tambourine": "鈴鼓", + "double_bass": "低音提琴", + "brass_instrument": "銅管樂器", + "cello": "大提琴", + "clarinet": "單簧管", + "pizzicato": "撥奏", + "hi_hat": "高帽鈸", + "wood_block": "木魚", + "tubular_bells": "管鐘", + "glockenspiel": "鐘琴", + "steelpan": "鋼鼓", + "wind_instrument": "管樂器", + "bell": "鐘", + "bicycle_bell": "單車鈴", + "wind_chime": "風鈴", + "church_bell": "教堂鐘聲", + "chime": "鈴聲", + "tuning_fork": "音叉", + "jingle_bell": "鈴鐺", + "accordion": "手風琴", + "bagpipes": "風笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷門琴", + "singing_bowl": "頌缽", + "scratching": "抓碟聲", + "pop_music": "流行音樂", + "hip_hop_music": "嘻哈音樂", + "harmonica": "口琴", + "beatboxing": "人聲節奏", + "country": "鄉村音樂", + "water": "水", + "scary_music": "恐怖音樂", + "music_of_asia": "亞洲音樂", + "dance_music": "舞曲", + "video_game_music": "電子遊戲音樂", + "thunderstorm": "雷雨", + "bluegrass": "藍草音樂", + "train_wheels_squealing": "火車車輪聲", + "techno": "電子舞曲", + "new-age_music": "新世紀音樂", + "background_music": "背景音樂", + "theme_music": "主題音樂", + "jingle": "鈴聲", + "bus": "巴士", + "emergency_vehicle": "緊急車輛", + "aircraft": "飛機", + "ice_cream_truck": "雪糕車", + "dubstep": "杜步音樂", + "aircraft_engine": "飛機引擎聲", + "funk": "放克音樂", + "lullaby": "搖籃曲", + "rain": "雨", + "happy_music": "快樂音樂", + "music_of_latin_america": "拉丁美洲音樂", + "soundtrack_music": "配樂", + "rock_music": "搖滾樂", + "rock_and_roll": "搖滾樂", + "psychedelic_rock": "迷幻搖滾", + "rhythm_and_blues": "節奏藍調", + "soul_music": "靈魂音樂", + "reggae": "雷鬼音樂", + "folk_music": "民謠音樂", + "middle_eastern_music": "中東音樂", + "jazz": "爵士樂", + "disco": "迪斯可音樂", + "classical_music": "古典音樂", + "opera": "歌劇", + "electronic_music": "電子音樂", + "house_music": "浩室音樂", + "electronic_dance_music": "電子舞曲", + "ambient_music": "環境音樂", + "trance_music": "迷幻音樂", + "salsa_music": "薩爾薩音樂", + "flamenco": "佛朗明哥", + "blues": "藍調音樂", + "music_for_children": "兒童音樂", + "vocal_music": "聲樂", + "a_capella": "無伴奏合唱", + "music_of_africa": "非洲音樂", + "afrobeat": "非洲節拍", + "christian_music": "基督教音樂", + "gospel_music": "福音音樂", + "music_of_bollywood": "寶萊塢音樂", + "ska": "斯卡音樂", + "traditional_music": "傳統音樂", + "independent_music": "獨立音樂", + "song": "歌曲", + "raindrop": "雨點聲", + "rain_on_surface": "雨打地面聲", + "stream": "小溪", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸氣", + "gurgling": "咕嚕聲", + "fire": "火", + "crackle": "劈啪聲", + "vehicle": "車輛", + "boat": "船", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "機動船", + "ship": "船", + "motor_vehicle": "機動車", + "car": "車", + "toot": "汽車響咹聲", + "car_alarm": "汽車防盜器", + "power_windows": "電動車窗", + "skidding": "車胎打滑聲", + "tire_squeal": "車胎尖叫聲", + "car_passing_by": "車駛過聲", + "race_car": "賽車", + "truck": "貨車", + "air_brake": "煞車聲", + "air_horn": "空氣喇叭", + "police_car": "警車", + "ambulance": "救護車", + "fire_engine": "消防車", + "motorcycle": "電單車", + "rail_transport": "鐵路運輸", + "train_whistle": "火車汽笛聲", + "train_horn": "火車喇叭聲", + "railroad_car": "火車車廂", + "propeller": "螺旋槳", + "helicopter": "直升機", + "fixed-wing_aircraft": "固定翼飛機", + "bicycle": "單車", + "skateboard": "滑板", + "engine": "引擎", + "light_engine": "小型引擎", + "dental_drill's_drill": "牙科鑽機", + "lawn_mower": "剪草機", + "chainsaw": "電鋸", + "medium_engine": "中型引擎", + "heavy_engine": "大型引擎", + "engine_knocking": "引擎敲擊聲", + "engine_starting": "引擎啟動聲", + "idling": "引擎空轉聲", + "accelerating": "加速聲", + "door": "門", + "doorbell": "門鈴", + "ding-dong": "叮咚聲", + "sliding_door": "趟門", + "knock": "敲門", + "tap": "輕敲門", + "squeak": "吱吱聲", + "cupboard_open_or_close": "櫃門開關聲", + "drawer_open_or_close": "抽屜開關聲", + "dishes": "餐具聲", + "cutlery": "刀叉", + "chopping": "切菜聲", + "frying": "炒煮", + "microwave_oven": "微波爐", + "water_tap": "水龍頭", + "electric_toothbrush": "電動牙刷", + "vacuum_cleaner": "吸塵機", + "zipper": "拉鍊", + "keys_jangling": "鎖匙碰撞聲", + "coin": "硬幣", + "scissors": "剪刀", + "electric_shaver": "電鬚刨", + "shuffling_cards": "洗牌", + "typing": "打字", + "computer_keyboard": "電腦鍵盤", + "telephone": "電話", + "telephone_bell_ringing": "電話鈴聲", + "siren": "警報聲", + "steam_whistle": "蒸氣汽笛", + "mechanisms": "機械聲", + "ratchet": "棘輪聲", + "clock": "時鐘", + "tick": "滴答聲", + "tick-tock": "滴答滴答聲", + "sewing_machine": "衣車", + "mechanical_fan": "機械風扇", + "printer": "印表機", + "camera": "鏡頭", + "single-lens_reflex_camera": "單反相機", + "tools": "工具", + "hammer": "鎚仔", + "sawing": "鋸", + "filing": "銼", + "sanding": "打磨", + "power_tool": "電動工具", + "drill": "鑽", + "explosion": "爆炸", + "fusillade": "連環射擊", + "artillery_fire": "火砲", + "cap_gun": "玩具槍", + "fireworks": "煙花", + "firecracker": "炮仗", + "burst": "爆裂", + "eruption": "爆發", + "wood": "木頭", + "chop": "劈木聲", + "splinter": "木刺聲", + "crack": "裂聲", + "shatter": "破碎聲", + "silence": "寂靜", + "sound_effect": "音效", + "white_noise": "白噪音", + "pink_noise": "粉紅噪音", + "television": "電視", + "radio": "收音機", + "field_recording": "現場錄音", + "angry_music": "憤怒音樂", + "sad_music": "悲傷音樂", + "wind": "風", + "wind_noise": "風聲", + "drum_and_bass": "鼓和貝斯", + "wedding_music": "婚禮音樂", + "progressive_rock": "前衛搖滾", + "grunge": "垃圾搖滾", + "punk_rock": "朋克搖滾", + "christmas_music": "聖誕音樂", + "subway": "地鐵", + "thunder": "雷聲", + "carnatic_music": "卡納蒂克音樂", + "traffic_noise": "交通噪音", + "train": "火車", + "slam": "砰門聲", + "tender_music": "溫柔音樂", + "reversing_beeps": "倒車提示聲", + "heavy_metal": "重金屬音樂", + "jet_engine": "噴射機引擎聲", + "rustling_leaves": "樹葉沙沙聲", + "electronica": "電子樂", + "swing_music": "搖擺音樂", + "exciting_music": "刺激音樂", + "ringtone": "鈴聲", + "pulleys": "滑輪", + "jackhammer": "風鑽", + "writing": "寫作", + "toilet_flush": "沖廁", + "whistle": "哨子聲", + "gunshot": "槍聲", + "alarm_clock": "鬧鐘", + "dial_tone": "電話按號聲", + "boom": "轟隆", + "typewriter": "打字機", + "blender": "攪拌機", + "toothbrush": "牙刷", + "cash_register": "收銀機", + "civil_defense_siren": "民防警報", + "machine_gun": "機關槍", + "sink": "洗手盆", + "fire_alarm": "火警鐘", + "bathtub": "浴缸", + "busy_signal": "線路繁忙聲", + "smoke_detector": "煙霧偵測器", + "hair_dryer": "吹風機", + "alarm": "警報", + "gears": "齒輪", + "telephone_dialing": "電話撥號聲", + "foghorn": "霧號", + "buzzer": "蜂鳴器聲", + "air_conditioning": "冷氣機", + "glass": "玻璃", + "chink": "碰撞聲", + "environmental_noise": "環境噪音", + "static": "靜電聲", + "scream": "尖叫聲" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/common.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/common.json new file mode 100644 index 0000000..a655503 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/common.json @@ -0,0 +1,272 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilRestart": "直到重新啟動", + "yesterday": "昨日", + "last7": "過去7日", + "last14": "過去14日", + "last30": "過去30日", + "thisWeek": "今個星期", + "lastMonth": "上個月", + "10minutes": "10分鐘", + "12hours": "12 小時", + "24hours": "24 小時", + "am": "上午", + "year_other": "{{time}}年", + "mo": "{{time}}月", + "m": "{{time}}分鐘", + "minute_other": "{{time}}分鐘", + "formattedTimestamp": { + "12hour": "M月d日 ah:mm:ss", + "24hour": "M月d日 HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ah:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampFilename": { + "24hour": "yy年MM月dd日 HH時mm分ss秒", + "12hour": "yy年MM月dd日 ah時mm分ss秒" + }, + "s": "{{time}}秒", + "formattedTimestamp2": { + "12hour": "MM月dd日 ah:mm:ss", + "24hour": "MM月dd日 HH:mm:ss" + }, + "thisMonth": "今個月", + "pm": "下午", + "formattedTimestampMonthDayHourMinute": { + "24hour": "M月d日 HH:mm", + "12hour": "M月d日 ah:mm" + }, + "justNow": "剛剛", + "day_other": "{{time}}日", + "hour_other": "{{time}}小時", + "30minutes": "30分鐘", + "5minutes": "5分鐘", + "yr": "{{time}}年", + "today": "今日", + "month_other": "{{time}}月", + "second_other": "{{time}}秒", + "untilForRestart": "直到 Frigate 重新啟動。", + "ago": "{{timeAgo}} 前", + "d": "{{time}}日", + "lastWeek": "上個星期", + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "yyyy年M月d日 ah:mm", + "24hour": "yyyy年M月d日 HH:mm" + }, + "1hour": "1 小時", + "h": "{{time}}小時", + "formattedTimestampMonthDay": "M月d日", + "formattedTimestampMonthDayYear": { + "24hour": "yy年MM月dd日", + "12hour": "yy年MM月dd日" + } + }, + "unit": { + "speed": { + "mph": "英里/小時", + "kph": "公里/小時" + }, + "length": { + "feet": "呎", + "meters": "米" + }, + "data": { + "kbps": "kB/秒", + "mbps": "MB/秒", + "gbps": "GB/秒", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" + } + }, + "label": { + "back": "返回" + }, + "button": { + "apply": "套用", + "reset": "重置", + "done": "完成", + "enabled": "已啟用", + "enable": "啟用", + "disabled": "已停用", + "disable": "停用", + "save": "儲存", + "saving": "儲存中…", + "cancel": "取消", + "close": "關閉", + "copy": "複製", + "back": "返回", + "history": "歷史記錄", + "fullscreen": "全螢幕", + "exitFullscreen": "離開全螢幕", + "pictureInPicture": "畫中畫", + "twoWayTalk": "雙向通話", + "cameraAudio": "鏡頭音訊", + "suspended": "已暫停", + "export": "匯出", + "deleteNow": "立即刪除", + "next": "下一步", + "play": "播放", + "no": "否", + "copyCoordinates": "複製座標", + "delete": "刪除", + "off": "關閉", + "edit": "編輯", + "on": "開啟", + "yes": "是", + "info": "資訊", + "download": "下載", + "unsuspended": "取消暫停", + "unselect": "取消選取" + }, + "menu": { + "system": "系統", + "systemMetrics": "系統指標", + "configuration": "設定", + "systemLogs": "系統日誌", + "settings": "設定", + "configurationEditor": "設定編輯器", + "languages": "語言", + "language": { + "en": "English (英文)", + "es": "Español (西班牙文)", + "zhCN": "简体中文 (簡體中文)", + "hi": "हिन्दी (印地文)", + "fr": "Français (法文)", + "de": "Deutsch (德文)", + "ja": "日本語 (日文)", + "it": "Italiano (意大利文)", + "tr": "Türkçe (土耳其文)", + "nl": "Nederlands (荷蘭文)", + "cs": "Čeština (捷克文)", + "nb": "Norsk Bokmål (挪威文)", + "ko": "한국어 (韓文)", + "vi": "Tiếng Việt (越南文)", + "fa": "فارسی (波斯文)", + "pl": "Polski (波蘭文)", + "el": "Ελληνικά (希臘文)", + "ro": "Română (羅馬尼亞文)", + "hu": "Magyar (匈牙利文)", + "fi": "Suomi (芬蘭文)", + "da": "Dansk (丹麥文)", + "sk": "Slovenčina (斯洛伐克文)", + "withSystem": { + "label": "使用系統語言設定" + }, + "ru": "Русский (俄文)", + "sv": "Svenska (瑞典文)", + "ar": "العربية (阿拉伯文)", + "pt": "Português (葡萄牙文)", + "uk": "Українська (烏克蘭文)", + "he": "עברית (希伯來文)", + "yue": "粵語 (廣東話)", + "th": "ไทย (泰文)", + "ca": "Català (加泰羅尼亞語)", + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)" + }, + "appearance": "外觀", + "darkMode": { + "label": "深色模式", + "light": "淺色", + "dark": "深色", + "withSystem": { + "label": "使用系統模式設定" + } + }, + "withSystem": "系統", + "theme": { + "label": "主題", + "blue": "藍色", + "green": "綠色", + "nord": "北歐風", + "red": "紅色", + "default": "預設", + "contrast": "高對比", + "highcontrast": "高對比度" + }, + "documentation": { + "title": "文件", + "label": "Frigate 文件" + }, + "restart": "重新啟動 Frigate", + "live": { + "title": "即時", + "allCameras": "所有鏡頭", + "cameras": { + "title": "鏡頭", + "count_other": "{{count}} 個鏡頭" + } + }, + "review": "審查", + "explore": "瀏覽", + "export": "匯出", + "uiPlayground": "UI 測試場", + "faceLibrary": "臉部資料庫", + "user": { + "title": "使用者", + "logout": "登出", + "account": "帳戶", + "current": "當前使用者:{{user}}", + "anonymous": "匿名", + "setPassword": "設定密碼" + }, + "help": "幫助" + }, + "role": { + "admin": "管理員", + "viewer": "檢視者", + "desc": "管理員擁有 Frigate UI 全功能存取權限。檢視者只能查看鏡頭、審查項目和歷史影像。", + "title": "角色" + }, + "pagination": { + "label": "分頁", + "previous": { + "title": "上一頁", + "label": "前往上一頁" + }, + "next": { + "title": "下一頁", + "label": "前往下一頁" + }, + "more": "更多頁數" + }, + "accessDenied": { + "title": "拒絕存取", + "documentTitle": "拒絕存取 - Frigate", + "desc": "你無權查看此頁面。" + }, + "selectItem": "選擇 {{item}}", + "toast": { + "save": { + "error": { + "noMessage": "儲存設定變更失敗", + "title": "儲存設定變更失敗:{{errorMessage}}" + }, + "title": "儲存" + }, + "copyUrlToClipboard": "已複製 URL 到剪貼簿。" + }, + "notFound": { + "documentTitle": "找不到頁面 - Frigate", + "desc": "找不到頁面", + "title": "404" + }, + "readTheDocumentation": "閱讀文件", + "information": { + "pixels": "{{area}}像素" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/auth.json new file mode 100644 index 0000000..ebc3b8d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/auth.json @@ -0,0 +1,15 @@ +{ + "form": { + "errors": { + "webUnknownError": "未知錯誤。請檢查控制台日誌。", + "rateLimit": "超過速率限制。請稍後再試。", + "usernameRequired": "必須填寫用戶名", + "passwordRequired": "必須填寫密碼", + "loginFailed": "登入失敗", + "unknownError": "未知錯誤。請檢查日誌。" + }, + "user": "用戶名", + "password": "密碼", + "login": "登入" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/camera.json new file mode 100644 index 0000000..ecfa463 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "camera": { + "setting": { + "audio": { + "tips": { + "title": "此串流必須從你的鏡頭輸出音訊並在 go2rtc 中設定。", + "document": "閱讀文件 " + } + }, + "streamMethod": { + "method": { + "continuousStreaming": { + "desc": { + "warning": "持續串流可能導致高頻寬使用及效能問題,請小心使用。", + "title": "即使沒有偵測到活動,只要在控制台上可見,鏡頭影像也會一直保持即時串流。" + }, + "label": "持續串流" + }, + "smartStreaming": { + "label": "智能串流(建議)", + "desc": "當沒有偵測到活動時,智能串流會每分鐘更新一次鏡頭影像以節省頻寬和資源。當偵測到活動時,影像會無縫切換到即時串流。" + }, + "noStreaming": { + "label": "不串流", + "desc": "鏡頭影像每分鐘只會更新一次,不會進行即時串流。" + } + }, + "label": "串流方式", + "placeholder": "選擇串流方式" + }, + "compatibilityMode": { + "label": "相容模式", + "desc": "只有當你的鏡頭串流出現色彩異常及右側有斜線時,才啟用此選項。" + }, + "label": "鏡頭串流設定", + "title": "{{cameraName}} 串流設定", + "desc": "更改此鏡頭群組控制台的即時串流選項。這些設定是裝置/瀏覽器專屬的。", + "audioIsAvailable": "此串流有提供音訊", + "audioIsUnavailable": "此串流沒有音訊", + "placeholder": "選擇串流來源", + "stream": "串流" + }, + "birdseye": "鳥瞰" + }, + "delete": { + "confirm": { + "title": "確認刪除", + "desc": "你確定要刪除鏡頭群組 {{name}} 嗎?" + }, + "label": "刪除鏡頭群組" + }, + "name": { + "errorMessage": { + "exists": "鏡頭群組名稱已存在。", + "invalid": "鏡頭群組名稱無效。", + "mustLeastCharacters": "鏡頭群組名稱必須至少包含兩個字元。", + "nameMustNotPeriod": "鏡頭群組名稱不能包含句號。" + }, + "placeholder": "請輸入名稱…", + "label": "名稱" + }, + "icon": "圖標", + "cameras": { + "desc": "為此群組選擇鏡頭。", + "label": "鏡頭" + }, + "label": "鏡頭群組", + "add": "新增鏡頭群組", + "edit": "編輯鏡頭群組", + "success": "鏡頭群組({{name}})已儲存。" + }, + "debug": { + "options": { + "label": "設定", + "title": "選項", + "showOptions": "顯示選項", + "hideOptions": "隱藏選項" + }, + "mask": "遮罩", + "boundingBox": "框選區", + "motion": "移動", + "regions": "大區域", + "timestamp": "時間戳記", + "zones": "區域" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/dialog.json new file mode 100644 index 0000000..1a39110 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/dialog.json @@ -0,0 +1,120 @@ +{ + "restart": { + "title": "你確定要重新啟動 Frigate 嗎?", + "button": "重新啟動", + "restarting": { + "title": "Frigate 正在重新啟動", + "content": "此頁面將在 {{countdown}} 秒後重新載入。", + "button": "立即強制重新載入" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "提交到 Frigate+", + "desc": "位於你想避免的區域的物件並不是誤判。提交這些作為誤判會令模型混淆。" + }, + "review": { + "question": { + "label": "確認此標籤給 Frigate Plus", + "ask_a": "此物件是 {{label}} 嗎?", + "ask_an": "此物件是 {{label}} 嗎?", + "ask_full": "此物件是 {{untranslatedLabel}}({{translatedLabel}})嗎?" + }, + "state": { + "submitted": "已提交" + } + } + }, + "video": { + "viewInHistory": "在歷史記錄中查看" + } + }, + "export": { + "time": { + "fromTimeline": "從時間線選取", + "lastHour_other": "最后{{count}}小時", + "end": { + "label": "選擇結束時間", + "title": "結束時間" + }, + "custom": "自訂", + "start": { + "title": "開始時間", + "label": "選擇開始時間" + } + }, + "name": { + "placeholder": "為匯出命名" + }, + "select": "選取", + "export": "匯出", + "selectOrExport": "選取或匯出", + "toast": { + "error": { + "failed": "無法開始匯出:{{error}}", + "noVaildTimeSelected": "沒有選取有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間必須在開始時間之後" + }, + "success": "成功開始匯出。請到 /exports 資料夾查看檔案。" + }, + "fromTimeline": { + "saveExport": "儲存匯出", + "previewExport": "預覽匯出" + } + }, + "streaming": { + "label": "串流", + "restreaming": { + "disabled": "此鏡頭未啟用重串流。", + "desc": { + "title": "設定 go2rtc 以啟用此鏡頭的更多即時預覽選項及音訊。", + "readTheDocumentation": "閱讀文件" + } + }, + "showStats": { + "desc": "啟用此選項可在鏡頭畫面上顯示串流統計資料。", + "label": "顯示串流統計資料" + }, + "debugView": "除錯檢視" + }, + "search": { + "saveSearch": { + "label": "儲存搜尋", + "desc": "請為這個已儲存的搜尋輸入名稱。", + "placeholder": "請輸入搜尋名稱", + "overwrite": "{{searchName}} 已存在。儲存將會覆蓋現有資料。", + "button": { + "save": { + "label": "儲存此搜尋" + } + }, + "success": "搜尋({{searchName}})已儲存。" + } + }, + "recording": { + "confirmDelete": { + "title": "確認刪除", + "desc": { + "selected": "你確定要刪除與此審查項目相關的所有錄影嗎?

    按住 Shift 鍵可略過未來此對話框。" + }, + "toast": { + "success": "已成功刪除所選審查項目相關的影片片段。", + "error": "刪除失敗:{{error}}" + } + }, + "button": { + "export": "匯出", + "markAsReviewed": "標記為已審查", + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審查" + } + }, + "imagePicker": { + "selectImage": "選取追蹤物件縮圖", + "search": { + "placeholder": "以標籤或子標籤搜尋..." + }, + "noImages": "未找到此鏡頭的縮圖" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/filter.json new file mode 100644 index 0000000..bfdc935 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/filter.json @@ -0,0 +1,136 @@ +{ + "reset": { + "label": "重設篩選條件為預設值" + }, + "subLabels": { + "all": "所有子標籤", + "label": "子標籤" + }, + "score": "分數", + "features": { + "label": "特徵", + "hasSnapshot": "有快照", + "hasVideoClip": "有影片片段", + "submittedToFrigatePlus": { + "label": "已提交到 Frigate+", + "tips": "你必須先篩選出有快照的追蹤物件。

    沒有快照的追蹤物件無法提交到 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期(由舊到新)", + "dateDesc": "日期(由新到舊)", + "scoreAsc": "物件分數(由細到大)", + "scoreDesc": "物件分數(由大到細)", + "speedAsc": "預計速度(由慢到快)", + "speedDesc": "預計速度(由快到慢)", + "relevance": "相關性" + }, + "cameras": { + "label": "鏡頭篩選", + "all": { + "title": "所有鏡頭", + "short": "鏡頭" + } + }, + "review": { + "showReviewed": "顯示已審查" + }, + "motion": { + "showMotionOnly": "只顯示有移動" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "summary": "摘要", + "unfilteredGrid": "未篩選網格", + "desc": "當未選取篩選條件時,顯示每個標籤最近追蹤物件的摘要,或顯示未篩選的網格。", + "title": "預設檢視" + }, + "gridColumns": { + "title": "網格欄數", + "desc": "選擇網格檢視中的欄數。" + }, + "searchSource": { + "label": "搜尋來源", + "desc": "選擇搜尋追蹤物件的縮圖還是描述。", + "options": { + "thumbnailImage": "縮圖", + "description": "描述" + } + } + }, + "date": { + "selectDateBy": { + "label": "選擇日期進行篩選" + } + } + }, + "logSettings": { + "filterBySeverity": "依嚴重程度篩選日誌", + "loading": { + "desc": "當日誌窗格捲動到底部時,新日誌將自動串流顯示。", + "title": "載入中" + }, + "label": "篩選日誌等級", + "allLogs": "所有日誌", + "disableLogStreaming": "停用日誌串流" + }, + "trackedObjectDelete": { + "title": "確認刪除", + "toast": { + "success": "成功刪除追蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + }, + "desc": "刪除這 {{objectLength}} 個追蹤物件將會移除快照、儲存的嵌入資料,以及相關的物件生命週期記錄。歷史檢視中的錄影檔案不會被刪除。

    你確定要繼續嗎?

    按住 Shift 鍵可略過未來此對話框。" + }, + "recognizedLicensePlates": { + "loading": "載入已識別車牌中…", + "noLicensePlatesFound": "找不到車牌。", + "selectPlatesFromList": "從列表中選取一個或多個車牌。", + "placeholder": "輸入以搜尋車牌…", + "title": "已識別車牌", + "loadFailed": "載入已識別車牌失敗。", + "selectAll": "全部選取", + "clearAll": "全部清除" + }, + "estimatedSpeed": "預計速度({{unit}})", + "labels": { + "label": "標籤", + "count_one": "{{count}} 個標籤", + "all": { + "title": "所有標籤", + "short": "標籤" + }, + "count_other": "{{count}} 個標籤" + }, + "zoneMask": { + "filterBy": "按區域遮罩篩選" + }, + "zones": { + "label": "區域", + "all": { + "short": "區域", + "title": "所有區域" + } + }, + "filter": "篩選", + "dates": { + "all": { + "title": "所有日期", + "short": "日期" + }, + "selectPreset": "選擇預設設定…" + }, + "more": "更多篩選條件", + "timeRange": "時間範圍", + "classes": { + "label": "分類", + "all": { + "title": "所有分類" + }, + "count_one": "{{count}} 個分類", + "count_other": "{{count}} 個分類" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/icons.json new file mode 100644 index 0000000..467858b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "選擇圖示", + "search": { + "placeholder": "搜尋圖示…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/input.json new file mode 100644 index 0000000..ed7eee7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下載影片", + "toast": { + "success": "你的審查項目影片已開始下載。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/player.json new file mode 100644 index 0000000..4fe43d2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "此時間段內沒有錄影", + "noPreviewFound": "找不到預覽", + "submitFrigatePlus": { + "submit": "提交", + "title": "提交此畫面至 Frigate+?" + }, + "streamOffline": { + "desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌", + "title": "串流已離線" + }, + "cameraDisabled": "鏡頭已停用", + "stats": { + "bandwidth": { + "short": "頻寬", + "title": "頻寬:" + }, + "latency": { + "value": "{{seconds}} 秒", + "short": { + "value": "{{seconds}} 秒", + "title": "延遲" + }, + "title": "延遲:" + }, + "totalFrames": "總畫面數:", + "droppedFrames": { + "short": { + "title": "已丟棄", + "value": "{{droppedFrames}} 個畫面" + }, + "title": "丟棄畫面數:" + }, + "decodedFrames": "解碼畫面數:", + "droppedFrameRate": "畫面丟棄率:", + "streamType": { + "title": "串流類型:", + "short": "類型" + } + }, + "toast": { + "success": { + "submittedFrigatePlus": "成功提交畫面至 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交畫面至 Frigate+ 失敗" + } + }, + "noPreviewFoundFor": "找不到 {{cameraName}} 的預覽", + "livePlayerRequiredIOSVersion": "此串流類型需要 iOS 17.1 或以上版本。" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/objects.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/objects.json new file mode 100644 index 0000000..b0838d7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/objects.json @@ -0,0 +1,120 @@ +{ + "vehicle": "車輛", + "car": "車", + "boat": "船", + "bus": "巴士", + "motorcycle": "電單車", + "train": "火車", + "bicycle": "單車", + "skateboard": "滑板", + "door": "門", + "blender": "攪拌機", + "sink": "洗手盆", + "scissors": "剪刀", + "clock": "時鐘", + "toothbrush": "牙刷", + "hair_dryer": "吹風機", + "person": "人", + "airplane": "飛機", + "traffic_light": "紅綠燈", + "fire_hydrant": "消防栓", + "street_sign": "街道標誌", + "stop_sign": "停車標誌", + "parking_meter": "咪錶", + "bench": "長凳", + "bird": "鳥", + "cat": "貓", + "sheep": "羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑馬", + "giraffe": "長頸鹿", + "backpack": "背囊", + "tie": "領呔", + "suitcase": "行李箱", + "frisbee": "飛碟", + "skis": "滑雪板", + "snowboard": "單板滑雪板", + "sports_ball": "運動球", + "kite": "風箏", + "baseball_bat": "棒球棍", + "baseball_glove": "棒球手套", + "surfboard": "衝浪板", + "tennis_racket": "網球拍", + "bottle": "樽", + "plate": "碟", + "wine_glass": "酒杯", + "cup": "杯", + "fork": "叉", + "bowl": "碗", + "banana": "香蕉", + "apple": "蘋果", + "sandwich": "三文治", + "orange": "橙", + "carrot": "紅蘿蔔", + "hot_dog": "熱狗", + "pizza": "薄餅", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "凳", + "couch": "梳化", + "laptop": "手提電腦", + "mouse": "滑鼠", + "remote": "遙控器", + "keyboard": "鍵盤", + "cell_phone": "手機", + "microwave": "微波爐", + "oven": "焗爐", + "toaster": "多士爐", + "refrigerator": "雪櫃", + "book": "書", + "vase": "花瓶", + "teddy_bear": "泰迪熊", + "hair_brush": "梳", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "動物", + "bark": "樹皮", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔", + "raccoon": "浣熊", + "robot_lawnmower": "自動剪草機", + "waste_bin": "垃圾桶", + "license_plate": "車牌", + "bbq_grill": "燒烤爐", + "amazon": "亞馬遜", + "usps": "美國郵政", + "ups": "UPS", + "postnl": "荷蘭郵政", + "nzpost": "新西蘭郵政", + "postnord": "北歐郵政", + "gls": "GLS", + "dpd": "DPD", + "broccoli": "西蘭花", + "umbrella": "雨傘", + "eye_glasses": "眼鏡", + "dog": "狗", + "desk": "書枱", + "tv": "電視", + "horse": "馬", + "mirror": "鏡", + "spoon": "匙羹", + "hat": "帽", + "shoe": "鞋", + "potted_plant": "盆栽植物", + "fedex": "聯邦快遞", + "handbag": "手袋", + "dining_table": "飯枱", + "an_post": "愛爾蘭郵政", + "knife": "刀", + "window": "窗", + "bed": "床", + "toilet": "廁所", + "purolator": "Purolator", + "on_demand": "按需要提供", + "face": "人臉", + "package": "包裹", + "dhl": "DHL" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/classificationModel.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/classificationModel.json @@ -0,0 +1 @@ +{} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/configEditor.json new file mode 100644 index 0000000..5bf9d8a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "設定編輯器 - Frigate", + "configEditor": "設定編輯器", + "copyConfig": "複製設定", + "saveAndRestart": "儲存並重新啟動", + "saveOnly": "只儲存", + "toast": { + "success": { + "copyToClipboard": "設定已複製到剪貼簿。" + }, + "error": { + "savingError": "儲存設定時出錯" + } + }, + "confirm": "是否不儲存就離開?", + "safeConfigEditor": "設定編輯器 (安全模式)", + "safeModeDescription": "Frigate 因配置驗證錯誤而進入安全模式。" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/events.json new file mode 100644 index 0000000..b5e9dc8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/events.json @@ -0,0 +1,40 @@ +{ + "camera": "鏡頭", + "alerts": "警報", + "empty": { + "alert": "沒有警報需要審查", + "detection": "沒有偵測到的項目需要審查", + "motion": "找不到移動數據" + }, + "timeline": "時間線", + "events": { + "label": "事件", + "noFoundForTimePeriod": "此時段內沒有找到事件。", + "aria": "選擇事件" + }, + "recordings": { + "documentTitle": "錄影 - Frigate" + }, + "calendarFilter": { + "last24Hours": "過去24小時" + }, + "markAsReviewed": "標記為已審查", + "markTheseItemsAsReviewed": "將這些項目標記為已審查", + "newReviewItems": { + "label": "查看新的審查項目", + "button": "有新的審查項目" + }, + "selected_one": "已選擇 {{count}} 項", + "selected_other": "已選擇 {{count}} 項", + "allCameras": "所有鏡頭", + "documentTitle": "審查 - Frigate", + "motion": { + "only": "只顯示移動", + "label": "移動" + }, + "detections": "偵測", + "timeline.aria": "選擇時間線", + "detected": "已偵測", + "suspiciousActivity": "可疑行為", + "threateningActivity": "威脅行為" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/explore.json new file mode 100644 index 0000000..e3a8c94 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/explore.json @@ -0,0 +1,224 @@ +{ + "documentTitle": "瀏覽 - Frigate", + "generativeAI": "生成式人工智能", + "exploreIsUnavailable": { + "title": "無法使用瀏覽功能", + "embeddingsReindexing": { + "startingUp": "啟動中…", + "estimatedTime": "預計剩餘時間:", + "finishingShortly": "即將完成", + "step": { + "thumbnailsEmbedded": "已嵌入縮圖: ", + "descriptionsEmbedded": "已嵌入描述: ", + "trackedObjectsProcessed": "已處理的追蹤物件: " + }, + "context": "完成重新索引追蹤物件的嵌入後即可使用瀏覽功能。" + }, + "downloadingModels": { + "tips": { + "context": "下載完成後,你可能需要重新索引追蹤物件的嵌入。", + "documentation": "閱讀文件" + }, + "error": "發生錯誤。請檢查 Frigate 日誌。", + "context": "Frigate 正在下載必要的嵌入模型以支援語意搜尋功能。這可能需要幾分鐘,視乎你的網絡速度。", + "setup": { + "textTokenizer": "文字分詞器", + "textModel": "文字模型", + "visionModelFeatureExtractor": "視覺模型特徵提取器", + "visionModel": "視覺模型" + } + } + }, + "trackedObjectDetails": "追蹤物件詳情", + "type": { + "details": "詳情", + "snapshot": "快照", + "video": "影片", + "object_lifecycle": "物件生命周期" + }, + "objectLifecycle": { + "title": "物件生命周期", + "noImageFound": "此時間點找不到圖像。", + "createObjectMask": "建立物件遮罩", + "lifecycleItemDesc": { + "active": "{{label}} 變為活躍", + "stationary": "{{label}} 變為靜止", + "attribute": { + "faceOrLicense_plate": "偵測到 {{label}} 的 {{attribute}}", + "other": "{{label}} 被識別為 {{attribute}}" + }, + "header": { + "zones": "區域", + "ratio": "比例", + "area": "區域範圍" + }, + "heard": "聽到 {{label}}", + "entered_zone": "{{label}} 進入了 {{zones}}", + "gone": "{{label}} 離開了", + "visible": "偵測到 {{label}}", + "external": "偵測到 {{label}}" + }, + "annotationSettings": { + "title": "註解設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "在物件進入區域的畫面上總是顯示區域。" + }, + "offset": { + "tips": "提示:試想像有一段事件片段,當中有人由左行到右。如果事件時間線上的方框一直偏向人物的左邊,則應該減少數值。相反,如果有人由左行到右,而方框一直走在人物前面,則應該增加數值。", + "desc": "此資料來自鏡頭的偵測串流,但覆蓋在錄影串流的畫面上。兩個串流通常無法完全同步。因此邊界框和影片可能無法完全對齊。不過可以使用 annotation_offset 欄位來調整。", + "label": "註解偏移量", + "documentation": "閱讀文件 ", + "millisecondsToOffset": "偵測註解的偏移毫秒數。預設:0", + "toast": { + "success": "{{camera}} 的註解偏移量已儲存到設定檔。請重新啟動 Frigate 以套用更改。" + } + } + }, + "carousel": { + "previous": "上一張", + "next": "下一張" + }, + "adjustAnnotationSettings": "調整註解設定", + "scrollViewTips": "滾動以查看此物件生命周期中的重要時刻。", + "autoTrackingTips": "自動追蹤鏡頭的邊界框位置可能不準確。", + "count": "第 {{first}} 個,共 {{second}} 個", + "trackedPoint": "追蹤點" + }, + "details": { + "item": { + "title": "審查項目詳情", + "desc": "審查項目詳情", + "button": { + "share": "分享此審查項目", + "viewInExplore": "在瀏覽中查看" + }, + "tips": { + "mismatch_other": "偵測到 {{count}} 個不可用的物件並包含在此審查項目中。這些物件可能未符合警報或偵測標準,或已被清除/刪除。", + "hasMissingObjects": "如果你想讓 Frigate 保存下列標籤的追蹤物件,請調整設定:{{objects}}" + }, + "toast": { + "success": { + "updatedSublabel": "成功更新子標籤。", + "updatedLPR": "成功更新車牌號碼。", + "regenerate": "已從 {{provider}} 請求新的描述。根據提供者的速度,生成新的描述可能需要一些時間。", + "audioTranscription": "成功請求音訊轉錄。" + }, + "error": { + "regenerate": "呼叫 {{provider}} 以獲取新描述失敗:{{errorMessage}}", + "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", + "updatedLPRFailed": "更新車牌號碼失敗:{{errorMessage}}", + "audioTranscription": "請求音訊轉錄失敗:{{errorMessage}}" + } + } + }, + "label": "標籤", + "recognizedLicensePlate": "已識別車牌", + "estimatedSpeed": "預計速度", + "objects": "物件", + "camera": "鏡頭", + "zones": "區域", + "timestamp": "時間戳記", + "tips": { + "descriptionSaved": "成功保存描述", + "saveDescriptionFailed": "更新描述失敗:{{errorMessage}}" + }, + "regenerateFromSnapshot": "從快照重新生成", + "button": { + "regenerate": { + "label": "重新生成追蹤物件描述", + "title": "重新生成" + }, + "findSimilar": "尋找相似項目" + }, + "description": { + "label": "描述", + "placeholder": "追蹤物件的描述", + "aiTips": "Frigate 會等到追蹤物件生命周期結束後,才向你的生成式 AI 提供者請求描述。" + }, + "editLPR": { + "descNoLabel": "為此追蹤物件輸入新的車牌號碼", + "title": "編輯車牌號碼", + "desc": "為此 {{label}} 輸入新的車牌號碼" + }, + "topScore": { + "label": "最高分數", + "info": "最高分數是追蹤物件的最高中位分數,因此可能與搜尋結果縮圖上顯示的分數不同。" + }, + "editSubLabel": { + "desc": "為此 {{label}} 輸入新的子標籤", + "title": "編輯子標籤", + "descNoLabel": "為此追蹤物件輸入新的子標籤" + }, + "snapshotScore": { + "label": "快照分數" + }, + "expandRegenerationMenu": "展開重新生成選單", + "regenerateFromThumbnails": "從縮圖重新生成", + "score": { + "label": "分數" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "下載影片", + "aria": "下載影片" + }, + "downloadSnapshot": { + "label": "下載快照", + "aria": "下載快照" + }, + "viewObjectLifecycle": { + "label": "查看物件生命周期", + "aria": "顯示物件生命周期" + }, + "findSimilar": { + "label": "尋找相似項目", + "aria": "尋找相似追蹤物件" + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "aria": "提交到 Frigate Plus" + }, + "viewInHistory": { + "label": "在歷史記錄中查看", + "aria": "在歷史記錄中查看" + }, + "deleteTrackedObject": { + "label": "刪除此追蹤物件" + }, + "addTrigger": { + "label": "新增觸發器", + "aria": "為此追蹤物件新增觸發器" + }, + "audioTranscription": { + "label": "轉錄音訊", + "aria": "請求音訊轉錄" + } + }, + "dialog": { + "confirmDelete": { + "title": "確認刪除", + "desc": "刪除此追蹤物件會移除快照、所有已保存的嵌入,以及相關的物件生命周期記錄。歷史記錄中的錄影不會被刪除。

    你確定要繼續嗎?" + } + }, + "noTrackedObjects": "找不到追蹤物件", + "fetchingTrackedObjectsFailed": "取得追蹤物件時出錯:{{errorMessage}}", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "追蹤物件已成功刪除。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + }, + "tooltip": "已配對{{type}}({{confidence}}% 信心" + }, + "trackedObjectsCount_other": "{{count}} 個追蹤物件 ", + "exploreMore": "瀏覽更多{{label}}物件", + "aiAnalysis": { + "title": "AI 分析" + }, + "concerns": { + "label": "關注" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/exports.json new file mode 100644 index 0000000..48d8397 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/exports.json @@ -0,0 +1,17 @@ +{ + "documentTitle": "匯出 - Frigate", + "search": "搜尋", + "noExports": "未找到匯出項目", + "deleteExport": "刪除匯出", + "editExport": { + "title": "重新命名匯出", + "desc": "請輸入新的匯出名稱。", + "saveExport": "儲存匯出" + }, + "toast": { + "error": { + "renameExportFailed": "重新命名匯出失敗:{{errorMessage}}" + } + }, + "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/faceLibrary.json new file mode 100644 index 0000000..53525d9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/faceLibrary.json @@ -0,0 +1,97 @@ +{ + "selectItem": "選擇 {{item}}", + "details": { + "timestamp": "時間戳記", + "person": "人", + "confidence": "信心指數", + "face": "人臉詳細資料", + "faceDesc": "產生這個人臉的追蹤物件的詳細資料", + "scoreInfo": "子標籤分數是所有已識別人臉的信心值加權分數,因此可能與快照上顯示的分數不同。", + "subLabelScore": "子標籤分數", + "unknown": "未知" + }, + "description": { + "addFace": "逐步了解如何新增一個人臉庫的集合。", + "placeholder": "請輸入此集合的名稱", + "invalidName": "名稱無效。名稱只可以包含英文字母、數字、空格、撇號(')、底線(_)同連字號(-)。" + }, + "documentTitle": "人臉庫 - Frigate", + "uploadFaceImage": { + "title": "上傳人臉圖片", + "desc": "上傳圖片以掃描人臉並納入 {{pageToggle}}" + }, + "createFaceLibrary": { + "title": "建立集合", + "desc": "建立新集合", + "new": "建立新的人臉", + "nextSteps": "建立穩固基礎:
  • 使用訓練分頁,為每位偵測到的人物選擇並訓練圖片。
  • 以正面照片為主,避免用側面或傾斜角度的人臉作訓練。
  • " + }, + "steps": { + "faceName": "請輸入人臉名稱", + "uploadFace": "上傳人臉圖片", + "nextSteps": "下一步", + "description": { + "uploadFace": "請上載一張{{name}}面向鏡頭的相片。相片不需要裁剪至只顯示人臉。" + } + }, + "train": { + "title": "訓練", + "aria": "選擇訓練", + "empty": "最近沒有人臉識別嘗試" + }, + "selectFace": "選擇人臉", + "deleteFaceLibrary": { + "title": "刪除名稱", + "desc": "你確定要刪除集合 {{name}} 嗎?這將永久刪除所有相關的人臉資料。" + }, + "renameFace": { + "title": "重新命名人臉", + "desc": "請輸入 {{name}} 的新名稱" + }, + "button": { + "uploadImage": "上傳圖片", + "reprocessFace": "重新處理人臉", + "deleteFace": "刪除人臉", + "addFace": "新增人臉", + "deleteFaceAttempts": "刪除人臉", + "renameFace": "重新命名人臉" + }, + "imageEntry": { + "validation": { + "selectImage": "請選擇一個圖片檔案。" + }, + "dropActive": "將圖片拖到這裡…", + "dropInstructions": "拖放圖片或貼上到此處,或點擊選取", + "maxSize": "最大檔案大小:{{size}}MB" + }, + "readTheDocs": "閱讀文件", + "trainFaceAs": "將人臉訓練為:", + "trainFace": "訓練人臉", + "toast": { + "success": { + "uploadedImage": "成功上傳圖片。", + "renamedFace": "成功將人臉重新命名為 {{name}}", + "trainedFace": "成功訓練人臉。", + "updatedFaceScore": "成功更新人臉分數。", + "deletedFace_other": "成功刪除 {{count}} 個人臉。", + "addFaceLibrary": "{{name}} 已成功加入人臉庫!", + "deletedName_other": "成功刪除 {{count}} 個人臉。" + }, + "error": { + "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", + "addFaceLibraryFailed": "設定人臉名稱失敗:{{errorMessage}}", + "deleteFaceFailed": "刪除失敗:{{errorMessage}}", + "deleteNameFailed": "刪除名稱失敗:{{errorMessage}}", + "renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}", + "trainFailed": "訓練失敗:{{errorMessage}}", + "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}" + } + }, + "collections": "集合", + "deleteFaceAttempts": { + "desc_other": "你確定要刪除{{count}}個人臉嗎?這個動作無法還原。", + "title": "刪除人臉" + }, + "nofaces": "沒有可用人臉", + "pixels": "{{area}} 像素" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/live.json new file mode 100644 index 0000000..bb3b440 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/live.json @@ -0,0 +1,185 @@ +{ + "documentTitle": "即時畫面 - Frigate", + "cameraAudio": { + "disable": "停用鏡頭音訊", + "enable": "啟用鏡頭音訊" + }, + "ptz": { + "move": { + "clickMove": { + "label": "點擊畫面以置中鏡頭", + "enable": "啟用點擊移動", + "disable": "停用點擊移動" + }, + "up": { + "label": "移動 PTZ 鏡頭向上" + }, + "right": { + "label": "移動 PTZ 鏡頭向右" + }, + "left": { + "label": "移動 PTZ 鏡頭向左" + }, + "down": { + "label": "移動 PTZ 鏡頭向下" + } + }, + "frame": { + "center": { + "label": "點擊畫面以置中 PTZ 鏡頭" + } + }, + "presets": "PTZ 鏡頭預設位置", + "zoom": { + "in": { + "label": "放大 PTZ 鏡頭" + }, + "out": { + "label": "縮小 PTZ 鏡頭" + } + }, + "focus": { + "in": { + "label": "PTZ 鏡頭拉近焦距" + }, + "out": { + "label": "PTZ 鏡頭拉遠焦距" + } + } + }, + "twoWayTalk": { + "enable": "啟用雙向通話", + "disable": "停用雙向通話" + }, + "lowBandwidthMode": "低頻寬模式", + "documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate", + "recording": { + "disable": "停用錄影", + "enable": "啟用錄影" + }, + "snapshots": { + "enable": "啟用快照", + "disable": "停用快照" + }, + "audioDetect": { + "enable": "啟用音訊偵測", + "disable": "停用音訊偵測" + }, + "autotracking": { + "enable": "啟用自動追蹤", + "disable": "停用自動追蹤" + }, + "streamStats": { + "enable": "顯示串流統計資料", + "disable": "隱藏串流統計資料" + }, + "manualRecording": { + "title": "按需", + "tips": "根據此鏡頭的錄影保留設定手動啟動事件。", + "debugView": "除錯視圖", + "start": "開始按需錄影", + "showStats": { + "label": "顯示統計資料", + "desc": "啟用此選項可在鏡頭畫面上疊加串流統計資料。" + }, + "playInBackground": { + "desc": "啟用此選項可在播放器隱藏時繼續串流播放。", + "label": "背景播放" + }, + "started": "已開始手動按需錄影。", + "end": "結束按需錄影", + "ended": "已結束手動按需錄影。", + "failedToEnd": "無法結束手動按需錄影。", + "failedToStart": "無法開始手動按需錄影。", + "recordDisabledTips": "由於此鏡頭的設定已停用或限制錄影,因此只會儲存快照。" + }, + "camera": { + "enable": "啟用鏡頭", + "disable": "停用鏡頭" + }, + "muteCameras": { + "enable": "所有鏡頭靜音", + "disable": "所有鏡頭取消靜音" + }, + "detect": { + "disable": "停用偵測", + "enable": "啟用偵測" + }, + "streamingSettings": "串流設定", + "notifications": "通知", + "audio": "音訊", + "suspend": { + "forTime": "暫停時間: " + }, + "stream": { + "title": "串流", + "audio": { + "tips": { + "documentation": "閱讀文件 ", + "title": "音訊必須從你的鏡頭輸出,並在 go2rtc 中正確設定此串流。" + }, + "available": "此串流支援音訊", + "unavailable": "此串流不支援音訊" + }, + "twoWayTalk": { + "tips.documentation": "閱讀文件 ", + "available": "此串流支援雙向通話", + "unavailable": "此串流不支援雙向通話", + "tips": "你的裝置必須支援此功能,且需設定 WebRTC 才能使用雙向通話。" + }, + "lowBandwidth": { + "tips": "因緩衝或串流錯誤,即時畫面已切換至低頻寬模式。", + "resetStream": "重置串流" + }, + "playInBackground": { + "tips": "啟用此選項可在播放器隱藏時繼續串流播放。", + "label": "背景播放" + }, + "debug": { + "picker": "除錯模式下無法選擇串流。除錯視圖永遠使用已分配偵測角色的串流。" + } + }, + "cameraSettings": { + "cameraEnabled": "鏡頭已啟用", + "objectDetection": "物件偵測", + "recording": "錄影", + "snapshots": "快照", + "autotracking": "自動追蹤", + "audioDetection": "音訊偵測", + "title": "{{camera}} 設定", + "transcription": "音訊轉錄" + }, + "history": { + "label": "顯示歷史影像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "移動", + "active_objects": "活躍物件" + }, + "notAllTips": "你的 {{source}} 錄影保留設定為 mode: {{effectiveRetainMode}},因此此按需錄影只會保留{{effectiveRetainModeName}}的片段。" + }, + "editLayout": { + "label": "編輯版面配置", + "group": { + "label": "編輯鏡頭群組" + }, + "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時音訊轉錄", + "disable": "停用即時音訊轉錄" + }, + "noCameras": { + "title": "未設置任何鏡頭", + "description": "連接鏡頭開始使用。", + "buttonText": "新增鏡頭" + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "無可用影片來源以擷取快照。", + "captureFailed": "擷取快照失敗。", + "downloadStarted": "已開始下載快照。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/recording.json new file mode 100644 index 0000000..34473d2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "篩選", + "export": "匯出", + "calendar": "日曆", + "filters": "篩選條件", + "toast": { + "error": { + "noValidTimeSelected": "未選擇有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間必須在開始時間之後" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/search.json new file mode 100644 index 0000000..fea8931 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "搜尋", + "savedSearches": "已儲存的搜尋", + "searchFor": "搜尋 {{inputValue}}", + "button": { + "clear": "清除搜尋", + "save": "儲存搜尋", + "delete": "刪除已儲存的搜尋", + "filterInformation": "篩選資料", + "filterActive": "篩選中" + }, + "trackedObjectId": "追蹤物件編號", + "filter": { + "label": { + "labels": "標籤", + "zones": "區域", + "search_type": "搜尋類型", + "time_range": "時間範圍", + "after": "之後", + "recognized_license_plate": "已辨識車牌", + "has_clip": "有片段", + "has_snapshot": "有快照", + "min_score": "最低分數", + "before": "之前", + "max_score": "最高分數", + "max_speed": "最高速度", + "min_speed": "最低速度", + "cameras": "鏡頭", + "sub_labels": "子標籤" + }, + "searchType": { + "thumbnail": "縮圖", + "description": "描述" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「結束」日期必須遲於「開始」日期。", + "afterDatebeEarlierBefore": "「開始」日期必須早於「結束」日期。", + "minScoreMustBeLessOrEqualMaxScore": "「最低分數」必須少於或等於「最高分數」。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最高分數」必須多於或等於「最低分數」。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最高速度」必須多於或等於「最低速度」。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最低速度」必須少於或等於「最高速度」。" + } + }, + "tips": { + "title": "如何使用文字篩選", + "desc": { + "step1": "輸入篩選鍵名後加上冒號(例如:\"cameras:\")。", + "step2": "從建議中選擇一個值,或者自行輸入。", + "step3": "可以用空格隔開,連續使用多個篩選條件。", + "step4": "日期篩選(before: 同 after:)要用 {{DateFormat}} 格式。", + "step5": "時間範圍篩選要用 {{exampleTime}} 格式。", + "step6": "點擊旁邊的「x」就可以移除篩選條件。", + "text": "篩選可以幫你縮窄搜尋結果。以下係使用方法:", + "exampleLabel": "例子:" + } + }, + "header": { + "activeFilters": "啟用中的篩選條件", + "noFilters": "篩選條件", + "currentFilterType": "篩選數值" + } + }, + "similaritySearch": { + "title": "相似搜尋", + "active": "正在進行相似搜尋", + "clear": "清除相似搜尋" + }, + "placeholder": { + "search": "搜尋…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/settings.json new file mode 100644 index 0000000..34982ab --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/settings.json @@ -0,0 +1,1118 @@ +{ + "documentTitle": { + "default": "設定 - Frigate", + "authentication": "認證設定 - Frigate", + "camera": "鏡頭設定 - Frigate", + "classification": "進階功能設定 - Frigate", + "masksAndZones": "遮罩與區域編輯器 - Frigate", + "motionTuner": "移動調校器 - Frigate", + "object": "除錯 - Frigate", + "general": "一般設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "enrichments": "進階功能設定 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "鏡頭檢視設定 - Frigate" + }, + "menu": { + "ui": "介面", + "classification": "進階功能", + "cameras": "鏡頭設定", + "masksAndZones": "遮罩/區域", + "motionTuner": "移動調校器", + "debug": "除錯", + "users": "用戶", + "notifications": "通知", + "frigateplus": "Frigate+", + "enrichments": "進階功能", + "triggers": "觸發器", + "roles": "角色", + "cameraManagement": "管理", + "cameraReview": "審查" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未儲存的更改。", + "desc": "你想在繼續前儲存更改嗎?" + } + }, + "cameraSetting": { + "camera": "鏡頭", + "noCamera": "沒有鏡頭" + }, + "general": { + "title": "一般設定", + "liveDashboard": { + "playAlertVideos": { + "label": "播放警報影片", + "desc": "預設情況下,即時儀表板上的最近警報會以小型循環影片形式播放。停用此選項後,只會在此裝置/瀏覽器上顯示警報的靜態圖片。" + }, + "automaticLiveView": { + "label": "自動即時檢視", + "desc": "當偵測到活動時,自動切換到該鏡頭的即時畫面。若停用此選項,即時儀表板上的鏡頭靜態畫面將每分鐘只更新一次。" + }, + "title": "即時儀表板" + }, + "storedLayouts": { + "title": "儲存的版面配置", + "clearAll": "清除所有版面配置", + "desc": "鏡頭群組內的鏡頭佈局可以拖動或調整大小。位置會儲存在你瀏覽器的本機儲存空間內。" + }, + "cameraGroupStreaming": { + "title": "鏡頭群組串流設定", + "clearAll": "清除所有串流設定", + "desc": "每個鏡頭群組的串流設定會儲存在你瀏覽器的本機儲存空間內。" + }, + "recordingsViewer": { + "defaultPlaybackRate": { + "desc": "錄影播放的預設播放速度。", + "label": "預設播放速度" + }, + "title": "錄影瀏覽器" + }, + "calendar": { + "title": "日曆", + "firstWeekday": { + "label": "每星期的第一天", + "sunday": "星期日", + "monday": "星期一", + "desc": "審查日曆中每星期開始的日子。" + } + }, + "toast": { + "success": { + "clearStoredLayout": "已清除 {{cameraName}} 的儲存版面配置", + "clearStreamingSettings": "已清除所有鏡頭群組的串流設定。" + }, + "error": { + "clearStoredLayoutFailed": "清除儲存版面配置失敗:{{errorMessage}}", + "clearStreamingSettingsFailed": "清除串流設定失敗:{{errorMessage}}" + } + } + }, + "classification": { + "birdClassification": { + "desc": "鳥類分類會使用量化 Tensorflow 模型識別已知鳥類。當辨識到已知鳥類時,牠的常見名稱會加到子標籤上。此資訊會顯示在介面、篩選器及通知中。", + "title": "鳥類分類" + }, + "semanticSearch": { + "title": "語意搜尋", + "desc": "Frigate 的語意搜尋功能讓你可以利用影像本身、自訂文字描述,或自動產生的描述,在審查項目中尋找已追蹤的物件。", + "readTheDocumentation": "閱讀文件", + "reindexNow": { + "label": "立即重建索引", + "confirmTitle": "確認重建索引", + "confirmButton": "重建索引", + "success": "重建索引已成功開始。", + "alreadyInProgress": "重建索引已在進行中。", + "error": "啟動重建索引失敗:{{errorMessage}}", + "confirmDesc": "你確定要重建索引所有已追蹤物件的嵌入向量嗎?這個過程會在背景運行,但可能會用盡你的 CPU,而且需要一定時間。你可以在「瀏覽」頁面查看進度。", + "desc": "重建索引會為所有已追蹤物件重新生成嵌入向量。這個過程會在背景運行,可能會用盡你的 CPU,所需時間取決於已追蹤物件的數量。" + }, + "modelSize": { + "label": "模型大小", + "desc": "用於語意搜尋的模型大小。", + "small": { + "title": "小型", + "desc": "使用小型模型會採用量化版本,較少佔用 RAM,在 CPU 上運行更快,而嵌入品質的差異非常細微。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用完整的 Jina 模型,並在適用情況下自動於 GPU 上運行。" + } + } + }, + "faceRecognition": { + "modelSize": { + "small": { + "title": "小型", + "desc": "使用小型模型會採用 FaceNet 臉部嵌入模型,在大多數 CPU 上能有效運行。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上運行。" + }, + "label": "模型大小", + "desc": "用於人臉識別的模型大小。" + }, + "readTheDocumentation": "閱讀文件", + "title": "人臉識別", + "desc": "人臉識別功能允許為人物分配名字,當辨識到他們的臉孔時,Frigate 會將名字加到子標籤上。此資訊會顯示於介面、篩選器及通知中。" + }, + "licensePlateRecognition": { + "title": "車牌識別", + "readTheDocumentation": "閱讀文件", + "desc": "Frigate 可以識別車輛上的車牌,自動將偵測到的字元加到已辨識車牌欄位,或將已知名稱加到屬於車輛類型的物件的子標籤上。常見用途包括讀取駛入車道或在街道上駛過的車輛的車牌。" + }, + "restart_required": "需要重新啟動(分類設定已變更)", + "toast": { + "error": "儲存設定變更失敗:{{errorMessage}}", + "success": "分類設定已儲存。請重新啟動 Frigate 以套用你的更改。" + }, + "title": "進階功能設定", + "unsavedChanges": "進階功能設定更改尚未儲存" + }, + "camera": { + "title": "鏡頭設定", + "streams": { + "title": "串流", + "desc": "暫時停用鏡頭直到Frigate重新啟動。停用鏡頭會完全停止 Frigate 處理此鏡頭的串流。將會無法使用偵測、錄影和除錯功能。
    注意:這不會停用 go2rtc 的轉播功能。" + }, + "review": { + "title": "審查", + "desc": "暫時啟用或停用此鏡頭的警報和偵測直到Frigate重新啟動。停用後,將不會產生新的審查項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "readTheDocumentation": "閱讀文件", + "noDefinedZones": "此鏡頭未定義任何區域。", + "zoneObjectAlertsTips": "在{{cameraName}}的{{zone}}區域偵測到的所有{{alertsLabels}}物件將會顯示為警報。", + "objectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "objectAlertsTips": "在{{cameraName}}上所有{{alertsLabels}}物件將會顯示為警報。", + "zoneObjectDetectionsTips": { + "text": "在{{cameraName}}的{{zone}}區域內所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "regardlessOfZoneObjectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "notSelectDetections": "無論位於哪個區域,在{{cameraName}}的{{zone}}區域偵測到、但未分類為警報的{{detectionsLabels}}物件將會顯示為偵測結果。" + }, + "selectDetectionsZones": "選擇偵測的區域", + "limitDetections": "限制偵測至特定區域", + "title": "審查分類", + "desc": "Frigate會將審查項目分類為「警報」同「偵測」。預設情況下,所有 的物件都會被視為警報。你可以透過設定所需區域,細分審查項目分類。", + "selectAlertsZones": "選擇警報的區域", + "toast": { + "success": "審查分類設定已儲存。請重新啟動Frigate以套用更改。" + }, + "unsavedChanges": "{{camera}}的審查分類設定尚未儲存" + }, + "object_descriptions": { + "title": "生成式 AI 物件描述", + "desc": "暫時啟用或停用此鏡頭生成式 AI 物件描述。停用時,不會為此鏡頭的追蹤物件請求 AI 描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審查描述", + "desc": "暫時啟用或停用此鏡頭生成式 AI 審查描述。停用時,不會為此鏡頭的審查項目請求 AI 描述。" + }, + "addCamera": "新增鏡頭", + "editCamera": "編輯鏡頭:", + "selectCamera": "選擇鏡頭", + "backToSettings": "返回鏡頭設定", + "cameraConfig": { + "add": "新增鏡頭", + "edit": "編輯鏡頭", + "description": "設定鏡頭,包括串流輸入同角色分配。", + "name": "鏡頭名稱", + "nameRequired": "必須填寫鏡頭名稱", + "nameLength": "鏡頭名稱不得多於 24 個字元。", + "namePlaceholder": "例如:front_door", + "enabled": "已啟用", + "ffmpeg": { + "inputs": "輸入串流", + "path": "串流路徑", + "pathRequired": "必須填寫串流路徑", + "pathPlaceholder": "rtsp://...", + "roles": "角色", + "rolesRequired": "至少需要分配一個角色", + "rolesUnique": "每個角色(音訊、偵測、錄影)只可分配到一個串流", + "addInput": "新增輸入串流", + "removeInput": "移除輸入串流", + "inputsRequired": "至少需要一個輸入串流" + }, + "toast": { + "success": "鏡頭 {{cameraName}} 已成功儲存" + } + } + }, + "masksAndZones": { + "toast": { + "error": { + "copyCoordinatesFailed": "無法將座標複製到剪貼簿。" + }, + "success": { + "copyCoordinates": "已將{{polyName}}的座標複製到剪貼簿。" + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "區域名稱必須至少有2個字元。", + "mustNotBeSameWithCamera": "區域名稱不得與鏡頭名稱相同。", + "alreadyExists": "此鏡頭已存在相同名稱的區域。", + "mustNotContainPeriod": "區域名稱不可包含句號。", + "hasIllegalCharacter": "區域名稱包含非法字元。" + } + }, + "distance": { + "error": { + "mustBeFilled": "必須填寫所有距離欄位,才能使用速度估算功能。", + "text": "距離必須大於或等於0.1。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性值必須大於0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "逗留時間必須大於或等於0。" + } + }, + "polygonDrawing": { + "removeLastPoint": "移除最後一個點", + "delete": { + "title": "確認刪除", + "desc": "確定要刪除{{type}} {{name}}嗎?", + "success": "已刪除{{name}}。" + }, + "error": { + "mustBeFinished": "必須完成多邊形繪製後才能儲存。" + }, + "snapPoints": { + "false": "不對齊點", + "true": "對齊點" + }, + "reset": { + "label": "清除所有點" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度閾值必須大於或等於 0.1。" + } + } + }, + "zones": { + "label": "區域", + "add": "新增區域", + "edit": "編輯區域", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "name": { + "title": "名稱", + "inputPlaceHolder": "請輸入名稱…", + "tips": "名稱必須至少有2個字元,且不可與鏡頭或其他區域同名。" + }, + "inertia": { + "title": "慣性", + "desc": "指定物件需在區域內停留多少個畫格,才會被視為進入該區域。預設:3" + }, + "loiteringTime": { + "desc": "設定物件必須於區域內停留的最少秒數,以觸發動作。預設:0", + "title": "逗留時間" + }, + "objects": { + "title": "物件", + "desc": "此區域適用的物件列表。" + }, + "allObjects": "所有物件", + "speedEstimation": { + "title": "速度估算", + "desc": "啟用此區域內物件的速度估算。區域必須正好有4個點。", + "docs": "閱讀文件", + "lineCDistance": "C 線距離 ({{unit}})", + "lineBDistance": "B 線距離 ({{unit}})", + "lineADistance": "A 線距離 ({{unit}})", + "lineDDistance": "D 線距離 ({{unit}})" + }, + "speedThreshold": { + "title": "速度門檻 ({{unit}})", + "desc": "指定物件於此區域被視為有效時所需的最小速度。", + "toast": { + "error": { + "loiteringTimeError": "設有逗留時間大於0的區域,不應同時使用速度估算功能。", + "pointLengthError": "此區域已停用速度估算功能。啟用速度估算的區域必須正好有4個點。" + } + } + }, + "toast": { + "success": "區域({{zoneName}})已儲存。請重新啟動Frigate以套用更改。" + }, + "desc": { + "title": "區域可讓你定義畫面中的特定範圍,以判斷物件是否進入該範圍。", + "documentation": "文件" + }, + "documentTitle": "編輯區域 - Frigate" + }, + "motionMasks": { + "label": "移動遮罩", + "documentTitle": "編輯移動遮罩 - Frigate", + "desc": { + "title": "移動遮罩可防止不需要的移動觸發偵測。遮罩過多會令物件追蹤變得困難。", + "documentation": "文件" + }, + "edit": "編輯移動遮罩", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "polygonAreaTooLarge": { + "title": "移動遮罩覆蓋了鏡頭畫面{{polygonArea}}%。建議不要使用過大的遮罩。", + "tips": "移動遮罩無法阻止物件被偵測,應使用必要區域來限制範圍。", + "documentation": "閱讀文件" + }, + "context": { + "title": "移動遮罩用於防止某些不需要的移動(如樹枝晃動、鏡頭時間戳記)觸發偵測。移動遮罩應該非常謹慎使用,過度遮罩會令物件追蹤更加困難。", + "documentation": "閱讀文件" + }, + "add": "新增移動遮罩", + "toast": { + "success": { + "title": "{{polygonName}}已儲存。請重新啟動Frigate以套用更改。", + "noName": "移動遮罩已儲存。請重新啟動Frigate以套用更改。" + } + } + }, + "objectMasks": { + "label": "物件遮罩", + "desc": { + "documentation": "文件", + "title": "物件過濾遮罩根據位置,過濾指定物件類型的誤判偵測。" + }, + "add": "新增物件遮罩", + "context": "物件過濾遮罩根據位置,過濾指定物件類型的誤判偵測。", + "point_other": "{{count}}個點", + "clickDrawPolygon": "在圖片上點擊以繪製多邊形。", + "objects": { + "title": "物件", + "desc": "此物件遮罩適用的物件類型。", + "allObjectTypes": "所有物件類型" + }, + "toast": { + "success": { + "title": "{{polygonName}}已儲存。請重新啟動Frigate以套用更改。", + "noName": "物件遮罩已儲存。請重新啟動Frigate以套用更改。" + } + }, + "documentTitle": "編輯物件遮罩 - Frigate", + "edit": "編輯物件遮罩" + }, + "filter": { + "all": "所有遮罩與區域" + }, + "restart_required": "需要重新啟動(遮罩/區域已變更)", + "motionMaskLabel": "移動遮罩 {{number}}", + "objectMaskLabel": "物件遮罩 {{number}}({{label}}" + }, + "motionDetectionTuner": { + "title": "移動偵測調校器", + "desc": { + "title": "Frigate首先利用移動偵測作初步篩選,以判斷畫面中是否出現值得進行物件偵測的情況。", + "documentation": "閱讀移動調整指南" + }, + "Threshold": { + "title": "門檻", + "desc": "門檻值決定像素亮度變化多少才會被視為移動。預設:30" + }, + "contourArea": { + "title": "輪廓面積", + "desc": "輪廓面積值用來決定哪些變化像素群符合移動標準。預設:10" + }, + "improveContrast": { + "title": "改善對比度", + "desc": "改善黑暗場景的對比度。預設:開啟" + }, + "toast": { + "success": "移動設定已儲存。" + }, + "unsavedChanges": "{{camera}}的移動調校設定尚未儲存" + }, + "debug": { + "title": "除錯", + "objectList": "物件列表", + "noObjects": "沒有物件", + "boundingBoxes": { + "title": "邊框框線", + "colors": { + "info": "
  • 啟動時,系統會為每個物件標籤指派不同顏色
  • 深藍色幼線代表目前沒有偵測到該物件
  • 灰色幼線代表該物件被偵測為靜止狀態
  • 粗線代表該物件正被自動追蹤(若已啟用)
  • ", + "label": "物件邊框顏色" + }, + "desc": "顯示追蹤物件周圍的邊框" + }, + "zones": { + "title": "區域", + "desc": "顯示所有已定義區域的輪廓" + }, + "motion": { + "title": "移動方框", + "desc": "顯示偵測到移動的區域方框", + "tips": "

    移動方框

    畫面中偵測到移動的地方將會顯示亮紅色方框

    " + }, + "regions": { + "desc": "顯示屬於物件偵測器感興趣範圍的方框", + "title": "偵測區", + "tips": "

    偵測區方框

    畫面中屬於物件偵測器感興趣的地方將會顯示亮綠色方框,並且進行分析

    " + }, + "desc": "除錯畫面會即時顯示追蹤到的物件及統計資料。物件列表則顯示偵測到物件的延遲總結。", + "timestamp": { + "title": "時間戳記", + "desc": "在圖片上疊加時間戳記" + }, + "detectorDesc": "Frigate 使用你的偵測器({{detectors}})來偵測鏡頭影片串流中的物件。", + "debugging": "除錯中", + "mask": { + "title": "移動遮罩", + "desc": "顯示移動遮罩多邊形" + }, + "objectShapeFilterDrawing": { + "title": "物件形狀篩選繪圖", + "document": "閱讀文件 ", + "score": "分數", + "area": "面積", + "ratio": "比例", + "desc": "在圖片上畫矩形以查看面積與比例詳情", + "tips": "啟用此選項後,會於鏡頭畫面上繪製矩形,以顯示其面積及比例。這些數值可用於設定物件形狀過濾參數。" + }, + "openCameraWebUI": "打開 {{camera}} 的網頁介面", + "audio": { + "title": "音訊", + "noAudioDetections": "未偵測到音訊", + "score": "分數", + "currentRMS": "目前 RMS", + "currentdbFS": "目前 dbFS" + }, + "paths": { + "title": "軌跡", + "desc": "顯示追蹤物件軌跡上的重要點", + "tips": "

    軌跡


    線條同圓圈會標示追蹤物件整個生命周期中移動過的重要點。

    " + } + }, + "users": { + "management": { + "desc": "管理此Frigate個體的用戶帳戶。", + "title": "用戶管理" + }, + "addUser": "新增用戶", + "updatePassword": "更新密碼", + "toast": { + "success": { + "createUser": "成功建立用戶{{user}}", + "deleteUser": "成功刪除用戶{{user}}", + "roleUpdated": "成功更新{{user}}的角色", + "updatePassword": "成功更新密碼。" + }, + "error": { + "createUserFailed": "建立用戶失敗:{{errorMessage}}", + "roleUpdateFailed": "更新角色失敗:{{errorMessage}}", + "setPasswordFailed": "儲存密碼失敗:{{errorMessage}}", + "deleteUserFailed": "刪除用戶失敗:{{errorMessage}}" + } + }, + "table": { + "username": "用戶名稱", + "role": "角色", + "noUsers": "找不到用戶。", + "changeRole": "更改用戶角色", + "password": "密碼", + "deleteUser": "刪除用戶", + "actions": "操作" + }, + "dialog": { + "form": { + "user": { + "title": "用戶名稱", + "desc": "只允許使用字母、數字、句號及底線。", + "placeholder": "輸入用戶名稱" + }, + "password": { + "title": "密碼", + "placeholder": "輸入密碼", + "confirm": { + "placeholder": "確認密碼", + "title": "確認密碼" + }, + "strength": { + "title": "密碼強度: ", + "weak": "弱", + "medium": "中等", + "strong": "強", + "veryStrong": "非常強" + }, + "match": "密碼相符", + "notMatch": "密碼不相符" + }, + "newPassword": { + "confirm": { + "placeholder": "重新輸入新密碼" + }, + "title": "新密碼", + "placeholder": "輸入新密碼" + }, + "usernameIsRequired": "必須輸入用戶名稱", + "passwordIsRequired": "必須填寫密碼" + }, + "createUser": { + "title": "建立新用戶", + "desc": "新增用戶帳戶,並指定可存取Frigate介面各區域的角色。", + "usernameOnlyInclude": "用戶名稱只可包含字母、數字、句號或底線", + "confirmPassword": "請確認你的密碼" + }, + "deleteUser": { + "title": "刪除用戶", + "desc": "此操作無法還原,將會永久刪除用戶帳戶及所有相關資料。", + "warn": "確定要刪除{{username}}嗎?" + }, + "changeRole": { + "title": "更改用戶角色", + "desc": "更新{{username}}的權限", + "roleInfo": { + "intro": "為此用戶選擇合適的角色:", + "adminDesc": "可使用所有功能。", + "viewer": "觀看者", + "viewerDesc": "只限使用即時儀表板、審查、瀏覽及匯出功能。", + "admin": "管理員", + "customDesc": "自訂角色,具特定鏡頭存取權限。" + }, + "select": "選擇角色" + }, + "passwordSetting": { + "setPassword": "設定密碼", + "updatePassword": "更新{{username}}的密碼", + "desc": "建立強密碼以保障此帳戶安全。", + "cannotBeEmpty": "密碼不能留空", + "doNotMatch": "密碼不相符" + } + }, + "title": "用戶" + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate可原生向你的裝置推送通知,無論在瀏覽器中運行或安裝為PWA。", + "documentation": "閱讀文件" + }, + "notificationUnavailable": { + "title": "無法使用通知功能", + "desc": "網頁推送通知需在安全環境下運作(https://…),這是瀏覽器的限制。請透過安全連線存取Frigate以使用通知功能。", + "documentation": "閱讀文件" + }, + "globalSettings": { + "title": "全域設定", + "desc": "暫停所有已註冊裝置上特定鏡頭的通知。" + }, + "email": { + "placeholder": "例如:example@email.com", + "desc": "需要提供有效的電郵地址,若推送服務出現問題,將會透過此地址通知你。", + "title": "電郵地址" + }, + "cameras": { + "title": "鏡頭", + "noCameras": "沒有可用鏡頭", + "desc": "選擇啟用通知功能的鏡頭。" + }, + "deviceSpecific": "裝置專屬設定", + "registerDevice": "登記此裝置", + "unregisterDevice": "取消登記此裝置", + "sendTestNotification": "發送測試通知", + "active": "通知功能已啟用", + "suspended": "通知功能已暫停{{time}}", + "suspendTime": { + "10minutes": "暫停10分鐘", + "12hours": "暫停12小時", + "30minutes": "暫停30分鐘", + "24hours": "暫停24小時", + "5minutes": "暫停5分鐘", + "1hour": "暫停1小時", + "untilRestart": "暫停至重新啟動", + "suspend": "暫停" + }, + "toast": { + "error": { + "registerFailed": "儲存通知登記失敗。" + }, + "success": { + "registered": "成功登記通知功能。必須重新啟動Frigate後,才能發送任何通知(包括測試通知)。", + "settingSaved": "通知設定已儲存。" + } + }, + "cancelSuspension": "取消暫停", + "unsavedRegistrations": "通知註冊尚未儲存", + "unsavedChanges": "通知設定更改尚未儲存" + }, + "frigatePlus": { + "title": "Frigate+設定", + "apiKey": { + "title": "Frigate+ API金鑰", + "validated": "已偵測並驗證Frigate+ API金鑰", + "notValidated": "未偵測到Frigate+ API金鑰或驗證失敗", + "desc": "Frigate+ API金鑰可啟用與Frigate+服務的整合功能。", + "plusLink": "了解更多Frigate+資料" + }, + "snapshotConfig": { + "title": "快照設定", + "documentation": "閱讀文件", + "table": { + "camera": "鏡頭", + "snapshots": "快照", + "cleanCopySnapshots": "clean_copy 快照" + }, + "cleanCopyWarning": "部分鏡頭已啟用快照,但未啟用乾淨副本。你必須於快照設定中啟用clean_copy,才能從這些鏡頭提交影像至Frigate+。", + "desc": "提交至Frigate+需要在設定中同時啟用快照及clean_copy快照功能。" + }, + "modelInfo": { + "title": "模型資料", + "modelType": "模型類型", + "trainDate": "訓練日期", + "baseModel": "基礎模型", + "plusModelType": { + "baseModel": "基礎模型", + "userModel": "微調" + }, + "supportedDetectors": "支援的偵測器", + "cameras": "鏡頭", + "availableModels": "可用模型", + "loadingAvailableModels": "正在載入可用模型…", + "modelSelect": "可於此選擇你在Frigate+上的可用模型。請注意,只能選擇與當前偵測器設定相容的模型。", + "loading": "正在載入模型資料…", + "error": "載入模型資料失敗" + }, + "toast": { + "error": "儲存設定變更失敗:{{errorMessage}}", + "success": "Frigate+設定已儲存。請重新啟動Frigate以套用更改。" + }, + "restart_required": "需要重新啟動(已更改Frigate+模型)", + "unsavedChanges": "Frigate+ 設定更改尚未儲存" + }, + "enrichments": { + "faceRecognition": { + "modelSize": { + "large": { + "title": "大型", + "desc": "使用大型模型會採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上運行。" + }, + "desc": "用於人臉識別的模型大小。", + "label": "模型大小", + "small": { + "title": "小型", + "desc": "使用小型模型會採用 FaceNet 臉部嵌入模型,在大多數 CPU 上能有效運行。" + } + }, + "desc": "人臉識別功能允許為人物分配名字,當辨識到他們的臉孔時,Frigate 會將名字加到子標籤上。此資訊會顯示於介面、篩選器及通知中。", + "title": "人臉識別", + "readTheDocumentation": "閱讀文件" + }, + "birdClassification": { + "title": "鳥類分類", + "desc": "鳥類分類會使用量化 Tensorflow 模型識別已知鳥類。當辨識到已知鳥類時,牠的常見名稱會加到子標籤上。此資訊會顯示在介面、篩選器及通知中。" + }, + "semanticSearch": { + "desc": "Frigate 的語意搜尋功能讓你可以利用影像本身、自訂文字描述,或自動產生的描述,在審查項目中尋找已追蹤的物件。", + "reindexNow": { + "confirmDesc": "你確定要重建索引所有已追蹤物件的嵌入向量嗎?這個過程會在背景運行,但可能會用盡你的 CPU,而且需要一定時間。你可以在「瀏覽」頁面查看進度。", + "label": "立即重建索引", + "desc": "重建索引會為所有已追蹤物件重新生成嵌入向量。這個過程會在背景運行,可能會用盡你的 CPU,所需時間取決於已追蹤物件的數量。", + "confirmTitle": "確認重建索引", + "confirmButton": "重建索引", + "success": "重建索引已成功開始。", + "alreadyInProgress": "重建索引已在進行中。", + "error": "啟動重建索引失敗:{{errorMessage}}" + }, + "title": "語意搜尋", + "readTheDocumentation": "閱讀文件", + "modelSize": { + "label": "模型大小", + "desc": "用於語意搜尋的模型大小。", + "small": { + "title": "小型", + "desc": "使用小型模型會採用量化版本,較少佔用 RAM,在 CPU 上運行更快,而嵌入品質的差異非常細微。" + }, + "large": { + "title": "大型", + "desc": "使用大型模型會採用完整的 Jina 模型,並在適用情況下自動於 GPU 上運行。" + } + } + }, + "licensePlateRecognition": { + "title": "車牌識別", + "desc": "Frigate 可以識別車輛上的車牌,自動將偵測到的字元加到已辨識車牌欄位,或將已知名稱加到屬於車輛類型的物件的子標籤上。常見用途包括讀取駛入車道或在街道上駛過的車輛的車牌。", + "readTheDocumentation": "閱讀文件" + }, + "title": "進階功能設定", + "unsavedChanges": "未儲存進階功能設定變更", + "restart_required": "需要重新啟動(進階功能設定已變更)", + "toast": { + "success": "進階功能設定已儲存。請重新啟動 Frigate 以套用你的更改。", + "error": "儲存設定變更失敗:{{errorMessage}}" + } + }, + "roles": { + "management": { + "title": "觀察者角色管理", + "desc": "管理自訂觀察者角色及其對此 Frigate 實例的鏡頭存取權限。" + }, + "addRole": "新增角色", + "table": { + "role": "角色", + "cameras": "鏡頭", + "actions": "操作", + "noRoles": "未找到自訂角色。", + "editCameras": "編輯鏡頭", + "deleteRole": "刪除角色" + }, + "toast": { + "success": { + "createRole": "角色 {{role}} 已成功建立", + "updateCameras": "角色 {{role}} 的鏡頭已更新", + "deleteRole": "角色 {{role}} 已成功刪除", + "userRolesUpdated_other": "{{count}} 位使用者被更新為「觀察者」角色,將可存取所有鏡頭。" + }, + "error": { + "createRoleFailed": "建立角色失敗:{{errorMessage}}", + "updateCamerasFailed": "更新鏡頭失敗:{{errorMessage}}", + "deleteRoleFailed": "刪除角色失敗:{{errorMessage}}", + "userUpdateFailed": "更新使用者角色失敗:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "建立新角色", + "desc": "新增角色,並指定鏡頭存取權限。" + }, + "editCameras": { + "title": "編輯角色鏡頭", + "desc": "更新角色 {{role}} 的鏡頭存取權限。" + }, + "deleteRole": { + "title": "刪除角色", + "desc": "此操作無法復原。將永久刪除該角色,並將使用此角色的所有使用者改為「觀察者」角色,可存取所有鏡頭。", + "warn": "你確定要刪除 {{role}} 嗎?", + "deleting": "正在刪除…" + }, + "form": { + "role": { + "title": "角色名稱", + "placeholder": "輸入角色名稱", + "desc": "只允許字母、數字、句號或底線。", + "roleIsRequired": "必須填寫角色名稱", + "roleOnlyInclude": "角色名稱只可包含字母、數字、句號或底線", + "roleExists": "已有相同名稱的角色存在。" + }, + "cameras": { + "title": "鏡頭", + "desc": "選擇此角色可存取的鏡頭。至少需要選擇一個鏡頭。", + "required": "至少需要選擇一個鏡頭。" + } + } + } + }, + "triggers": { + "documentTitle": "觸發器", + "semanticSearch": { + "title": "語意搜尋已停用", + "desc": "必須啟用語意搜尋才能使用觸發器。" + }, + "management": { + "title": "觸發器管理", + "desc": "管理 {{camera}} 的觸發器。使用縮圖類型可對與所選追蹤物件相似的縮圖觸發,使用描述類型可對與你指定文字描述相似的事件觸發。" + }, + "addTrigger": "新增觸發器", + "table": { + "name": "名稱", + "type": "類型", + "content": "內容", + "threshold": "閾值", + "actions": "操作", + "noTriggers": "此鏡頭尚未設定任何觸發器。", + "edit": "編輯", + "deleteTrigger": "刪除觸發器", + "lastTriggered": "上次觸發" + }, + "type": { + "thumbnail": "縮圖", + "description": "描述" + }, + "actions": { + "alert": "標記為警報", + "notification": "發送通知" + }, + "dialog": { + "createTrigger": { + "title": "建立觸發器", + "desc": "為鏡頭 {{camera}} 建立觸發器" + }, + "editTrigger": { + "title": "編輯觸發器", + "desc": "編輯鏡頭 {{camera}} 的觸發器設定" + }, + "deleteTrigger": { + "title": "刪除觸發器", + "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作無法復原。" + }, + "form": { + "name": { + "title": "名稱", + "placeholder": "輸入觸發器名稱", + "error": { + "minLength": "名稱至少需 2 個字元。", + "invalidCharacters": "名稱只可包含字母、數字、底線及連字符。", + "alreadyExists": "此鏡頭已有相同名稱的觸發器。" + } + }, + "enabled": { + "description": "啟用或停用此觸發器" + }, + "type": { + "title": "類型", + "placeholder": "選擇觸發器類型" + }, + "friendly_name": { + "title": "顯示名稱", + "placeholder": "為此觸發器命名或描述", + "description": "此觸發器的可選顯示名稱或描述文字。" + }, + "content": { + "title": "內容", + "imagePlaceholder": "選擇圖片", + "textPlaceholder": "輸入文字內容", + "imageDesc": "選擇圖片,當偵測到相似圖片時觸發此動作。", + "textDesc": "輸入文字,當偵測到相似追蹤物件描述時觸發此動作。", + "error": { + "required": "必須提供內容。" + } + }, + "threshold": { + "title": "閾值", + "error": { + "min": "閾值至少為 0", + "max": "閾值最多為 1" + } + }, + "actions": { + "title": "操作", + "desc": "預設情況下,Frigate 會對所有觸發器發送 MQTT 訊息。可選擇額外操作,在觸發器觸發時執行。", + "error": { + "min": "至少需要選擇一個操作。" + } + } + } + }, + "toast": { + "success": { + "createTrigger": "觸發器 {{name}} 已成功建立。", + "updateTrigger": "觸發器 {{name}} 已成功更新。", + "deleteTrigger": "觸發器 {{name}} 已成功刪除。" + }, + "error": { + "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}", + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}", + "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}" + } + } + }, + "cameraWizard": { + "title": "新增鏡頭", + "description": "請依照以下步驟,將新鏡頭加入 Frigate。", + "steps": { + "nameAndConnection": "名稱與連線", + "streamConfiguration": "串流設定", + "validationAndTesting": "驗證與測試" + }, + "save": { + "success": "已成功儲存新鏡頭 {{cameraName}}。", + "failure": "儲存 {{cameraName}} 時發生錯誤。" + }, + "testResultLabels": { + "resolution": "解析度", + "video": "影像", + "audio": "音訊", + "fps": "每秒影格數" + }, + "commonErrors": { + "noUrl": "請輸入有效的串流網址", + "testFailed": "串流測試失敗:{{error}}" + }, + "step1": { + "description": "輸入鏡頭詳細資料並測試連線。", + "cameraName": "鏡頭名稱", + "cameraNamePlaceholder": "例如:front_door 或 back_yard_overview", + "host": "主機名稱/IP 位址", + "port": "連接埠", + "username": "用戶名稱", + "usernamePlaceholder": "可選", + "password": "密碼", + "passwordPlaceholder": "選擇傳輸協定", + "selectTransport": "選擇傳輸協定", + "cameraBrand": "鏡頭品牌", + "selectBrand": "選擇鏡頭品牌以套用 URL 模板", + "customUrl": "自訂串流網址", + "brandInformation": "品牌資訊", + "brandUrlFormat": "適用於 RTSP 網址格式如下的鏡頭:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://username:password@host:port/path", + "testConnection": "測試連線", + "testSuccess": "連線測試成功!", + "testFailed": "連線測試失敗,請檢查輸入內容後再試一次。", + "streamDetails": "串流詳情", + "warnings": { + "noSnapshot": "無法從設定的串流中擷取快照。" + }, + "errors": { + "brandOrCustomUrlRequired": "請選擇包含主機/IP 的鏡頭品牌,或選擇「其他」並輸入自訂網址", + "nameRequired": "必須輸入鏡頭名稱", + "nameLength": "鏡頭名稱長度不得超過 64 個字元", + "invalidCharacters": "鏡頭名稱包含無效字元", + "nameExists": "鏡頭名稱已存在", + "brands": { + "reolink-rtsp": "不建議使用 Reolink RTSP。建議在鏡頭設定中啟用 HTTP,並重新啟動鏡頭設定精靈。" + } + }, + "docs": { + "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" + } + }, + "step2": { + "description": "設定鏡頭的串流角色,並可新增額外串流。", + "streamsTitle": "鏡頭串流", + "addStream": "新增串流", + "addAnotherStream": "新增另一個串流", + "streamTitle": "串流 {{number}}", + "streamUrl": "串流網址", + "streamUrlPlaceholder": "rtsp://username:password@host:port/path", + "url": "網址", + "resolution": "解析度", + "selectResolution": "選擇解析度", + "quality": "畫質", + "selectQuality": "選擇畫質", + "roles": "角色", + "roleLabels": { + "detect": "物件偵測", + "record": "錄影", + "audio": "音訊" + }, + "testStream": "測試連線", + "testSuccess": "串流測試成功!", + "testFailed": "串流測試失敗", + "testFailedTitle": "測試失敗", + "connected": "已連線", + "notConnected": "未連線", + "featuresTitle": "功能", + "go2rtc": "減少與鏡頭的連線數", + "detectRoleWarning": "至少需有一個串流設定為「偵測」角色才能繼續。", + "rolesPopover": { + "title": "串流角色", + "detect": "用於物件偵測的主要影像來源。", + "record": "根據設定儲存影片片段。", + "audio": "用於音訊偵測的來源。" + }, + "featuresPopover": { + "title": "串流功能", + "description": "使用 go2rtc 轉串流以減少與鏡頭的直接連線。" + } + }, + "step3": { + "description": "在儲存新鏡頭前進行最後驗證與分析。請先連線所有串流後再儲存。", + "validationTitle": "串流驗證", + "connectAllStreams": "連線所有串流", + "reconnectionSuccess": "重新連線成功。", + "reconnectionPartial": "部分串流重新連線失敗。", + "streamUnavailable": "無法預覽串流", + "reload": "重新載入", + "connecting": "正在連線...", + "streamTitle": "串流 {{number}}", + "valid": "有效", + "failed": "失敗", + "notTested": "未測試", + "connectStream": "連線", + "connectingStream": "連線中", + "disconnectStream": "中斷連線", + "estimatedBandwidth": "預計頻寬", + "roles": "角色", + "none": "無", + "error": "錯誤", + "streamValidated": "串流 {{number}} 驗證成功", + "streamValidationFailed": "串流 {{number}} 驗證失敗", + "saveAndApply": "儲存新鏡頭", + "saveError": "設定無效,請檢查你的設定。", + "issues": { + "title": "串流驗證", + "videoCodecGood": "影片編碼格式為 {{codec}}。", + "audioCodecGood": "音訊編碼格式為 {{codec}}。", + "noAudioWarning": "此串流未偵測到音訊,錄影將不會有聲音。", + "audioCodecRecordError": "錄影要支援音訊,必須使用 AAC 編碼。", + "audioCodecRequired": "要支援音訊偵測,必須有音訊串流。", + "restreamingWarning": "若減少錄影串流與鏡頭的連線,CPU 使用率可能會略微增加。", + "dahua": { + "substreamWarning": "子串流 1 被鎖定為低解析度。許多 Dahua / Amcrest / EmpireTech 鏡頭支援額外子串流,需要在鏡頭設定中啟用。建議如有可用,檢查並使用這些子串流。" + }, + "hikvision": { + "substreamWarning": "子串流 1 被鎖定為低解析度。許多 Hikvision 鏡頭支援額外子串流,需要在鏡頭設定中啟用。建議如有可用,檢查並使用這些子串流。" + } + } + } + }, + "cameraManagement": { + "title": "管理鏡頭", + "addCamera": "新增鏡頭", + "editCamera": "編輯鏡頭:", + "selectCamera": "選擇鏡頭", + "backToSettings": "返回鏡頭設定", + "streams": { + "title": "啟用/停用鏡頭", + "desc": "暫時停用鏡頭,直到 Frigate 重新啟動。停用鏡頭會完全停止 Frigate 對該鏡頭串流的處理。偵測、錄影及除錯功能將無法使用。
    注意:這不會停用 go2rtc 轉串流。" + }, + "cameraConfig": { + "add": "新增鏡頭", + "edit": "編輯鏡頭", + "description": "設定鏡頭,包括串流輸入與角色。", + "name": "鏡頭名稱", + "nameRequired": "必須輸入鏡頭名稱", + "nameLength": "鏡頭名稱長度不得超過 64 個字元。", + "namePlaceholder": "例如:front_door 或 back_yard_overview", + "enabled": "已啟用", + "ffmpeg": { + "inputs": "輸入串流", + "path": "串流路徑", + "pathRequired": "必須提供串流路徑", + "pathPlaceholder": "rtsp://...", + "roles": "角色", + "rolesRequired": "至少需要一個角色", + "rolesUnique": "每個角色(音訊 / 偵測 / 錄影)只能分配給一個串流", + "addInput": "新增輸入串流", + "removeInput": "移除輸入串流", + "inputsRequired": "至少需要一個輸入串流" + }, + "go2rtcStreams": "go2rtc 串流", + "streamUrls": "串流網址", + "addUrl": "新增網址", + "addGo2rtcStream": "新增 go2rtc 串流", + "toast": { + "success": "鏡頭 {{cameraName}} 已成功儲存" + } + } + }, + "cameraReview": { + "title": "鏡頭檢視設定", + "object_descriptions": { + "title": "生成式 AI 物件描述", + "desc": "暫時啟用/停用此鏡頭的生成式 AI 物件描述。停用時,系統不會為此鏡頭的追蹤物件生成 AI 描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審查描述", + "desc": "暫時啟用/停用此鏡頭的生成式 AI 審查描述。停用時,系統不會為此鏡頭的審查項目生成 AI 描述。" + }, + "review": { + "title": "審查", + "desc": "暫時啟用/停用此鏡頭的警報與偵測,直到 Frigate 重啟。停用時,不會產生新的審查項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "title": "審查分類", + "desc": "Frigate 將審查項目分類為警報與偵測。預設情況下,所有 personcar 物件會視為警報。你可以透過設定對應區域來精確分類審查項目。", + "noDefinedZones": "此鏡頭未定義任何區域。", + "objectAlertsTips": "在{{cameraName}}上所有{{alertsLabels}}物件將會顯示為警報。", + "zoneObjectAlertsTips": "在{{cameraName}}的{{zone}}區域偵測到的所有{{alertsLabels}}物件將會顯示為警報。", + "objectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "zoneObjectDetectionsTips": { + "text": "在{{cameraName}}的{{zone}}區域內所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。", + "notSelectDetections": "無論位於哪個區域,在{{cameraName}}的{{zone}}區域偵測到、但未分類為警報的{{detectionsLabels}}物件將會顯示為偵測結果。", + "regardlessOfZoneObjectDetectionsTips": "無論位於哪個區域,在{{cameraName}}上所有未分類的{{detectionsLabels}}物件將會顯示為偵測結果。" + }, + "unsavedChanges": "{{camera}}的審查分類設定尚未儲存", + "selectAlertsZones": "選擇警報的區域", + "selectDetectionsZones": "選擇偵測的區域", + "limitDetections": "限制偵測至特定區域", + "toast": { + "success": "審查分類設定已儲存。請重新啟動Frigate以套用更改。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/system.json new file mode 100644 index 0000000..6b52401 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/yue-Hant/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "鏡頭統計 - Frigate", + "storage": "儲存裝置統計 - Frigate", + "general": "一般統計 - Frigate", + "enrichments": "進階功能統計 - Frigate", + "logs": { + "frigate": "Frigate 日誌 - Frigate", + "nginx": "Nginx 日誌 - Frigate", + "go2rtc": "Go2RTC 日誌 - Frigate" + } + }, + "title": "系統", + "metrics": "系統指標", + "logs": { + "download": { + "label": "下載日誌" + }, + "type": { + "timestamp": "時間戳記", + "tag": "標籤", + "message": "訊息", + "label": "類型" + }, + "tips": "正在從伺服器串流日誌", + "toast": { + "error": { + "fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}", + "whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}" + } + }, + "copy": { + "error": "無法將日誌複製到剪貼簿", + "label": "複製到剪貼簿", + "success": "已將日誌複製到剪貼簿" + } + }, + "general": { + "detector": { + "inferenceSpeed": "偵測器推理速度", + "memoryUsage": "偵測器記憶體使用量", + "title": "偵測器", + "cpuUsage": "偵測器 CPU 使用率", + "temperature": "偵測器溫度", + "cpuUsageInformation": "CPU 用於準備偵測模型的輸入同輸出數據。此數值不計算推理運算,即使使用 GPU 或加速器也是一樣。" + }, + "hardwareInfo": { + "gpuUsage": "GPU 使用率", + "gpuInfo": { + "vainfoOutput": { + "processOutput": "程序輸出:", + "processError": "程序錯誤:", + "title": "Vainfo 輸出", + "returnCode": "返回代碼:{{code}}" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 輸出", + "vbios": "VBios 資訊:{{vbios}}", + "cudaComputerCapability": "CUDA 計算能力:{{cuda_compute}}", + "name": "名稱:{{name}}", + "driver": "驅動程式:{{driver}}" + }, + "closeInfo": { + "label": "關閉 GPU 資訊" + }, + "toast": { + "success": "已將 GPU 資訊複製到剪貼簿" + }, + "copyInfo": { + "label": "複製 GPU 資訊" + } + }, + "title": "硬件資訊", + "npuUsage": "NPU 使用率", + "gpuMemory": "GPU 記憶體", + "gpuEncoder": "GPU 編碼器", + "gpuDecoder": "GPU 解碼器", + "npuMemory": "NPU 記憶體" + }, + "otherProcesses": { + "title": "其他程序", + "processCpuUsage": "程序 CPU 使用率", + "processMemoryUsage": "程序記憶體使用量" + }, + "title": "一般" + }, + "storage": { + "title": "儲存裝置", + "overview": "概覽", + "recordings": { + "title": "錄影檔案", + "earliestRecording": "最早可用錄影檔案:", + "tips": "此數值代表 Frigate 資料庫中錄影檔案的總儲存使用量。Frigate 不會追蹤磁碟上所有檔案的儲存使用量。" + }, + "cameraStorage": { + "camera": "鏡頭", + "unusedStorageInformation": "未使用儲存資訊", + "storageUsed": "已使用儲存", + "bandwidth": "每小時使用量", + "unused": { + "tips": "若您的磁碟中存有其他檔案,該數值可能無法準確反映 Frigate 可用的空間。Frigate 只追蹤其錄影檔案的儲存使用量。", + "title": "未使用" + }, + "title": "鏡頭儲存", + "percentageOfTotalUsed": "佔總量百分比" + }, + "shm": { + "title": "SHM(共享記憶體) 分配", + "warning": "目前 SHM 大小 {{total}}MB 太小,請增加至至少 {{min_shm}}MB。" + } + }, + "cameras": { + "info": { + "streamDataFromFFPROBE": "串流資料是透過 ffprobe 取得。", + "fetching": "正在取得鏡頭資料", + "video": "影片:", + "codec": "編碼器:", + "resolution": "解像度:", + "tips": { + "title": "鏡頭詳細資訊" + }, + "stream": "串流 {{idx}}", + "audio": "音訊:", + "fps": "每秒影格數 (FPS):", + "unknown": "未知", + "error": "錯誤:{{error}}", + "cameraProbeInfo": "{{camera}} 鏡頭詳細資訊", + "aspectRatio": "長寬比" + }, + "framesAndDetections": "畫面 / 偵測", + "label": { + "camera": "鏡頭", + "detect": "偵測", + "skipped": "略過", + "ffmpeg": "FFmpeg", + "capture": "讀取影像", + "overallFramesPerSecond": "整體每秒畫面數", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraDetect": "{{camName}} 偵測", + "cameraFramesPerSecond": "{{camName}} 每秒畫面數", + "cameraDetectionsPerSecond": "{{camName}} 每秒偵測次數", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒略過偵測次數", + "overallSkippedDetectionsPerSecond": "整體每秒略過偵測次數", + "cameraCapture": "{{camName}} 讀取影像", + "overallDetectionsPerSecond": "整體每秒偵測次數" + }, + "title": "鏡頭", + "overview": "概覽", + "toast": { + "success": { + "copyToClipboard": "已將鏡頭資料複製到剪貼簿。" + }, + "error": { + "unableToProbeCamera": "無法取得鏡頭資料:{{errorMessage}}" + } + } + }, + "lastRefreshed": "最後更新: ", + "stats": { + "detectIsSlow": "{{detect}} 偵測速度慢 ({{speed}} 毫秒)", + "detectIsVerySlow": "{{detect}} 偵測速度非常慢 ({{speed}} 毫秒)", + "cameraIsOffline": "{{camera}} 已離線", + "detectHighCpuUsage": "{{camera}} 的偵測 CPU 使用率過高 ({{detectAvg}}%)", + "healthy": "系統運作正常", + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率過高 ({{ffmpegAvg}}%)", + "reindexingEmbeddings": "重新索引嵌入資料 (已完成 {{processed}}%)", + "shmTooLow": "/dev/shm 分配({{total}} MB)太小,請增加至至少 {{min}} MB。" + }, + "enrichments": { + "title": "進階功能", + "infPerSecond": "每秒推理次數", + "embeddings": { + "image_embedding": "圖片嵌入", + "face_embedding_speed": "人臉嵌入速度", + "face_recognition_speed": "人臉辨識速度", + "plate_recognition_speed": "車牌辨識速度", + "face_recognition": "人臉辨識", + "text_embedding": "文字嵌入", + "yolov9_plate_detection": "YOLOv9 車牌偵測", + "text_embedding_speed": "文字嵌入速度", + "yolov9_plate_detection_speed": "YOLOv9 車牌偵測速度", + "plate_recognition": "車牌辨識", + "image_embedding_speed": "圖片嵌入速度" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/audio.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/audio.json new file mode 100644 index 0000000..848418f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/audio.json @@ -0,0 +1,503 @@ +{ + "speech": "谈话", + "babbling": "喋喋不休", + "yell": "大喊", + "bellow": "吼叫", + "whoop": "欢呼", + "whispering": "耳语", + "laughter": "笑声", + "snicker": "窃笑", + "crying": "哭泣", + "sigh": "叹息", + "singing": "唱歌", + "choir": "合唱", + "yodeling": "山歌", + "chant": "吟唱", + "mantra": "咒语", + "child_singing": "儿童歌唱", + "synthetic_singing": "合成歌声", + "rapping": "说唱", + "humming": "哼唱", + "groan": "呻吟", + "grunt": "咕哝", + "whistling": "口哨", + "breathing": "呼吸", + "wheeze": "喘息", + "snoring": "打鼾", + "gasp": "倒抽气", + "pant": "喘气", + "snort": "哼声", + "cough": "咳嗽", + "throat_clearing": "清嗓子", + "sneeze": "打喷嚏", + "sniff": "抽鼻子", + "run": "跑步", + "shuffle": "拖步", + "footsteps": "脚步声", + "chewing": "咀嚼", + "biting": "咬", + "gargling": "漱口", + "stomach_rumble": "肚子咕噜", + "burping": "打嗝", + "hiccup": "打嗝", + "fart": "放屁", + "hands": "手", + "finger_snapping": "打响指", + "clapping": "鼓掌", + "heartbeat": "心跳", + "heart_murmur": "心脏杂音", + "cheering": "欢呼", + "applause": "掌声", + "chatter": "闲聊", + "crowd": "人群", + "children_playing": "儿童玩耍", + "animal": "动物", + "pets": "宠物", + "dog": "狗", + "bark": "狗叫", + "yip": "吠叫", + "howl": "嚎叫", + "bow_wow": "汪汪", + "growling": "咆哮", + "whimper_dog": "狗呜咽", + "cat": "猫", + "purr": "咕噜", + "meow": "喵喵", + "hiss": "嘶嘶声", + "caterwaul": "猫叫春", + "livestock": "牲畜", + "horse": "马", + "clip_clop": "蹄声", + "neigh": "嘶鸣", + "cattle": "牛", + "moo": "哞哞", + "cowbell": "牛铃", + "pig": "猪", + "oink": "哼哼", + "goat": "山羊", + "bleat": "咩咩", + "sheep": "绵羊", + "fowl": "家禽", + "chicken": "鸡", + "cluck": "咯咯", + "cock_a_doodle_doo": "喔喔", + "turkey": "火鸡", + "gobble": "咯咯", + "duck": "鸭子", + "quack": "嘎嘎", + "goose": "鹅", + "honk": "鸣笛/鹅叫声", + "wild_animals": "野生动物", + "roaring_cats": "吼叫的猫科动物", + "roar": "吼叫", + "bird": "鸟", + "chirp": "啾啾", + "squawk": "啼叫", + "pigeon": "鸽子", + "coo": "咕咕", + "crow": "乌鸦", + "caw": "呱呱", + "owl": "猫头鹰", + "hoot": "呜呜", + "flapping_wings": "翅膀拍打", + "dogs": "狗群", + "rats": "老鼠", + "mouse": "鼠标", + "patter": "啪嗒声", + "insect": "昆虫", + "cricket": "蟋蟀", + "mosquito": "蚊子", + "fly": "苍蝇", + "buzz": "嗡嗡", + "frog": "青蛙", + "croak": "呱呱", + "snake": "蛇", + "rattle": "响尾", + "whale_vocalization": "鲸鱼叫声", + "music": "音乐", + "musical_instrument": "乐器", + "plucked_string_instrument": "弹拨乐器", + "guitar": "吉他", + "electric_guitar": "电吉他", + "bass_guitar": "贝斯", + "acoustic_guitar": "原声吉他", + "steel_guitar": "钢弦吉他", + "tapping": "敲击", + "strum": "扫弦", + "banjo": "班卓琴", + "sitar": "西塔琴", + "mandolin": "曼陀林", + "zither": "古筝", + "ukulele": "尤克里里", + "keyboard": "键盘", + "piano": "钢琴", + "electric_piano": "电钢琴", + "organ": "风琴", + "electronic_organ": "电子琴", + "hammond_organ": "哈蒙德风琴", + "synthesizer": "合成器", + "sampler": "采样器", + "harpsichord": "大键琴", + "percussion": "打击乐器", + "drum_kit": "架子鼓", + "drum_machine": "鼓机", + "drum": "鼓", + "snare_drum": "军鼓", + "rimshot": "鼓边击", + "drum_roll": "滚鼓", + "bass_drum": "大鼓", + "timpani": "定音鼓", + "tabla": "塔布拉鼓", + "cymbal": "钹", + "hi_hat": "踩镲", + "wood_block": "木鱼", + "tambourine": "铃鼓", + "maraca": "沙锤", + "gong": "锣", + "tubular_bells": "管钟", + "mallet_percussion": "槌击打击乐器", + "marimba": "马林巴", + "glockenspiel": "钟琴", + "vibraphone": "颤音琴", + "steelpan": "钢鼓", + "orchestra": "管弦乐队", + "brass_instrument": "铜管乐器", + "french_horn": "圆号", + "trumpet": "小号", + "trombone": "长号", + "bowed_string_instrument": "弓弦乐器", + "string_section": "弦乐组", + "violin": "小提琴", + "pizzicato": "拨弦", + "cello": "大提琴", + "double_bass": "低音提琴", + "wind_instrument": "管乐器", + "flute": "长笛", + "saxophone": "萨克斯", + "clarinet": "单簧管", + "harp": "竖琴", + "bell": "铃", + "church_bell": "教堂钟", + "jingle_bell": "铃铛", + "bicycle_bell": "自行车铃", + "tuning_fork": "音叉", + "chime": "风铃", + "wind_chime": "风铃", + "harmonica": "口琴", + "accordion": "手风琴", + "bagpipes": "风笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷门琴", + "singing_bowl": "颂钵", + "scratching": "刮擦声", + "pop_music": "流行音乐", + "hip_hop_music": "嘻哈音乐", + "beatboxing": "人声节拍", + "rock_music": "摇滚音乐", + "heavy_metal": "重金属", + "punk_rock": "朋克摇滚", + "grunge": "垃圾摇滚", + "progressive_rock": "前卫摇滚", + "rock_and_roll": "摇滚乐", + "psychedelic_rock": "迷幻摇滚", + "rhythm_and_blues": "节奏布鲁斯", + "soul_music": "灵魂乐", + "reggae": "雷鬼", + "country": "乡村音乐", + "swing_music": "摇摆乐", + "bluegrass": "蓝草音乐", + "funk": "放克", + "folk_music": "民谣", + "middle_eastern_music": "中东音乐", + "jazz": "爵士乐", + "disco": "迪斯科", + "classical_music": "古典音乐", + "opera": "歌剧", + "electronic_music": "电子音乐", + "house_music": "浩室音乐", + "techno": "科技舞曲", + "dubstep": "回响贝斯", + "drum_and_bass": "鼓打贝斯", + "electronica": "电子乐", + "electronic_dance_music": "电子舞曲", + "ambient_music": "环境音乐", + "trance_music": "迷幻舞曲", + "music_of_latin_america": "拉丁美洲音乐", + "salsa_music": "萨尔萨", + "flamenco": "弗拉门戈", + "blues": "蓝调", + "music_for_children": "儿童音乐", + "new-age_music": "新世纪音乐", + "vocal_music": "声乐", + "a_capella": "无伴奏合唱", + "music_of_africa": "非洲音乐", + "afrobeat": "非洲节拍", + "christian_music": "基督教音乐", + "gospel_music": "福音音乐", + "music_of_asia": "亚洲音乐", + "carnatic_music": "卡纳提克音乐", + "music_of_bollywood": "宝莱坞音乐", + "ska": "斯卡", + "traditional_music": "传统音乐", + "independent_music": "独立音乐", + "song": "歌曲", + "background_music": "背景音乐", + "theme_music": "主题音乐", + "jingle": "广告歌", + "soundtrack_music": "配乐", + "lullaby": "摇篮曲", + "video_game_music": "电子游戏音乐", + "christmas_music": "圣诞音乐", + "dance_music": "舞曲", + "wedding_music": "婚礼音乐", + "happy_music": "欢快音乐", + "sad_music": "悲伤音乐", + "tender_music": "温柔音乐", + "exciting_music": "激动音乐", + "angry_music": "愤怒音乐", + "scary_music": "恐怖音乐", + "wind": "风", + "rustling_leaves": "树叶沙沙声", + "wind_noise": "风声", + "thunderstorm": "雷暴", + "thunder": "雷声", + "water": "水", + "rain": "雨", + "raindrop": "雨滴", + "rain_on_surface": "雨打表面", + "stream": "溪流", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸汽", + "gurgling": "汩汩声", + "fire": "火", + "crackle": "噼啪声", + "vehicle": "车辆", + "boat": "船", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "摩托艇", + "ship": "轮船", + "motor_vehicle": "机动车", + "car": "汽车", + "toot": "鸣笛", + "car_alarm": "汽车警报", + "power_windows": "电动车窗", + "skidding": "轮胎打滑", + "tire_squeal": "轮胎尖叫", + "car_passing_by": "汽车驶过", + "race_car": "赛车", + "truck": "卡车", + "air_brake": "气闸", + "air_horn": "气笛", + "reversing_beeps": "倒车提示音", + "ice_cream_truck": "冰淇淋车", + "bus": "公交车", + "emergency_vehicle": "应急车辆", + "police_car": "警车", + "ambulance": "救护车", + "fire_engine": "消防车", + "motorcycle": "摩托车", + "traffic_noise": "交通噪音", + "rail_transport": "铁路运输", + "train": "火车", + "train_whistle": "火车汽笛", + "train_horn": "火车鸣笛", + "railroad_car": "铁路车厢", + "train_wheels_squealing": "火车轮子尖叫", + "subway": "地铁", + "aircraft": "飞行器", + "aircraft_engine": "飞机引擎", + "jet_engine": "喷气引擎", + "propeller": "螺旋桨", + "helicopter": "直升机", + "fixed-wing_aircraft": "固定翼飞机", + "bicycle": "自行车", + "skateboard": "滑板", + "engine": "引擎", + "light_engine": "轻型引擎", + "dental_drill's_drill": "牙科钻", + "lawn_mower": "割草机", + "chainsaw": "电锯", + "medium_engine": "中型引擎", + "heavy_engine": "重型引擎", + "engine_knocking": "引擎敲击", + "engine_starting": "引擎启动", + "idling": "怠速", + "accelerating": "加速", + "door": "门", + "doorbell": "门铃", + "ding-dong": "叮咚", + "sliding_door": "滑动门", + "slam": "猛关", + "knock": "敲门", + "tap": "轻敲", + "squeak": "吱吱声", + "cupboard_open_or_close": "橱柜开关", + "drawer_open_or_close": "抽屉开关", + "dishes": "餐具", + "cutlery": "刀叉", + "chopping": "切菜", + "frying": "煎炸", + "microwave_oven": "微波炉", + "blender": "搅拌机", + "water_tap": "水龙头", + "sink": "水槽", + "bathtub": "浴缸", + "hair_dryer": "吹风机", + "toilet_flush": "马桶冲水", + "toothbrush": "牙刷", + "electric_toothbrush": "电动牙刷", + "vacuum_cleaner": "吸尘器", + "zipper": "拉链", + "keys_jangling": "钥匙叮当", + "coin": "硬币", + "scissors": "剪刀", + "electric_shaver": "电动剃须刀", + "shuffling_cards": "洗牌", + "typing": "打字", + "typewriter": "打字机", + "computer_keyboard": "电脑键盘", + "writing": "书写", + "alarm": "警报", + "telephone": "电话", + "telephone_bell_ringing": "电话铃声", + "ringtone": "手机铃声", + "telephone_dialing": "电话拨号", + "dial_tone": "拨号音", + "busy_signal": "忙音", + "alarm_clock": "闹钟", + "siren": "警笛", + "civil_defense_siren": "防空警报", + "buzzer": "蜂鸣器", + "smoke_detector": "烟雾检测器", + "fire_alarm": "火灾警报器", + "foghorn": "雾笛", + "whistle": "哨子", + "steam_whistle": "蒸汽汽笛", + "mechanisms": "机械装置", + "ratchet": "棘轮", + "clock": "时钟", + "tick": "滴答", + "tick-tock": "滴答滴答", + "gears": "齿轮", + "pulleys": "滑轮", + "sewing_machine": "缝纫机", + "mechanical_fan": "机械风扇", + "air_conditioning": "空调", + "cash_register": "收银机", + "printer": "打印机", + "camera": "相机", + "single-lens_reflex_camera": "单反相机", + "tools": "工具", + "hammer": "锤子", + "jackhammer": "风镐", + "sawing": "锯", + "filing": "锉", + "sanding": "砂磨", + "power_tool": "电动工具", + "drill": "电钻", + "explosion": "爆炸", + "gunshot": "枪声", + "machine_gun": "机关枪", + "fusillade": "齐射", + "artillery_fire": "炮火", + "cap_gun": "玩具枪", + "fireworks": "烟花", + "firecracker": "鞭炮", + "burst": "爆裂", + "eruption": "爆发", + "boom": "轰隆", + "wood": "木头", + "chop": "砍", + "splinter": "碎裂", + "crack": "破裂", + "glass": "玻璃", + "chink": "叮当", + "shatter": "粉碎", + "silence": "寂静", + "sound_effect": "音效", + "environmental_noise": "环境噪音", + "static": "静电噪音", + "white_noise": "白噪音", + "pink_noise": "粉红噪音", + "television": "电视", + "radio": "收音机", + "field_recording": "实地录音", + "scream": "尖叫", + "sodeling": "索德铃", + "chird": "啾鸣", + "change_ringing": "变奏钟声", + "shofar": "羊角号", + "liquid": "液体", + "splash": "液体飞溅", + "slosh": "液体晃动", + "squish": "挤压", + "drip": "水滴声", + "pour": "倒水声", + "trickle": "细流水声", + "gush": "液体喷涌", + "fill": "注水声", + "spray": "喷洒", + "pump": "泵送", + "stir": "搅拌声", + "boiling": "沸腾声", + "sonar": "声呐声", + "arrow": "箭矢声", + "whoosh": "呼啸声", + "thump": "砰击声", + "thunk": "沉闷声", + "electronic_tuner": "电子调音器", + "effects_unit": "效果器", + "chorus_effect": "合唱效果", + "basketball_bounce": "篮球反弹声", + "bang": "砰声", + "slap": "拍击声", + "whack": "重击声", + "smash": "猛击声", + "breaking": "破碎声", + "bouncing": "弹跳声", + "whip": "鞭打声", + "flap": "扑动声", + "scratch": "刮擦声", + "scrape": "刮擦声", + "rub": "摩擦声", + "roll": "滚动声", + "crushing": "压碎声", + "crumpling": "揉皱声", + "tearing": "撕裂声", + "beep": "哔声", + "ping": "嘀声", + "ding": "叮声", + "clang": "铛声", + "squeal": "尖锐声", + "creak": "嘎吱声", + "rustle": "沙沙声", + "whir": "嗡声", + "clatter": "哐啷声", + "sizzle": "滋滋声", + "clicking": "点击声", + "clickety_clack": "咔嗒声", + "rumble": "隆隆声", + "plop": "扑通声", + "hum": "嗡鸣声", + "zing": "嗖声", + "boing": "嘣声", + "crunch": "咔嚓声", + "sine_wave": "正弦波声", + "harmonic": "谐波声", + "chirp_tone": "啾声", + "pulse": "脉冲", + "inside": "室内声", + "outside": "室外声", + "reverberation": "混响", + "echo": "回声", + "noise": "噪声", + "mains_hum": "电流嗡声", + "distortion": "失真声", + "sidetone": "旁音", + "cacophony": "刺耳噪声", + "throbbing": "脉动声", + "vibration": "振动声" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/common.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/common.json new file mode 100644 index 0000000..b9bdbad --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/common.json @@ -0,0 +1,300 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilForRestart": "直到 Frigate 重启。", + "untilRestart": "直到重启", + "ago": "{{timeAgo}} 前", + "justNow": "刚才", + "today": "今天", + "yesterday": "昨天", + "last7": "最后 7 天", + "last14": "最后 14 天", + "last30": "最后 30 天", + "thisWeek": "本周", + "lastWeek": "上个周", + "thisMonth": "本月", + "lastMonth": "上个月", + "5minutes": "5 分钟", + "10minutes": "10 分钟", + "30minutes": "30 分钟", + "1hour": "1 小时", + "12hours": "12 小时", + "24hours": "24 小时", + "pm": "下午", + "am": "上午", + "yr": "{{time}}年", + "year_other": "{{time}}年", + "mo": "{{time}}月", + "month_other": "{{time}}月", + "d": "{{time}}天", + "day_other": "{{time}}天", + "h": "{{time}}小时", + "hour_other": "{{time}}小时", + "m": "{{time}}分钟", + "minute_other": "{{time}}分钟", + "s": "{{time}}秒", + "second_other": "{{time}}秒", + "formattedTimestamp": { + "12hour": "M月d日 ah:mm:ss", + "24hour": "M月d日 HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM月dd日 ah:mm:ss", + "24hour": "MM月dd日 HH:mm:ss" + }, + "formattedTimestampExcludeSeconds": { + "12hour": "%m月%-d日 %I:%M %p", + "24hour": "%m月%-d日 %H:%M" + }, + "formattedTimestampWithYear": { + "12hour": "%Y年%m月%-d日 %I:%M:%S %p", + "24hour": "%Y年%m月%-d日 %H:%M" + }, + "formattedTimestampOnlyMonthAndDay": "%m月%-d日", + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "ah:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "M月d日 ah:mm", + "24hour": "M月d日 HH:mm" + }, + "formattedTimestampMonthDayYearHourMinute": { + "24hour": "yyyy年M月d日 HH:mm", + "12hour": "yyyy年M月d日 ah:mm" + }, + "formattedTimestampMonthDay": "M月d日", + "formattedTimestampFilename": { + "12hour": "yy年MM月dd日 ah时mm分ss秒", + "24hour": "yy年MM月dd日 HH时mm分ss秒" + }, + "formattedTimestampMonthDayYear": { + "12hour": "yy年MM月dd日", + "24hour": "yy年MM月dd日" + }, + "inProgress": "进行中", + "invalidStartTime": "无效的开始时间", + "invalidEndTime": "无效的结束时间" + }, + "unit": { + "speed": { + "mph": "英里/小时", + "kph": "公里/小时" + }, + "length": { + "feet": "英尺", + "meters": "米" + }, + "data": { + "kbps": "kB/s", + "mbps": "MB/s", + "gbps": "GB/s", + "kbph": "kB/每小时", + "mbph": "MB/每小时", + "gbph": "GB/每小时" + } + }, + "label": { + "back": "返回", + "hide": "隐藏 {{item}}", + "show": "显示 {{item}}", + "ID": "ID", + "none": "无", + "all": "所有" + }, + "pagination": { + "label": "分页", + "previous": { + "title": "上一页", + "label": "转到上一页" + }, + "next": { + "title": "下一页", + "label": "转到下一页" + }, + "more": "更多页面" + }, + "button": { + "apply": "应用", + "reset": "重置", + "done": "完成", + "enabled": "启用", + "enable": "启用", + "disabled": "禁用", + "disable": "禁用", + "save": "保存", + "saving": "保存中…", + "cancel": "取消", + "close": "关闭", + "copy": "复制", + "back": "返回", + "history": "历史", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏", + "pictureInPicture": "画中画", + "on": "开", + "off": "关", + "edit": "编辑", + "copyCoordinates": "复制坐标", + "delete": "删除", + "yes": "是", + "no": "否", + "download": "下载", + "info": "信息", + "suspended": "已暂停", + "unsuspended": "取消暂停", + "play": "播放", + "unselect": "取消选择", + "export": "导出", + "deleteNow": "立即删除", + "next": "下一个", + "cameraAudio": "摄像头音频", + "twoWayTalk": "双向对话", + "continue": "继续" + }, + "menu": { + "system": "系统", + "systemMetrics": "系统信息", + "configuration": "配置", + "systemLogs": "系统日志", + "settings": "设置", + "configurationEditor": "配置编辑器", + "languages": "Languages / 语言", + "language": { + "en": "英语 (English)", + "zhCN": "简体中文", + "withSystem": { + "label": "使用系统语言设置" + }, + "hi": "印地语 (हिन्दी)", + "es": "西班牙语 (Español)", + "fr": "法语 (Français)", + "ar": "阿拉伯语 (العربية)", + "pt": "葡萄牙语 (Português)", + "de": "德语 (Deutsch)", + "ja": "日语 (日本語)", + "tr": "土耳其语 (Türkçe)", + "it": "意大利语 (Italiano)", + "nl": "荷兰语 (Nederlands)", + "sv": "瑞典语 (Svenska)", + "nb": "挪威博克马尔语 (Norsk Bokmål)", + "ko": "韩语 (한국어)", + "vi": "越南语 (Tiếng Việt)", + "fa": "波斯语 (فارسی)", + "pl": "波兰语 (Polski)", + "uk": "乌克兰语 (Українська)", + "he": "希伯来语 (עברית)", + "el": "希腊语 (Ελληνικά)", + "ro": "罗马尼亚语 (Română)", + "hu": "马扎尔语 (Magyar)", + "fi": "芬兰语 (Suomi)", + "da": "丹麦语 (Dansk)", + "sk": "斯拉夫语 (Slovenčina)", + "ru": "俄语 (Русский)", + "cs": "捷克语 (Čeština)", + "yue": "粤语 (粵語)", + "th": "泰语(ไทย)", + "ca": "加泰罗尼亚语 (Català )", + "ptBR": "巴西葡萄牙语 (Português brasileiro)", + "sr": "塞尔维亚语 (Српски)", + "sl": "斯洛文尼亚语 (Slovenščina)", + "lt": "立陶宛语 (Lietuvių)", + "bg": "保加利亚语 (Български)", + "gl": "加利西亚语 (Galego)", + "id": "印度尼西亚语 (Bahasa Indonesia)", + "ur": "乌尔都语 (اردو)" + }, + "appearance": "外观", + "darkMode": { + "label": "深色模式", + "light": "浅色", + "dark": "深色", + "withSystem": { + "label": "使用系统深色模式设置" + } + }, + "withSystem": "跟随系统", + "theme": { + "label": "主题", + "blue": "蓝色", + "green": "绿色", + "nord": "Nord", + "red": "红色", + "contrast": "高对比度", + "default": "默认", + "highcontrast": "高对比" + }, + "help": "帮助", + "documentation": { + "title": "文档", + "label": "Frigate 的官方文档" + }, + "live": { + "title": "实时监控", + "allCameras": "所有摄像头", + "cameras": { + "title": "摄像头", + "count_other": "{{count}} 个摄像头" + } + }, + "review": "核查", + "explore": "浏览", + "export": "导出", + "uiPlayground": "UI 演示", + "faceLibrary": "人脸管理", + "user": { + "account": "账号", + "current": "当前用户:{{user}}", + "anonymous": "匿名", + "logout": "登出", + "setPassword": "设置密码", + "title": "用户" + }, + "restart": "重启 Frigate", + "classification": "目标分类" + }, + "toast": { + "copyUrlToClipboard": "已复制链接到剪贴板。", + "save": { + "title": "保存", + "error": { + "title": "保存配置信息失败: {{errorMessage}}", + "noMessage": "保存配置信息失败" + } + } + }, + "role": { + "title": "权限组", + "admin": "管理员", + "viewer": "成员", + "desc": "管理员可以完全访问Frigate界面上所有功能。成员则仅能查看摄像头、核查项和历史录像。" + }, + "accessDenied": { + "documentTitle": "没有权限 - Frigate", + "title": "没有权限", + "desc": "您没有权限查看此页面。" + }, + "notFound": { + "documentTitle": "没有找到页面 - Frigate", + "title": "404", + "desc": "页面未找到" + }, + "selectItem": "选择 {{item}}", + "readTheDocumentation": "阅读文档", + "information": { + "pixels": "{{area}} 像素" + }, + "list": { + "two": "{{0}} 和 {{1}}", + "many": "{{items}} 以及 {{last}}", + "separatorWithSpace": "、 " + }, + "field": { + "optional": "可选", + "internalID": "Frigate 在配置与数据库中使用的内部 ID" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/auth.json new file mode 100644 index 0000000..dbfc349 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "用户名", + "password": "密码", + "login": "登录", + "errors": { + "usernameRequired": "用户名不能为空", + "passwordRequired": "密码不能为空", + "rateLimit": "超出请求限制,请稍后再试。", + "loginFailed": "登录失败", + "unknownError": "未知错误,请检查日志。", + "webUnknownError": "未知错误,请检查控制台日志。" + }, + "firstTimeLogin": "首次尝试登录?请从 Frigate 日志中查找生成的登录密码等信息。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/camera.json new file mode 100644 index 0000000..1ac6a63 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "摄像头组", + "add": "添加摄像头组", + "edit": "编辑摄像头组", + "delete": { + "label": "删除摄像头组", + "confirm": { + "title": "确认删除", + "desc": "你确定要删除摄像头组 {{name}} 吗?" + } + }, + "name": { + "label": "名称", + "placeholder": "请输入名称…", + "errorMessage": { + "mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。", + "exists": "摄像头组名称已存在。", + "nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。", + "invalid": "无效的摄像头组名称。" + } + }, + "cameras": { + "label": "摄像头", + "desc": "选择添加至该组的摄像头。" + }, + "icon": "图标", + "success": "摄像头组({{name}})保存成功。", + "camera": { + "setting": { + "label": "摄像头视频流设置", + "title": "{{cameraName}} 视频流设置", + "desc": "更改此摄像头组仪表板的实时视频流选项。这些设置特定于设备/浏览器。", + "audioIsAvailable": "此视频流支持音频", + "audioIsUnavailable": "此视频流不支持音频", + "audio": { + "tips": { + "title": "音频必须从您的摄像头输出并在 go2rtc 中配置此流。", + "document": "阅读文档 " + } + }, + "streamMethod": { + "label": "视频流方法", + "method": { + "noStreaming": { + "label": "无视频流", + "desc": "摄像头图像每分钟仅更新一次,不会进行实时视频流播放。" + }, + "smartStreaming": { + "label": "智能视频流(推荐)", + "desc": "智能视频流在没有检测到活动时,每分钟更新一次摄像头图像,以节省带宽和资源。当检测到活动时,图像会无缝切换到实时视频流。" + }, + "continuousStreaming": { + "label": "持续视频流", + "desc": { + "title": "当摄像头画面在仪表板上可见时,始终为实时视频流,即使未检测到活动。", + "warning": "持续视频流可能会导致高带宽使用和性能问题,请谨慎使用。" + } + } + }, + "placeholder": "选择视频流传输方式" + }, + "compatibilityMode": { + "label": "兼容模式", + "desc": "仅在摄像头的实时视频流显示颜色伪影,并且图像右侧有一条对角线时启用此选项。" + }, + "stream": "视频流", + "placeholder": "选择视频流" + }, + "birdseye": "鸟瞰图" + } + }, + "debug": { + "options": { + "label": "设置", + "title": "选项", + "showOptions": "显示选项", + "hideOptions": "隐藏选项" + }, + "boundingBox": "边界框", + "timestamp": "时间戳", + "zones": "区域", + "mask": "遮罩", + "motion": "画面变动", + "regions": "区域" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/dialog.json new file mode 100644 index 0000000..3d9da61 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/dialog.json @@ -0,0 +1,130 @@ +{ + "restart": { + "title": "你确定要重启 Frigate?", + "button": "重启", + "restarting": { + "title": "Frigate 正在重启", + "content": "该页面将会在 {{countdown}} 秒后自动刷新。", + "button": "强制刷新" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "提交至 Frigate+", + "desc": "您希望避开的地点中的物体不应被视为误报。若将其作为误报提交,可能会导致AI模型容易混淆相关物体的识别。" + }, + "review": { + "true": { + "label": "为 Frigate Plus 确认此标签", + "true_other": "这是 {{label}}" + }, + "false": { + "label": "不为 Frigate Plus 确认此标签", + "false_other": "这不是 {{label}}" + }, + "state": { + "submitted": "已提交" + }, + "question": { + "label": "为 Frigate Plus 确认此标签", + "ask_a": "这个目标/物体是 {{label}} 吗?", + "ask_an": "这个目标/物体是 {{label}} 吗?", + "ask_full": "这个目标/物体是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?" + } + } + }, + "video": { + "viewInHistory": "在历史中查看" + } + }, + "export": { + "time": { + "fromTimeline": "从时间线选择", + "lastHour_other": "最后 {{count}} 小时", + "custom": "自定义", + "start": { + "title": "开始时间", + "label": "选择开始时间" + }, + "end": { + "title": "结束时间", + "label": "选择结束时间" + } + }, + "name": { + "placeholder": "导出项目的名字" + }, + "select": "选择", + "export": "导出", + "selectOrExport": "选择或导出", + "toast": { + "success": "导出成功。进入 导出 页面查看文件。", + "error": { + "failed": "导出失败:{{error}}", + "endTimeMustAfterStartTime": "结束时间必须在开始时间之后", + "noVaildTimeSelected": "未选择有效的时间范围" + }, + "view": "查看" + }, + "fromTimeline": { + "saveExport": "保存导出", + "previewExport": "预览导出" + } + }, + "streaming": { + "label": "视频流", + "restreaming": { + "disabled": "此摄像头未启用视频流转发功能。", + "desc": { + "title": "为此摄像头设置 go2rtc,以获取额外的实时预览选项和音频支持。", + "readTheDocumentation": "阅读文档" + } + }, + "showStats": { + "label": "显示视频流统计信息", + "desc": "启用后将在摄像头画面上叠加显示视频流统计信息。" + }, + "debugView": "调试界面" + }, + "search": { + "saveSearch": { + "label": "保存搜索", + "desc": "请为此已保存的搜索提供一个名称。", + "placeholder": "请输入搜索名称", + "overwrite": "{{searchName}} 已存在。保存将覆盖现有值。", + "success": "搜索 ({{searchName}}) 已保存。", + "button": { + "save": { + "label": "保存此搜索" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "确认删除", + "desc": { + "selected": "你确定要删除与此核查项相关的所有录制视频吗?

    提示:按住 Shift 键点击删除可跳过此对话框。" + }, + "toast": { + "success": "已删除与所选核查项关联的视频片段。", + "error": "删除失败:{{error}}" + } + }, + "button": { + "export": "导出", + "markAsReviewed": "标记为已核查", + "deleteNow": "立即删除", + "markAsUnreviewed": "标记为未核查" + } + }, + "imagePicker": { + "selectImage": "选择追踪目标的缩略图", + "search": { + "placeholder": "通过标签或子标签搜索……" + }, + "noImages": "未在此摄像头找到缩略图", + "unknownLabel": "已保存触发的图片" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/filter.json new file mode 100644 index 0000000..8911c6a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/filter.json @@ -0,0 +1,137 @@ +{ + "filter": "过滤器", + "labels": { + "label": "标签", + "all": { + "title": "所有标签", + "short": "标签" + }, + "count": "{{count}} 个标签", + "count_other": "{{count}} 个标签", + "count_one": "{{count}} 个标签" + }, + "zones": { + "all": { + "title": "所有区域", + "short": "区域" + }, + "label": "区域" + }, + "dates": { + "all": { + "title": "所有日期", + "short": "日期" + }, + "selectPreset": "选择预定时间…" + }, + "more": "更多筛选项", + "reset": { + "label": "重置筛选器为默认值" + }, + "timeRange": "时间范围", + "subLabels": { + "label": "子标签", + "all": "所有子标签" + }, + "score": "分值", + "estimatedSpeed": "预计速度({{unit}})", + "features": { + "label": "特性", + "hasSnapshot": "包含快照", + "hasVideoClip": "包含视频片段", + "submittedToFrigatePlus": { + "label": "提交至 Frigate+", + "tips": "你必须要先筛选有快照的追踪目标。

    没有快照的追踪目标无法提交至 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期 (正序)", + "dateDesc": "日期 (倒序)", + "scoreAsc": "目标分值 (正序)", + "scoreDesc": "目标分值 (倒序)", + "speedAsc": "预计速度 (正序)", + "speedDesc": "预计速度 (倒序)", + "relevance": "关联性" + }, + "cameras": { + "label": "摄像头筛选", + "all": { + "title": "所有摄像头", + "short": "摄像头" + } + }, + "review": { + "showReviewed": "显示已核查的项目" + }, + "motion": { + "showMotionOnly": "仅显示画面变动" + }, + "explore": { + "settings": { + "title": "设置", + "defaultView": { + "title": "默认视图", + "desc": "当未选择任何筛选条件时,将显示每个标签下最近追踪目标的汇总信息,或者显示未筛选的网格视图。", + "summary": "摘要", + "unfilteredGrid": "未过滤网格" + }, + "gridColumns": { + "title": "网格列数", + "desc": "选择网格视图中的列数。" + }, + "searchSource": { + "label": "搜索源", + "desc": "选择是搜索缩略图还是追踪目标的描述。", + "options": { + "thumbnailImage": "缩略图", + "description": "描述" + } + } + }, + "date": { + "selectDateBy": { + "label": "选择日期进行筛选" + } + } + }, + "logSettings": { + "label": "日志级别筛选", + "filterBySeverity": "按严重程度筛选日志", + "loading": { + "title": "加载中", + "desc": "当日志面板滚动到底部时,新的日志会自动流式加载。" + }, + "disableLogStreaming": "禁用日志流式加载", + "allLogs": "所有日志" + }, + "trackedObjectDelete": { + "title": "确认删除", + "desc": "删除这 {{objectLength}} 个已追踪目标将移除它们的快照、所有已保存的嵌入向量数据以及任何相关的目标全周期条目,但在 历史 页面中这些追踪目标的录制视频片段将不会被删除。

    您确定要继续吗?

    以后按住 Shift 键进行删除可跳过此提醒。", + "toast": { + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "按区域遮罩筛选" + }, + "recognizedLicensePlates": { + "title": "识别的车牌", + "loadFailed": "加载识别的车牌失败。", + "loading": "正在加载识别的车牌…", + "placeholder": "输入以搜索车牌…", + "noLicensePlatesFound": "未找到车牌。", + "selectPlatesFromList": "从列表中选择一个或多个车牌。", + "selectAll": "选择所有", + "clearAll": "清除所有" + }, + "classes": { + "label": "分类", + "all": { + "title": "所有分类" + }, + "count_one": "{{count}} 个分类", + "count_other": "{{count}} 个分类" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/icons.json new file mode 100644 index 0000000..20f2012 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "选择图标", + "search": { + "placeholder": "搜索图标…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/input.json new file mode 100644 index 0000000..add854a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下载视频", + "toast": { + "success": "你的核查视频已开始下载。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/player.json new file mode 100644 index 0000000..0336c32 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "找不到此次录制", + "noPreviewFound": "没有找到预览", + "noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览", + "submitFrigatePlus": { + "title": "提交此帧到 Frigate+?", + "submit": "提交" + }, + "livePlayerRequiredIOSVersion": "此直播流类型需要 iOS 17.1 或更高版本。", + "streamOffline": { + "title": "视频流离线", + "desc": "未在 {{cameraName}} 的 detect 流上接收到任何帧,请检查错误日志" + }, + "cameraDisabled": "摄像头已禁用", + "stats": { + "streamType": { + "title": "流类型:", + "short": "类型" + }, + "bandwidth": { + "title": "带宽:", + "short": "带宽" + }, + "latency": { + "title": "延迟:", + "value": "{{seconds}} 秒", + "short": { + "title": "延迟", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "总帧数:", + "droppedFrames": { + "title": "丢帧数:", + "short": { + "title": "丢帧", + "value": "{{droppedFrames}} 帧" + } + }, + "decodedFrames": "解码帧数:", + "droppedFrameRate": "丢帧率:" + }, + "toast": { + "success": { + "submittedFrigatePlus": "已成功提交帧到 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交帧到 Frigate+ 失败" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/objects.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/objects.json new file mode 100644 index 0000000..193f871 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/objects.json @@ -0,0 +1,120 @@ +{ + "person": "人", + "bicycle": "自行车", + "car": "汽车", + "motorcycle": "摩托车", + "airplane": "飞机", + "bus": "公交车", + "train": "火车", + "boat": "船", + "traffic_light": "交通灯", + "fire_hydrant": "消防栓", + "street_sign": "路标", + "stop_sign": "停车标志", + "parking_meter": "停车计时器", + "bench": "长椅", + "bird": "鸟", + "cat": "猫", + "dog": "狗", + "horse": "马", + "sheep": "绵羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑马", + "giraffe": "长颈鹿", + "hat": "帽子", + "backpack": "背包", + "umbrella": "雨伞", + "shoe": "鞋子", + "eye_glasses": "眼镜", + "handbag": "手提包", + "tie": "领带", + "suitcase": "手提箱", + "frisbee": "飞盘", + "skis": "滑雪板", + "snowboard": "滑雪板", + "sports_ball": "运动球", + "kite": "风筝", + "baseball_bat": "棒球棒", + "baseball_glove": "棒球手套", + "skateboard": "滑板", + "surfboard": "冲浪板", + "tennis_racket": "网球拍", + "bottle": "瓶子", + "plate": "盘子", + "wine_glass": "酒杯", + "cup": "杯子", + "fork": "叉子", + "knife": "刀", + "spoon": "勺子", + "bowl": "碗", + "banana": "香蕉", + "apple": "苹果", + "sandwich": "三明治", + "orange": "橙子", + "broccoli": "西兰花", + "carrot": "胡萝卜", + "hot_dog": "热狗", + "pizza": "披萨", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "椅子", + "couch": "沙发", + "potted_plant": "盆栽植物", + "bed": "床", + "mirror": "镜子", + "dining_table": "餐桌", + "window": "窗户", + "desk": "桌子", + "toilet": "厕所", + "door": "门", + "tv": "电视", + "laptop": "笔记本电脑", + "mouse": "鼠标", + "remote": "遥控器", + "keyboard": "键盘", + "cell_phone": "手机", + "microwave": "微波炉", + "oven": "烤箱", + "toaster": "烤面包机", + "sink": "水槽", + "refrigerator": "冰箱", + "blender": "搅拌机", + "book": "书", + "clock": "时钟", + "vase": "花瓶", + "scissors": "剪刀", + "teddy_bear": "泰迪熊", + "hair_dryer": "吹风机", + "toothbrush": "牙刷", + "hair_brush": "发刷", + "vehicle": "车辆", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "动物", + "bark": "狗叫", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔子", + "raccoon": "浣熊", + "robot_lawnmower": "自动割草机", + "waste_bin": "垃圾桶", + "on_demand": "手动", + "face": "人脸", + "license_plate": "车牌", + "package": "包裹", + "bbq_grill": "烧烤架", + "amazon": "亚马逊", + "usps": "美国邮政", + "ups": "UPS", + "fedex": "联邦快递", + "dhl": "DHL", + "an_post": "爱尔兰邮政", + "purolator": "普罗莱特", + "postnl": "荷兰邮政", + "nzpost": "新西兰邮政", + "postnord": "北欧邮政", + "gls": "GLS", + "dpd": "DPD" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/classificationModel.json new file mode 100644 index 0000000..a3fa01a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/classificationModel.json @@ -0,0 +1,180 @@ +{ + "documentTitle": "分类模型 - Frigate", + "button": { + "deleteClassificationAttempts": "删除分类图片", + "renameCategory": "重命名类别", + "deleteCategory": "删除类别", + "deleteImages": "删除图片", + "trainModel": "训练模型", + "addClassification": "添加分类", + "deleteModels": "删除模型", + "editModel": "编辑模型" + }, + "toast": { + "success": { + "deletedCategory": "删除类别", + "deletedImage": "删除图片", + "categorizedImage": "成功分类图片", + "trainedModel": "训练模型成功。", + "trainingModel": "已开始训练模型。", + "deletedModel_other": "已删除 {{count}} 个模型", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改类别名称为 {{name}}" + }, + "error": { + "deleteImageFailed": "删除失败:{{errorMessage}}", + "deleteCategoryFailed": "删除类别失败:{{errorMessage}}", + "categorizeFailed": "图片分类失败:{{errorMessage}}", + "trainingFailed": "训练模型失败,请查看 Frigate 日志获取详情。", + "deleteModelFailed": "删除模型失败:{{errorMessage}}", + "updateModelFailed": "更新模型失败:{{errorMessage}}", + "trainingFailedToStart": "开始训练模型失败:{{errorMessage}}", + "renameCategoryFailed": "修改类别名称失败:{{errorMessage}}" + } + }, + "deleteCategory": { + "title": "删除类别", + "desc": "确定要删除类别 {{name}} 吗?此操作将永久删除所有关联的图片,并需要重新训练模型。", + "minClassesTitle": "无法删除此类别", + "minClassesDesc": "分类模型必须至少有2个类别。你需要先添加一个新的类别,然后再删除当前这个类别。" + }, + "deleteDatasetImages": { + "title": "删除图片数据集", + "desc": "确定要从 {{dataset}} 中删除 {{count}} 张图片吗?此操作无法撤销,并将需要重新训练模型。" + }, + "deleteTrainImages": { + "title": "删除训练的图片", + "desc": "确定要删除 {{count}} 张图片吗?此操作无法撤销。" + }, + "renameCategory": { + "title": "重命名类别", + "desc": "请输入 {{name}} 的新名称。名称变更后需要重新训练模型。" + }, + "description": { + "invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。" + }, + "train": { + "title": "最近分类记录", + "aria": "选择最近分类记录", + "titleShort": "近期" + }, + "categories": "类别", + "createCategory": { + "new": "创建新类别" + }, + "categorizeImageAs": "图片分类为:", + "categorizeImage": "图片分类", + "noModels": { + "object": { + "title": "未创建目标/物体分类模型", + "description": "创建自定义模型以分类检测到的目标。", + "buttonText": "创建目标/物体模型" + }, + "state": { + "title": "尚未创建状态分类模型", + "description": "创建自定义模型以监控并分类摄像头特定区域的状态变化。", + "buttonText": "创建状态模型" + } + }, + "wizard": { + "title": "创建新分类", + "steps": { + "nameAndDefine": "名称与定义", + "stateArea": "状态区域", + "chooseExamples": "选择范例" + }, + "step1": { + "description": "状态模型用于监控摄像头固定区域的状态变化(例如门是否开启或关闭)。目标/物体模型用于为检测到的目标添加分类标签(例如区分宠物、快递员等)。", + "name": "名称", + "namePlaceholder": "请输入模型名称……", + "type": "类型", + "typeState": "状态", + "typeObject": "目标/物体", + "objectLabel": "目标/物体标签", + "objectLabelPlaceholder": "请选择目标类型……", + "classificationType": "分类方式", + "classificationTypeTip": "了解分类方式", + "classificationTypeDesc": "子标签会为目标标签添加附加文本(例如:“人员:美团”)。属性是可搜索的元数据,独立存储在目标的元信息中。", + "classificationSubLabel": "子标签", + "classificationAttribute": "属性", + "classes": "类别", + "classesTip": "了解类别", + "classesStateDesc": "定义摄像头区域内可能出现的不同状态。例如:车库门的“开启”和“关闭”。", + "classesObjectDesc": "定义用于分类检测目标的不同类别。例如:人员分类中的“快递员”、“居民”、“陌生人”。", + "classPlaceholder": "请输入分类名称……", + "errors": { + "nameRequired": "模型名称为必填项", + "nameLength": "模型名称长度不能超过 64 个字符", + "nameOnlyNumbers": "模型名称不能仅包含数字", + "classRequired": "至少需要一个类别", + "classesUnique": "类别名称必须唯一", + "stateRequiresTwoClasses": "状态模型至少需要两个类别", + "objectLabelRequired": "请选择一个目标标签", + "objectTypeRequired": "请选择一个目标标签" + }, + "states": "状态" + }, + "step2": { + "description": "选择摄像头,并为摄像头定义要监控的区域。模型将对这些区域的状态进行分类。", + "cameras": "摄像头", + "selectCamera": "选择摄像头", + "noCameras": "点击 + 符号添加摄像头", + "selectCameraPrompt": "从列表中选择一个摄像头以定义其检测区域" + }, + "step3": { + "selectImagesPrompt": "选择所有属于 {{className}} 的图片", + "selectImagesDescription": "点击图像进行选择,完成该类别后点击“继续”。", + "generating": { + "title": "正在生成样本图片", + "description": "Frigate 正在从录像中提取代表性图片。这可能需要一些时间……" + }, + "training": { + "title": "正在训练模型", + "description": "系统正在后台训练模型。你可以关闭此对话框,训练完成后模型将自动开始运行。" + }, + "retryGenerate": "重新生成", + "noImages": "未生成样本图像", + "classifying": "正在分类与训练……", + "trainingStarted": "已开始模型训练", + "errors": { + "noCameras": "未配置摄像头", + "noObjectLabel": "未选择目标标签", + "generateFailed": "示例生成失败:{{error}}", + "generationFailed": "生成失败,请重试。", + "classifyFailed": "图片分类失败:{{error}}" + }, + "generateSuccess": "样本图片生成成功", + "allImagesRequired_other": "请对所有图片进行分类。还有 {{count}} 张图片需要分类。", + "modelCreated": "模型创建成功。请在“最近分类”页面为缺失的状态添加图片,然后训练模型。", + "missingStatesWarning": { + "title": "缺失状态示例", + "description": "建议为所有状态都选择示例图片以获得最佳效果。你也可以跳过当前为分类状态选择图片,但需要所有状态都有对应的图片,模型才能够进行训练。跳过后你可通过“最近分类”页面为缺失的状态分类添加图片,然后再训练模型。" + } + } + }, + "deleteModel": { + "title": "删除分类模型", + "single": "你确定要删除 {{name}} 吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。", + "desc": "你确定要删除 {{count}} 个模型吗?此操作将永久删除所有相关数据,包括图片和训练数据,且无法撤销。" + }, + "menu": { + "objects": "目标", + "states": "状态" + }, + "details": { + "scoreInfo": "得分表示该目标所有检测结果的平均分类置信度。" + }, + "edit": { + "title": "编辑分类模型", + "descriptionState": "编辑此状态分类模型的类别;更改后需要重新训练模型。", + "descriptionObject": "编辑此目标分类模型的目标类型和分类类型。", + "stateClassesInfo": "注意:更改状态类别后需使用更新后的类别重新训练模型。" + }, + "tooltip": { + "trainingInProgress": "模型正在训练中", + "noNewImages": "没有新的图片可用于训练。请先对数据集中的更多图片进行分类。", + "noChanges": "自上次训练以来,数据集未作任何更改。", + "modelNotReady": "模型尚未准备好进行训练" + }, + "none": "无标签" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/configEditor.json new file mode 100644 index 0000000..a4ca5c5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "配置编辑器 - Frigate", + "configEditor": "配置编辑器", + "copyConfig": "复制配置", + "saveAndRestart": "保存并重启", + "saveOnly": "只保存", + "toast": { + "success": { + "copyToClipboard": "配置已复制到剪贴板。" + }, + "error": { + "savingError": "保存配置时出错" + } + }, + "confirm": "是否退出并不保存?", + "safeConfigEditor": "配置编辑器(安全模式)", + "safeModeDescription": "由于验证配置出现错误,Frigate目前为安全模式。" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/events.json new file mode 100644 index 0000000..ac795e2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/events.json @@ -0,0 +1,64 @@ +{ + "alerts": "警报", + "detections": "检测", + "motion": { + "label": "画面变动", + "only": "仅变动画面" + }, + "allCameras": "所有摄像头", + "empty": { + "alert": "还没有“警报”类核查项", + "detection": "还没有“检测”类核查项", + "motion": "还没有画面变动类数据" + }, + "timeline": "时间线", + "timeline.aria": "选择时间线", + "events": { + "label": "事件", + "aria": "选择事件", + "noFoundForTimePeriod": "未找到该时间段的事件。" + }, + "documentTitle": "核查 - Frigate", + "recordings": { + "documentTitle": "回放 - Frigate" + }, + "calendarFilter": { + "last24Hours": "过去24小时" + }, + "markAsReviewed": "标记为已核查", + "markTheseItemsAsReviewed": "将这些项目标记为已核查", + "newReviewItems": { + "label": "查看新的核查项目", + "button": "核查新项目" + }, + "camera": "摄像头", + "selected": "已选择 {{count}} 个", + "selected_one": "已选择 {{count}} 个", + "selected_other": "已选择 {{count}} 个", + "detected": "已检测", + "suspiciousActivity": "可疑活动", + "threateningActivity": "风险类活动", + "detail": { + "noDataFound": "没有可供核查的详细数据", + "aria": "切换详细视图", + "trackedObject_one": "{{count}}个目标或物体", + "trackedObject_other": "{{count}}个目标或物体", + "noObjectDetailData": "没有目标详细信息。", + "label": "详细信息", + "settings": "详细视图设置", + "alwaysExpandActive": { + "title": "始终展开当前项", + "desc": "在可用情况下,将始终展开当前核查项的目标详细信息。" + } + }, + "objectTrack": { + "trackedPoint": "追踪点", + "clickToSeek": "点击从该时间进行寻找" + }, + "zoomIn": "放大", + "zoomOut": "缩小", + "normalActivity": "正常", + "needsReview": "需要核查", + "securityConcern": "安全隐患", + "select_all": "所有" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/explore.json new file mode 100644 index 0000000..e94442e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/explore.json @@ -0,0 +1,291 @@ +{ + "documentTitle": "浏览 - Frigate", + "generativeAI": "生成式 AI", + "exploreIsUnavailable": { + "title": "浏览功能不可用", + "embeddingsReindexing": { + "context": "完成追踪目标嵌入重新索引后,才可以使用 浏览 功能。", + "startingUp": "启动中…", + "estimatedTime": "预计剩余时间:", + "finishingShortly": "即将完成", + "step": { + "thumbnailsEmbedded": "缩略图嵌入:", + "descriptionsEmbedded": "描述嵌入:", + "trackedObjectsProcessed": "追踪目标已处理: " + } + }, + "downloadingModels": { + "context": "Frigate正在下载支持语义搜索功能所需的嵌入模型。根据网络连接速度,这可能需要几分钟。", + "setup": { + "visionModel": "视觉模型", + "visionModelFeatureExtractor": "视觉模型特征提取器", + "textModel": "文本模型", + "textTokenizer": "文本分词器" + }, + "tips": { + "context": "模型下载完成后,您可能需要重新索引追踪目标的嵌入。", + "documentation": "阅读文档" + }, + "error": "发生错误。请检查Frigate日志。" + } + }, + "trackedObjectDetails": "目标追踪详情", + "type": { + "details": "详情", + "snapshot": "快照", + "video": "视频", + "object_lifecycle": "目标全周期", + "thumbnail": "缩略图", + "tracking_details": "追踪详情" + }, + "objectLifecycle": { + "title": "目标全周期", + "noImageFound": "未找到此时间戳的图像。", + "createObjectMask": "创建目标/物体遮罩", + "adjustAnnotationSettings": "调整标注设置", + "scrollViewTips": "滚动查看此目标全周期的关键节点。", + "autoTrackingTips": "自动跟踪摄像头的边界框位置可能不准确。", + "lifecycleItemDesc": { + "visible": "检测到 {{label}}", + "entered_zone": "{{label}} 进入 {{zones}}", + "active": "{{label}} 变为活动状态", + "stationary": "{{label}} 变为静止状态", + "attribute": { + "faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}}", + "other": "{{label}} 识别为 {{attribute}}" + }, + "gone": "{{label}} 离开", + "heard": "听到 {{label}}", + "external": "检测到 {{label}}", + "header": { + "ratio": "得分", + "zones": "区域", + "area": "坐标区域" + } + }, + "annotationSettings": { + "title": "标注设置", + "showAllZones": { + "title": "显示所有区域", + "desc": "始终在目标进入区域的帧上显示区域标记。" + }, + "offset": { + "label": "标注偏移", + "desc": "这些数据来自摄像头的检测源,但是叠加在录制源的图像上。这两个流不太可能完全同步。因此,边界框和录像不会完全对齐。但是,可以使用 annotation_offset 字段来调整这个问题。", + "documentation": "阅读文档 ", + "millisecondsToOffset": "检测标注的偏移毫秒数。默认值:0", + "tips": "提示:假设有一个人从左向右走的事件片段。如果事件时间线上的边界框始终在人的左侧,则应该减小该值。同样,如果一个人从左向右走,而边界框始终在人的前面,则应该增加该值。", + "toast": { + "success": "{{camera}} 的标注偏移量已成功保存至配置文件。请重启Frigate生效。" + } + } + }, + "carousel": { + "previous": "上一张", + "next": "下一张" + }, + "count": "第 {{first}} 个,共 {{second}} 个", + "trackedPoint": "追踪点" + }, + "details": { + "item": { + "title": "回放项目详情", + "desc": "核查项详情", + "button": { + "share": "分享该核查项", + "viewInExplore": "在 浏览 中查看" + }, + "tips": { + "mismatch_other": "检测到 {{count}} 个不可用的目标,并已包含在此核查项中。这些目标可能未达到警报或检测标准,或者已被清理/删除。", + "hasMissingObjects": "如果希望 Frigate 保存 {{objects}} 标签的追踪目标,请调整您的配置" + }, + "toast": { + "success": { + "regenerate": "已向 {{provider}} 请求新的描述。根据提供商的速度,生成新描述可能需要一些时间。", + "updatedSublabel": "成功更新子标签。", + "updatedLPR": "成功更新车牌。", + "audioTranscription": "成功请求音频转录。根据你运行 Frigate 的服务器速度,转录可能需要一些时间才能完成。" + }, + "error": { + "regenerate": "调用 {{provider}} 生成新描述失败:{{errorMessage}}", + "updatedSublabelFailed": "更新子标签失败:{{errorMessage}}", + "updatedLPRFailed": "更新车牌失败:{{errorMessage}}", + "audioTranscription": "请求音频转录失败:{{errorMessage}}" + } + } + }, + "label": "标签", + "editSubLabel": { + "title": "编辑子标签", + "desc": "为 {{label}} 输入新的子标签", + "descNoLabel": "为该追踪目标输入新的子标签" + }, + "topScore": { + "label": "最高得分", + "info": "最高分是追踪目标的中位分数最高值,因此可能与搜索结果缩略图中显示的分数有所不同。" + }, + "estimatedSpeed": "预计速度", + "objects": "目标/物体", + "camera": "摄像头", + "zones": "区域", + "timestamp": "时间戳", + "button": { + "findSimilar": "查找相似项", + "regenerate": { + "title": "重新生成", + "label": "重新生成追踪目标的描述" + } + }, + "description": { + "label": "描述", + "placeholder": "追踪目标的描述", + "aiTips": "在追踪目标的目标全周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。" + }, + "expandRegenerationMenu": "展开重新生成菜单", + "regenerateFromSnapshot": "从快照重新生成", + "regenerateFromThumbnails": "从缩略图重新生成", + "tips": { + "descriptionSaved": "已保存描述", + "saveDescriptionFailed": "更新描述失败:{{errorMessage}}" + }, + "editLPR": { + "desc": "为 {{label}} 输入新的车牌值", + "descNoLabel": "为检测到的目标输入新的车牌值", + "title": "编辑车牌" + }, + "recognizedLicensePlate": "识别的车牌", + "snapshotScore": { + "label": "快照得分" + }, + "score": { + "label": "分值" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "下载视频", + "aria": "下载视频" + }, + "downloadSnapshot": { + "label": "下载快照", + "aria": "下载快照" + }, + "viewObjectLifecycle": { + "label": "查看目标全周期", + "aria": "显示目标的全周期" + }, + "findSimilar": { + "label": "查找相似项", + "aria": "查看相似的目标/物体" + }, + "submitToPlus": { + "label": "提交至 Frigate+", + "aria": "提交至 Frigate Plus" + }, + "viewInHistory": { + "label": "在历史记录中查看", + "aria": "在历史记录中查看" + }, + "deleteTrackedObject": { + "label": "删除此追踪目标" + }, + "addTrigger": { + "label": "添加触发器", + "aria": "为该追踪目标添加触发器" + }, + "audioTranscription": { + "label": "转录", + "aria": "请求音频转录" + }, + "showObjectDetails": { + "label": "显示目标轨迹" + }, + "hideObjectDetails": { + "label": "隐藏目标轨迹" + }, + "viewTrackingDetails": { + "label": "查看追踪详情", + "aria": "显示追踪详情" + }, + "downloadCleanSnapshot": { + "label": "下载干净快照", + "aria": "下载干净快照" + } + }, + "dialog": { + "confirmDelete": { + "title": "确认删除", + "desc": "删除此追踪目标后,将移除快照、所有已保存的嵌入向量数据以及任何相关的目标追踪详情条目,但在 历史 页面中追踪目标的录制视频片段不会被删除。

    你确定要继续删除该追踪目标吗?" + } + }, + "noTrackedObjects": "未找到追踪目标", + "fetchingTrackedObjectsFailed": "获取追踪目标失败:{{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 个追踪目标 ", + "searchResult": { + "deleteTrackedObject": { + "toast": { + "success": "删除追踪目标成功。", + "error": "删除追踪目标失败:{{errorMessage}}" + } + }, + "tooltip": "与 {{type}} 匹配度为 {{confidence}}%", + "previousTrackedObject": "上一个追踪目标", + "nextTrackedObject": "下一个追踪目标" + }, + "exploreMore": "浏览更多的 {{label}}", + "aiAnalysis": { + "title": "AI分析" + }, + "concerns": { + "label": "风险等级" + }, + "trackingDetails": { + "title": "追踪细节", + "noImageFound": "在该时间内没找到图片。", + "createObjectMask": "创建目标遮罩", + "adjustAnnotationSettings": "调整注释设置", + "scrollViewTips": "点击以查看该目标全周期中的关键时刻。", + "autoTrackingTips": "自动追踪摄像头的边框定位可能不准确。", + "count": "{{first}} / {{second}}", + "trackedPoint": "追踪点", + "lifecycleItemDesc": { + "visible": "已检测到 {{label}}", + "entered_zone": "{{label}} 进入 {{zones}}", + "active": "{{label}} 正在活动", + "stationary": "{{label}} 变为静止", + "attribute": { + "faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}} 属性", + "other": "{{label}} 被识别为 {{attribute}}" + }, + "gone": "{{label}} 离开", + "heard": "{{label}} 被听到", + "external": "已检测到 {{label}}", + "header": { + "zones": "区", + "ratio": "占比", + "area": "坐标区域", + "score": "分数" + } + }, + "annotationSettings": { + "title": "标记设置", + "showAllZones": { + "title": "显示所有区", + "desc": "在目标进入区域的帧中始终显示区域框。" + }, + "offset": { + "label": "标记偏移量", + "desc": "此数据来自摄像头的检测视频流,但叠加在录制视频流的画面上。两个视频流可能不会完全同步,因此边框与画面可能无法完全对齐。可以使用此设置将标记在时间轴上向前或向后偏移,以更好地与录制画面对齐。", + "millisecondsToOffset": "用于偏移检测标记的毫秒数。 默认值:0", + "tips": "提示:假设有一段人从左向右走的事件录制,如果事件时间轴中的边框始终在人的左侧(即后方),则应该减小偏移值;反之,如果边框始终领先于人物,则应增大偏移值。", + "toast": { + "success": "{{camera}} 的标记偏移量已保存。" + } + } + }, + "carousel": { + "previous": "上一张图", + "next": "下一张图" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/exports.json new file mode 100644 index 0000000..3270dc4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/exports.json @@ -0,0 +1,23 @@ +{ + "documentTitle": "导出 - Frigate", + "search": "搜索", + "noExports": "没有找到导出的项目", + "deleteExport": "删除导出的项目", + "deleteExport.desc": "你确定要删除 {{exportName}} 吗?", + "editExport": { + "title": "重命名导出", + "desc": "为此导出项目输入新名称。", + "saveExport": "保存导出" + }, + "toast": { + "error": { + "renameExportFailed": "重命名导出失败:{{errorMessage}}" + } + }, + "tooltip": { + "shareExport": "分享导出", + "downloadVideo": "下载视频", + "editName": "编辑名称", + "deleteExport": "删除导出" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/faceLibrary.json new file mode 100644 index 0000000..b4bde06 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/faceLibrary.json @@ -0,0 +1,98 @@ +{ + "description": { + "addFace": "我们将引导你如何向人脸库中添加新的特征库。", + "placeholder": "请输入此特征库的名称", + "invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。" + }, + "details": { + "person": "人", + "confidence": "置信度", + "face": "人脸详情", + "faceDesc": "生成此人脸特征的追踪目标详细信息", + "timestamp": "时间戳", + "subLabelScore": "子标签得分", + "scoreInfo": "子标签分数是基于所有识别到的人脸置信度的加权评分,因此可能与快照中显示的分数有所不同。", + "unknown": "未知" + }, + "documentTitle": "人脸库 - Frigate", + "uploadFaceImage": { + "title": "上传人脸图片", + "desc": "上传图片以扫描人脸并包含在{{pageToggle}}中" + }, + "createFaceLibrary": { + "title": "创建特征库", + "desc": "创建一个新的特征库", + "new": "新建人脸", + "nextSteps": "建议按以下步骤建立可靠的特征库:
  • 使用近期识别记录选项卡为每个检测到的人员选择并训练图像
  • 优先使用正脸图像以获得最佳效果,尽可能避免使用侧脸图像进行训练
  • " + }, + "train": { + "title": "近期识别记录", + "aria": "选择近期识别记录", + "empty": "近期未检测到人脸识别操作", + "titleShort": "近期" + }, + "selectItem": "选择 {{item}}", + "selectFace": "选择人脸", + "deleteFaceLibrary": { + "title": "删除名称", + "desc": "确定要删除特征库 {{name}} 吗?此操作将永久删除所有关联的人脸特征数据。" + }, + "button": { + "deleteFaceAttempts": "删除人脸", + "addFace": "添加人脸", + "uploadImage": "上传图片", + "reprocessFace": "重新处理人脸", + "renameFace": "重命名人脸", + "deleteFace": "删除人脸" + }, + "imageEntry": { + "validation": { + "selectImage": "请选择图片文件。" + }, + "dropActive": "拖动图片文件到这里…", + "dropInstructions": "拖动或粘贴图片文件到此处,也可以点击选择文件", + "maxSize": "最大文件大小:{{size}}MB" + }, + "readTheDocs": "阅读文档", + "trainFaceAs": "将人脸特征训练为:", + "trainFace": "训练人脸特征", + "toast": { + "success": { + "uploadedImage": "图片上传成功。", + "addFaceLibrary": "{{name}} 已成功添加至人脸库!", + "deletedFace_other": "成功删除 {{count}} 个 人脸特征。", + "deletedName_other": "成功删除 {{count}} 个 人脸特征。", + "trainedFace": "人脸特征训练成功。", + "updatedFaceScore": "更新 {{name}} 人脸特征评分({{score}})成功。", + "renamedFace": "成功重命名人脸为{{name}}" + }, + "error": { + "uploadingImageFailed": "图片上传失败:{{errorMessage}}", + "addFaceLibraryFailed": "人脸命名失败:{{errorMessage}}", + "deleteFaceFailed": "删除失败:{{errorMessage}}", + "deleteNameFailed": "特征集删除失败:{{errorMessage}}", + "trainFailed": "训练失败:{{errorMessage}}", + "updateFaceScoreFailed": "更新人脸评分失败:{{errorMessage}}", + "renameFaceFailed": "重命名人脸失败:{{errorMessage}}" + } + }, + "steps": { + "faceName": "输入人脸姓名", + "uploadFace": "上传人脸照片", + "nextSteps": "下一步", + "description": { + "uploadFace": "上传一张{{name}}的正面人脸照片。图片无需裁剪为仅显示面部。" + } + }, + "renameFace": { + "desc": "为 {{name}} 输入新的名称", + "title": "重命名人脸" + }, + "collections": "特征库", + "deleteFaceAttempts": { + "desc_other": "你确定要删除 {{count}} 张人脸数据吗?此操作不可撤销。", + "title": "删除人脸" + }, + "pixels": "{{area}} 像素", + "nofaces": "没有可用的人脸" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/live.json new file mode 100644 index 0000000..aeefa18 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/live.json @@ -0,0 +1,189 @@ +{ + "documentTitle": "实时监控 - Frigate", + "documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate", + "lowBandwidthMode": "低带宽模式", + "twoWayTalk": { + "enable": "开启实时对话", + "disable": "关闭实时通话" + }, + "cameraAudio": { + "enable": "开启音频输出", + "disable": "关闭音频输出" + }, + "ptz": { + "move": { + "clickMove": { + "label": "点击画面以使摄像头居中", + "enable": "启用点击移动", + "disable": "禁用点击移动" + }, + "left": { + "label": "PTZ摄像头向左移动" + }, + "up": { + "label": "PTZ摄像头向上移动" + }, + "down": { + "label": "PTZ摄像头向下移动" + }, + "right": { + "label": "PTZ摄像头向右移动" + } + }, + "zoom": { + "in": { + "label": "PTZ摄像头放大" + }, + "out": { + "label": "PTZ摄像头缩小" + } + }, + "frame": { + "center": { + "label": "点击将PTZ摄像头画面居中" + } + }, + "presets": "PTZ摄像头预设", + "focus": { + "in": { + "label": "PTZ摄像头聚焦" + }, + "out": { + "label": "PTZ摄像头拉远" + } + } + }, + "camera": { + "enable": "开启摄像头", + "disable": "关闭摄像头" + }, + "muteCameras": { + "enable": "屏蔽所有摄像头", + "disable": "取消屏蔽所有摄像头" + }, + "detect": { + "enable": "启用检测", + "disable": "关闭检测" + }, + "recording": { + "enable": "启用录制", + "disable": "关闭录制" + }, + "snapshots": { + "enable": "启用快照", + "disable": "关闭快照" + }, + "audioDetect": { + "enable": "启用音频检测", + "disable": "关闭音频检测" + }, + "autotracking": { + "enable": "启用自动追踪", + "disable": "关闭自动追踪" + }, + "streamStats": { + "enable": "显示视频流统计信息", + "disable": "隐藏视频流统计信息" + }, + "manualRecording": { + "title": "按需录制", + "tips": "根据此摄像头的录像存储设置,可以下载即时快照或手动触发事件记录。", + "playInBackground": { + "label": "后台播放", + "desc": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "showStats": { + "label": "显示统计信息", + "desc": "启用此选项可在摄像头画面上叠加显示视频流统计信息。" + }, + "debugView": "调试视图", + "start": "开始手动按需录制", + "started": "已启用手动按需录制。", + "failedToStart": "启动手动录制失败。", + "recordDisabledTips": "由于此摄像头的配置中禁用了录制或对其进行了限制,将只会保存快照。", + "end": "停止手动按需录制", + "ended": "已完成手动按需录制。", + "failedToEnd": "停止手动录制失败。" + }, + "streamingSettings": "视频流设置", + "notifications": "通知", + "audio": "音频", + "suspend": { + "forTime": "暂停时长: " + }, + "stream": { + "title": "视频流", + "audio": { + "tips": { + "title": "音频必须从摄像头输出并在 go2rtc 中配置为此视频流使用。", + "documentation": "阅读文档 " + }, + "available": "此视频流支持音频", + "unavailable": "此视频流不支持音频" + }, + "twoWayTalk": { + "tips": "您的设备必须支持此功能,并且必须配置 WebRTC 以支持双向对讲。", + "tips.documentation": "阅读文档 ", + "available": "此视频流支持双向对讲", + "unavailable": "此视频流不支持双向对讲" + }, + "lowBandwidth": { + "tips": "由于缓冲或视频流错误,实时视图处于低带宽模式。", + "resetStream": "重置视频流" + }, + "playInBackground": { + "label": "后台播放", + "tips": "启用此选项可在播放器隐藏时继续视频流播放。" + }, + "debug": { + "picker": "调试模式下无法切换视频流。调试将始终使用检测(detect)功能的视频流。" + } + }, + "cameraSettings": { + "title": "{{camera}} 设置", + "cameraEnabled": "摄像头已启用", + "objectDetection": "目标检测", + "recording": "录制", + "snapshots": "快照", + "audioDetection": "音频检测", + "autotracking": "自动追踪", + "transcription": "音频转录" + }, + "history": { + "label": "显示历史录像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "画面变动", + "active_objects": "活动目标" + }, + "notAllTips": "您的 {{source}} 录制保留配置设置为 mode: {{effectiveRetainMode}},因此此按需录制将仅保留包含 {{effectiveRetainModeName}} 的片段。" + }, + "editLayout": { + "label": "编辑布局", + "group": { + "label": "编辑摄像头分组" + }, + "exitEdit": "退出编辑" + }, + "transcription": { + "enable": "启用实时音频转录", + "disable": "关闭实时音频转录" + }, + "noCameras": { + "title": "未设置摄像头", + "description": "准备开始连接摄像头至 Frigate 。", + "buttonText": "添加摄像头", + "restricted": { + "title": "无可用摄像头", + "description": "你没有权限查看此分组中的任何摄像头。" + } + }, + "snapshot": { + "takeSnapshot": "下载即时快照", + "noVideoSource": "当前无可用于快照的视频源。", + "captureFailed": "捕获快照失败。", + "downloadStarted": "快照下载已开始。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/recording.json new file mode 100644 index 0000000..77443e1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/recording.json @@ -0,0 +1,12 @@ +{ + "export": "导出", + "calendar": "日历", + "filter": "过滤器", + "filters": "筛选条件", + "toast": { + "error": { + "noValidTimeSelected": "未选择有效的时间范围", + "endTimeMustAfterStartTime": "结束时间必须晚于开始时间" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/search.json new file mode 100644 index 0000000..8a25c11 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/search.json @@ -0,0 +1,74 @@ +{ + "search": "搜索", + "savedSearches": "已保存的搜索", + "searchFor": "搜索 {{inputValue}}", + "button": { + "clear": "清除搜索", + "save": "保存搜索", + "delete": "删除已保存的搜索", + "filterInformation": "筛选信息", + "filterActive": "筛选器已激活" + }, + "trackedObjectId": "追踪目标 ID", + "filter": { + "label": { + "cameras": "摄像头", + "labels": "标签", + "zones": "区域", + "sub_labels": "子标签", + "search_type": "搜索类型", + "time_range": "时间范围", + "before": "之前", + "after": "之后", + "min_score": "最低分数", + "max_score": "最高分数", + "min_speed": "最低速度", + "max_speed": "最高速度", + "recognized_license_plate": "识别的车牌", + "has_clip": "包含片段", + "has_snapshot": "包含快照" + }, + "searchType": { + "thumbnail": "缩略图", + "description": "描述" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "结束日期必须晚于开始日期。", + "afterDatebeEarlierBefore": "开始日期必须早于结束日期。", + "minScoreMustBeLessOrEqualMaxScore": "最低分数必须小于或等于最高分数。", + "maxScoreMustBeGreaterOrEqualMinScore": "最高分数必须大于或等于最低分数。", + "minSpeedMustBeLessOrEqualMaxSpeed": "最低速度必须小于或等于最高速度。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "最高速度必须大于或等于最低速度。" + } + }, + "tips": { + "title": "如何使用文本筛选器", + "desc": { + "text": "筛选器可帮助您缩小搜索范围。注意,目前还暂不支持中文搜索。以下是在输入字段中使用筛选器的方法:", + "step": "
    • 输入筛选器名称后跟一个冒号(例如:“cameras:”)。
    • 从建议中选择一个值或输入您自己的值。
    • 使用多个筛选器时,可以在它们之间用空格分隔。
    • 日期筛选器(before: 和 after:)使用 {{DateFormat}} 格式。
    • 时间范围筛选器使用 {{exampleTime}} 格式。
    • 点击筛选器旁边的“x”即可移除筛选条件。
    ", + "example": "示例:cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM", + "step2": "选择给出的建议值或自行输入;", + "step3": "多个过滤器之间用空格分隔;", + "step5": "时间范围过滤器使用 {{exampleTime}} 格式;", + "step6": "点击过滤器旁的'x'可移除该过滤选项。", + "exampleLabel": "范例:", + "step1": "输入过滤键名后接英文冒号(例如 \"cameras:\" );", + "step4": "日期过滤器(before: 和 after:)使用 {{DateFormat}} 格式;" + } + }, + "header": { + "currentFilterType": "筛选值", + "noFilters": "筛选条件", + "activeFilters": "激活的筛选项" + } + }, + "similaritySearch": { + "title": "相似搜索", + "active": "相似搜索已激活", + "clear": "清除相似搜索" + }, + "placeholder": { + "search": "搜索…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/settings.json new file mode 100644 index 0000000..5a5130f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/settings.json @@ -0,0 +1,1306 @@ +{ + "documentTitle": { + "default": "设置 - Frigate", + "authentication": "身份验证设置 - Frigate", + "camera": "摄像头设置 - Frigate", + "classification": "分类设置 - Frigate", + "masksAndZones": "遮罩和区域编辑器 - Frigate", + "motionTuner": "画面变动调整器 - Frigate", + "object": "调试 - Frigate", + "general": "页面设置 - Frigate", + "frigatePlus": "Frigate+ 设置 - Frigate", + "notifications": "通知设置 - Frigate", + "enrichments": "增强功能设置 - Frigate", + "cameraManagement": "管理摄像头 - Frigate", + "cameraReview": "摄像头核查设置 - Frigate" + }, + "menu": { + "ui": "界面设置", + "classification": "分类设置", + "cameras": "摄像头设置", + "masksAndZones": "遮罩/ 区域", + "motionTuner": "画面变动调整器", + "debug": "调试", + "users": "用户", + "notifications": "通知", + "frigateplus": "Frigate+", + "enrichments": "增强功能", + "triggers": "触发器", + "roles": "权限组", + "cameraManagement": "管理", + "cameraReview": "核查" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未保存的更改。", + "desc": "是否要在继续之前保存更改?" + } + }, + "cameraSetting": { + "camera": "摄像头", + "noCamera": "没有摄像头" + }, + "general": { + "title": "页面设置", + "liveDashboard": { + "title": "实时监控面板", + "automaticLiveView": { + "label": "自动实时预览", + "desc": "检测到画面活动时将自动切换至该摄像头实时画面。禁用此选项会导致实时监控页面的摄像头图像每分钟只更新一次。" + }, + "playAlertVideos": { + "label": "播放警报视频", + "desc": "默认情况下,实时监控页面上的最新警报会以一小段循环视频的形式进行播放。禁用此选项将仅显示浏览器本地缓存的静态图片。" + }, + "displayCameraNames": { + "label": "始终显示摄像头名称", + "desc": "在有多摄像头情况下的实时监控页面,将始终显示摄像头名称标签。" + }, + "liveFallbackTimeout": { + "label": "实时监控播放器回退超时", + "desc": "当摄像头的高清实时监控流不可用时,将在此时间后回退到低带宽模式。默认值:3秒。" + } + }, + "storedLayouts": { + "title": "存储监控面板布局", + "desc": "可以在监控面板调整或拖动摄像头的布局。这些设置将保存在浏览器的本地存储中。", + "clearAll": "清除所有布局" + }, + "cameraGroupStreaming": { + "title": "摄像头组视频流设置", + "desc": "每个摄像头组的视频流设置将保存在浏览器的本地存储中。", + "clearAll": "清除所有视频流设置" + }, + "recordingsViewer": { + "title": "回放查看", + "defaultPlaybackRate": { + "label": "默认播放速率", + "desc": "调整播放录像时默认的速率。" + } + }, + "calendar": { + "title": "日历", + "firstWeekday": { + "label": "每周第一天", + "desc": "设置每周第一天是星期几。", + "sunday": "星期天", + "monday": "星期一" + } + }, + "toast": { + "success": { + "clearStoredLayout": "已清除 {{cameraName}} 的存储布局", + "clearStreamingSettings": "已清除所有摄像头组的视频流设置。" + }, + "error": { + "clearStoredLayoutFailed": "清除存储布局失败:{{errorMessage}}", + "clearStreamingSettingsFailed": "清除视频流设置失败:{{errorMessage}}" + } + } + }, + "classification": { + "title": "分类设置", + "semanticSearch": { + "title": "语义搜索", + "desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。", + "readTheDocumentation": "阅读文档(英文)", + "reindexNow": { + "label": "立即重建索引", + "desc": "重建索引将为所有跟踪对象重新生成特征向量。该过程将在后台运行,可能会使CPU满载,所需时间取决于跟踪对象的数量。", + "confirmTitle": "确认重建索引", + "confirmDesc": "确定要为所有跟踪对象重建特征向量索引吗?此过程将在后台运行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", + "confirmButton": "重建索引", + "success": "重建索引已成功启动。", + "alreadyInProgress": "重建索引已在执行中。", + "error": "启动重建索引失败:{{errorMessage}}" + }, + "modelSize": { + "label": "模型大小", + "desc": "用于语义搜索的语言模型大小。", + "small": { + "title": "小", + "desc": "使用 模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。" + }, + "large": { + "title": "大", + "desc": "使用 模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。" + } + } + }, + "faceRecognition": { + "title": "人脸识别", + "desc": "人脸识别功能允许为人物分配名称,当识别到他们的面孔时,Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。", + "readTheDocumentation": "阅读文档(英文)", + "modelSize": { + "label": "模型大小", + "desc": "用于人脸识别的模型尺寸。", + "small": { + "title": "小", + "desc": "使用模型将采用FaceNet人脸特征提取模型,可在大多数CPU上高效运行。" + }, + "large": { + "title": "大", + "desc": "使用模型将采用ArcFace人脸特征提取模型,若条件允许将自动使用GPU运行。" + } + } + }, + "licensePlateRecognition": { + "title": "车牌识别", + "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 recognized_license_plate 字段中,或将已知名称作为子标签添加到汽车类型的对象中。常见的使用场景可能是读取驶入车道的汽车车牌或经过街道的汽车车牌。", + "readTheDocumentation": "阅读文档(英文)" + }, + "toast": { + "success": "分类设置已保存,请重启 Frigate 以应用更改。", + "error": "保存配置更改失败:{{errorMessage}}" + }, + "birdClassification": { + "title": "鸟类识别分类", + "desc": "鸟类识别分类采用量化TensorFlow模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。该信息将显示在用户界面、过滤器及通知中。" + }, + "restart_required": "需要重启(分类设置已修改)", + "unsavedChanges": "分类设置未保存" + }, + "camera": { + "title": "摄像头设置", + "streams": { + "title": "视频流", + "desc": "暂时禁用摄像头,除非重启Frigate否则将保持禁用。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理。检测、录制和调试功能都将不可用。
    注意:该选项不会禁用 go2rtc 转播。" + }, + "review": { + "title": "核查", + "desc": "启用/禁用摄像头的警报和检测。禁用后,除非重启Frigate,否则不会生成新的核查项。 ", + "alerts": "警报 ", + "detections": "检测 " + }, + "reviewClassification": { + "title": "核查分级", + "desc": "Frigate 将核查项分为“警报”和“检测”。默认情况下,所有的 汽车 对象都将视为警报。你可以通过修改配置文件配置区域来细分。", + "readTheDocumentation": "阅读文档", + "noDefinedZones": "该摄像头没有设置区域。", + "objectAlertsTips": "所有 {{alertsLabels}} 对象在 {{cameraName}} 下都将显示为警报。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 类的目标或物体在 {{cameraName}} 下的 {{zone}} 区内都将显示为警报。", + "objectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区,都将显示为检测。", + "zoneObjectDetectionsTips": { + "text": "所有未在 {{cameraName}} 上归类为 {{detectionsLabels}} 的目标或物体在 {{zone}} 区内都将显示为检测。", + "notSelectDetections": "所有在 {{cameraName}} 下的 {{zone}} 区内检测到的 {{detectionsLabels}} 目标或物体,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", + "regardlessOfZoneObjectDetectionsTips": "所有未在 {{cameraName}} 归类的 {{detectionsLabels}} 目标或物体,无论它位于哪个区域,都将显示为检测。" + }, + "selectAlertsZones": "选择警报区", + "selectDetectionsZones": "选择检测区域", + "limitDetections": "限制仅在特定区域内进行检测", + "toast": { + "success": "核查分级配置已保存。请重启 Frigate 以应用更改。" + }, + "unsavedChanges": "{{camera}} 的核查分类设置未保存" + }, + "object_descriptions": { + "title": "生成式AI对象描述", + "desc": "临时启用/禁用此摄像头的生成式AI对象描述功能。禁用后,系统将不再请求该摄像头追踪对象的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式AI核查描述", + "desc": "临时启用/禁用本摄像头的生成式AI核查描述功能。禁用后,系统将不再为该摄像头的核查项目请求AI生成的描述内容。" + }, + "addCamera": "添加新摄像头", + "editCamera": "编辑摄像头:", + "selectCamera": "选择摄像头", + "backToSettings": "返回摄像头设置", + "cameraConfig": { + "add": "添加摄像头", + "edit": "编辑摄像头", + "description": "配置摄像头设置,包括视频流输入和视频流功能选择。", + "name": "摄像头名称", + "nameRequired": "摄像头名称为必填项", + "nameInvalid": "摄像头名称只能包含字母、数字、下划线或连字符", + "namePlaceholder": "比如:front_door", + "enabled": "开启", + "ffmpeg": { + "inputs": "视频流输入", + "path": "视频流路径", + "pathRequired": "视频流路径为必填项", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少需要指定一个功能", + "rolesUnique": "每个功能(音频、检测、录制)只能用于一个视频流,不能够重复分配到多个视频流", + "addInput": "添加视频流输入", + "removeInput": "移除视频流输入", + "inputsRequired": "至少需要一个视频流" + }, + "toast": { + "success": "摄像头 {{cameraName}} 保存已保存" + }, + "nameLength": "摄像头名称必须少于24个字符。" + } + }, + "masksAndZones": { + "filter": { + "all": "所有遮罩和区域" + }, + "toast": { + "success": { + "copyCoordinates": "已复制 {{polyName}} 的坐标到剪贴板。" + }, + "error": { + "copyCoordinatesFailed": "无法复制坐标到剪贴板。" + } + }, + "form": { + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "区域名称必须至少包含 2 个字符。", + "mustNotBeSameWithCamera": "区域名称不能与摄像头名称相同。", + "alreadyExists": "该摄像头已有相同的区域名称。", + "mustNotContainPeriod": "区域名称不能包含句点。", + "hasIllegalCharacter": "区域名称包含非法字符。", + "mustHaveAtLeastOneLetter": "区域名称必须至少包含一个字母。" + } + }, + "distance": { + "error": { + "text": "距离必须大于或等于 0.1。", + "mustBeFilled": "所有距离字段必须填写才能使用速度估算。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "惯性必须大于 0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "徘徊时间必须大于或等于 0。" + } + }, + "polygonDrawing": { + "removeLastPoint": "删除最后一个点", + "reset": { + "label": "清除所有点" + }, + "snapPoints": { + "true": "启用点对齐", + "false": "禁用点对齐" + }, + "delete": { + "title": "确认删除", + "desc": "你确定要删除{{type}} {{name}} 吗?", + "success": "{{name}} 已被删除。" + }, + "error": { + "mustBeFinished": "多边形绘制必须完成闭合后才能保存。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度阈值必须大于或等于0.1。" + } + } + }, + "zones": { + "label": "区域", + "documentTitle": "编辑区域 - Frigate", + "desc": { + "title": "该功能允许你定义特定区域,以便你可以确定特定目标或物体是否在该区域内。", + "documentation": "文档" + }, + "add": "添加区域", + "edit": "编辑区域", + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "name": { + "title": "区域名称", + "inputPlaceHolder": "请输入名称…", + "tips": "名称至少包含两个字符,且不能和摄像头名或该摄像头下的其他区域同名。" + }, + "inertia": { + "title": "惯性", + "desc": "识别指定目标前该目标必须在这个区域内出现了多少帧。默认值:3" + }, + "loiteringTime": { + "title": "停留时间", + "desc": "设置目标必须在区域中至少要活动多少时间(单位为秒)。默认值:0" + }, + "objects": { + "title": "目标/物体", + "desc": "将在此区域应用的目标/物体类别列表。" + }, + "allObjects": "所有目标/物体", + "speedEstimation": { + "title": "速度估算", + "desc": "启用此区域内物体的速度估算。该区域必须恰好包含 4 个点。", + "docs": "阅读文档", + "lineBDistance": "B线距离({{unit}})", + "lineCDistance": "C线距离({{unit}})", + "lineDDistance": "D线距离({{unit}})", + "lineADistance": "A线距离({{unit}})" + }, + "speedThreshold": { + "title": "速度阈值 ({{unit}})", + "desc": "指定物体在此区域内被视为有效的最低速度。", + "toast": { + "error": { + "pointLengthError": "此区域的速度估算已禁用。启用速度估算的区域必须恰好包含 4 个点。", + "loiteringTimeError": "徘徊时间大于 0 的区域不应与速度估算一起使用。" + } + } + }, + "toast": { + "success": "区域 ({{zoneName}}) 已保存。" + } + }, + "motionMasks": { + "label": "画面变动遮罩", + "documentTitle": "编辑画面变动遮罩 - Frigate", + "desc": { + "title": "画面变动遮罩用于防止触发不必要的画面变动检测。过度的设置遮罩将使目标更加难以被追踪。", + "documentation": "文档" + }, + "add": "添加画面变动遮罩", + "edit": "编辑画面变动遮罩", + "context": { + "title": "画面变动遮罩用于防止不需要的画面变动触发检测(例如:容易被风吹动的树枝、摄像头画面上显示的时间等)。画面变动遮罩应谨慎使用,过度的遮罩会导致追踪目标变得更加困难。", + "documentation": "阅读文档" + }, + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "polygonAreaTooLarge": { + "title": "画面变动遮罩的大小达到了摄像头画面的{{polygonArea}}%。不建议设置太大的画面变动遮罩。", + "tips": "画面变动遮罩并不会使该区域无法检测到指定目标/物体,如有需要,你应该使用 区域 来限制检测的目标/物体类型。", + "documentation": "阅读文档" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已保存。", + "noName": "画面变动遮罩已保存。" + } + } + }, + "objectMasks": { + "label": "目标遮罩", + "documentTitle": "编辑目标遮罩 - Frigate", + "desc": { + "title": "目标过滤器用于防止特定位置出现对某个目标/物体的误报。", + "documentation": "文档" + }, + "add": "添加目标遮罩", + "edit": "编辑目标遮罩", + "context": "目标过滤器用于防止特定位置的指定目标会误报。", + "point_other": "{{count}} 点", + "clickDrawPolygon": "在图像上点击添加点绘制多边形区域。", + "objects": { + "title": "目标/物体", + "desc": "将应用于此目标遮罩的目标或物体类型。", + "allObjectTypes": "所有目标或物体类型" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已保存。", + "noName": "目标遮罩已保存。" + } + } + }, + "restart_required": "需要重启(遮罩与区域已修改)", + "motionMaskLabel": "画面变动遮罩 {{number}}", + "objectMaskLabel": "目标/物体遮罩 {{number}}({{label}})" + }, + "motionDetectionTuner": { + "title": "画面变动检测调整器", + "desc": { + "title": "Frigate 将使用画面变化检测作为首个步骤,以确认一帧画面中是否有目标或物体需要使用目标检测。", + "documentation": "阅读有关画面变动检测的文档" + }, + "Threshold": { + "title": "阈值", + "desc": "阈值决定像素亮度变化达到多少时会被认为是画面变动。默认值:30" + }, + "contourArea": { + "title": "轮廓面积", + "desc": "轮廓面积值用于判断哪些相连的像素变化区域可被认定为画面变动。默认值:10" + }, + "improveContrast": { + "title": "提高对比度", + "desc": "提高较暗场景的对比度。默认值:启用" + }, + "toast": { + "success": "画面变动设置已保存。" + }, + "unsavedChanges": "{{camera}} 的画面变动调整器设置未保存" + }, + "debug": { + "title": "调试", + "detectorDesc": "Frigate 将使用检测器({{detectors}})来检测摄像头视频流中的目标或物体。", + "desc": "调试界面将实时显示被追踪的目标以及统计信息,目标列表将显示检测到的目标和延迟显示的概览。", + "debugging": "调试选项", + "objectList": "目标列表", + "noObjects": "没有目标", + "boundingBoxes": { + "title": "边界框", + "desc": "将在被追踪的目标周围显示边界框", + "colors": { + "label": "目标边界框颜色定义", + "info": "
  • 启用后,将会为每个目标的标签分配不同的颜色
  • 深蓝色细线代表该目标或物体在当前时间点未被检测到
  • 灰色细线代表检测到的目标或物体静止不动
  • 粗线表示在启动自动追踪时,该目标为自动追踪的主体
  • " + } + }, + "timestamp": { + "title": "时间戳", + "desc": "在图像上显示时间戳" + }, + "zones": { + "title": "区域", + "desc": "显示已定义的区域图层" + }, + "mask": { + "title": "画面变动遮罩", + "desc": "显示画面变动遮罩图层" + }, + "motion": { + "title": "画面变动区域框", + "desc": "在检测到画面变动的区域显示区域框", + "tips": "

    画面变动区域框


    将在当前检测到画面变动的区域内显示红色区域框。

    " + }, + "regions": { + "title": "范围", + "desc": "显示发送给目标检测器感兴趣的区域框", + "tips": "

    范围框


    将在帧中发送到目标检测器的感兴趣范围上叠加绿色框。

    " + }, + "objectShapeFilterDrawing": { + "title": "允许绘制“目标形状过滤器”", + "desc": "在图像上绘制矩形,以查看区域和比例详细信息", + "tips": "启用此选项,能够在摄像头画面上绘制矩形,将显示其区域和比例。你可以通过使用这些值在配置中设置目标形状过滤器的参数。", + "document": "阅读文档 ", + "score": "分数", + "ratio": "比例", + "area": "区域" + }, + "paths": { + "title": "行动轨迹", + "desc": "显示被追踪目标的行动轨迹关键点", + "tips": "

    行动轨迹

    将使用线条来标示被追踪目标在其活动周期内移动的关键位置点。

    " + }, + "audio": { + "title": "音频", + "noAudioDetections": "未检测到音频事件", + "score": "分值", + "currentRMS": "当前均方根值(RMS)", + "currentdbFS": "当前满量程相对分贝值(dbFS)" + }, + "openCameraWebUI": "打开 {{camera}} 的管理页面" + }, + "users": { + "title": "用户", + "management": { + "title": "用户管理", + "desc": "管理此 Frigate 实例的用户账户。" + }, + "addUser": "添加用户", + "updatePassword": "修改密码", + "toast": { + "success": { + "createUser": "用户 {{user}} 创建成功", + "deleteUser": "用户 {{user}} 删除成功", + "updatePassword": "已成功修改密码。", + "roleUpdated": "已更新 {{user}} 的权限组" + }, + "error": { + "setPasswordFailed": "保存密码出现错误:{{errorMessage}}", + "createUserFailed": "创建用户失败:{{errorMessage}}", + "deleteUserFailed": "删除用户失败:{{errorMessage}}", + "roleUpdateFailed": "更新权限组失败:{{errorMessage}}" + } + }, + "table": { + "username": "用户名", + "actions": "操作", + "role": "权限组", + "noUsers": "未找到用户。", + "changeRole": "更改用户角色", + "password": "密码", + "deleteUser": "删除用户" + }, + "dialog": { + "form": { + "user": { + "title": "用户名", + "desc": "仅允许使用字母、数字、句点和下划线。", + "placeholder": "请输入用户名" + }, + "password": { + "title": "密码", + "placeholder": "请输入密码", + "confirm": { + "title": "确认密码", + "placeholder": "请再次输入密码" + }, + "strength": { + "title": "密码强度: ", + "weak": "弱", + "medium": "中等", + "strong": "强", + "veryStrong": "非常强" + }, + "match": "密码匹配", + "notMatch": "密码不匹配", + "show": "显示密码", + "hide": "隐藏密码", + "requirements": { + "title": "密码要求:", + "length": "至少8个字符", + "uppercase": "至少一个大写字母", + "digit": "至少一位数字", + "special": "至少一个特殊符号 (!@#$%^&*(),.?\":{}|<>)" + } + }, + "newPassword": { + "title": "新密码", + "placeholder": "请输入新密码", + "confirm": { + "placeholder": "请再次输入新密码" + } + }, + "usernameIsRequired": "用户名为必填项", + "passwordIsRequired": "必须输入密码", + "currentPassword": { + "title": "当前密码", + "placeholder": "请输入当前密码" + } + }, + "createUser": { + "title": "创建新用户", + "desc": "创建一个新用户账户,并指定一个角色以控制访问 Frigate UI 的权限。", + "usernameOnlyInclude": "用户名只能包含字母、数字和 _", + "confirmPassword": "请确认你的密码" + }, + "deleteUser": { + "title": "删除该用户", + "desc": "此操作无法撤销。这将永久删除用户账户并移除所有相关数据。", + "warn": "你确定要删除 {{username}} 吗?" + }, + "passwordSetting": { + "updatePassword": "更新 {{username}} 的密码", + "setPassword": "设置密码", + "desc": "创建一个强密码来保护此账户。", + "doNotMatch": "两次输入密码不匹配", + "cannotBeEmpty": "密码不能为空", + "currentPasswordRequired": "当前密码为必填", + "incorrectCurrentPassword": "当前密码错误", + "passwordVerificationFailed": "验证密码失败", + "multiDeviceWarning": "其他已登录的设备将需要在 {{refresh_time}} 内重新登录。", + "multiDeviceAdmin": "你也可以通过轮换你的 JWT 密钥,强制所有用户立即重新登录验证。" + }, + "changeRole": { + "title": "更改用户权限组", + "desc": "更新 {{username}} 的权限", + "roleInfo": { + "admin": "管理员", + "viewer": "成员", + "viewerDesc": "仅能够查看实时监控面板、核查、浏览和导出功能。", + "adminDesc": "完全功能与访问权限。", + "intro": "为该用户选择一个合适的权限组:", + "customDesc": "自定义特定摄像头的访问规则。" + }, + "select": "选择权限组" + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知设置", + "desc": "Frigate 在浏览器中运行或作为 PWA 安装时,可以原生向您的设备发送推送通知。", + "documentation": "阅读文档" + }, + "globalSettings": { + "title": "全局设置", + "desc": "临时暂停所有已注册设备上特定摄像头的通知。" + }, + "notificationUnavailable": { + "title": "通知功能不可用", + "desc": "网页推送通知需要安全连接(https://…)。这是浏览器的限制。请通过安全方式访问 Frigate 以使用通知功能。", + "documentation": "阅读文档" + }, + "email": { + "title": "电子邮箱", + "placeholder": "例如:example@email.com", + "desc": "需要输入有效的电子邮件,在推送服务出现问题时,将使用此电子邮件进行通知。" + }, + "cameras": { + "title": "摄像头", + "noCameras": "没有可用的摄像头", + "desc": "选择要启用通知的摄像头。" + }, + "deviceSpecific": "设备专用设置", + "registerDevice": "注册该设备", + "unregisterDevice": "取消注册该设备", + "sendTestNotification": "发送测试通知", + "active": "通知已启用", + "suspended": "通知已暂停 {{time}}", + "suspendTime": { + "5minutes": "暂停 5 分钟", + "10minutes": "暂停 10 分钟", + "30minutes": "暂停 30 分钟", + "1hour": "暂停 1 小时", + "12hours": "暂停 12 小时", + "24hours": "暂停 24 小时", + "untilRestart": "暂停直到重启", + "suspend": "暂停" + }, + "cancelSuspension": "取消暂停", + "toast": { + "success": { + "registered": "已成功注册通知。需要重启 Frigate 才能发送任何通知(包括测试通知)。", + "settingSaved": "通知设置已保存。" + }, + "error": { + "registerFailed": "通知注册失败。" + } + }, + "unsavedRegistrations": "未保存通知注册", + "unsavedChanges": "未保存通知设置更改" + }, + "frigatePlus": { + "title": "Frigate+ 设置", + "apiKey": { + "title": "Frigate+ API 密钥", + "validated": "Frigate+ API 密钥已检测并验证通过", + "notValidated": "未检测到 Frigate+ API 密钥或验证未通过", + "desc": "Frigate+ API 密钥用于启用与 Frigate+ 服务的集成。", + "plusLink": "了解更多关于 Frigate+" + }, + "snapshotConfig": { + "title": "快照配置", + "desc": "提交到 Frigate+ 需要同时在配置中启用快照和 clean_copy 快照。", + "documentation": "阅读文档", + "cleanCopyWarning": "部分摄像头已启用快照但未启用 clean_copy。您需要在快照配置中启用 clean_copy,才能将这些摄像头的图像提交到 Frigate+。", + "table": { + "camera": "摄像头", + "snapshots": "快照", + "cleanCopySnapshots": "clean_copy 快照" + } + }, + "modelInfo": { + "title": "模型信息", + "modelType": "模型类型", + "trainDate": "训练日期", + "baseModel": "基础模型", + "supportedDetectors": "支持的检测器", + "dimensions": "大小", + "cameras": "摄像头", + "loading": "正在加载模型信息…", + "error": "加载模型信息失败", + "availableModels": "可用模型", + "loadingAvailableModels": "正在加载可用模型…", + "modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前检测器配置兼容的模型。", + "plusModelType": { + "baseModel": "基础模型", + "userModel": "定向调优" + } + }, + "toast": { + "success": "Frigate+ 设置已保存。请重启 Frigate 以应用更改。", + "error": "配置更改保存失败:{{errorMessage}}" + }, + "restart_required": "需要重启(Frigate+模型已修改)", + "unsavedChanges": "未保存Frigate+变更设置" + }, + "enrichments": { + "title": "增强功能设置", + "birdClassification": { + "desc": "鸟类分类通过量化的TensorFlow模型识别已知鸟类。当识别到已知鸟类时,其通用名称将作为子标签(sub_label)添加。此信息包含在用户界面、筛选器以及通知中。", + "title": "鸟类分类" + }, + "semanticSearch": { + "reindexNow": { + "desc": "重建索引将为所有追踪目标重新生成特征向量信息。该过程将在后台进行,可能会使CPU满载,所需时间取决于追踪目标的数量。", + "label": "立即重建索引", + "confirmTitle": "确认重建索引", + "confirmDesc": "确定要为所有追踪目标重建特征向量索引信息吗?此过程将在后台进行,但可能会导致CPU满载并耗费较长时间。您可以在 浏览 页面查看进度。", + "confirmButton": "重建索引", + "success": "重建索引已成功启动。", + "alreadyInProgress": "重建索引已在执行中。", + "error": "启动重建索引失败:{{errorMessage}}" + }, + "modelSize": { + "label": "模型大小", + "desc": "用于语义搜索的语言模型大小。", + "small": { + "title": "小", + "desc": "将使用 模型。该模型将使用少量的内存,在CPU上也能较快的运行,质量较好。" + }, + "large": { + "title": "大", + "desc": "将使用 模型。该选项使用了完整的Jina模型,在合适的时候将自动使用GPU。" + } + }, + "title": "分类搜索", + "desc": "Frigate中的语义搜索功能允许您通过图片、用户自定义的文本描述,或自动生成的文本描述等方式在核查项目中查找目标/物体。", + "readTheDocumentation": "阅读文档" + }, + "licensePlateRecognition": { + "desc": "Frigate 可以识别车辆的车牌,并自动将检测到的字符添加到 recognized_license_plate 字段中,或将已知车牌对应的名称作为子标签添加到该车辆目标中。一般常用于读取驶入车道的车辆车牌或经过街道的车辆车牌。", + "title": "车牌识别", + "readTheDocumentation": "阅读文档" + }, + "faceRecognition": { + "desc": "人脸识别功能允许为人物分配名称,当识别到他们的面孔时,Frigate 会将人物的名字作为子标签进行分配。这些信息会显示在界面、过滤器以及通知中。", + "title": "人脸识别", + "readTheDocumentation": "阅读文档", + "modelSize": { + "label": "模型大小", + "desc": "用于人脸识别的模型大小。", + "small": { + "title": "小", + "desc": "将使用模型。该选项采用FaceNet人脸特征提取模型,可在大多数CPU上高效运行。" + }, + "large": { + "title": "大", + "desc": "将使用模型。该选项使用ArcFace人脸特征提取模型,在需要的时候自动使用GPU运行。" + } + } + }, + "toast": { + "success": "增强功能设置已保存。请重启 Frigate 以应用更改。", + "error": "配置更改保存失败:{{errorMessage}}" + }, + "unsavedChanges": "增强功能设置未保存", + "restart_required": "需要重启(增强功能设置已保存)" + }, + "triggers": { + "documentTitle": "触发器", + "management": { + "title": "触发器", + "desc": "管理 {{camera}} 的触发器。你可以使用“缩略图”类型,将通过与追踪目标相似的缩略图来触发;也可以使用“描述”类型,基于与你指定的文本相似的描述来触发(中文描述需要使用jina v2模型,对配置要求更高)。" + }, + "addTrigger": "添加触发器", + "table": { + "name": "名称", + "type": "类型", + "content": "触发内容", + "threshold": "阈值", + "actions": "动作", + "noTriggers": "此摄像头未配置任何触发器。", + "edit": "编辑", + "deleteTrigger": "删除触发器", + "lastTriggered": "最后一个触发项" + }, + "type": { + "thumbnail": "缩略图", + "description": "描述" + }, + "actions": { + "alert": "标记为警报", + "notification": "发送通知", + "sub_label": "添加子标签", + "attribute": "添加属性" + }, + "dialog": { + "createTrigger": { + "title": "创建触发器", + "desc": "为摄像头 {{camera}} 创建触发器" + }, + "editTrigger": { + "title": "编辑触发器", + "desc": "编辑摄像头 {{camera}} 的触发器设置" + }, + "deleteTrigger": { + "title": "删除触发器", + "desc": "你确定要删除触发器 {{triggerName}} 吗?此操作不可撤销。" + }, + "form": { + "name": { + "title": "名称", + "placeholder": "触发器名称", + "error": { + "minLength": "该字段至少需要两个字符。", + "invalidCharacters": "该字段只能包含字母、数字、下划线和连字符。", + "alreadyExists": "此摄像头已存在同名触发器。" + }, + "description": "请输入用于识别此触发器的唯一名称或描述" + }, + "enabled": { + "description": "开启/关闭此触发器" + }, + "type": { + "title": "类型", + "placeholder": "选择触发类型", + "description": "当检测到相似的追踪目标描述时触发", + "thumbnail": "当检测到相似的追踪目标缩略图时触发" + }, + "content": { + "title": "内容", + "imagePlaceholder": "选择图片", + "textPlaceholder": "输入文字内容", + "imageDesc": "仅显示最近的 100 张缩略图。如果找不到需要的图片,请前往“浏览”页面查看更早的目标,并从菜单中设置触发器。", + "textDesc": "输入文本,当检测到相似的追踪目标描述时触发此操作。", + "error": { + "required": "内容为必填项。" + } + }, + "threshold": { + "title": "阈值", + "error": { + "min": "阈值必须大于 0", + "max": "阈值必须小于 1" + }, + "desc": "设置此触发器的相似度阈值。阈值越高,触发所需的匹配就越精确。" + }, + "actions": { + "title": "动作", + "desc": "默认情况下,Frigate 会为所有触发器发送 MQTT 消息。子标签会将触发器名称添加到目标标签中。属性是可搜索的元数据,独立存储在追踪目标的元数据中。", + "error": { + "min": "必须至少选择一项动作。" + } + }, + "friendly_name": { + "title": "友好名称", + "placeholder": "为此触发器命名或添加描述", + "description": "(可选)为触发器添加友好名称或描述。" + } + } + }, + "toast": { + "success": { + "createTrigger": "触发器 {{name}} 创建成功。", + "updateTrigger": "触发器 {{name}} 更新成功。", + "deleteTrigger": "触发器 {{name}} 已删除。" + }, + "error": { + "createTriggerFailed": "创建触发器失败:{{errorMessage}}", + "updateTriggerFailed": "更新触发器失败:{{errorMessage}}", + "deleteTriggerFailed": "删除触发器失败:{{errorMessage}}" + } + }, + "semanticSearch": { + "title": "语义搜索已关闭", + "desc": "必须启用语义搜索功能才能使用触发器。" + }, + "wizard": { + "title": "创建触发器", + "step1": { + "description": "配置触发器的基础设置。" + }, + "step2": { + "description": "设置触发此操作的内容。" + }, + "step3": { + "description": "配置此触发器的相似度阈值与执行动作。" + }, + "steps": { + "nameAndType": "名称与类型", + "configureData": "配置数据", + "thresholdAndActions": "阈值与动作" + } + } + }, + "roles": { + "management": { + "title": "成员权限组管理", + "desc": "管理此 Frigate 实例的自定义权限组及其摄像头访问权限。" + }, + "addRole": "添加权限组", + "table": { + "role": "权限组", + "cameras": "摄像头", + "actions": "操作", + "noRoles": "没有找到自定义权限组。", + "editCameras": "编辑摄像头", + "deleteRole": "删除权限组" + }, + "toast": { + "success": { + "createRole": "权限组 {{role}} 创建成功", + "updateCameras": "已更新摄像头至 {{role}} 权限组", + "deleteRole": "已删除 {{role}} 权限组", + "userRolesUpdated_other": "已将分配到此权限组的 {{count}} 位用户更新为 “成员”,该权限组可访问所有摄像头。" + }, + "error": { + "createRoleFailed": "创建权限组失败:{{errorMessage}}", + "updateCamerasFailed": "更新摄像头失败:{{errorMessage}}", + "deleteRoleFailed": "删除权限组失败:{{errorMessage}}", + "userUpdateFailed": "更新用户权限组失败:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "创建新权限组", + "desc": "添加新权限组并分配摄像头访问权限。" + }, + "editCameras": { + "title": "编辑权限组的摄像头", + "desc": "为权限组 {{role}} 更新摄像头访问权限。" + }, + "deleteRole": { + "title": "删除权限组", + "desc": "此操作无法撤销。这将永久删除该权限组,并将所有拥有此角色的用户分配到 “成员” 权限组,该权限组将赋予用户查看所有摄像头的权限。", + "warn": "你确定要删除权限组 {{role}} 吗?", + "deleting": "删除中…" + }, + "form": { + "role": { + "title": "权限组名称", + "placeholder": "输入权限组名称", + "desc": "仅允许使用字母、数字、句点和下划线。", + "roleIsRequired": "必须输入权限组名称", + "roleOnlyInclude": "权限组名称仅支持字母、数字、英文句号和下划线", + "roleExists": "该权限组名称已存在。" + }, + "cameras": { + "title": "摄像头", + "desc": "请选择该权限组能够访问的摄像头。至少需要选择一个摄像头。", + "required": "至少要选择一个摄像头。" + } + } + } + }, + "cameraWizard": { + "title": "添加摄像头", + "description": "请按照以下步骤添加摄像头至Frigate中。", + "steps": { + "nameAndConnection": "名称与连接", + "streamConfiguration": "视频流配置", + "validationAndTesting": "验证与测试", + "probeOrSnapshot": "探测或快照" + }, + "save": { + "success": "已保存新摄像头 {{cameraName}}。", + "failure": "保存摄像头 {{cameraName}} 遇到了错误。" + }, + "testResultLabels": { + "resolution": "分辨率", + "video": "视频", + "audio": "音频", + "fps": "帧率" + }, + "commonErrors": { + "noUrl": "请提供正确的视频流地址", + "testFailed": "视频流测试失败:{{error}}" + }, + "step1": { + "description": "请输入你的摄像头信息,并选择是自动探测摄像头信息还是手动指定品牌。", + "cameraName": "摄像头名称", + "cameraNamePlaceholder": "例如:大门,后院等", + "host": "主机/IP地址", + "port": "端口号", + "username": "用户名", + "usernamePlaceholder": "可选", + "password": "密码", + "passwordPlaceholder": "可选", + "selectTransport": "选择传输协议", + "cameraBrand": "摄像头品牌", + "selectBrand": "选择摄像头品牌用于生成URL地址模板", + "customUrl": "自定义视频流地址", + "brandInformation": "品牌信息", + "brandUrlFormat": "对于采用RTSP URL格式的摄像头,其格式为:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://用户名:密码@主机或IP地址:端口/路径", + "testConnection": "测试连接", + "testSuccess": "连接测试通过!", + "testFailed": "连接测试失败。请检查输入是否正确并重试。", + "streamDetails": "视频流信息", + "warnings": { + "noSnapshot": "无法从配置的视频流中获取快照。" + }, + "errors": { + "brandOrCustomUrlRequired": "请选择摄像头品牌并配置主机/IP地址,或选择“其他”后手动配置视频流地址", + "nameRequired": "摄像头名称为必填项", + "nameLength": "摄像头名称要少于64个字符", + "invalidCharacters": "摄像头名称内有不允许使用的字符", + "nameExists": "该摄像头名称已存在", + "brands": { + "reolink-rtsp": "不建议使用萤石 RTSP 协议。建议在摄像头设置中启用 HTTP 协议,并重新运行摄像头添加向导。" + }, + "customUrlRtspRequired": "自定义URL必须以“rtsp://”开头;对于非 RTSP 协议的摄像头流,需手动添加至配置文件。" + }, + "docs": { + "reolink": "https://docs.frigate-cn.video/configuration/camera_specific.html#reolink-cameras" + }, + "testing": { + "probingMetadata": "正在获取摄像头基本数据……", + "fetchingSnapshot": "正在获取摄像头快照……" + }, + "connectionSettings": "连接设置", + "detectionMethod": "视频流检测方法", + "onvifPort": "ONVIF 端口", + "probeMode": "探测摄像头", + "manualMode": "手动选择", + "detectionMethodDescription": "如果支持 ONVIF 协议,将使用该协议探测摄像头,以自动获取摄像头视频流地址;若不支持,也可手动选择摄像头品牌来使用预设地址。如需输入自定义RTSP地址,请选择手动模式并选取\"其他\"选项。", + "onvifPortDescription": "对于支持ONVIF协议的摄像头,该端口通常为80或8080。", + "useDigestAuth": "使用摘要认证", + "useDigestAuthDescription": "为ONVIF协议启用HTTP摘要认证。部分摄像头可能需要专用的 ONVIF 用户名/密码,而非默认的admin账户。" + }, + "step2": { + "description": "将根据你选择的检测方式,将会自动查找摄像头可用流配置,或进行手动配置。", + "streamsTitle": "摄像头视频流", + "addStream": "添加视频流", + "addAnotherStream": "添加另一个视频流", + "streamTitle": "{{number}} 号视频流", + "streamUrl": "视频流地址", + "streamUrlPlaceholder": "rtsp://用户名:密码@主机或IP:端口/路径", + "url": "URL地址", + "resolution": "分辨率", + "selectResolution": "选择分辨率", + "quality": "质量", + "selectQuality": "选择质量", + "roles": "功能", + "roleLabels": { + "detect": "目标/物体检测", + "record": "录制", + "audio": "音频" + }, + "testStream": "测试连接", + "testSuccess": "连接测试通过!", + "testFailed": "连接测试失败,请检查输入项后重试。", + "testFailedTitle": "测试失败", + "connected": "已连接", + "notConnected": "未连接", + "featuresTitle": "特殊功能", + "go2rtc": "减少摄像头连接数", + "detectRoleWarning": "至少需要一个视频流分配\"detect\"功能才能继续。", + "rolesPopover": { + "title": "视频流功能", + "detect": "目标/物体的主数据流。", + "record": "根据配置设置保存视频流的片段。", + "audio": "用于音频的检测的输入流。" + }, + "featuresPopover": { + "title": "视频流特殊功能", + "description": "将使用go2rtc的转流功能来减少摄像头连接数。" + }, + "streamDetails": "视频流详情", + "probing": "正在检测摄像头中……", + "retry": "重试", + "testing": { + "probingMetadata": "正在查询摄像头参数……", + "fetchingSnapshot": "正在获取摄像头快照……" + }, + "probeFailed": "检测摄像头失败:{{error}}", + "probingDevice": "寻找设备中……", + "probeSuccessful": "检测成功", + "probeError": "检测遇到错误", + "probeNoSuccess": "检测未成功", + "deviceInfo": "设备信息", + "manufacturer": "制造商", + "model": "型号", + "firmware": "固件", + "profiles": "配置文件", + "ptzSupport": "支持 PTZ", + "autotrackingSupport": "支持自动追踪", + "presets": "预设配置", + "rtspCandidates": "RTSP候选地址", + "rtspCandidatesDescription": "通过摄像头自动检测发现了以下RTSP地址。测试连接以查看视频流参数。", + "noRtspCandidates": "未从摄像头检测到任何 RTSP 地址。可能是你的账号密码错误,或者摄像头不支持 ONVIF 协议,亦或是当前采用的 RTSP 地址获取方式无效。请返回上一步,尝试手动输入RTSP地址。", + "candidateStreamTitle": "候选{{number}}", + "useCandidate": "使用", + "uriCopy": "复制", + "uriCopied": "地址已复制到剪贴板", + "testConnection": "测试连接", + "toggleUriView": "点击切换完整 URI 显示", + "errors": { + "hostRequired": "主机/IP地址为必填" + } + }, + "step3": { + "description": "为你的摄像头配置视频流功能并添加额外视频流。", + "validationTitle": "视频流验证", + "connectAllStreams": "连接所有视频流", + "reconnectionSuccess": "重连成功。", + "reconnectionPartial": "有些视频流重连失败了。", + "streamUnavailable": "视频流预览不可用", + "reload": "重新加载", + "connecting": "连接中……", + "streamTitle": "{{number}} 号视频流", + "valid": "通过", + "failed": "失败", + "notTested": "未测试", + "connectStream": "连接", + "connectingStream": "连接中", + "disconnectStream": "断开连接", + "estimatedBandwidth": "预计带宽", + "roles": "功能", + "none": "无", + "error": "错误", + "streamValidated": "{{number}} 号视频流验证通过", + "streamValidationFailed": "{{number}} 号视频流验证失败", + "saveAndApply": "保存新摄像头", + "saveError": "配置无效,请检查你的设置。", + "issues": { + "title": "视频流验证", + "videoCodecGood": "视频编码为 {{codec}}。", + "audioCodecGood": "音频编码为 {{codec}}。", + "noAudioWarning": "未检测到此视频流包含音频,录制将不会有声音。", + "audioCodecRecordError": "录制音频需要支持AAC音频编码器。", + "audioCodecRequired": "需要带音频的流才能开启声音检测。", + "restreamingWarning": "为录制流开启减少与摄像头的连接数可能会导致 CPU 使用率略有提升。", + "dahua": { + "substreamWarning": "子码流1被锁定为低分辨率。多数大华的摄像头支持额外的子码流,但需要在摄像头设置中手动开启。如果可以,建议检查并使用这些子码流。" + }, + "hikvision": { + "substreamWarning": "子码流1被锁定为低分辨率。多数海康威视的摄像头支持额外的子码流,但需要在摄像头设置中手动开启。如果可以,建议检查并使用这些子码流。" + }, + "resolutionHigh": "使用 {{resolution}} 分辨率可能会导致占用更多的系统资源。", + "resolutionLow": "使用 {{resolution}} 分辨率可能过低,难以检测较小的物体。" + }, + "ffmpegModule": "使用视频流兼容模式", + "ffmpegModuleDescription": "如果多次尝试后视频流仍无法加载,可以尝试启用此功能。启用后,Frigate 将使用集成 go2rtc 的 ffmpeg 模块,这可能会提高与某些摄像头视频流的兼容性。", + "streamsTitle": "摄像头视频流", + "addStream": "添加视频流", + "addAnotherStream": "添加其他视频流", + "streamUrl": "视频流地址", + "streamUrlPlaceholder": "rtsp://用户名:密码@主机:端口/路径", + "selectStream": "选择一个视频流", + "searchCandidates": "搜索候选项……", + "noStreamFound": "没有找到视频流", + "url": "URL地址", + "resolution": "分辨率", + "selectResolution": "选择分辨率", + "quality": "质量", + "selectQuality": "选择质量", + "roleLabels": { + "detect": "目标检测", + "record": "录制", + "audio": "音频检测" + }, + "testStream": "测试连接", + "testSuccess": "视频流测试成功!", + "testFailed": "视频流测试失败", + "testFailedTitle": "测试失败", + "connected": "已连接", + "notConnected": "未连接", + "featuresTitle": "功能特性", + "go2rtc": "减少与摄像头的连接数", + "detectRoleWarning": "必须得有一个视频流设置了“检测”功能才能继续操作。", + "rolesPopover": { + "title": "视频流功能", + "detect": "用于目标检测的主码流。", + "record": "根据配置设置保存视频流片段。", + "audio": "用于音频检测的音视频流。" + }, + "featuresPopover": { + "title": "视频流功能特性", + "description": "使用 go2rtc 中继转流功能,减少与摄像头的网络连接数,提升效率。" + } + }, + "step4": { + "description": "将进行保存新摄像头配置前的最终验证与分析,请在保存前确保所有视频流均已连接。", + "validationTitle": "视频流验证", + "connectAllStreams": "连接所有视频流", + "reconnectionSuccess": "重新连接成功。", + "reconnectionPartial": "部分视频流重新连接失败。", + "streamUnavailable": "视频流预览不可用", + "reload": "重新加载", + "connecting": "连接中……", + "streamTitle": "视频流 {{number}}", + "valid": "通过", + "failed": "失败", + "notTested": "未测试", + "connectStream": "连接", + "connectingStream": "连接中", + "disconnectStream": "断开连接", + "estimatedBandwidth": "预估带宽", + "roles": "功能", + "ffmpegModule": "使用视频流兼容模式", + "ffmpegModuleDescription": "若多次尝试后仍无法加载视频流,可尝试启用此功能。启用后,Frigate 将通过 go2rtc 调用 ffmpeg 模块。这可能会提升与部分摄像头视频流的兼容性。", + "none": "无", + "error": "错误", + "streamValidated": "视频流 {{number}} 验证成功", + "streamValidationFailed": "视频流 {{number}} 验证失败", + "saveAndApply": "保存新摄像头", + "saveError": "配置无效,请检查您的设置。", + "issues": { + "title": "视频流验证", + "videoCodecGood": "视频编解码器为 {{codec}}。", + "audioCodecGood": "音频编解码器为 {{codec}}。", + "resolutionHigh": "使用 {{resolution}} 分辨率可能导致资源使用率增加。", + "resolutionLow": "{{resolution}} 分辨率可能过低,难以可靠检测小型目标或物体。", + "noAudioWarning": "检测到该视频流无音频信号,录制视频将没有声音。", + "audioCodecRecordError": "录制功能需要 AAC 音频编解码器以实现音频支持。", + "audioCodecRequired": "要实现音频检测功能,必须要有音频流。", + "restreamingWarning": "为录制流开启“减少与摄像头的连接数”可能会略微增加CPU使用率。", + "brands": { + "reolink-rtsp": "不建议使用 Reolink 的 RTSP 协议。请在摄像头后台设置中启用 HTTP协议,并重新启动向导。", + "reolink-http": "Reolink HTTP 视频流应该使用 FFmpeg 以获得更好的兼容性,为此视频流启用“使用流兼容模式”。" + }, + "dahua": { + "substreamWarning": "子码流1当前被锁定为低分辨率。多数大华、安讯士、EmpireTech品牌的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。" + }, + "hikvision": { + "substreamWarning": "子码流1当前被锁定为低分辨率。多数海康威视的摄像头都支持额外的子码流,这些子码流需要在摄像头设置中手动启用。如果你的设备支持,建议你检查并使用这些高分辨率子码流。" + } + } + } + }, + "cameraManagement": { + "title": "管理摄像头", + "addCamera": "添加新摄像头", + "editCamera": "编辑摄像头:", + "selectCamera": "选择摄像头", + "backToSettings": "返回摄像头设置", + "streams": { + "title": "开启或关闭摄像头", + "desc": "将临时禁用摄像头直至Frigate重启。禁用摄像头将完全停止Frigate对该摄像头视频流的处理,届时检测、录制及调试功能均不可用。
    注意:此操作不会影响go2rtc的转流服务。" + }, + "cameraConfig": { + "add": "添加摄像头", + "edit": "编辑摄像头", + "description": "配置摄像头设置,包括视频流输入和功能选择。", + "name": "摄像头名称", + "nameRequired": "摄像头名称为必填项", + "nameLength": "摄像头名称必须少于64个字符。", + "namePlaceholder": "例如:大门、后院等", + "enabled": "开启", + "ffmpeg": { + "inputs": "视频流输入", + "path": "视频流地址", + "pathRequired": "视频流地址为必填项", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少选择一个功能", + "rolesUnique": "每个功能(音频audio、检测detect、录制record)只能分配给一个视频流", + "addInput": "添加输入视频流", + "removeInput": "移除输入视频流", + "inputsRequired": "至少需要一个输入视频流" + }, + "go2rtcStreams": "go2rtc 视频流", + "streamUrls": "视频流地址", + "addUrl": "添加地址", + "addGo2rtcStream": "添加 go2rtc 视频流", + "toast": { + "success": "摄像头 {{cameraName}} 已保存" + } + } + }, + "cameraReview": { + "title": "摄像头核查设置", + "object_descriptions": { + "title": "生成式AI目标描述", + "desc": "临时启用或禁用此摄像头的 生成式AI目标描述 功能。禁用后,系统将不再请求该摄像头追踪目标和物体的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式AI核查描述", + "desc": "临时启用或禁用此摄像头的 生成式AI核查描述 功能。禁用后,系统将不再请求该摄像头核查项目的AI生成描述。" + }, + "review": { + "title": "核查", + "desc": "临时禁用/启用此摄像头的警报与检测功能,直至Frigate重启。禁用期间,系统将不再生成新的核查项目。 ", + "alerts": "警报 ", + "detections": "检测 " + }, + "reviewClassification": { + "title": "核查分类", + "desc": "Frigate 将核查项分为“警报”和“检测”。默认情况下,所有的汽车 目标都将视为警报。你可以通过修改配置文件配置区域来细分。", + "noDefinedZones": "此摄像头未设置任何监控区。", + "objectAlertsTips": "所有 {{alertsLabels}} 类目标或物体在 {{cameraName}} 下都将显示为警报。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 类目标或物体在 {{cameraName}} 下的 {{zone}} 区内都将显示为警报。", + "objectDetectionsTips": "所有在摄像头 {{cameraName}} 上,检测到的 {{detectionsLabels}} 目标或物体,无论它位于哪个区,都将显示为检测。", + "zoneObjectDetectionsTips": { + "text": "所有在摄像头 {{cameraName}} 下的 {{zone}} 区内检测到未分类的 {{detectionsLabels}} 目标或物体,都将显示为检测。", + "notSelectDetections": "所有在摄像头 {{cameraName}}下的 {{zone}} 区内检测到的 {{detectionsLabels}} 目标或物体,如果它未归类为警报,无论它位于哪个区,都将显示为检测。", + "regardlessOfZoneObjectDetectionsTips": "在摄像头 {{cameraName}} 上,所有未分类的 {{detectionsLabels}} 检测目标或物体,无论出现在哪个区域,都将显示为检测。" + }, + "unsavedChanges": "摄像头 {{camera}} 的核查分类设置尚未保存", + "selectAlertsZones": "选择警报区", + "selectDetectionsZones": "选择检测区", + "limitDetections": "限制仅在特定区内进行检测", + "toast": { + "success": "核查分类设置已保存,重启后生效。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/system.json new file mode 100644 index 0000000..3d6eff0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-CN/views/system.json @@ -0,0 +1,198 @@ +{ + "documentTitle": { + "cameras": "摄像头统计 - Frigate", + "storage": "存储统计 - Frigate", + "general": "常规统计 - Frigate", + "enrichments": "增强功能统计 - Frigate", + "logs": { + "frigate": "Frigate 日志 - Frigate", + "go2rtc": "Go2RTC 日志 - Frigate", + "nginx": "Nginx 日志 - Frigate" + } + }, + "title": "系统", + "metrics": "系统指标", + "logs": { + "download": { + "label": "下载日志" + }, + "copy": { + "label": "复制到剪贴板", + "success": "已复制日志到剪贴板", + "error": "无法复制日志到剪贴板" + }, + "type": { + "label": "类型", + "timestamp": "时间戳", + "tag": "标签", + "message": "消息" + }, + "tips": "日志正在从服务器流式传输", + "toast": { + "error": { + "fetchingLogsFailed": "获取日志出错:{{errorMessage}}", + "whileStreamingLogs": "流式传输日志时出错:{{errorMessage}}" + } + } + }, + "general": { + "title": "常规", + "detector": { + "title": "检测器", + "inferenceSpeed": "检测器推理速度", + "cpuUsage": "检测器CPU使用率", + "memoryUsage": "检测器内存使用率", + "temperature": "检测器温度", + "cpuUsageInformation": "用于准备输入和输出数据的 CPU 资源,这些数据是供检测模型使用或由检测模型产生的。该数值并不衡量推理过程中的 CPU 使用情况,即使使用了 GPU 或加速器也是如此。" + }, + "hardwareInfo": { + "title": "硬件信息", + "gpuUsage": "GPU使用率", + "gpuMemory": "GPU显存", + "gpuEncoder": "GPU编码", + "gpuDecoder": "GPU解码", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 输出", + "returnCode": "返回代码:{{code}}", + "processOutput": "进程输出:", + "processError": "进程错误:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 输出", + "name": "名称:{{name}}", + "driver": "驱动:{{driver}}", + "cudaComputerCapability": "CUDA计算能力:{{cuda_compute}}", + "vbios": "VBios信息:{{vbios}}" + }, + "closeInfo": { + "label": "关闭GPU信息" + }, + "copyInfo": { + "label": "复制GPU信息" + }, + "toast": { + "success": "已复制GPU信息到剪贴板" + } + }, + "npuMemory": "NPU内存", + "npuUsage": "NPU使用率", + "intelGpuWarning": { + "title": "Intel GPU 处于警告状态", + "message": "GPU 状态不可用", + "description": "这是 Intel 的 GPU 状态报告工具(intel_gpu_top)的已知问题:该工具会失效并反复返回 GPU 使用率为 0%,即使在硬件加速和目标检测已在 (i)GPU 上正常运行的情况下也是如此,这并不是 Frigate 的 bug。你可以通过重启主机来临时修复该问题,并确认 GPU 正常工作。该问题并不会影响性能。" + } + }, + "otherProcesses": { + "title": "其他进程", + "processCpuUsage": "主进程CPU使用率", + "processMemoryUsage": "主进程内存使用率" + } + }, + "storage": { + "title": "存储", + "overview": "概览", + "recordings": { + "title": "录制内容", + "tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。", + "earliestRecording": "最早的可用录制:" + }, + "cameraStorage": { + "title": "摄像头存储", + "camera": "摄像头", + "unusedStorageInformation": "未使用存储信息", + "storageUsed": "存储使用", + "percentageOfTotalUsed": "总使用率", + "bandwidth": "带宽", + "unused": { + "title": "未使用", + "tips": "如果您的驱动器上存储了除 Frigate 录制内容之外的其他文件,该值可能无法准确反映 Frigate 可用的剩余空间。Frigate 不会追踪录制内容以外的存储使用情况。" + } + }, + "shm": { + "title": "共享内存(SHM)分配", + "warning": "当前共享内存(SHM)容量过小( {{total}}MB),请将其至少增加到 {{min_shm}}MB。" + } + }, + "cameras": { + "title": "摄像头", + "overview": "概览", + "info": { + "cameraProbeInfo": "{{camera}} 的摄像头信息", + "streamDataFromFFPROBE": "流数据信息通过ffprobe获取。", + "fetching": "正在获取摄像头数据", + "stream": "视频流{{idx}}", + "video": "视频:", + "codec": "编解码器:", + "resolution": "分辨率:", + "fps": "帧率:", + "unknown": "未知", + "audio": "音频:", + "error": "错误:{{error}}", + "tips": { + "title": "摄像头信息" + }, + "aspectRatio": "宽高比" + }, + "framesAndDetections": "帧数/检测次数", + "label": { + "camera": "摄像头", + "detect": "检测", + "skipped": "跳过", + "ffmpeg": "FFmpeg编码器", + "capture": "捕获", + "overallFramesPerSecond": "每秒总帧数", + "overallDetectionsPerSecond": "每秒总检测数", + "overallSkippedDetectionsPerSecond": "每秒跳过检测数", + "cameraCapture": "{{camName}} 捕获", + "cameraDetect": "{{camName}} 检测", + "cameraDetectionsPerSecond": "{{camName}} 每秒检测数", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳过检测数", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraFramesPerSecond": "{{camName}} 每秒帧数" + }, + "toast": { + "success": { + "copyToClipboard": "已复制检测数据到剪贴板。" + }, + "error": { + "unableToProbeCamera": "无法检测到摄像头:{{errorMessage}}" + } + } + }, + "lastRefreshed": "最后刷新时间: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率较高({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 的 检测器 CPU 使用率较高({{detectAvg}}%)", + "healthy": "系统运行正常", + "reindexingEmbeddings": "正在重新索引嵌入(已完成 {{processed}}%)", + "detectIsSlow": "{{detect}} 运行缓慢({{speed}}毫秒)", + "detectIsVerySlow": "{{detect}} 运行非常缓慢({{speed}}毫秒)", + "cameraIsOffline": "{{camera}} 已离线", + "shmTooLow": "/dev/shm 的分配空间过低(当前 {{total}} MB),应至少增加到 {{min}} MB。" + }, + "enrichments": { + "title": "增强功能", + "infPerSecond": "每秒推理次数", + "embeddings": { + "image_embedding_speed": "图像特征提取速度", + "face_embedding_speed": "人脸特征提取速度", + "plate_recognition_speed": "车牌识别速度", + "text_embedding_speed": "文本编码速度", + "face_recognition_speed": "人脸识别速度", + "image_embedding": "图像特征提取", + "text_embedding": "文字编码", + "face_recognition": "人脸特征提取", + "plate_recognition": "车牌识别", + "yolov9_plate_detection_speed": "YOLOv9 车牌检测速度", + "yolov9_plate_detection": "YOLOv9 车牌检测", + "review_description": "核查描述", + "review_description_speed": "核查描述速度", + "review_description_events_per_second": "核查描述", + "object_description": "目标描述", + "object_description_speed": "目标描述速度", + "object_description_events_per_second": "目标描述" + }, + "averageInf": "平均推理时间" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/audio.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/audio.json new file mode 100644 index 0000000..9a458ce --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/audio.json @@ -0,0 +1,81 @@ +{ + "speech": "說話聲", + "babbling": "牙牙學語", + "bicycle": "腳踏車", + "yell": "大叫", + "car": "車", + "bellow": "吼叫", + "boat": "船", + "crying": "哭聲", + "sigh": "嘆氣", + "singing": "歌聲", + "choir": "合唱", + "yodeling": "山歌", + "chant": "誦經", + "mantra": "咒語", + "camera": "鏡頭", + "motorcycle": "摩托車", + "bus": "巴士", + "train": "火車", + "bird": "鳥", + "cat": "貓", + "dog": "狗", + "horse": "馬", + "sheep": "羊", + "skateboard": "滑板", + "door": "門", + "mouse": "滑鼠", + "keyboard": "鍵盤", + "sink": "水槽", + "blender": "果汁機", + "clock": "時鐘", + "scissors": "剪刀", + "hair_dryer": "吹風機", + "toothbrush": "牙刷", + "vehicle": "車輛", + "animal": "動物", + "bark": "樹皮", + "goat": "山羊", + "whoop": "大叫", + "whispering": "講話", + "laughter": "笑聲", + "snicker": "竊笑", + "child_singing": "小孩歌聲", + "synthetic_singing": "合成音樂聲", + "rapping": "饒舌聲", + "humming": "哼歌聲", + "groan": "呻吟聲", + "grunt": "咕噥聲", + "whistling": "口哨聲", + "breathing": "呼吸聲", + "wheeze": "喘息聲", + "snoring": "打呼聲", + "gasp": "倒抽一口氣", + "pant": "喘氣聲", + "snort": "鼻息聲", + "cough": "咳嗽聲", + "throat_clearing": "清喉嚨聲", + "sneeze": "打噴嚏聲", + "sniff": "嗅聞聲", + "run": "跑步聲", + "shuffle": "拖著腳走路聲", + "footsteps": "腳步聲", + "chewing": "咀嚼聲", + "biting": "咬", + "gargling": "漱口", + "stomach_rumble": "腸胃蠕動", + "burping": "打嗝", + "hiccup": "打噎", + "fart": "放屁", + "hands": "手", + "finger_snapping": "彈手指聲", + "clapping": "拍手", + "heartbeat": "心跳聲", + "heart_murmur": "心臟雜音", + "cheering": "歡呼聲", + "applause": "掌聲", + "chatter": "嘈雜聲", + "crowd": "人群聲", + "children_playing": "兒童嬉鬧聲", + "pets": "寵物" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/common.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/common.json new file mode 100644 index 0000000..b785c8b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/common.json @@ -0,0 +1,290 @@ +{ + "time": { + "untilForTime": "直到 {{time}}", + "untilForRestart": "直到 Frigate 重新啟動。", + "untilRestart": "直到重新啟動", + "ago": "{{timeAgo}} 前", + "last14": "過去 14 天", + "last30": "過去 30 天", + "thisWeek": "這週", + "lastWeek": "上週", + "thisMonth": "這個月", + "lastMonth": "上個月", + "justNow": "剛剛", + "today": "今天", + "yesterday": "昨天", + "last7": "過去 7 天", + "5minutes": "5 分鐘", + "10minutes": "10 分鐘", + "30minutes": "30 分鐘", + "1hour": "1 小時", + "12hours": "12 小時", + "24hours": "24 小時", + "pm": "下午", + "am": "上午", + "yr": "{{time}} 年", + "year_other": "{{time}} 年", + "mo": "{{time}} 月", + "month_other": "{{time}} 月", + "d": "{{time}} 日", + "day_other": "{{time}}天", + "h": "{{time}}時", + "hour_other": "{{time}}小時", + "m": "{{time}}分", + "minute_other": "{{time}}分鐘", + "s": "{{time}}秒", + "second_other": "{{time}}秒鐘", + "formattedTimestamp": { + "12hour": "M 月 d 日 ah:mm:ss", + "24hour": "M 月 d 日 HH:mm:ss" + }, + "formattedTimestamp2": { + "12hour": "MM/dd h:mm:ssa", + "24hour": "d MMM HH:mm:ss" + }, + "formattedTimestampHourMinute": { + "12hour": "a h:mm", + "24hour": "HH:mm" + }, + "formattedTimestampHourMinuteSecond": { + "12hour": "a h:mm:ss", + "24hour": "HH:mm:ss" + }, + "formattedTimestampMonthDayHourMinute": { + "12hour": "M 月 d 日 ah:mm", + "24hour": "M 月 d 日 HH:mm" + }, + "formattedTimestampMonthDayYear": { + "12hour": "yy 年 MM 月 dd 日", + "24hour": "yy 年 MM 月 dd 日" + }, + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "yyyy 年 M 月 d 日 ah:mm", + "24hour": "yyyy 年 M 月 d 日 HH:mm" + }, + "formattedTimestampMonthDay": "M 月 d 日", + "formattedTimestampFilename": { + "12hour": "MM-dd-yy-h-mm-ss-a", + "24hour": "yy年MM月dd日 HH時mm分ss秒" + }, + "inProgress": "處理中", + "invalidStartTime": "無效的起始時間", + "invalidEndTime": "無效的結束時間" + }, + "unit": { + "speed": { + "mph": "英里/小時", + "kph": "公里/小時" + }, + "length": { + "feet": "英尺", + "meters": "公尺" + }, + "data": { + "kbps": "kB/秒", + "mbps": "MB/秒", + "gbps": "GB/秒", + "kbph": "kB/小時", + "mbph": "MB/小時", + "gbph": "GB/小時" + } + }, + "label": { + "back": "返回", + "hide": "隱藏{{item}}", + "show": "顯示{{item}}", + "ID": "ID", + "none": "無", + "all": "全部" + }, + "button": { + "apply": "套用", + "reset": "重置", + "done": "完成", + "enabled": "已啟用", + "enable": "啟用", + "disabled": "已停用", + "disable": "停用", + "save": "保存", + "saving": "保存中…", + "cancel": "取消", + "close": "關閉", + "copy": "複製", + "back": "返回", + "history": "歷史紀錄", + "fullscreen": "全螢幕", + "exitFullscreen": "退出全螢幕", + "pictureInPicture": "子母畫面", + "twoWayTalk": "雙向通話", + "cameraAudio": "鏡頭音訊", + "on": "開", + "off": "關", + "edit": "編輯", + "copyCoordinates": "複製座標", + "delete": "刪除", + "yes": "是", + "no": "否", + "download": "下載", + "info": "資訊", + "suspended": "已暫停", + "unsuspended": "取消暫停", + "play": "播放", + "unselect": "取消選取", + "export": "匯出", + "deleteNow": "立即刪除", + "next": "繼續", + "continue": "繼續" + }, + "menu": { + "system": "系統", + "systemMetrics": "系統訊息", + "configuration": "設定", + "systemLogs": "系統日誌", + "settings": "設定", + "configurationEditor": "設定編輯器", + "languages": "語言", + "language": { + "en": "English (英文)", + "es": "Español (西班牙文)", + "zhCN": "简体中文 (簡體中文)", + "hi": "हिन्दी (印地文)", + "fr": "Français (法文)", + "ar": "العربية (阿拉伯文)", + "pt": "Português (葡萄牙文)", + "ru": "Русский (俄文)", + "de": "Deutsch (德文)", + "ja": "日本語 (日文)", + "tr": "Türkçe (土耳其文)", + "it": "Italiano (義大利文)", + "nl": "Nederlands (荷蘭文)", + "sv": "Svenska (瑞典文)", + "cs": "Čeština (捷克文)", + "nb": "Norsk Bokmål (挪威文)", + "ko": "한국어 (韓文)", + "vi": "Tiếng Việt (越南文)", + "fa": "فارسی (波斯文)", + "pl": "Polski (波蘭文)", + "uk": "Українська (烏克蘭文)", + "he": "עברית (希伯來文)", + "el": "Ελληνικά (希臘文)", + "ro": "Română (羅馬尼亞文)", + "hu": "Magyar (匈牙利文)", + "fi": "Suomi (芬蘭文)", + "da": "Dansk (丹麥文)", + "sk": "Slovenčina (斯洛伐克文)", + "yue": "粵語 (粵語)", + "th": "ไทย (泰文)", + "ca": "Català (加泰隆尼亞文)", + "withSystem": { + "label": "使用系統語言設定" + }, + "ptBR": "Português brasileiro (巴西葡萄牙文)", + "sr": "Српски (塞爾維亞文)", + "sl": "Slovenščina (斯洛文尼亞文)", + "lt": "Lietuvių (立陶宛文)", + "bg": "Български (保加利亞文)", + "gl": "Galego (加利西亞文)", + "id": "Bahasa Indonesia (印尼文)", + "ur": "اردو (烏爾都文)" + }, + "appearance": "外觀", + "darkMode": { + "label": "深色模式", + "light": "淺色", + "dark": "深色", + "withSystem": { + "label": "使用系統外觀模式設定" + } + }, + "withSystem": "系統", + "theme": { + "label": "主題", + "blue": "藍色", + "green": "綠色", + "nord": "北歐風", + "red": "紅色", + "highcontrast": "高對比", + "default": "預設" + }, + "help": "幫助", + "documentation": { + "title": "文件", + "label": "Frigate 文件" + }, + "restart": "重新啟動 Frigate", + "live": { + "title": "即時影像", + "allCameras": "所有鏡頭", + "cameras": { + "title": "鏡頭", + "count_other": "{{count}} 個鏡頭" + } + }, + "review": "審核", + "explore": "瀏覽", + "export": "匯出", + "uiPlayground": "UI 測試區", + "faceLibrary": "人臉資料庫", + "user": { + "title": "使用者", + "account": "帳號", + "current": "當前使用者:{{user}}", + "anonymous": "匿名", + "logout": "登出", + "setPassword": "設定密碼" + }, + "classification": "標籤分類" + }, + "toast": { + "copyUrlToClipboard": "已複製連結至剪貼簿。", + "save": { + "title": "保存", + "error": { + "title": "保存設定變更失敗:{{errorMessage}}", + "noMessage": "保存設定變更失敗" + } + } + }, + "role": { + "title": "角色", + "admin": "管理員", + "viewer": "檢視者", + "desc": "管理員可以存取 Frigate UI 的全部功能。檢視者僅能查看鏡頭影像、審核物件及歷史紀錄。" + }, + "pagination": { + "label": "分頁", + "previous": { + "title": "上一頁", + "label": "前往上一頁" + }, + "next": { + "title": "下一頁", + "label": "前往下一頁" + }, + "more": "更多頁面" + }, + "accessDenied": { + "documentTitle": "拒絕存取 - Frigate", + "title": "拒絕存取", + "desc": "你沒有瀏覽此頁面的權限。" + }, + "notFound": { + "documentTitle": "找不到頁面 - Frigate", + "title": "404", + "desc": "找不到頁面" + }, + "selectItem": "選擇 {{item}}", + "readTheDocumentation": "閱讀文件", + "list": { + "two": "{{0}} 和 {{1}}", + "many": "{{items}}, 及 {{last}}", + "separatorWithSpace": ", " + }, + "field": { + "optional": "可選的", + "internalID": "在Frigate 設定檔與資料庫使用的內部ID" + }, + "information": { + "pixels": "{{area}}px" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/auth.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/auth.json new file mode 100644 index 0000000..fbc70c4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/auth.json @@ -0,0 +1,16 @@ +{ + "form": { + "user": "使用者名稱", + "password": "密碼", + "login": "登入", + "errors": { + "usernameRequired": "使用者名稱不可為空", + "webUnknownError": "未知的錯誤,請檢查控制台日誌。", + "passwordRequired": "密碼不可為空", + "rateLimit": "超過次數限制,請稍後再試。", + "loginFailed": "登入失敗", + "unknownError": "未知錯誤,請檢查日誌。" + }, + "firstTimeLogin": "首次嘗試登入嗎?請從 Frigate 的日誌中查找產生的登入密碼等相關資訊。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/camera.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/camera.json new file mode 100644 index 0000000..3bace4d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/camera.json @@ -0,0 +1,87 @@ +{ + "group": { + "label": "鏡頭群組", + "add": "新增鏡頭群組", + "edit": "編輯鏡頭群組", + "delete": { + "label": "刪除鏡頭群組", + "confirm": { + "title": "確認刪除", + "desc": "你確定要刪除鏡頭群組 {{name}} 嗎?" + } + }, + "name": { + "errorMessage": { + "mustLeastCharacters": "鏡頭群組名稱需至少包含兩個字元。", + "exists": "鏡頭群組名稱已存在。", + "nameMustNotPeriod": "鏡頭群組名稱不可包含點(.)。", + "invalid": "無效的鏡頭群組名稱。" + }, + "label": "名稱", + "placeholder": "請輸入名稱…" + }, + "cameras": { + "label": "鏡頭", + "desc": "選擇欲添加至群組的鏡頭。" + }, + "icon": "圖標", + "success": "鏡頭群組({{name}})已儲存。", + "camera": { + "setting": { + "label": "鏡頭串流設定", + "title": "{{cameraName}} 串流設定", + "desc": "更改此鏡頭群組控制台的串流選項。這些設定是裝置/瀏覽器專屬的。", + "audioIsAvailable": "此串流有提供音訊", + "audioIsUnavailable": "此串流不提供音訊", + "audio": { + "tips": { + "title": "此串流必須從你的鏡頭輸出音訊並在 go2rtc 中設定。", + "document": "請參照文件 " + } + }, + "stream": "串流", + "placeholder": "選擇串流", + "streamMethod": { + "label": "串流方式", + "placeholder": "選擇串流方式", + "method": { + "noStreaming": { + "label": "沒有串流", + "desc": "鏡頭影像每分鐘只會更新一次,並且不會進行串流。" + }, + "smartStreaming": { + "label": "智慧串流(建議)", + "desc": "智慧串流在沒有偵測到活動時會每分鐘只更新一次鏡頭影像以節約頻寬及資源。並在偵測到活動時切換為即時串流。" + }, + "continuousStreaming": { + "label": "持續串流", + "desc": { + "title": "即使為偵測到任何活動,鏡頭影像在處於畫面中時會保持即時串流。", + "warning": "持續串流可能會佔用較高的頻寬並且可能會對效能造成影響,請謹慎使用。" + } + } + } + }, + "compatibilityMode": { + "label": "相容模式", + "desc": "只有在鏡頭的串流影像中出現色彩異常及右側有斜線時才啟用此選項。" + } + }, + "birdseye": "鳥瞰" + } + }, + "debug": { + "options": { + "label": "設定", + "title": "選項", + "showOptions": "顯示選項", + "hideOptions": "隱藏選項" + }, + "boundingBox": "定界框", + "timestamp": "時間戳", + "zones": "區域", + "mask": "遮罩", + "motion": "移動", + "regions": "區塊" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/dialog.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/dialog.json new file mode 100644 index 0000000..3927a8c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/dialog.json @@ -0,0 +1,121 @@ +{ + "restart": { + "title": "你確定要重新啟動 Frigate 嗎?", + "button": "重新啟動", + "restarting": { + "title": "Frigate 正在重新啟動", + "content": "此頁面將在 {{countdown}} 秒後重新載入。", + "button": "立即重新載入" + } + }, + "explore": { + "plus": { + "review": { + "question": { + "label": "確認此 Frigate Plus 標籤", + "ask_a": "此物件是 {{label}} 嗎?", + "ask_an": "此物件是 {{label}} 嗎?", + "ask_full": "此物件是 {{untranslatedLabel}} ({{translatedLabel}}) 嗎?" + }, + "state": { + "submitted": "已提交" + } + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "desc": "在你欲遮蓋範圍內的物件並不是誤判。將他們標示為誤判並提交會使模型混淆。" + } + }, + "video": { + "viewInHistory": "在歷史記錄中查看" + } + }, + "export": { + "time": { + "fromTimeline": "從時間線選擇", + "lastHour_other": "過去 {{count}} 個小時", + "custom": "自定義", + "start": { + "title": "開始時間", + "label": "選擇開始時間" + }, + "end": { + "title": "結束時間", + "label": "選擇結束時間" + } + }, + "name": { + "placeholder": "替匯出資料命名" + }, + "select": "選擇", + "export": "匯出", + "selectOrExport": "選擇或匯出", + "toast": { + "success": "成功開始匯出。至 /exports 頁查看匯出資料。", + "error": { + "failed": "匯出失敗:{{error}}", + "endTimeMustAfterStartTime": "結束時間必須要在開始時間之後", + "noVaildTimeSelected": "沒有選取有效的時間範圍" + }, + "view": "查看" + }, + "fromTimeline": { + "saveExport": "保存匯出資料", + "previewExport": "預覽匯出資料" + } + }, + "streaming": { + "label": "串流", + "restreaming": { + "disabled": "此鏡頭並未啟用串流重導向。", + "desc": { + "title": "設定 go2rtc 以啟用更多此鏡頭的預覽選項及音訊。", + "readTheDocumentation": "閱讀文件" + } + }, + "showStats": { + "label": "顯示串流統計資料", + "desc": "啟用此選項以在鏡頭畫面上顯示串流統計資料。" + }, + "debugView": "除錯檢視" + }, + "search": { + "saveSearch": { + "label": "保存搜尋", + "desc": "替此保存的搜尋命名。", + "placeholder": "請輸入搜尋的名稱", + "overwrite": "{{searchName}} 已存在。保存將會覆蓋現有資料。", + "success": "搜尋 {{searchName}} 已保存。", + "button": { + "save": { + "label": "保存此次搜尋" + } + } + } + }, + "recording": { + "confirmDelete": { + "title": "確認刪除", + "desc": { + "selected": "你確定要刪除所有與此查核項目有關的錄影檔案嗎?

    按住Shift以在未來跳過此確認步驟。" + }, + "toast": { + "success": "已成功刪除與選擇的審核物件有關的影片片段。", + "error": "刪除失敗:{{error}}" + } + }, + "button": { + "export": "匯出", + "markAsReviewed": "標記為已審核", + "deleteNow": "立即刪除", + "markAsUnreviewed": "標記為未審核" + } + }, + "imagePicker": { + "selectImage": "選取追蹤物件預覽圖", + "unknownLabel": "已儲存觸發圖片", + "search": { + "placeholder": "以標籤或子標籤搜尋..." + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/filter.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/filter.json new file mode 100644 index 0000000..29ccaa5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/filter.json @@ -0,0 +1,134 @@ +{ + "filter": "過濾", + "labels": { + "label": "標籤", + "all": { + "title": "所有標籤", + "short": "標籤" + }, + "count_one": "{{count}} 個標籤", + "count_other": "{{count}} 個標籤" + }, + "zones": { + "all": { + "title": "所有區域", + "short": "區域" + }, + "label": "區域" + }, + "dates": { + "selectPreset": "選擇預設設定…", + "all": { + "title": "所有日期", + "short": "日期" + } + }, + "more": "更多過濾條件", + "reset": { + "label": "重設過濾條件為預設值" + }, + "timeRange": "時間範圍", + "subLabels": { + "label": "子標籤", + "all": "所有子標籤" + }, + "score": "分數", + "estimatedSpeed": "估計速度({{unit}})", + "features": { + "label": "特徵", + "hasSnapshot": "包含截圖", + "hasVideoClip": "包含影片片段", + "submittedToFrigatePlus": { + "label": "已提交至 Frigate+", + "tips": "你必須先過濾出包含截圖的追蹤物件。

    無法提交不包含截圖的追蹤物件至 Frigate+。" + } + }, + "sort": { + "label": "排序", + "dateAsc": "日期(由舊到新)", + "dateDesc": "日期(由新到舊)", + "scoreAsc": "物件分數(由小到大)", + "scoreDesc": "物件分數(由大到小)", + "speedAsc": "估計速度(由慢到快)", + "speedDesc": "估計速度(由快到慢)", + "relevance": "相關性" + }, + "cameras": { + "label": "鏡頭過濾", + "all": { + "title": "所有鏡頭", + "short": "鏡頭" + } + }, + "review": { + "showReviewed": "顯示已審核的內容" + }, + "motion": { + "showMotionOnly": "僅顯示有移動的內容" + }, + "explore": { + "settings": { + "title": "設定", + "defaultView": { + "title": "預設檢視", + "desc": "當未選擇過濾條件時,顯示每個標籤最近追蹤物件的摘要,或顯示未過濾的網格。", + "summary": "摘要", + "unfilteredGrid": "未過濾的網格" + }, + "gridColumns": { + "title": "網格欄位數", + "desc": "選擇檢視網格的欄位數量。" + }, + "searchSource": { + "label": "搜尋來源", + "desc": "選擇搜尋追蹤物件的截圖或是說明。", + "options": { + "thumbnailImage": "截圖", + "description": "說明" + } + } + }, + "date": { + "selectDateBy": { + "label": "選擇要過濾的日期" + } + } + }, + "logSettings": { + "label": "過濾日誌等級", + "filterBySeverity": "依嚴重程度過濾日誌", + "loading": { + "title": "讀取中", + "desc": "當日誌頁面捲動到底部時,新日誌將自動更新顯示。" + }, + "disableLogStreaming": "停用日誌串流", + "allLogs": "所有日誌" + }, + "trackedObjectDelete": { + "title": "確認刪除", + "desc": "刪除這 {{objectLength}} 個追蹤物件將會刪除截圖、儲存的嵌入資料,以及相關的物件生命週期紀錄。歷史記錄中的錄影檔案不會被刪除。

    你確定要繼續嗎?

    按住 Shift 可在未來跳過這個確認內容。", + "toast": { + "success": "成功刪除追蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + }, + "zoneMask": { + "filterBy": "按區域遮罩過濾" + }, + "recognizedLicensePlates": { + "title": "已辨識車牌", + "loadFailed": "讀取已辨識車牌失敗。", + "loading": "讀取已辨識車牌中…", + "placeholder": "輸入以搜尋車牌…", + "noLicensePlatesFound": "未找到車牌。", + "selectPlatesFromList": "從列表中選擇一個或多個車牌。" + }, + "classes": { + "label": "類別", + "all": { + "title": "所有類別" + }, + "count_one": "{{count}} 個類別", + "count_other": "{{count}} 個類別" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/icons.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/icons.json new file mode 100644 index 0000000..467858b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/icons.json @@ -0,0 +1,8 @@ +{ + "iconPicker": { + "selectIcon": "選擇圖示", + "search": { + "placeholder": "搜尋圖示…" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/input.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/input.json new file mode 100644 index 0000000..ed7eee7 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/input.json @@ -0,0 +1,10 @@ +{ + "button": { + "downloadVideo": { + "label": "下載影片", + "toast": { + "success": "你的審查項目影片已開始下載。" + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/player.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/player.json new file mode 100644 index 0000000..dbecdb2 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/components/player.json @@ -0,0 +1,51 @@ +{ + "noRecordingsFoundForThisTime": "此時間段內沒有錄影", + "noPreviewFound": "找不到預覽", + "noPreviewFoundFor": "找不到 {{cameraName}} 的預覽", + "submitFrigatePlus": { + "title": "提交此畫面至 Frigate+?", + "submit": "提交" + }, + "streamOffline": { + "desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌", + "title": "串流離線" + }, + "cameraDisabled": "鏡頭已關閉", + "stats": { + "streamType": { + "title": "串流類型:", + "short": "類型" + }, + "bandwidth": { + "title": "頻寬:", + "short": "頻寬" + }, + "latency": { + "title": "延遲:", + "value": "{{seconds}} 秒", + "short": { + "title": "延遲", + "value": "{{seconds}} 秒" + } + }, + "totalFrames": "總幀數:", + "droppedFrames": { + "title": "掉幀數:", + "short": { + "title": "掉幀", + "value": "{{droppedFrames}} 幀" + } + }, + "decodedFrames": "已解碼幀數:", + "droppedFrameRate": "掉幀率:" + }, + "livePlayerRequiredIOSVersion": "此串流類型需要 IOS 17.1 或以上版本。", + "toast": { + "success": { + "submittedFrigatePlus": "成功提交畫面至 Frigate+" + }, + "error": { + "submitFrigatePlusFailed": "提交畫面至 Frigate+ 失敗" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/objects.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/objects.json new file mode 100644 index 0000000..092506c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/objects.json @@ -0,0 +1,120 @@ +{ + "person": "人", + "bicycle": "腳踏車", + "car": "車", + "boat": "船", + "traffic_light": "紅綠燈", + "fire_hydrant": "消防栓", + "street_sign": "道路標示", + "stop_sign": "停止標示", + "parking_meter": "停車柱", + "bench": "長椅", + "motorcycle": "摩托車", + "airplane": "飛機", + "bus": "巴士", + "train": "火車", + "bird": "鳥", + "cat": "貓", + "dog": "狗", + "horse": "馬", + "sheep": "羊", + "cow": "牛", + "elephant": "大象", + "bear": "熊", + "zebra": "斑馬", + "giraffe": "長頸鹿", + "hat": "帽子", + "backpack": "背包", + "umbrella": "雨傘", + "shoe": "鞋子", + "eye_glasses": "眼睛", + "handbag": "手提包", + "tie": "領帶", + "suitcase": "行李箱", + "frisbee": "飛盤", + "skis": "滑雪板", + "snowboard": "單板滑雪板", + "sports_ball": "運動球", + "kite": "風箏", + "baseball_bat": "棒球棍", + "baseball_glove": "棒球手套", + "skateboard": "滑板", + "surfboard": "衝浪板", + "tennis_racket": "網球拍", + "bottle": "瓶子", + "plate": "盤子", + "wine_glass": "酒杯", + "cup": "杯子", + "fork": "叉子", + "knife": "刀子", + "spoon": "湯匙", + "bowl": "碗", + "banana": "香蕉", + "apple": "蘋果", + "sandwich": "三明治", + "orange": "橘子", + "broccoli": "花椰菜", + "carrot": "紅蘿蔔", + "hot_dog": "熱狗", + "pizza": "披薩", + "donut": "甜甜圈", + "cake": "蛋糕", + "chair": "椅子", + "couch": "沙發", + "potted_plant": "盆栽植物", + "bed": "床", + "mirror": "鏡子", + "dining_table": "餐桌", + "window": "窗戶", + "desk": "桌子", + "toilet": "廁所", + "door": "門", + "tv": "電視", + "laptop": "筆電", + "mouse": "滑鼠", + "remote": "遠端", + "keyboard": "鍵盤", + "cell_phone": "手機", + "microwave": "微波爐", + "oven": "烤箱", + "toaster": "烤麵包機", + "sink": "水槽", + "refrigerator": "冰箱", + "blender": "果汁機", + "book": "書", + "clock": "時鐘", + "vase": "花瓶", + "scissors": "剪刀", + "teddy_bear": "泰迪熊", + "hair_dryer": "吹風機", + "toothbrush": "牙刷", + "hair_brush": "梳子", + "vehicle": "車輛", + "squirrel": "松鼠", + "deer": "鹿", + "animal": "動物", + "bark": "樹皮", + "fox": "狐狸", + "goat": "山羊", + "rabbit": "兔子", + "raccoon": "浣熊", + "robot_lawnmower": "自動割草機", + "waste_bin": "垃圾桶", + "on_demand": "隨選服務", + "face": "臉部", + "license_plate": "車牌", + "package": "包裹", + "bbq_grill": "烤肉架", + "amazon": "亞馬遜(Amazon)", + "usps": "美國郵政(USPS)", + "ups": "UPS", + "fedex": "聯邦快遞(FedEx)", + "dhl": "DHL", + "an_post": "愛爾蘭郵政(An Post)", + "purolator": "加拿大普洛特快遞", + "postnl": "荷蘭郵政(PostNL)", + "nzpost": "紐西蘭郵政(NZ Post)", + "postnord": "北歐郵政(PostNord)", + "gls": "GLS 快遞", + "dpd": "DPD 快遞" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/classificationModel.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/classificationModel.json new file mode 100644 index 0000000..f0b1d0c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/classificationModel.json @@ -0,0 +1,85 @@ +{ + "toast": { + "success": { + "deletedImage": "已刪除的圖片", + "deletedModel_other": "成功刪除 {{count}} 個模型", + "deletedCategory": "刪除分類", + "categorizedImage": "成功分類圖片", + "trainedModel": "訓練模型成功。", + "trainingModel": "已開始訓練模型。", + "updatedModel": "已更新模型配置", + "renamedCategory": "成功修改分類名稱為{{name}}" + }, + "error": { + "deleteImageFailed": "刪除失敗:{{errorMessage}}", + "deleteCategoryFailed": "刪除分類標籤失敗: {{errorMessage}}", + "deleteModelFailed": "刪除模型失敗: {{errorMessage}}", + "categorizeFailed": "圖片分類失敗: {{errorMessage}}", + "trainingFailed": "模型訓練失敗。請至Frigate 日誌查看詳情。", + "trainingFailedToStart": "模型訓練啟動失敗: {{errorMessage}}", + "updateModelFailed": "模型更新失敗: {{errorMessage}}", + "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}" + } + }, + "documentTitle": "分類模型", + "details": { + "scoreInfo": "分數表示該目標所有偵測結果的平均分類置信度。" + }, + "button": { + "deleteClassificationAttempts": "刪除分類圖片", + "renameCategory": "重新命名分類", + "deleteCategory": "刪除分類", + "deleteImages": "刪除圖片", + "trainModel": "訓練模型", + "addClassification": "添加分類", + "deleteModels": "刪除模型", + "editModel": "編輯模型" + }, + "tooltip": { + "trainingInProgress": "模型正在訓練中", + "noNewImages": "沒有新的圖片可用於訓練。請先對數據集中的更多圖片進行分類。", + "noChanges": "自上次訓練以來,數據集未作任何更改。", + "modelNotReady": "模型尚未準備好進行訓練" + }, + "deleteCategory": { + "title": "刪除類別", + "desc": "你確定要刪除類別{{name}}嗎? 這將刪除所有有關的圖片並需要重新訓練模型。", + "minClassesTitle": "無法刪除此類別", + "minClassesDesc": "分類模型必須至少擁有2個類別,新增一個新的類別已刪除這個。" + }, + "deleteModel": { + "title": "刪除分類模型", + "single": "你確定要刪除{{name}}嗎? 這將永久刪除包含圖片和訓練資料在內的所有相關資料。這個操作無法被復原。", + "desc_other": "你確定要刪除{{count}}個模型? 這將永久刪除包含圖片和訓練資料在內的所有相關資料。這個操作無法被復原。" + }, + "edit": { + "title": "編輯分類模型", + "descriptionState": "編輯這個狀態分類模型的類別,變更將需要重新訓練模型。", + "descriptionObject": "編輯這個物件分類模型的物件種類與分類種類。", + "stateClassesInfo": "注意: 變更狀態類別後需要以更新後的類別重新訓練模型。" + }, + "deleteDatasetImages": { + "title": "刪除圖片資料集合", + "desc_other": "你確定要從{{dataset}}中刪除{{count}}個圖片嗎? 這個操作將無法被復原且將需要重新訓練模型。" + }, + "deleteTrainImages": { + "title": "刪除訓練圖片", + "desc_other": "你確定要刪除{{count}}個圖片? 這個操作無法被復原。" + }, + "renameCategory": { + "title": "重新命名類別", + "desc": "輸入 {{name}} 的新名稱。您需要在名稱變更後重新訓練模型以套用變更。" + }, + "description": { + "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。" + }, + "train": { + "title": "最近的分類紀錄", + "titleShort": "最近", + "aria": "選取最近的分類紀錄" + }, + "categories": "類別", + "createCategory": { + "new": "建立新的類別" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/configEditor.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/configEditor.json new file mode 100644 index 0000000..f1943ed --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/configEditor.json @@ -0,0 +1,18 @@ +{ + "documentTitle": "設定編輯器 - Frigate", + "configEditor": "設定編輯器", + "copyConfig": "複製設定", + "saveAndRestart": "保存並重新啟動", + "toast": { + "error": { + "savingError": "保存設定時出錯" + }, + "success": { + "copyToClipboard": "已複製設定製剪貼簿。" + } + }, + "saveOnly": "僅保存", + "confirm": "是否不保存就離開?", + "safeConfigEditor": "設定編輯器(安全模式)", + "safeModeDescription": "由於設定驗證有誤,Frigate 進入安全模式。" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/events.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/events.json new file mode 100644 index 0000000..314779d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/events.json @@ -0,0 +1,62 @@ +{ + "alerts": "警報", + "detections": "偵測", + "motion": { + "label": "移動", + "only": "只顯示移動" + }, + "empty": { + "motion": "未找到移動資料", + "alert": "沒有警告需要審核", + "detection": "沒有偵測到的內容需要審核" + }, + "timeline": "時間線", + "timeline.aria": "選擇時間線", + "events": { + "label": "事件", + "aria": "選擇事件", + "noFoundForTimePeriod": "此時間段內沒有找到事件。" + }, + "documentTitle": "審核 - Frigate", + "allCameras": "所有鏡頭", + "recordings": { + "documentTitle": "錄影 - Frigate" + }, + "calendarFilter": { + "last24Hours": "過去 24 小時" + }, + "markAsReviewed": "標示為已審核", + "markTheseItemsAsReviewed": "將這些內容標記為已審核", + "newReviewItems": { + "label": "查看新的審核項目", + "button": "有新的審核項目" + }, + "selected_one": "已選擇 {{count}} 個", + "selected_other": "已選擇 {{count}} 個", + "camera": "鏡頭", + "detected": "已偵測", + "suspiciousActivity": "可疑的活動", + "threateningActivity": "有威脅性的活動", + "zoomIn": "放大", + "zoomOut": "縮小", + "detail": { + "label": "詳細資訊", + "noDataFound": "沒有可供檢視的詳細資訊", + "aria": "開關詳細資訊視圖", + "trackedObject_one": "{{count}} 個物件", + "trackedObject_other": "{{count}} 個物件", + "noObjectDetailData": "沒有可用物件細節。", + "settings": "細節視圖設定", + "alwaysExpandActive": { + "title": "總是展開", + "desc": "在可用時總是展開當前物件的詳細資訊。" + } + }, + "objectTrack": { + "trackedPoint": "追蹤點", + "clickToSeek": "點擊從此時間點尋找" + }, + "normalActivity": "正常", + "needsReview": "待審核", + "securityConcern": "安全隱憂" +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/explore.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/explore.json new file mode 100644 index 0000000..59602f9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/explore.json @@ -0,0 +1,252 @@ +{ + "documentTitle": "瀏覽 - Frigate", + "generativeAI": "生成式 AI", + "exploreMore": "瀏覽更多 {{label}} 物件", + "exploreIsUnavailable": { + "title": "無法使用瀏覽功能", + "embeddingsReindexing": { + "finishingShortly": "即將完成", + "step": { + "thumbnailsEmbedded": "已嵌入縮圖: ", + "descriptionsEmbedded": "已嵌入說明: ", + "trackedObjectsProcessed": "已處理的追蹤物件: " + }, + "context": "在重新建立完追蹤物件的嵌入索引後可以使用瀏覽功能。", + "startingUp": "啟動中…", + "estimatedTime": "預計剩餘時間:" + }, + "downloadingModels": { + "context": "Frigate 正在下載所需的嵌入式模型以支援語意搜尋功能。根據你的網路連接速度,這可能會需要幾分鐘。", + "setup": { + "visionModel": "視覺模型", + "visionModelFeatureExtractor": "視覺模型特徵提取器", + "textModel": "文字模型", + "textTokenizer": "文字分詞器" + }, + "tips": { + "context": "在模型下載完成後,你可能會需要重新建立追蹤物件的特徵索引。", + "documentation": "閱讀文件" + }, + "error": "發生錯誤。請檢查 Frigate 日誌。" + } + }, + "details": { + "timestamp": "時間戳", + "item": { + "title": "審核項目詳情", + "desc": "審核項目詳情", + "button": { + "share": "分享此審核項目", + "viewInExplore": "在瀏覽中查看" + }, + "tips": { + "mismatch_other": "在此審核項目中偵測到 {{count}} 個不可用物件。這些物件可能不符合警示或偵測標準,或者已被清除/刪除。", + "hasMissingObjects": "如果你想要 Frigate 儲存以下標籤的追蹤物件:{{objects}},請調整設定" + }, + "toast": { + "success": { + "regenerate": "已從 {{provider}} 請求新的說明。根據提供者的速度,生成新的說明可能會需要一段時間。", + "updatedSublabel": "成功更新子標籤。", + "updatedLPR": "成功更新車牌。" + }, + "error": { + "regenerate": "請求 {{provider}} 生成新的說明失敗:{{errorMessage}}", + "updatedSublabelFailed": "更新子標籤失敗:{{errorMessage}}", + "updatedLPRFailed": "更新車牌失敗:{{errorMessage}}" + } + } + }, + "label": "標籤", + "editSubLabel": { + "title": "編輯子標籤", + "desc": "輸入 {{label}} 的新子標籤", + "descNoLabel": "輸入此追蹤物件的新子標籤" + }, + "editLPR": { + "title": "編輯車牌", + "desc": "輸入此 {{label}} 的新車牌號碼", + "descNoLabel": "輸入此追蹤物件的新車牌號碼" + }, + "snapshotScore": { + "label": "截圖分數" + }, + "topScore": { + "label": "最高分數", + "info": "最高分數是追蹤物件的最高中位數,因此可能會與搜尋結果的截圖顯示的分數有所不同。" + }, + "recognizedLicensePlate": "已辨識車牌", + "estimatedSpeed": "估計速度", + "objects": "物件", + "camera": "鏡頭", + "zones": "區域", + "button": { + "findSimilar": "尋找相似項目", + "regenerate": { + "title": "重新生成", + "label": "重新生成追蹤物件的說明" + } + }, + "description": { + "label": "說明", + "placeholder": "追蹤物件的說明", + "aiTips": "在追蹤物件的生命週期結束前,Frigate 不會向你設定的生成式 AI 提供者請求說明。" + }, + "expandRegenerationMenu": "展開重新生成選單", + "regenerateFromSnapshot": "從截圖重新生成", + "regenerateFromThumbnails": "從縮圖重新生成", + "tips": { + "descriptionSaved": "成功保存說明", + "saveDescriptionFailed": "更新說明失敗:{{errorMessage}}" + } + }, + "trackedObjectDetails": "追蹤物件詳情", + "type": { + "details": "詳情", + "snapshot": "截圖", + "video": "影片", + "object_lifecycle": "物件生命週期", + "thumbnail": "預覽圖", + "tracking_details": "追蹤詳情" + }, + "objectLifecycle": { + "title": "物件生命週期", + "noImageFound": "此時間點找不到圖片。", + "createObjectMask": "建立物件遮罩", + "adjustAnnotationSettings": "調整標注設定", + "scrollViewTips": "滾動以查看此物件生命週期中的重要時刻。", + "autoTrackingTips": "自動追蹤鏡頭的定界框位置可能不準確。", + "count": "第 {{first}} 個,共 {{second}} 個", + "trackedPoint": "追蹤點", + "lifecycleItemDesc": { + "visible": "偵測到 {{label}}", + "entered_zone": "{{label}} 進入了 {{zones}}", + "active": "{{label}} 開始活動", + "stationary": "{{label}} 停止活動", + "attribute": { + "faceOrLicense_plate": "偵測到 {{label}} 的 {{attribute}}", + "other": "{{label}} 被辨識為 {{attribute}}" + }, + "gone": "{{label}} 離開了", + "heard": "聽到 {{label}}", + "external": "偵測到 {{label}}", + "header": { + "zones": "區域", + "ratio": "比例", + "area": "範圍" + } + }, + "annotationSettings": { + "title": "標注設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "總是在畫面上顯示有物件進入的區域。" + }, + "offset": { + "label": "標注偏移量", + "desc": "此資料是來自鏡頭的偵測串流,但被覆蓋在錄影串流上。通常兩個串流沒辦法完美的同步,因此,影片片段中的定界框可能無法完全對齊。不過,這可以透過 annotation_offset 進行調整。", + "documentation": "閱讀文件 ", + "millisecondsToOffset": "偵測註解偏移的毫秒數。預設:0", + "tips": "提示:試想在一個片段中有個人從畫面左邊走到右邊。如果事件時間線上的定界框一直出現在人物的左邊,則應該減少數值。在同樣的畫面中,如果定界框持續出現在人的前方,則應該增加數值。", + "toast": { + "success": "{{camera}} 的標注偏移量已保存到設定檔。重新啟動 Frigate 以套用更改。" + } + } + }, + "carousel": { + "previous": "上一張", + "next": "下一張" + } + }, + "itemMenu": { + "downloadVideo": { + "label": "下載影片", + "aria": "下載影片" + }, + "downloadSnapshot": { + "label": "下載截圖", + "aria": "下載截圖" + }, + "viewObjectLifecycle": { + "label": "查看物件生命週期", + "aria": "顯示物件生命週期" + }, + "findSimilar": { + "label": "尋找相似項目", + "aria": "尋找相似的追蹤物件" + }, + "submitToPlus": { + "label": "提交到 Frigate+", + "aria": "提交到 Frigate Plus" + }, + "viewInHistory": { + "label": "於歷史記錄中查看", + "aria": "於歷史記錄中查看" + }, + "deleteTrackedObject": { + "label": "刪除此追蹤物件" + } + }, + "dialog": { + "confirmDelete": { + "title": "確認刪除", + "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會被刪除。

    你確定要刪除嗎?" + } + }, + "noTrackedObjects": "找不到追蹤物件", + "fetchingTrackedObjectsFailed": "取得追蹤物件時錯誤:{{errorMessage}}", + "trackedObjectsCount_other": "{{count}} 個追蹤物件 ", + "searchResult": { + "tooltip": "與 {{type}} 相似的程度為 {{confidence}}%", + "deleteTrackedObject": { + "toast": { + "success": "成功刪除蹤物件。", + "error": "刪除追蹤物件失敗:{{errorMessage}}" + } + } + }, + "trackingDetails": { + "title": "追蹤詳情", + "noImageFound": "沒有找到在此時間點的圖片。", + "createObjectMask": "建立物件遮罩", + "adjustAnnotationSettings": "調整標記設定", + "scrollViewTips": "點擊查看物件周期的關鍵時間。", + "autoTrackingTips": "自動追蹤鏡頭的邊框位置可能不準確。", + "count": "{{second}}之{{first}}", + "trackedPoint": "追蹤點", + "lifecycleItemDesc": { + "visible": "偵測到 {{label}}", + "entered_zone": "{{label}} 已進入 {{zones}}", + "active": "{{label}} 正在活動", + "stationary": "{{label}} 變為靜止", + "attribute": { + "faceOrLicense_plate": "偵測到{{label}} {{attribute}}", + "other": "{{label}} 被識別為 {{attribute}}" + }, + "gone": "{{label}} 已離開", + "heard": "聽到了 {{label}}", + "external": "偵測到 {{label}}", + "header": { + "zones": "區域", + "ratio": "比例", + "score": "分數", + "area": "面積" + } + }, + "annotationSettings": { + "title": "標記設定", + "showAllZones": { + "title": "顯示所有區域", + "desc": "總是在物件進入區域時在畫面上顯示區域範圍。" + }, + "offset": { + "label": "標記偏移量", + "desc": "這個資料來自您的鏡頭的偵測串流源,但是被疊加在錄影串流源的畫面上,兩個串流源不太可能完美的同步,因此邊框與畫面無法完美的對齊。您可以用這項設定調整標記在時間上前後偏移的補償量來更好的將其與錄影畫面對齊。", + "millisecondsToOffset": "偵測標記偏移補償的毫秒數。預設值: 0", + "tips": "如果影片播放進度超前於方框和路徑點,則降低該值;如果影片播放進度落後於方框和路徑點,則增加該數值。該值可以為負數。", + "toast": { + "success": "{{camera}} 的標記偏移補償量已儲存至設定檔,重新啟動 Frigate 以套用變更。" + } + } + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/exports.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/exports.json new file mode 100644 index 0000000..3d3f9e8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/exports.json @@ -0,0 +1,23 @@ +{ + "search": "搜尋", + "documentTitle": "匯出 - Frigate", + "noExports": "找不到匯出內容", + "deleteExport": "刪除匯出內容", + "editExport": { + "saveExport": "儲存匯出內容", + "title": "重新命名匯出內容", + "desc": "請輸入此匯出內容的新名稱。" + }, + "toast": { + "error": { + "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}" + } + }, + "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?", + "tooltip": { + "shareExport": "分享匯出", + "downloadVideo": "下載影片", + "editName": "編輯名稱", + "deleteExport": "刪除匯出" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/faceLibrary.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/faceLibrary.json new file mode 100644 index 0000000..99158e3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/faceLibrary.json @@ -0,0 +1,96 @@ +{ + "description": { + "addFace": "上傳您的第一張照片至臉部資料庫以新增一個新的集合。", + "placeholder": "輸入此集合的名稱", + "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。" + }, + "details": { + "person": "人", + "timestamp": "時間戳", + "unknown": "未知", + "subLabelScore": "子標籤分數", + "scoreInfo": "子標籤分數是所有已辨識的人臉信心值的加權平均,因此可能與截圖上顯示的分數不同。", + "face": "人臉詳細資料", + "faceDesc": "組成此人臉的追蹤物件的詳細資料" + }, + "documentTitle": "人臉資料庫 - Frigate", + "uploadFaceImage": { + "title": "上傳人臉圖片", + "desc": "上傳圖片以掃描人臉並將其加入 {{pageToggle}}" + }, + "collections": "集合", + "selectItem": "選擇 {{item}}", + "createFaceLibrary": { + "title": "建立集合", + "desc": "建立新集合", + "new": "建立新人臉", + "nextSteps": "為了建立可靠的模型基底:
  • 在最近的識別紀錄分頁中選擇並針對每個偵測到人的圖片進行訓練。
  • 請優先使用正臉照以獲得最佳效果,請盡量避免使用從側面或有傾斜角度的人臉
  • " + }, + "steps": { + "faceName": "輸入人臉名稱", + "uploadFace": "上傳人臉圖片", + "nextSteps": "下一步", + "description": { + "uploadFace": "上傳一張 {{name}} 的正臉圖片。圖片不需要裁剪到只剩下臉部。" + } + }, + "train": { + "title": "最近的識別紀錄", + "aria": "選擇最近的識別紀錄", + "empty": "最近沒有辨識人臉的操作" + }, + "selectFace": "選擇人臉", + "deleteFaceLibrary": { + "title": "刪除名稱", + "desc": "你確定要刪除 {{name}} 集合嗎?這會刪除所有有關的人臉資料。" + }, + "deleteFaceAttempts": { + "title": "刪除人臉", + "desc_other": "你確定要刪除 {{count}} 個人臉嗎?這個步驟無法復原。" + }, + "renameFace": { + "title": "重新命名人臉", + "desc": "輸入 {{name}} 的新名稱" + }, + "button": { + "deleteFaceAttempts": "刪除人臉", + "addFace": "新增人臉", + "renameFace": "重新命名人臉", + "uploadImage": "上傳圖片", + "reprocessFace": "重新處理人臉", + "deleteFace": "刪除人臉" + }, + "imageEntry": { + "validation": { + "selectImage": "請選擇一個圖片檔。" + }, + "dropActive": "將圖片拖到這裡…", + "dropInstructions": "拖放或貼上圖片至此處,或點擊以選取", + "maxSize": "最大檔案大小:{{size}}MB" + }, + "nofaces": "沒有可用的人臉", + "pixels": "{{area}}px", + "readTheDocs": "閱讀文件", + "trainFaceAs": "將人臉訓練為:", + "trainFace": "訓練人臉", + "toast": { + "success": { + "uploadedImage": "成功上傳圖片。", + "addFaceLibrary": "已成功將 {{name}} 加入至人臉資料庫!", + "deletedFace_other": "成功刪除 {{count}} 個人臉。", + "deletedName_other": "{{count}} 個人臉已成功刪除。", + "renamedFace": "成功將人臉重新命名為 {{name}}", + "trainedFace": "成功訓練人臉。", + "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。" + }, + "error": { + "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", + "addFaceLibraryFailed": "設定人臉名稱失敗:{{errorMessage}}", + "deleteFaceFailed": "刪除失敗:{{errorMessage}}", + "deleteNameFailed": "刪除名稱失敗:{{errorMessage}}", + "renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}", + "trainFailed": "訓練失敗:{{errorMessage}}", + "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/live.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/live.json new file mode 100644 index 0000000..a839b4b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/live.json @@ -0,0 +1,176 @@ +{ + "documentTitle": "即時畫面 - Frigate", + "documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate", + "lowBandwidthMode": "低流量模式", + "twoWayTalk": { + "enable": "啟用雙向通話", + "disable": "停用雙向通話" + }, + "ptz": { + "move": { + "clickMove": { + "label": "點擊畫面以置中鏡頭", + "enable": "啟用點擊移動", + "disable": "停用點擊移動" + }, + "left": { + "label": "向左移動 PTZ 鏡頭" + }, + "up": { + "label": "向上移動 PTZ 鏡頭" + }, + "down": { + "label": "向下移動 PTZ 鏡頭" + }, + "right": { + "label": "向右移動 PTZ 鏡頭" + } + }, + "zoom": { + "in": { + "label": "放大 PTZ 鏡頭" + }, + "out": { + "label": "縮小 PTZ 鏡頭" + } + }, + "frame": { + "center": { + "label": "點擊畫面以置中 PTZ 鏡頭" + } + }, + "presets": "PTZ 鏡頭預設", + "focus": { + "in": { + "label": "聚焦 PTZ 鏡頭" + }, + "out": { + "label": "離焦 PTZ 鏡頭" + } + } + }, + "cameraAudio": { + "enable": "啟用鏡頭音訊", + "disable": "停用鏡頭音訊" + }, + "camera": { + "enable": "啟用鏡頭", + "disable": "停用鏡頭" + }, + "muteCameras": { + "enable": "所有鏡頭靜音", + "disable": "所有鏡頭取消靜音" + }, + "detect": { + "enable": "啟用偵測", + "disable": "停用偵測" + }, + "recording": { + "enable": "啟用錄影", + "disable": "停用錄影" + }, + "snapshots": { + "enable": "啟用截圖", + "disable": "停用截圖" + }, + "audioDetect": { + "enable": "啟用音訊偵測", + "disable": "停用音訊偵測" + }, + "autotracking": { + "enable": "啟用自動追蹤", + "disable": "停用自動追蹤" + }, + "streamStats": { + "enable": "顯示串流統計資料", + "disable": "隱藏串流統計資料" + }, + "manualRecording": { + "title": "應需", + "tips": "根據此鏡頭的錄影保留設定,下載快照或手動啟動事件。", + "playInBackground": { + "label": "背景播放", + "desc": "啟用此選項以在播放器被隱藏時繼續播放串流。" + }, + "showStats": { + "label": "顯示統計資料", + "desc": "啟用此選項以在鏡頭畫面上顯示串流的統計資料。" + }, + "debugView": "除錯畫面", + "start": "開始應需錄影", + "started": "開始手動應需錄影。", + "failedToStart": "手動開始應需錄影失敗。", + "recordDisabledTips": "因為此鏡頭的錄影功能被停用或限制,因此僅會保存截圖。", + "end": "結束應需錄影", + "ended": "已結束手動應需錄影。", + "failedToEnd": "結束手動應需錄影失敗。" + }, + "streamingSettings": "串流設定", + "notifications": "通知", + "audio": "音訊", + "suspend": { + "forTime": "暫停: " + }, + "stream": { + "title": "串流", + "audio": { + "tips": { + "title": "此串流的音訊必須要從鏡頭輸出並且在 go2rtc 中被設定。", + "documentation": "閱讀文件 " + }, + "available": "此串流支援音訊", + "unavailable": "此串流不支援音訊" + }, + "twoWayTalk": { + "tips": "你的裝置被需支援此功能,並且需設定 WebRTC 以使用雙向通話。", + "tips.documentation": "閱讀文件 ", + "available": "此串流支援雙向通話", + "unavailable": "此串流不支援雙向通話" + }, + "lowBandwidth": { + "tips": "因為緩衝區或串流錯誤,即時畫面已切換至低流量模式。", + "resetStream": "重設串流" + }, + "playInBackground": { + "label": "背景播放", + "tips": "啟用此選項以在播放器被隱藏時繼續播放串流。" + } + }, + "cameraSettings": { + "title": "{{camera}} 設定", + "cameraEnabled": "鏡頭已啟用", + "objectDetection": "物件偵測", + "recording": "錄影", + "snapshots": "截圖", + "audioDetection": "音訊偵測", + "autotracking": "自動追蹤" + }, + "history": { + "label": "顯示歷史影像" + }, + "effectiveRetainMode": { + "modes": { + "all": "全部", + "motion": "移動", + "active_objects": "活躍物件" + }, + "notAllTips": "你的 {{source}} 錄影保留設定為 {{effectiveRetainMode}} 模式,因此此應需錄影僅會保留 {{effectiveRetainModeName}} 片段。" + }, + "editLayout": { + "label": "編輯版面配置", + "group": { + "label": "編輯鏡頭群組" + }, + "exitEdit": "結束編輯" + }, + "transcription": { + "enable": "啟用即時語音轉錄", + "disable": "停用即時語音轉錄" + }, + "snapshot": { + "takeSnapshot": "下載即時快照", + "noVideoSource": "沒有可用的影片資源以擷取快照。", + "captureFailed": "快照擷取失敗。", + "downloadStarted": "已開始下載快照。" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/recording.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/recording.json new file mode 100644 index 0000000..1b10c05 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/recording.json @@ -0,0 +1,12 @@ +{ + "filter": "過濾", + "export": "匯出", + "calendar": "日曆", + "filters": "過濾條件", + "toast": { + "error": { + "noValidTimeSelected": "未選擇有效的時間範圍", + "endTimeMustAfterStartTime": "結束時間需晚於開始時間" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/search.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/search.json new file mode 100644 index 0000000..09e408f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/search.json @@ -0,0 +1,72 @@ +{ + "search": "搜尋", + "savedSearches": "已儲存的搜尋", + "searchFor": "搜尋 {{inputValue}}", + "button": { + "clear": "清空搜尋", + "filterActive": "過濾中", + "save": "儲存搜尋", + "delete": "刪除保存的搜尋", + "filterInformation": "過濾資訊" + }, + "trackedObjectId": "追蹤物件編號", + "filter": { + "label": { + "cameras": "鏡頭", + "labels": "標籤", + "zones": "區域", + "sub_labels": "子標籤", + "search_type": "搜尋類型", + "before": "結束時間", + "after": "開始時間", + "min_score": "最低分數", + "max_score": "最高分數", + "min_speed": "最低速度", + "max_speed": "最高速度", + "recognized_license_plate": "已辨識的車牌", + "has_clip": "包含片段", + "has_snapshot": "包含截圖", + "time_range": "時間範圍" + }, + "searchType": { + "thumbnail": "截圖", + "description": "說明" + }, + "toast": { + "error": { + "beforeDateBeLaterAfter": "「結束」日期必須要在「開始」日期之後。", + "afterDatebeEarlierBefore": "「開始」日期需要在「結束」日期之前。", + "minScoreMustBeLessOrEqualMaxScore": "「最低分數」必須小於或等於「最高分數」。", + "maxScoreMustBeGreaterOrEqualMinScore": "「最高分數」必須要大於或等於「最低分數」。", + "minSpeedMustBeLessOrEqualMaxSpeed": "「最低速度」必須要小於或等於「最高速度」。", + "maxSpeedMustBeGreaterOrEqualMinSpeed": "「最高速度」必須要大於或等於「最低速度」。" + } + }, + "tips": { + "title": "如何使用文字過濾", + "desc": { + "text": "過濾功能可以幫助你縮小搜尋範圍。以下是使用方法:", + "step1": "輸入過濾標的名稱後加上冒號(例如:\"cameras:\")。", + "step2": "從建議中選擇一個值,或者自行輸入。", + "step3": "若有多個過濾條件可以使用空格隔開。", + "step4": "過濾日期(before: 以及 after:)需使用 {{DateFormat}} 格式。", + "step5": "過濾時間範圍時需使用 {{exampleTime}} 格式。", + "step6": "點擊旁邊的「x」可以移除對應的過濾條件。", + "exampleLabel": "範例:" + } + }, + "header": { + "currentFilterType": "過濾內容", + "noFilters": "過濾條件", + "activeFilters": "已套用的過濾條件" + } + }, + "similaritySearch": { + "title": "相似搜尋", + "active": "已啟用相似搜尋", + "clear": "清空相似搜尋" + }, + "placeholder": { + "search": "搜尋…" + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/settings.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/settings.json new file mode 100644 index 0000000..701a2f1 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/settings.json @@ -0,0 +1,138 @@ +{ + "documentTitle": { + "default": "設定 - Frigate", + "authentication": "認證設定 - Frigate", + "camera": "鏡頭設定 - Frigate", + "enrichments": "進階功能設定 - Frigate", + "general": "使用者介面設定 - Frigate", + "frigatePlus": "Frigate+ 設定 - Frigate", + "notifications": "通知設定 - Frigate", + "masksAndZones": "遮罩與區域編輯器 - Frigate", + "motionTuner": "移動偵測調教器 - Frigate", + "object": "除錯 - Frigate", + "cameraManagement": "管理鏡頭 - Frigate", + "cameraReview": "相機預覽設置 - Frigate" + }, + "menu": { + "ui": "使用者介面", + "enrichments": "進階功能", + "cameras": "鏡頭設定", + "masksAndZones": "遮罩 / 區域", + "motionTuner": "移動偵測調教器", + "debug": "除錯", + "users": "使用者", + "notifications": "通知", + "frigateplus": "Frigate+", + "triggers": "觸發", + "cameraManagement": "管理", + "cameraReview": "預覽", + "roles": "角色" + }, + "dialog": { + "unsavedChanges": { + "title": "你有未保存的變更。", + "desc": "再繼續之前,你想先儲存你的變更嗎?" + } + }, + "cameraSetting": { + "camera": "鏡頭", + "noCamera": "沒有鏡頭" + }, + "general": { + "title": "使用者介面設定", + "liveDashboard": { + "title": "即時監控面板", + "automaticLiveView": { + "label": "自動即時檢視", + "desc": "在偵測到移動時自動切換至即時影像。停用此設定將使得在即時監控面板上的靜態畫面每分鐘更新一次。" + }, + "playAlertVideos": { + "label": "播放警報影片", + "desc": "最近的警報影片預設會在即時監控面板中連續循環播放。取消這個選項,可以只顯示靜態的最近警報擷圖(僅套用於該裝置/瀏覽器)。" + }, + "displayCameraNames": { + "label": "總是顯示鏡頭名稱", + "desc": "總是在多鏡頭直播頁面顯示鏡頭的名稱標籤。" + }, + "liveFallbackTimeout": { + "label": "直播播放器回退逾時", + "desc": "當高畫質串流直播無法使用時,在此秒數後會回退成低流量模式。預設值: 3。" + } + }, + "storedLayouts": { + "title": "儲存的排版", + "desc": "在鏡頭群組內的鏡頭排版可以拖拉或縮放調整。這個排版設定儲存於目前瀏覽器的本機儲存空間。", + "clearAll": "清除所有排版" + }, + "cameraGroupStreaming": { + "title": "鏡頭群組串流播放設定", + "desc": "每個鏡頭群組的串流播放設定都儲存在目前瀏覽器的本機儲存空間。", + "clearAll": "清除所有串流播放設定" + }, + "recordingsViewer": { + "title": "錄影檢視器", + "defaultPlaybackRate": { + "label": "預設播放速度", + "desc": "錄影回放的預設播放速度。" + } + }, + "calendar": { + "title": "月曆", + "firstWeekday": { + "label": "第一個工作天", + "desc": "在檢視月曆中,每個禮拜從禮拜幾開始。", + "sunday": "禮拜天", + "monday": "禮拜一" + } + }, + "toast": { + "success": { + "clearStoredLayout": "清除 {{cameraName}} 儲存的排版", + "clearStreamingSettings": "清除所有鏡頭群組的串流播放設定。" + }, + "error": { + "clearStoredLayoutFailed": "清除儲存的排版設定失敗: {{errorMessage}}", + "clearStreamingSettingsFailed": "清除串流播放設定失敗: {{errorMessage}}" + } + } + }, + "enrichments": { + "title": "進階功能設定", + "unsavedChanges": "尚未儲存的強化設定變更", + "semanticSearch": { + "modelSize": { + "label": "模型大小", + "small": { + "title": "小" + } + } + }, + "faceRecognition": { + "title": "人臉識別" + } + }, + "cameraWizard": { + "title": "新增相機", + "testResultLabels": { + "resolution": "解析度", + "video": "影像", + "audio": "語音" + }, + "commonErrors": { + "testFailed": "串流測試失敗: {{error}}" + }, + "step1": { + "description": "輸入相機詳細資訊並選擇自動偵測或手動選擇相機品牌。", + "cameraName": "相機名稱", + "cameraNamePlaceholder": "例: 前門 / 後院", + "host": "主機/IP 位置", + "port": "埠", + "username": "用戶名稱", + "usernamePlaceholder": "選填", + "password": "密碼", + "passwordPlaceholder": "選填", + "selectTransport": "選擇協議", + "cameraBrand": "相機品牌" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/system.json b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/system.json new file mode 100644 index 0000000..7c87b59 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/locales/zh-Hant/views/system.json @@ -0,0 +1,186 @@ +{ + "documentTitle": { + "cameras": "鏡頭統計 - Frigate", + "storage": "儲存裝置統計 - Frigate", + "general": "一般統計 - Frigate", + "enrichments": "進階功能統計 - Frigate", + "logs": { + "frigate": "Frigate 日誌 - Frigate", + "go2rtc": "Go2RTC 日誌 - Frigate", + "nginx": "Nginx 日誌 - Frigate" + } + }, + "title": "系統", + "metrics": "系統指標", + "logs": { + "download": { + "label": "下載日誌" + }, + "copy": { + "label": "複製到剪貼簿", + "success": "已將日誌複製到剪貼簿", + "error": "無法將日誌複製到剪貼簿" + }, + "type": { + "label": "類型", + "timestamp": "時間戳", + "tag": "標籤", + "message": "訊息" + }, + "tips": "正在從伺服器串流日誌", + "toast": { + "error": { + "fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}", + "whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}" + } + } + }, + "general": { + "title": "一般", + "detector": { + "title": "偵測器", + "inferenceSpeed": "偵測器推理速度", + "temperature": "偵測器溫度", + "cpuUsage": "偵測器 CPU 使用率", + "memoryUsage": "偵測器記憶體使用量", + "cpuUsageInformation": "用於準備輸入和輸出數據至/從偵測模型的CPU。此值不衡量推論使用量,即使使用GPU或加速器。" + }, + "hardwareInfo": { + "title": "硬體資訊", + "gpuUsage": "GPU 使用率", + "gpuMemory": "GPU 記憶體", + "gpuEncoder": "GPU 編碼器", + "gpuDecoder": "GPU 解碼器", + "gpuInfo": { + "vainfoOutput": { + "title": "Vainfo 輸出", + "returnCode": "返回代碼:{{code}}", + "processOutput": "行程輸出:", + "processError": "行程錯誤:" + }, + "nvidiaSMIOutput": { + "title": "Nvidia SMI 輸出", + "name": "名稱:{{name}}", + "driver": "驅動程式:{{driver}}", + "cudaComputerCapability": "CUDA 計算能力:{{cuda_compute}}", + "vbios": "VBios 資訊:{{vbios}}" + }, + "closeInfo": { + "label": "關閉 GPU 資訊" + }, + "copyInfo": { + "label": "複製 GPU 訊息" + }, + "toast": { + "success": "已複製 GPU 訊息至剪貼簿" + } + }, + "npuUsage": "NPU 使用率", + "npuMemory": "NPU 記憶體", + "intelGpuWarning": { + "title": "Intel GPU 狀態警告", + "message": "GPU 狀態資訊不可用", + "description": "這是一個在Intel GPU 狀態回報工具 (intel_gpu_top) 中已知的 Bug,該工具會故障並重複的回報 GPU占用率為 0%,甚至在硬體加速與物件偵測在 (i)GPU上正確運作時也是如此。這不是 Frigate 的 Bug。您可以透過重新啟動主機來暫時修復此問題以確認 GPU 運作正常。這不會影響效能。" + } + }, + "otherProcesses": { + "title": "其他行程", + "processCpuUsage": "行程 CPU 使用率", + "processMemoryUsage": "行程記憶體使用量" + } + }, + "storage": { + "title": "儲存裝置", + "overview": "總覽", + "recordings": { + "title": "錄影檔案", + "tips": "此數值僅代表 Frigate 資料庫中錄影資料的儲存空間用量。Frigate 不會追蹤硬碟上所有檔案的使用量。", + "earliestRecording": "最早的錄影檔案:" + }, + "cameraStorage": { + "title": "鏡頭儲存", + "camera": "鏡頭", + "unusedStorageInformation": "未使用的儲存空間資訊", + "storageUsed": "已使用的儲存空間", + "percentageOfTotalUsed": "佔總量百分比", + "bandwidth": "頻寬", + "unused": { + "title": "未使用", + "tips": "在磁碟中有除了 Frigate 錄影內容以外的檔案時,此數值可能無法正確反應可用的空間。Frigate 不會追蹤錄影資料以外的檔案的儲存空間用量。" + } + } + }, + "cameras": { + "title": "鏡頭", + "overview": "總覽", + "info": { + "aspectRatio": "長寬比", + "cameraProbeInfo": "{{camera}} 的詳細資訊", + "streamDataFromFFPROBE": "串流資料是透過 ffprobe取得。", + "fetching": "正在讀取鏡頭資訊", + "stream": "串流 {{idx}}", + "video": "影片:", + "codec": "編解碼器:", + "resolution": "解析度:", + "fps": "幀率:", + "unknown": "未知", + "audio": "音訊:", + "error": "錯誤:{{error}}", + "tips": { + "title": "鏡頭詳細資訊" + } + }, + "framesAndDetections": "幀數 / 偵測數", + "label": { + "camera": "鏡頭", + "detect": "偵測", + "skipped": "跳過", + "ffmpeg": "FFmpeg", + "capture": "抓取", + "overallFramesPerSecond": "總體幀率", + "overallDetectionsPerSecond": "總體每秒偵測幀數", + "overallSkippedDetectionsPerSecond": "總體每秒跳過偵測幀率", + "cameraFfmpeg": "{{camName}} FFmpeg", + "cameraCapture": "{{camName}} 抓取", + "cameraDetect": "{{camName}} 偵測", + "cameraFramesPerSecond": "{{camName}} 幀率", + "cameraDetectionsPerSecond": "{{camName}} 每秒偵測幀率", + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率" + }, + "toast": { + "success": { + "copyToClipboard": "已複製檢測資料至剪貼簿。" + }, + "error": { + "unableToProbeCamera": "無法檢測鏡頭:{{errorMessage}}" + } + } + }, + "lastRefreshed": "最後更新: ", + "stats": { + "ffmpegHighCpuUsage": "{{camera}} 的 FFmpeg CPU 使用率較高 ({{ffmpegAvg}}%)", + "detectHighCpuUsage": "{{camera}} 的偵測 CPU 使用率較高 ({{detectAvg}}%)", + "healthy": "系統運作正常", + "reindexingEmbeddings": "正在重新替嵌入資料建立索引(已完成 {{processed}}%)", + "cameraIsOffline": "{{camera}} 已離線", + "detectIsSlow": "{{detect}} 偵測速度較慢({{speed}} 毫秒)", + "detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)" + }, + "enrichments": { + "title": "進階功能", + "infPerSecond": "每秒推理次數", + "embeddings": { + "image_embedding": "圖片特徵提取", + "text_embedding": "文字提取", + "face_recognition": "人臉辨識", + "plate_recognition": "車牌辨識", + "image_embedding_speed": "圖片特徵提取速度", + "face_embedding_speed": "人臉特徵提取速度", + "face_recognition_speed": "人臉辨識速度", + "plate_recognition_speed": "車牌辨識速度", + "text_embedding_speed": "文字提取速度", + "yolov9_plate_detection_speed": "YOLOv9 車牌偵測速度", + "yolov9_plate_detection": "YOLOv9 車牌辨識" + } + } +} diff --git a/sam2-cpu/frigate-dev/web/public/notifications-worker.js b/sam2-cpu/frigate-dev/web/public/notifications-worker.js new file mode 100644 index 0000000..ba4e033 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/notifications-worker.js @@ -0,0 +1,71 @@ +// Notifications Worker + +self.addEventListener("push", function (event) { + // @ts-expect-error we know this exists + if (event.data) { + // @ts-expect-error we know this exists + const data = event.data.json(); + + let actions = []; + + switch (data.type ?? "unknown") { + case "alert": + actions = [ + { + action: "markReviewed", + title: "Mark as Reviewed", + }, + ]; + break; + } + + // @ts-expect-error we know this exists + self.registration.showNotification(data.title, { + body: data.message, + icon: "/images/maskable-icon.png", + image: data.image, + badge: "/images/maskable-badge.png", + tag: data.id, + data: { id: data.id, link: data.direct_url }, + actions, + }); + } else { + // pass + // This push event has no data + } +}); + +self.addEventListener("notificationclick", (event) => { + // @ts-expect-error we know this exists + if (event.notification) { + // @ts-expect-error we know this exists + event.notification.close(); + + switch (event.action ?? "default") { + case "markReviewed": + if (event.notification.data) { + event.waitUntil( + fetch("/api/reviews/viewed", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": 1, + }, + body: JSON.stringify({ ids: [event.notification.data.id] }), + }), // eslint-disable-line comma-dangle + ); + } + break; + default: + // @ts-expect-error we know this exists + if (event.notification.data) { + const url = event.notification.data.link; + // eslint-disable-next-line no-undef + if (clients.openWindow) { + // eslint-disable-next-line no-undef + event.waitUntil(clients.openWindow(url)); + } + } + } + } +}); diff --git a/sam2-cpu/frigate-dev/web/public/robots.txt b/sam2-cpu/frigate-dev/web/public/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/sam2-cpu/frigate-dev/web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/sam2-cpu/frigate-dev/web/site.webmanifest b/sam2-cpu/frigate-dev/web/site.webmanifest new file mode 100644 index 0000000..7040ce5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/site.webmanifest @@ -0,0 +1,34 @@ +{ + "name": "Frigate", + "short_name": "Frigate", + "start_url": "/BASE_PATH/", + "icons": [ + { + "src": "/BASE_PATH/images/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/BASE_PATH/images/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/BASE_PATH/images/maskable-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/BASE_PATH/images/maskable-badge.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/sam2-cpu/frigate-dev/web/src/App.tsx b/sam2-cpu/frigate-dev/web/src/App.tsx new file mode 100644 index 0000000..b458d9e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/App.tsx @@ -0,0 +1,124 @@ +import Providers from "@/context/providers"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Wrapper from "@/components/Wrapper"; +import Sidebar from "@/components/navigation/Sidebar"; + +import { isDesktop, isMobile } from "react-device-detect"; +import Statusbar from "./components/Statusbar"; +import Bottombar from "./components/navigation/Bottombar"; +import { Suspense, lazy } from "react"; +import { Redirect } from "./components/navigation/Redirect"; +import { cn } from "./lib/utils"; +import { isPWA } from "./utils/isPWA"; +import ProtectedRoute from "@/components/auth/ProtectedRoute"; +import { AuthProvider } from "@/context/auth-context"; +import useSWR from "swr"; +import { FrigateConfig } from "./types/frigateConfig"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; + +const Live = lazy(() => import("@/pages/Live")); +const Events = lazy(() => import("@/pages/Events")); +const Explore = lazy(() => import("@/pages/Explore")); +const Exports = lazy(() => import("@/pages/Exports")); +const ConfigEditor = lazy(() => import("@/pages/ConfigEditor")); +const System = lazy(() => import("@/pages/System")); +const Settings = lazy(() => import("@/pages/Settings")); +const UIPlayground = lazy(() => import("@/pages/UIPlayground")); +const FaceLibrary = lazy(() => import("@/pages/FaceLibrary")); +const Classification = lazy(() => import("@/pages/ClassificationModel")); +const Logs = lazy(() => import("@/pages/Logs")); +const AccessDenied = lazy(() => import("@/pages/AccessDenied")); + +function App() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + return ( + + + + + {config?.safe_mode ? : } + + + + + ); +} + +function DefaultAppView() { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + // Compute required roles for main routes, ensuring we have config first + // to prevent race condition where custom roles are temporarily unavailable + const mainRouteRoles = config?.auth?.roles + ? Object.keys(config.auth.roles) + : undefined; + + return ( +
    + {isDesktop && } + {isDesktop && } + {isMobile && } +
    + + + + ) : ( + + ) + } + > + } /> + } /> + } /> + } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + + +
    +
    + ); +} + +function SafeAppView() { + return ( +
    +
    + + + +
    +
    + ); +} + +export default App; diff --git a/sam2-cpu/frigate-dev/web/src/api/auth-redirect.ts b/sam2-cpu/frigate-dev/web/src/api/auth-redirect.ts new file mode 100644 index 0000000..f19e2b8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/api/auth-redirect.ts @@ -0,0 +1,12 @@ +// Module-level flag to prevent multiple simultaneous redirects +// (eg, when multiple SWR queries fail with 401 at once, or when +// both ApiProvider and ProtectedRoute try to redirect) +let _isRedirectingToLogin = false; + +export function isRedirectingToLogin(): boolean { + return _isRedirectingToLogin; +} + +export function setRedirectingToLogin(value: boolean): void { + _isRedirectingToLogin = value; +} diff --git a/sam2-cpu/frigate-dev/web/src/api/baseUrl.ts b/sam2-cpu/frigate-dev/web/src/api/baseUrl.ts new file mode 100644 index 0000000..fb7faa6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/api/baseUrl.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + baseUrl?: string; + } +} + +export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || "/"}`; diff --git a/sam2-cpu/frigate-dev/web/src/api/index.tsx b/sam2-cpu/frigate-dev/web/src/api/index.tsx new file mode 100644 index 0000000..e5c5617 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/api/index.tsx @@ -0,0 +1,60 @@ +import { baseUrl } from "./baseUrl"; +import { SWRConfig } from "swr"; +import { WsProvider } from "./ws"; +import axios from "axios"; +import { ReactNode } from "react"; +import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect"; + +axios.defaults.baseURL = `${baseUrl}api/`; + +type ApiProviderType = { + children?: ReactNode; + options?: Record; +}; + +export function ApiProvider({ children, options }: ApiProviderType) { + axios.defaults.headers.common = { + "X-CSRF-TOKEN": 1, + "X-CACHE-BYPASS": 1, + }; + + return ( + { + const [path, params] = Array.isArray(key) ? key : [key, undefined]; + return axios.get(path, { params }).then((res) => res.data); + }, + onError: (error, _key) => { + if ( + error.response && + [401, 302, 307].includes(error.response.status) + ) { + // redirect to the login page if not already there + const loginPage = error.response.headers.get("location") ?? "login"; + if (window.location.href !== loginPage && !isRedirectingToLogin()) { + setRedirectingToLogin(true); + window.location.href = loginPage; + } + } + }, + ...options, + }} + > + {children} + + ); +} + +type WsWithConfigType = { + children: ReactNode; +}; + +function WsWithConfig({ children }: WsWithConfigType) { + return {children}; +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useApiHost() { + return baseUrl; +} diff --git a/sam2-cpu/frigate-dev/web/src/api/ws.tsx b/sam2-cpu/frigate-dev/web/src/api/ws.tsx new file mode 100644 index 0000000..44d45ea --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/api/ws.tsx @@ -0,0 +1,653 @@ +import { baseUrl } from "./baseUrl"; +import { useCallback, useEffect, useState } from "react"; +import useWebSocket, { ReadyState } from "react-use-websocket"; +import { + EmbeddingsReindexProgressType, + FrigateCameraState, + FrigateEvent, + FrigateReview, + ModelState, + ToggleableSetting, + TrackedObjectUpdateReturnType, + TriggerStatus, + FrigateAudioDetections, +} from "@/types/ws"; +import { FrigateStats } from "@/types/stats"; +import { createContainer } from "react-tracked"; +import useDeepMemo from "@/hooks/use-deep-memo"; + +type Update = { + topic: string; + payload: unknown; + retain: boolean; +}; + +type WsState = { + [topic: string]: unknown; +}; + +type useValueReturn = [WsState, (update: Update) => void]; + +function useValue(): useValueReturn { + const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; + + // main state + + const [wsState, setWsState] = useState({}); + + useEffect(() => { + const activityValue: string = wsState["camera_activity"] as string; + + if (!activityValue) { + return; + } + + const cameraActivity: { [key: string]: FrigateCameraState } = + JSON.parse(activityValue); + + if (Object.keys(cameraActivity).length === 0) { + return; + } + + const cameraStates: WsState = {}; + + Object.entries(cameraActivity).forEach(([name, state]) => { + const { + record, + detect, + enabled, + snapshots, + audio, + audio_transcription, + notifications, + notifications_suspended, + autotracking, + alerts, + detections, + object_descriptions, + review_descriptions, + } = state["config"]; + cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF"; + cameraStates[`${name}/enabled/state`] = enabled ? "ON" : "OFF"; + cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF"; + cameraStates[`${name}/snapshots/state`] = snapshots ? "ON" : "OFF"; + cameraStates[`${name}/audio/state`] = audio ? "ON" : "OFF"; + cameraStates[`${name}/audio_transcription/state`] = audio_transcription + ? "ON" + : "OFF"; + cameraStates[`${name}/notifications/state`] = notifications + ? "ON" + : "OFF"; + cameraStates[`${name}/notifications/suspended`] = + notifications_suspended || 0; + cameraStates[`${name}/ptz_autotracker/state`] = autotracking + ? "ON" + : "OFF"; + cameraStates[`${name}/review_alerts/state`] = alerts ? "ON" : "OFF"; + cameraStates[`${name}/review_detections/state`] = detections + ? "ON" + : "OFF"; + cameraStates[`${name}/object_descriptions/state`] = object_descriptions + ? "ON" + : "OFF"; + cameraStates[`${name}/review_descriptions/state`] = review_descriptions + ? "ON" + : "OFF"; + }); + + setWsState((prevState) => ({ + ...prevState, + ...cameraStates, + })); + + // we only want this to run initially when the config is loaded + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wsState["camera_activity"]]); + + // ws handler + const { sendJsonMessage, readyState } = useWebSocket(wsUrl, { + onMessage: (event) => { + const data: Update = JSON.parse(event.data); + + if (data) { + setWsState((prevState) => ({ + ...prevState, + [data.topic]: data.payload, + })); + } + }, + onOpen: () => { + sendJsonMessage({ + topic: "onConnect", + message: "", + retain: false, + }); + }, + onClose: () => {}, + shouldReconnect: () => true, + retryOnError: true, + }); + + const setState = useCallback( + (message: Update) => { + if (readyState === ReadyState.OPEN) { + sendJsonMessage({ + topic: message.topic, + payload: message.payload, + retain: message.retain, + }); + } + }, + [readyState, sendJsonMessage], + ); + + return [wsState, setState]; +} + +export const { + Provider: WsProvider, + useTrackedState: useWsState, + useUpdate: useWsUpdate, +} = createContainer(useValue, { defaultState: {}, concurrentMode: true }); + +export function useWs(watchTopic: string, publishTopic: string) { + const state = useWsState(); + const sendJsonMessage = useWsUpdate(); + + const value = { payload: state[watchTopic] || null }; + + const send = useCallback( + (payload: unknown, retain = false) => { + sendJsonMessage({ + topic: publishTopic || watchTopic, + payload, + retain, + }); + }, + [sendJsonMessage, watchTopic, publishTopic], + ); + + return { value, send }; +} + +export function useEnabledState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/enabled/state`, `${camera}/enabled/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useDetectState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/detect/state`, `${camera}/detect/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useRecordingsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useSnapshotsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAudioState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/audio/state`, `${camera}/audio/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAudioTranscriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/audio_transcription/state`, + `${camera}/audio_transcription/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAutotrackingState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/ptz_autotracker/state`, `${camera}/ptz_autotracker/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useAlertsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/review_alerts/state`, `${camera}/review_alerts/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useDetectionsState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/review_detections/state`, + `${camera}/review_detections/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useObjectDescriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/object_descriptions/state`, + `${camera}/object_descriptions/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useReviewDescriptionState(camera: string): { + payload: ToggleableSetting; + send: (payload: ToggleableSetting, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/review_descriptions/state`, + `${camera}/review_descriptions/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function usePtzCommand(camera: string): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/ptz`, `${camera}/ptz`); + return { payload: payload as string, send }; +} + +export function useRestart(): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs("restart", "restart"); + return { payload: payload as string, send }; +} + +export function useFrigateEvents(): { payload: FrigateEvent } { + const { + value: { payload }, + } = useWs("events", ""); + return { payload: JSON.parse(payload as string) }; +} + +export function useAudioDetections(): { payload: FrigateAudioDetections } { + const { + value: { payload }, + } = useWs("audio_detections", ""); + return { payload: JSON.parse(payload as string) }; +} + +export function useFrigateReviews(): FrigateReview { + const { + value: { payload }, + } = useWs("reviews", ""); + return useDeepMemo(JSON.parse(payload as string)); +} + +export function useFrigateStats(): FrigateStats { + const { + value: { payload }, + } = useWs("stats", ""); + return useDeepMemo(JSON.parse(payload as string)); +} + +export function useInitialCameraState( + camera: string, + revalidateOnFocus: boolean, +): { + payload: FrigateCameraState; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs("camera_activity", "onConnect"); + + const data = useDeepMemo(JSON.parse(payload as string)); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("onConnect"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("onConnect"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // only refresh when onRefresh value changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data ? data[camera] : undefined }; +} + +export function useModelState( + model: string, + revalidateOnFocus: boolean = true, +): { payload: ModelState } { + const { + value: { payload }, + send: sendCommand, + } = useWs("model_state", "modelState"); + + const data = useDeepMemo(JSON.parse(payload as string)); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("modelState"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("modelState"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data ? data[model] : undefined }; +} + +export function useEmbeddingsReindexProgress( + revalidateOnFocus: boolean = true, +): { + payload: EmbeddingsReindexProgressType; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs("embeddings_reindex_progress", "embeddingsReindexProgress"); + + const data = useDeepMemo(JSON.parse(payload as string)); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("embeddingsReindexProgress"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("embeddingsReindexProgress"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data }; +} + +export function useAudioTranscriptionProcessState( + revalidateOnFocus: boolean = true, +): { payload: string } { + const { + value: { payload }, + send: sendCommand, + } = useWs("audio_transcription_state", "audioTranscriptionState"); + + const data = useDeepMemo( + payload ? (JSON.parse(payload as string) as string) : "idle", + ); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("audioTranscriptionState"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("audioTranscriptionState"); + } + }; + addEventListener("visibilitychange", listener); + } + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data || "idle" }; +} + +export function useBirdseyeLayout(revalidateOnFocus: boolean = true): { + payload: string; +} { + const { + value: { payload }, + send: sendCommand, + } = useWs("birdseye_layout", "birdseyeLayout"); + + const data = useDeepMemo(JSON.parse(payload as string)); + + useEffect(() => { + let listener = undefined; + if (revalidateOnFocus) { + sendCommand("birdseyeLayout"); + listener = () => { + if (document.visibilityState == "visible") { + sendCommand("birdseyeLayout"); + } + }; + addEventListener("visibilitychange", listener); + } + + return () => { + if (listener) { + removeEventListener("visibilitychange", listener); + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [revalidateOnFocus]); + + return { payload: data }; +} + +export function useMotionActivity(camera: string): { payload: string } { + const { + value: { payload }, + } = useWs(`${camera}/motion`, ""); + return { payload: payload as string }; +} + +export function useAudioActivity(camera: string): { payload: number } { + const { + value: { payload }, + } = useWs(`${camera}/audio/rms`, ""); + return { payload: payload as number }; +} + +export function useAudioLiveTranscription(camera: string): { + payload: string; +} { + const { + value: { payload }, + } = useWs(`${camera}/audio/transcription`, ""); + return { payload: payload as string }; +} + +export function useMotionThreshold(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/motion_threshold/state`, + `${camera}/motion_threshold/set`, + ); + return { payload: payload as string, send }; +} + +export function useMotionContourArea(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/motion_contour_area/state`, + `${camera}/motion_contour_area/set`, + ); + return { payload: payload as string, send }; +} + +export function useImproveContrast(camera: string): { + payload: ToggleableSetting; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/improve_contrast/state`, + `${camera}/improve_contrast/set`, + ); + return { payload: payload as ToggleableSetting, send }; +} + +export function useTrackedObjectUpdate(): { + payload: TrackedObjectUpdateReturnType; +} { + const { + value: { payload }, + } = useWs("tracked_object_update", ""); + const parsed = payload + ? JSON.parse(payload as string) + : { type: "", id: "", camera: "" }; + return { payload: useDeepMemo(parsed) }; +} + +export function useNotifications(camera: string): { + payload: ToggleableSetting; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs(`${camera}/notifications/state`, `${camera}/notifications/set`); + return { payload: payload as ToggleableSetting, send }; +} + +export function useNotificationSuspend(camera: string): { + payload: string; + send: (payload: number, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs( + `${camera}/notifications/suspended`, + `${camera}/notifications/suspend`, + ); + return { payload: payload as string, send }; +} + +export function useNotificationTest(): { + payload: string; + send: (payload: string, retain?: boolean) => void; +} { + const { + value: { payload }, + send, + } = useWs("notification_test", "notification_test"); + return { payload: payload as string, send }; +} + +export function useTriggers(): { payload: TriggerStatus } { + const { + value: { payload }, + } = useWs("triggers", ""); + const parsed = payload + ? JSON.parse(payload as string) + : { name: "", camera: "", event_id: "", type: "", score: 0 }; + return { payload: useDeepMemo(parsed) }; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/Logo.tsx b/sam2-cpu/frigate-dev/web/src/components/Logo.tsx new file mode 100644 index 0000000..ca2897a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/Logo.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils"; + +type LogoProps = { + className?: string; +}; +export default function Logo({ className }: LogoProps) { + return ( + + + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/Statusbar.tsx b/sam2-cpu/frigate-dev/web/src/components/Statusbar.tsx new file mode 100644 index 0000000..ab22a11 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/Statusbar.tsx @@ -0,0 +1,178 @@ +import { useEmbeddingsReindexProgress } from "@/api/ws"; +import { + StatusBarMessagesContext, + StatusMessage, +} from "@/context/statusbar-provider"; +import useStats, { useAutoFrigateStats } from "@/hooks/use-stats"; +import { useContext, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +import { FaCheck } from "react-icons/fa"; +import { IoIosWarning } from "react-icons/io"; +import { MdCircle } from "react-icons/md"; +import { Link } from "react-router-dom"; + +export default function Statusbar() { + const { t } = useTranslation(["views/system"]); + + const { messages, addMessage, clearMessages } = useContext( + StatusBarMessagesContext, + )!; + + const stats = useAutoFrigateStats(); + + const cpuPercent = useMemo(() => { + const systemCpu = stats?.cpu_usages["frigate.full_system"]?.cpu; + + if (!systemCpu || systemCpu == "0.0") { + return null; + } + + return parseInt(systemCpu); + }, [stats]); + + const { potentialProblems } = useStats(stats); + + useEffect(() => { + clearMessages("stats"); + potentialProblems.forEach((problem) => { + addMessage( + "stats", + problem.text, + problem.color, + undefined, + problem.relevantLink, + ); + }); + }, [potentialProblems, addMessage, clearMessages]); + + const { payload: reindexState } = useEmbeddingsReindexProgress(); + + useEffect(() => { + if (reindexState) { + if (reindexState.status == "indexing") { + clearMessages("embeddings-reindex"); + addMessage( + "embeddings-reindex", + t("stats.reindexingEmbeddings", { + processed: Math.floor( + (reindexState.processed_objects / reindexState.total_objects) * + 100, + ), + }), + ); + } + if (reindexState.status === "completed") { + clearMessages("embeddings-reindex"); + } + } + }, [reindexState, addMessage, clearMessages, t]); + + return ( +
    +
    + {cpuPercent && ( + +
    + + CPU {cpuPercent}% +
    + + )} + {Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => { + if (name == "error-gpu") { + return; + } + + let gpuTitle; + switch (name) { + case "amd-vaapi": + gpuTitle = "AMD GPU"; + break; + case "intel-vaapi": + case "intel-qsv": + gpuTitle = "Intel GPU"; + break; + case "rockchip": + gpuTitle = "Rockchip GPU"; + break; + default: + gpuTitle = name; + break; + } + + const gpu = parseInt(stats.gpu); + + if (isNaN(gpu)) { + return; + } + + return ( + + {" "} +
    + + {gpuTitle} {gpu}% +
    + + ); + })} +
    +
    + {Object.entries(messages).length === 0 ? ( +
    + + {t("stats.healthy")} +
    + ) : ( + Object.entries(messages).map(([key, messageArray]) => ( +
    + {messageArray.map(({ text, color, link }: StatusMessage) => { + const message = ( +
    + + {text} +
    + ); + + if (link) { + return ( + + {message} + + ); + } else { + return message; + } + })} +
    + )) + )} +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/Wrapper.tsx b/sam2-cpu/frigate-dev/web/src/components/Wrapper.tsx new file mode 100644 index 0000000..4b1d389 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/Wrapper.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from "react"; + +type TWrapperProps = { + children: ReactNode; +}; + +const Wrapper = ({ children }: TWrapperProps) => { + return
    {children}
    ; +}; + +export default Wrapper; diff --git a/sam2-cpu/frigate-dev/web/src/components/audio/AudioLevelGraph.tsx b/sam2-cpu/frigate-dev/web/src/components/audio/AudioLevelGraph.tsx new file mode 100644 index 0000000..4f0e757 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/audio/AudioLevelGraph.tsx @@ -0,0 +1,165 @@ +import { useEffect, useMemo, useState, useCallback } from "react"; +import { MdCircle } from "react-icons/md"; +import Chart from "react-apexcharts"; +import { useTheme } from "@/context/theme-provider"; +import { useWs } from "@/api/ws"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; + +const GRAPH_COLORS = ["#3b82f6", "#ef4444"]; // RMS, dBFS + +interface AudioLevelGraphProps { + cameraName: string; +} + +export function AudioLevelGraph({ cameraName }: AudioLevelGraphProps) { + const [audioData, setAudioData] = useState< + { timestamp: number; rms: number; dBFS: number }[] + >([]); + const [maxDataPoints] = useState(50); + + // config for time formatting + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const locale = useDateLocale(); + const { t } = useTranslation(["common"]); + + const { + value: { payload: audioRms }, + } = useWs(`${cameraName}/audio/rms`, ""); + const { + value: { payload: audioDBFS }, + } = useWs(`${cameraName}/audio/dBFS`, ""); + + useEffect(() => { + if (typeof audioRms === "number") { + const now = Date.now(); + setAudioData((prev) => { + const next = [ + ...prev, + { + timestamp: now, + rms: audioRms, + dBFS: typeof audioDBFS === "number" ? audioDBFS : 0, + }, + ]; + return next.slice(-maxDataPoints); + }); + } + }, [audioRms, audioDBFS, maxDataPoints]); + + const series = useMemo( + () => [ + { + name: "RMS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.rms })), + }, + { + name: "dBFS", + data: audioData.map((p) => ({ x: p.timestamp, y: p.dBFS })), + }, + ], + [audioData], + ); + + const lastValues = useMemo(() => { + if (!audioData.length) return undefined; + const last = audioData[audioData.length - 1]; + return [last.rms, last.dBFS]; + }, [audioData]); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const formatString = useMemo( + () => + t(`time.formattedTimestampHourMinuteSecond.${timeFormat}`, { + ns: "common", + }), + [t, timeFormat], + ); + + const formatTime = useCallback( + (val: unknown) => { + const seconds = Math.round(Number(val) / 1000); + return formatUnixTimestampToDateTime(seconds, { + timezone: config?.ui.timezone, + date_format: formatString, + locale, + }); + }, + [config?.ui.timezone, formatString, locale], + ); + + const { theme, systemTheme } = useTheme(); + + const options = useMemo(() => { + return { + chart: { + id: `${cameraName}-audio`, + selection: { enabled: false }, + toolbar: { show: false }, + zoom: { enabled: false }, + animations: { enabled: false }, + }, + colors: GRAPH_COLORS, + grid: { + show: true, + borderColor: "#374151", + strokeDashArray: 3, + xaxis: { lines: { show: true } }, + yaxis: { lines: { show: true } }, + }, + legend: { show: false }, + dataLabels: { enabled: false }, + stroke: { width: 1 }, + markers: { size: 0 }, + tooltip: { + theme: systemTheme || theme, + x: { formatter: (val: number) => formatTime(val) }, + y: { formatter: (v: number) => v.toFixed(1) }, + }, + xaxis: { + type: "datetime", + labels: { + rotate: 0, + formatter: formatTime, + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + axisBorder: { show: false }, + axisTicks: { show: false }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.round(val).toString(), + style: { colors: "#6B6B6B", fontSize: "10px" }, + }, + }, + } as ApexCharts.ApexOptions; + }, [cameraName, theme, systemTheme, formatTime]); + + return ( +
    + {lastValues && ( +
    + {["RMS", "dBFS"].map((label, idx) => ( +
    + +
    {label}
    +
    + {lastValues[idx].toFixed(1)} +
    +
    + ))} +
    + )} + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/auth/AuthForm.tsx b/sam2-cpu/frigate-dev/web/src/components/auth/AuthForm.tsx new file mode 100644 index 0000000..8798b5d --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/auth/AuthForm.tsx @@ -0,0 +1,170 @@ +"use client"; + +import * as React from "react"; + +import { baseUrl } from "../../api/baseUrl"; +import { cn } from "@/lib/utils"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import axios, { AxiosError } from "axios"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { AuthContext } from "@/context/auth-context"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { LuExternalLink } from "react-icons/lu"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { Card, CardContent } from "@/components/ui/card"; + +interface UserAuthFormProps extends React.HTMLAttributes {} + +export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + const { t } = useTranslation(["components/auth", "common"]); + const { getLocaleDocUrl } = useDocDomain(); + const [isLoading, setIsLoading] = React.useState(false); + const { login } = React.useContext(AuthContext); + + // need to use local fetcher because useSWR default fetcher is not set up in this context + const fetcher = (path: string) => axios.get(path).then((res) => res.data); + const { data } = useSWR("/auth/first_time_login", fetcher); + const showFirstTimeLink = data?.admin_first_time_login === true; + + const formSchema = z.object({ + user: z.string().min(1, t("form.errors.usernameRequired")), + password: z.string().min(1, t("form.errors.passwordRequired")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { user: "", password: "" }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await axios.post( + "/login", + { + user: values.user, + password: values.password, + }, + { + headers: { "X-CSRF-TOKEN": 1 }, + }, + ); + const profileRes = await axios.get("/profile", { withCredentials: true }); + login({ + username: profileRes.data.username, + role: profileRes.data.role || "viewer", + }); + window.location.href = baseUrl; + } catch (error) { + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + if (err.response?.status === 429) { + toast.error(t("form.errors.rateLimit"), { + position: "top-center", + }); + } else if (err.response?.status === 401) { + toast.error(t("form.errors.loginFailed"), { + position: "top-center", + }); + } else { + toast.error(t("form.errors.unknownError"), { + position: "top-center", + }); + } + } else { + toast.error(t("form.errors.webUnknownError"), { + position: "top-center", + }); + } + + setIsLoading(false); + } + }; + + return ( +
    +
    + + ( + + {t("form.user")} + + + + + )} + /> + ( + + {t("form.password")} + + + + + )} + /> +
    + +
    + + + {showFirstTimeLink && ( + + +

    + {t("form.firstTimeLogin")} +

    + + {t("readTheDocumentation", { ns: "common" })} + + +
    +
    + )} + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/auth/ProtectedRoute.tsx b/sam2-cpu/frigate-dev/web/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..55edc60 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,60 @@ +import { useContext, useEffect } from "react"; +import { Navigate, Outlet } from "react-router-dom"; +import { AuthContext } from "@/context/auth-context"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { + isRedirectingToLogin, + setRedirectingToLogin, +} from "@/api/auth-redirect"; + +export default function ProtectedRoute({ + requiredRoles, +}: { + requiredRoles: string[]; +}) { + const { auth } = useContext(AuthContext); + + // Redirect to login page when not authenticated + // don't use because we need a full page load to reset state + useEffect(() => { + if ( + !auth.isLoading && + auth.isAuthenticated && + !auth.user && + !isRedirectingToLogin() + ) { + setRedirectingToLogin(true); + window.location.href = "/login"; + } + }, [auth.isLoading, auth.isAuthenticated, auth.user]); + + if (auth.isLoading) { + return ( + + ); + } + + // Unauthenticated mode + if (!auth.isAuthenticated) { + return ; + } + + // Authenticated mode (8971): require login + if (!auth.user) { + return ( + + ); + } + + // If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback + // though isAuthenticated should catch this + if (auth.user.role === null) { + return ; + } + + if (!requiredRoles.includes(auth.user.role)) { + return ; + } + + return ; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/button/BlurredIconButton.tsx b/sam2-cpu/frigate-dev/web/src/components/button/BlurredIconButton.tsx new file mode 100644 index 0000000..8fe17f8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/button/BlurredIconButton.tsx @@ -0,0 +1,28 @@ +import React, { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +type BlurredIconButtonProps = React.HTMLAttributes; + +const BlurredIconButton = forwardRef( + ({ className = "", children, ...rest }, ref) => { + return ( +
    +
    +
    + {children} +
    +
    + ); + }, +); + +BlurredIconButton.displayName = "BlurredIconButton"; + +export default BlurredIconButton; diff --git a/sam2-cpu/frigate-dev/web/src/components/button/DownloadVideoButton.tsx b/sam2-cpu/frigate-dev/web/src/components/button/DownloadVideoButton.tsx new file mode 100644 index 0000000..607458a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/button/DownloadVideoButton.tsx @@ -0,0 +1,62 @@ +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { FaDownload } from "react-icons/fa"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { useMemo } from "react"; + +type DownloadVideoButtonProps = { + source: string; + camera: string; + startTime: number; + className?: string; +}; + +export function DownloadVideoButton({ + source, + camera, + startTime, + className, +}: DownloadVideoButtonProps) { + const { t } = useTranslation(["components/input"]); + const { data: config } = useSWR("config"); + const locale = useDateLocale(); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const format = useMemo(() => { + return t(`time.formattedTimestampFilename.${timeFormat}`, { ns: "common" }); + }, [t, timeFormat]); + + const formattedDate = formatUnixTimestampToDateTime(startTime, { + date_format: format, + locale, + }); + const filename = `${camera}_${formattedDate}.mp4`; + + const handleDownloadStart = () => { + toast.success(t("button.downloadVideo.toast.success"), { + position: "top-center", + }); + }; + + return ( +
    + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/camera/AutoUpdatingCameraImage.tsx b/sam2-cpu/frigate-dev/web/src/components/camera/AutoUpdatingCameraImage.tsx new file mode 100644 index 0000000..95d90d9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/camera/AutoUpdatingCameraImage.tsx @@ -0,0 +1,103 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import CameraImage from "./CameraImage"; + +type AutoUpdatingCameraImageProps = { + camera: string; + searchParams?: URLSearchParams; + showFps?: boolean; + className?: string; + cameraClasses?: string; + reloadInterval?: number; + periodicCache?: boolean; +}; + +const MIN_LOAD_TIMEOUT_MS = 200; + +export default function AutoUpdatingCameraImage({ + camera, + searchParams = undefined, + showFps = true, + className, + cameraClasses, + reloadInterval = MIN_LOAD_TIMEOUT_MS, + periodicCache = false, +}: AutoUpdatingCameraImageProps) { + const [key, setKey] = useState(Date.now()); + const [fps, setFps] = useState("0"); + const timeoutRef = useRef(null); + + useEffect(() => { + if (reloadInterval == -1) { + return; + } + + setKey(Date.now()); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reloadInterval]); + + const handleLoad = useCallback(() => { + setIsCached(true); + + if (reloadInterval == -1) { + return; + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + const loadTime = Date.now() - key; + + if (showFps) { + setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1)); + } + + timeoutRef.current = setTimeout( + () => { + setKey(Date.now()); + }, + loadTime > reloadInterval ? 1 : reloadInterval, + ); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, setFps]); + + // periodic cache to reduce loading indicator + + const [isCached, setIsCached] = useState(false); + + const cacheKey = useMemo(() => { + let baseParam = ""; + + if (periodicCache && !isCached) { + const date = new Date(key); + date.setMinutes(date.getMinutes() - (date.getMinutes() % 10), 0, 0); + + baseParam = `store=1&cache=${date.getTime() / 1000}`; + } else { + baseParam = `cache=${key}`; + } + + return `${baseParam}${searchParams ? `&${searchParams}` : ""}`; + }, [isCached, periodicCache, key, searchParams]); + + return ( +
    + + {showFps ? Displaying at {fps}fps : null} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/camera/CameraImage.tsx b/sam2-cpu/frigate-dev/web/src/components/camera/CameraImage.tsx new file mode 100644 index 0000000..716e63f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/camera/CameraImage.tsx @@ -0,0 +1,110 @@ +import { useApiHost } from "@/api"; +import { useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { isDesktop } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { useEnabledState } from "@/api/ws"; + +type CameraImageProps = { + className?: string; + camera: string; + onload?: () => void; + searchParams?: string; +}; + +export default function CameraImage({ + className, + camera, + onload, + searchParams = "", +}: CameraImageProps) { + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + const [imageLoaded, setImageLoaded] = useState(false); + const containerRef = useRef(null); + const imgRef = useRef(null); + + const { name } = config ? config.cameras[camera] : ""; + const { payload: enabledState } = useEnabledState(camera); + const enabled = enabledState ? enabledState === "ON" : true; + + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + + const requestHeight = useMemo(() => { + if (!config || containerHeight == 0) { + return 360; + } + + return Math.min( + config.cameras[camera].detect.height, + Math.round(containerHeight * (isDesktop ? 1.1 : 1.25)), + ); + }, [config, camera, containerHeight]); + + const [isPortraitImage, setIsPortraitImage] = useState(false); + + useEffect(() => { + setImageLoaded(false); + setIsPortraitImage(false); + }, [camera]); + + useEffect(() => { + if (!config || !imgRef.current) { + return; + } + + const newSrc = `${apiHost}api/${name}/latest.webp?height=${requestHeight}${ + searchParams ? `&${searchParams}` : "" + }`; + + if (imgRef.current.src !== newSrc) { + imgRef.current.src = newSrc; + } + }, [apiHost, name, searchParams, requestHeight, config, camera]); + + const handleImageLoad = () => { + if (imgRef.current && containerWidth && containerHeight) { + const { naturalWidth, naturalHeight } = imgRef.current; + setIsPortraitImage( + naturalWidth / naturalHeight < containerWidth / containerHeight, + ); + } + + setImageLoaded(true); + + if (onload) { + onload(); + } + }; + + return ( +
    + {enabled ? ( + + ) : ( +
    + )} + {!imageLoaded && enabled ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/camera/DebugCameraImage.tsx b/sam2-cpu/frigate-dev/web/src/components/camera/DebugCameraImage.tsx new file mode 100644 index 0000000..924eb86 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/camera/DebugCameraImage.tsx @@ -0,0 +1,173 @@ +import { Switch } from "../ui/switch"; +import { Label } from "../ui/label"; +import { CameraConfig } from "@/types/frigateConfig"; +import { Button } from "../ui/button"; +import { LuSettings } from "react-icons/lu"; +import { useCallback, useMemo, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage"; +import { useTranslation } from "react-i18next"; + +type Options = { [key: string]: boolean }; + +const emptyObject = Object.freeze({}); + +type DebugCameraImageProps = { + className?: string; + cameraConfig: CameraConfig; +}; + +export default function DebugCameraImage({ + className, + cameraConfig, +}: DebugCameraImageProps) { + const { t } = useTranslation(["components/camera"]); + const [showSettings, setShowSettings] = useState(false); + const [options, setOptions] = useUserPersistence( + `${cameraConfig?.name}-feed`, + emptyObject, + ); + const handleSetOption = useCallback( + (id: string, value: boolean) => { + const newOptions = { ...options, [id]: value }; + setOptions(newOptions); + }, + [options, setOptions], + ); + const searchParams = useMemo( + () => + new URLSearchParams( + Object.keys(options || {}).reduce((memo, key) => { + //@ts-expect-error we know this is correct + memo.push([key, options[key] === true ? "1" : "0"]); + return memo; + }, []), + ), + [options], + ); + const handleToggleSettings = useCallback(() => { + setShowSettings(!showSettings); + }, [showSettings]); + + return ( +
    + + + {showSettings ? ( + + + {t("debug.options.title")} + + + + + + ) : null} +
    + ); +} + +type DebugSettingsProps = { + handleSetOption: (id: string, value: boolean) => void; + options: Options; +}; + +function DebugSettings({ handleSetOption, options }: DebugSettingsProps) { + const { t } = useTranslation(["components/camera"]); + return ( +
    +
    + { + handleSetOption("bbox", isChecked); + }} + /> + +
    +
    + { + handleSetOption("timestamp", isChecked); + }} + /> + +
    +
    + { + handleSetOption("zones", isChecked); + }} + /> + +
    +
    + { + handleSetOption("mask", isChecked); + }} + /> + +
    +
    + { + handleSetOption("motion", isChecked); + }} + /> + +
    +
    + { + handleSetOption("regions", isChecked); + }} + /> + +
    +
    + { + handleSetOption("paths", isChecked); + }} + /> + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/camera/FriendlyNameLabel.tsx b/sam2-cpu/frigate-dev/web/src/components/camera/FriendlyNameLabel.tsx new file mode 100644 index 0000000..ca09788 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/camera/FriendlyNameLabel.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useZoneFriendlyName } from "@/hooks/use-zone-friendly-name"; + +interface CameraNameLabelProps + extends React.ComponentPropsWithoutRef { + camera?: string | CameraConfig; +} + +interface ZoneNameLabelProps + extends React.ComponentPropsWithoutRef { + zone: string; + camera?: string; +} + +const CameraNameLabel = React.forwardRef< + React.ElementRef, + CameraNameLabelProps +>(({ className, camera, ...props }, ref) => { + const displayName = useCameraFriendlyName(camera); + return ( + + {displayName} + + ); +}); +CameraNameLabel.displayName = LabelPrimitive.Root.displayName; + +const ZoneNameLabel = React.forwardRef< + React.ElementRef, + ZoneNameLabelProps +>(({ className, zone, camera, ...props }, ref) => { + const displayName = useZoneFriendlyName(zone, camera); + return ( + + {displayName} + + ); +}); +ZoneNameLabel.displayName = LabelPrimitive.Root.displayName; + +export { CameraNameLabel, ZoneNameLabel }; diff --git a/sam2-cpu/frigate-dev/web/src/components/camera/ResizingCameraImage.tsx b/sam2-cpu/frigate-dev/web/src/components/camera/ResizingCameraImage.tsx new file mode 100644 index 0000000..fbb5767 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/camera/ResizingCameraImage.tsx @@ -0,0 +1,123 @@ +import { useApiHost } from "@/api"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import useSWR from "swr"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { cn } from "@/lib/utils"; + +type CameraImageProps = { + className?: string; + camera: string; + onload?: (event: Event) => void; + searchParams?: string; + stretch?: boolean; // stretch to fit width + fitAspect?: number; // shrink to fit height +}; + +export default function CameraImage({ + className, + camera, + onload, + searchParams = "", + stretch = false, + fitAspect, +}: CameraImageProps) { + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + const [hasLoaded, setHasLoaded] = useState(false); + const containerRef = useRef(null); + const canvasRef = useRef(null); + const [{ width: containerWidth, height: containerHeight }] = + useResizeObserver(containerRef); + + // Add scrollbar width (when visible) to the available observer width to eliminate screen juddering. + // https://github.com/blakeblackshear/frigate/issues/1657 + let scrollBarWidth = 0; + if (window.innerWidth && document.body.offsetWidth) { + scrollBarWidth = window.innerWidth - document.body.offsetWidth; + } + const availableWidth = scrollBarWidth + ? containerWidth + scrollBarWidth + : containerWidth; + + const { name } = config ? config.cameras[camera] : ""; + const enabled = config ? config.cameras[camera].enabled : "True"; + const { width, height } = config + ? config.cameras[camera].detect + : { width: 1, height: 1 }; + const aspectRatio = width / height; + + const scaledHeight = useMemo(() => { + const scaledHeight = + aspectRatio < (fitAspect ?? 0) + ? Math.floor(containerHeight) + : Math.floor(availableWidth / aspectRatio); + const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height); + + if (finalHeight > 0) { + return finalHeight; + } + + return 100; + }, [ + availableWidth, + aspectRatio, + containerHeight, + fitAspect, + height, + stretch, + ]); + const scaledWidth = useMemo( + () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth), + [scaledHeight, aspectRatio, scrollBarWidth], + ); + + const img = useMemo(() => new Image(), []); + img.onload = useCallback( + (event: Event) => { + setHasLoaded(true); + if (canvasRef.current) { + const ctx = canvasRef.current.getContext("2d"); + ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight); + } + onload && onload(event); + }, + [img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef], + ); + + useEffect(() => { + if (!config || scaledHeight === 0 || !canvasRef.current) { + return; + } + img.src = `${apiHost}api/${name}/latest.webp?height=${scaledHeight}${ + searchParams ? `&${searchParams}` : "" + }`; + }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); + + return ( +
    + {enabled ? ( + + ) : ( +
    Camera is disabled.
    + )} + {!hasLoaded && enabled ? ( +
    + +
    + ) : null} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/AnimatedEventCard.tsx b/sam2-cpu/frigate-dev/web/src/components/card/AnimatedEventCard.tsx new file mode 100644 index 0000000..a67dd83 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/AnimatedEventCard.tsx @@ -0,0 +1,247 @@ +import TimeAgo from "../dynamic/TimeAgo"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; +import { useNavigate } from "react-router-dom"; +import { RecordingStartingPoint } from "@/types/record"; +import axios from "axios"; +import { isCurrentHour } from "@/utils/dateUtil"; +import { useCameraPreviews } from "@/hooks/use-camera-previews"; +import { baseUrl } from "@/api/baseUrl"; +import { VideoPreview } from "../preview/ScrubbablePreview"; +import { useApiHost } from "@/api"; +import { isDesktop, isSafari } from "react-device-detect"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import { Skeleton } from "../ui/skeleton"; +import { Button } from "../ui/button"; +import { FaCircleCheck } from "react-icons/fa6"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +type AnimatedEventCardProps = { + event: ReviewSegment; + selectedGroup?: string; + updateEvents: () => void; +}; +export function AnimatedEventCard({ + event, + selectedGroup, + updateEvents, +}: AnimatedEventCardProps) { + const { t } = useTranslation(["views/events"]); + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + + const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); + + const initialTimeRange = useMemo(() => { + return { + after: Math.round(event.start_time), + before: Math.round(event.end_time || event.start_time + 20), + }; + }, [event]); + + // preview + + const previews = useCameraPreviews(initialTimeRange, { + camera: event.camera, + fetchPreviews: !currentHour, + }); + + const tooltipText = useMemo(() => { + if (event?.data?.metadata?.title) { + return event.data.metadata.title; + } + + return ( + `${[ + ...new Set([ + ...(event.data.objects || []), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter((item) => item !== undefined && !item.includes("-verified")) + .map((text) => text.charAt(0).toUpperCase() + text.substring(1)) + .sort() + .join(", ") + .replaceAll("-verified", "")} ` + t("detected") + ); + }, [event, t]); + + // visibility + + const [windowVisible, setWindowVisible] = useState(true); + const visibilityListener = useCallback(() => { + setWindowVisible(document.visibilityState == "visible"); + }, []); + + useEffect(() => { + addEventListener("visibilitychange", visibilityListener); + + return () => { + removeEventListener("visibilitychange", visibilityListener); + }; + }, [visibilityListener]); + + const [isLoaded, setIsLoaded] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + // interaction + + const navigate = useNavigate(); + const onOpenReview = useCallback(() => { + const url = + selectedGroup && selectedGroup != "default" + ? `review?group=${selectedGroup}` + : "review"; + navigate(url, { + state: { + severity: event.severity, + recording: { + camera: event.camera, + startTime: event.start_time - REVIEW_PADDING, + severity: event.severity, + } as RecordingStartingPoint, + }, + }); + axios.post(`reviews/viewed`, { ids: [event.id] }); + }, [navigate, selectedGroup, event]); + + // image behavior + + const [alertVideos, _, alertVideosLoaded] = useUserPersistence( + "alertVideos", + true, + ); + + const aspectRatio = useMemo(() => { + if ( + !config || + !alertVideos || + !Object.keys(config.cameras).includes(event.camera) + ) { + return 16 / 9; + } + + const detect = config.cameras[event.camera].detect; + return detect.width / detect.height; + }, [alertVideos, config, event]); + + return ( + + +
    setIsHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setIsHovered(false) : undefined} + > + {isHovered && ( + + + + + {t("markAsReviewed")} + + )} + {previews != undefined && alertVideosLoaded && ( +
    { + if (e.button === 1) { + window + .open(`${baseUrl}review?id=${event.id}`, "_blank") + ?.focus(); + } + }} + > + {!alertVideos ? ( + setIsLoaded(true)} + /> + ) : ( + <> + {previews.length ? ( + {}} + setIgnoreClick={() => {}} + isPlayingBack={() => {}} + onTimeUpdate={() => { + if (!isLoaded) { + setIsLoaded(true); + } + }} + windowVisible={windowVisible} + /> + ) : ( + + )} + + )} +
    + )} + {isLoaded && ( +
    +
    + +
    +
    + )} + {!isLoaded && ( + + )} +
    +
    + {tooltipText} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/ClassificationCard.tsx b/sam2-cpu/frigate-dev/web/src/components/card/ClassificationCard.tsx new file mode 100644 index 0000000..4e5f224 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/ClassificationCard.tsx @@ -0,0 +1,422 @@ +import { baseUrl } from "@/api/baseUrl"; +import useContextMenu from "@/hooks/use-contextmenu"; +import { cn } from "@/lib/utils"; +import { + ClassificationItemData, + ClassificationThreshold, +} from "@/types/classification"; +import { Event } from "@/types/event"; +import { forwardRef, useMemo, useRef, useState } from "react"; +import { isDesktop, isIOS, isMobile, isMobileOnly } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import TimeAgo from "../dynamic/TimeAgo"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { LuSearch, LuInfo } from "react-icons/lu"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { useNavigate } from "react-router-dom"; +import { HiSquare2Stack } from "react-icons/hi2"; +import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, + MobilePageTrigger, +} from "../mobile/MobilePage"; + +type ClassificationCardProps = { + className?: string; + imgClassName?: string; + data: ClassificationItemData; + threshold?: ClassificationThreshold; + selected: boolean; + i18nLibrary: string; + showArea?: boolean; + count?: number; + onClick: (data: ClassificationItemData, meta: boolean) => void; + children?: React.ReactNode; +}; +export const ClassificationCard = forwardRef< + HTMLDivElement, + ClassificationCardProps +>(function ClassificationCard( + { + className, + imgClassName, + data, + threshold, + selected, + i18nLibrary, + showArea = true, + count, + onClick, + children, + }, + ref, +) { + const { t } = useTranslation([i18nLibrary]); + const [imageLoaded, setImageLoaded] = useState(false); + + const scoreStatus = useMemo(() => { + if (!data.score || !threshold) { + return "unknown"; + } + + if (data.score >= threshold.recognition) { + return "match"; + } else if (data.score >= threshold.unknown) { + return "potential"; + } else { + return "unknown"; + } + }, [data, threshold]); + + // interaction + + const imgRef = useRef(null); + + useContextMenu(imgRef, () => { + onClick(data, true); + }); + + const imageArea = useMemo(() => { + if (!showArea || imgRef.current == null || !imageLoaded) { + return undefined; + } + + return imgRef.current.naturalWidth * imgRef.current.naturalHeight; + }, [showArea, imageLoaded]); + + return ( +
    { + const isMeta = e.metaKey || e.ctrlKey; + if (isMeta) { + e.stopPropagation(); + } + onClick(data, isMeta); + }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onClick(data, true); + }} + > + setImageLoaded(true)} + src={`${baseUrl}${data.filepath}`} + /> + + {count && ( +
    +
    {count}
    {" "} + +
    + )} + {!count && imageArea != undefined && ( +
    + {t("information.pixels", { ns: "common", area: imageArea })} +
    + )} +
    +
    +
    +
    + {data.name == "unknown" ? t("details.unknown") : data.name} +
    + {data.score != undefined && ( +
    + {Math.round(data.score * 100)}% +
    + )} +
    +
    + {children} +
    +
    +
    + ); +}); + +type GroupedClassificationCardProps = { + group: ClassificationItemData[]; + event?: Event; + threshold?: ClassificationThreshold; + selectedItems: string[]; + i18nLibrary: string; + objectType: string; + noClassificationLabel?: string; + onClick: (data: ClassificationItemData | undefined) => void; + children?: (data: ClassificationItemData) => React.ReactNode; +}; +export function GroupedClassificationCard({ + group, + event, + threshold, + selectedItems, + i18nLibrary, + noClassificationLabel = "details.none", + onClick, + children, +}: GroupedClassificationCardProps) { + const navigate = useNavigate(); + const { t } = useTranslation(["views/explore", i18nLibrary]); + const [detailOpen, setDetailOpen] = useState(false); + + // data + + const bestItem = useMemo(() => { + let best: undefined | ClassificationItemData = undefined; + + group.forEach((item) => { + if (item?.name != undefined && item.name != "none") { + if ( + best?.score == undefined || + (item.score && best.score < item.score) + ) { + best = item; + } + } + }); + + if (!best) { + return group.at(-1); + } + + const bestTyped: ClassificationItemData = best; + return { + ...bestTyped, + name: event + ? event.sub_label && event.sub_label !== "none" + ? event.sub_label + : t(noClassificationLabel) + : bestTyped.name, + score: event?.data?.sub_label_score, + }; + }, [group, event, noClassificationLabel, t]); + + const bestScoreStatus = useMemo(() => { + if (!bestItem?.score || !threshold) { + return "unknown"; + } + + if (bestItem.score >= threshold.recognition) { + return "match"; + } else if (bestItem.score >= threshold.unknown) { + return "potential"; + } else { + return "unknown"; + } + }, [bestItem, threshold]); + + const time = useMemo(() => { + const item = group[0]; + + if (!item?.timestamp) { + return undefined; + } + + return item.timestamp * 1000; + }, [group]); + + if (!bestItem) { + return null; + } + + const Overlay = isDesktop ? Dialog : MobilePage; + const Trigger = isDesktop ? DialogTrigger : MobilePageTrigger; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const ContentTitle = isDesktop ? DialogTitle : MobilePageTitle; + const ContentDescription = isDesktop + ? DialogDescription + : MobilePageDescription; + + return ( + <> + { + if (meta || selectedItems.length > 0) { + onClick(undefined); + } else { + setDetailOpen(true); + } + }} + /> + { + if (!open) { + setDetailOpen(false); + } + }} + > + + e.preventDefault()} + > + <> +
    +
    + + {event?.sub_label && event.sub_label !== "none" + ? event.sub_label + : t(noClassificationLabel)} + {event?.sub_label && event.sub_label !== "none" && ( +
    +
    {`${Math.round((event.data.sub_label_score || 0) * 100)}%`}
    + + + + + + {t("details.scoreInfo", { ns: i18nLibrary })} + + +
    + )} +
    + + {time && ( + + )} + +
    + {isDesktop && ( +
    + {event && ( + + +
    { + navigate(`/explore?event_id=${event.id}`); + }} + > + +
    +
    + + + {t("details.item.button.viewInExplore", { + ns: "views/explore", + })} + + +
    + )} +
    + )} +
    +
    + {group.map((data: ClassificationItemData) => ( +
    + {}} + > + {children?.(data)} + +
    + ))} +
    + +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/EmptyCard.tsx b/sam2-cpu/frigate-dev/web/src/components/card/EmptyCard.tsx new file mode 100644 index 0000000..de93448 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/EmptyCard.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Button } from "../ui/button"; +import Heading from "../ui/heading"; +import { Link } from "react-router-dom"; + +type EmptyCardProps = { + icon: React.ReactNode; + title: string; + description: string; + buttonText?: string; + link?: string; +}; +export function EmptyCard({ + icon, + title, + description, + buttonText, + link, +}: EmptyCardProps) { + return ( +
    + {icon} + {title} +
    {description}
    + {buttonText?.length && ( + + )} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/ExportCard.tsx b/sam2-cpu/frigate-dev/web/src/components/card/ExportCard.tsx new file mode 100644 index 0000000..0215245 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/ExportCard.tsx @@ -0,0 +1,261 @@ +import ActivityIndicator from "../indicators/activity-indicator"; +import { LuTrash } from "react-icons/lu"; +import { Button } from "../ui/button"; +import { useCallback, useState } from "react"; +import { isDesktop, isMobile } from "react-device-detect"; +import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa"; +import { Skeleton } from "../ui/skeleton"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { DeleteClipType, Export } from "@/types/export"; +import { MdEditSquare } from "react-icons/md"; +import { baseUrl } from "@/api/baseUrl"; +import { cn } from "@/lib/utils"; +import { shareOrCopy } from "@/utils/browserUtil"; +import { useTranslation } from "react-i18next"; +import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; +import BlurredIconButton from "../button/BlurredIconButton"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { useIsAdmin } from "@/hooks/use-is-admin"; + +type ExportProps = { + className: string; + exportedRecording: Export; + onSelect: (selected: Export) => void; + onRename: (original: string, update: string) => void; + onDelete: ({ file, exportName }: DeleteClipType) => void; +}; + +export default function ExportCard({ + className, + exportedRecording, + onSelect, + onRename, + onDelete, +}: ExportProps) { + const { t } = useTranslation(["views/exports"]); + const isAdmin = useIsAdmin(); + const [hovered, setHovered] = useState(false); + const [loading, setLoading] = useState( + exportedRecording.thumb_path.length > 0, + ); + + // editing name + + const [editName, setEditName] = useState<{ + original: string; + update?: string; + }>(); + + const submitRename = useCallback(() => { + if (editName == undefined) { + return; + } + + onRename(exportedRecording.id, editName.update ?? ""); + setEditName(undefined); + }, [editName, exportedRecording, onRename, setEditName]); + + useKeyboardListener( + editName != undefined ? ["Enter"] : [], + (key, modifiers) => { + if ( + key == "Enter" && + modifiers.down && + !modifiers.repeat && + editName && + (editName.update?.length ?? 0) > 0 + ) { + submitRename(); + return true; + } + + return false; + }, + ); + + return ( + <> + { + if (!open) { + setEditName(undefined); + } + }} + > + { + if (isMobile) { + e.preventDefault(); + } + }} + > + {t("editExport.title")} + {t("editExport.desc")} + {editName && ( + <> + + setEditName({ + original: editName.original ?? "", + update: e.target.value, + }) + } + /> + + + + + )} + + + +
    setHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setHovered(false) : undefined} + onClick={isDesktop ? undefined : () => setHovered(!hovered)} + > + {exportedRecording.in_progress ? ( + + ) : ( + <> + {exportedRecording.thumb_path.length > 0 ? ( + setLoading(false)} + /> + ) : ( +
    + )} + + )} + {hovered && ( + <> +
    +
    +
    + {!exportedRecording.in_progress && ( + + + + shareOrCopy( + `${baseUrl}export?id=${exportedRecording.id}`, + exportedRecording.name.replaceAll("_", " "), + ) + } + > + + + + {t("tooltip.shareExport")} + + )} + {!exportedRecording.in_progress && ( + + + + + + + + + {t("tooltip.downloadVideo")} + + + + )} + {isAdmin && !exportedRecording.in_progress && ( + + + + setEditName({ + original: exportedRecording.name, + update: undefined, + }) + } + > + + + + {t("tooltip.editName")} + + )} + {isAdmin && ( + + + + onDelete({ + file: exportedRecording.id, + exportName: exportedRecording.name, + }) + } + > + + + + {t("tooltip.deleteExport")} + + )} +
    +
    + + {!exportedRecording.in_progress && ( + + )} + + )} + {loading && ( + + )} + +
    + {exportedRecording.name.replaceAll("_", " ")} +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/ReviewCard.tsx b/sam2-cpu/frigate-dev/web/src/components/card/ReviewCard.tsx new file mode 100644 index 0000000..e8d8121 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/ReviewCard.tsx @@ -0,0 +1,379 @@ +import { baseUrl } from "@/api/baseUrl"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; +import { getIconForLabel } from "@/utils/iconUtil"; +import { isDesktop, isIOS, isSafari } from "react-device-detect"; +import useSWR from "swr"; +import TimeAgo from "../dynamic/TimeAgo"; +import { useCallback, useRef, useState } from "react"; +import useImageLoaded from "@/hooks/use-image-loaded"; +import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; +import { FaCompactDisc } from "react-icons/fa"; +import { FaCircleCheck } from "react-icons/fa6"; +import { HiTrash } from "react-icons/hi"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "../ui/context-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { Drawer, DrawerContent } from "../ui/drawer"; +import axios from "axios"; +import { toast } from "sonner"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { capitalizeFirstLetter } from "@/utils/stringUtil"; +import { Button, buttonVariants } from "../ui/button"; +import { Trans, useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import { LuCircle } from "react-icons/lu"; +import { MdAutoAwesome } from "react-icons/md"; + +type ReviewCardProps = { + event: ReviewSegment; + activeReviewItem?: ReviewSegment; + onClick?: () => void; +}; +export default function ReviewCard({ + event, + activeReviewItem, + onClick, +}: ReviewCardProps) { + const { t } = useTranslation(["components/dialog"]); + const { data: config } = useSWR("config"); + const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + const formattedDate = useFormattedTimestamp( + event.start_time, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestampHourMinute.24hour", { ns: "common" }) + : t("time.formattedTimestampHourMinute.12hour", { ns: "common" }), + config?.ui.timezone, + ); + + const [optionsOpen, setOptionsOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const bypassDialogRef = useRef(false); + + const onMarkAsReviewed = useCallback(async () => { + await axios.post(`reviews/viewed`, { ids: [event.id] }); + event.has_been_reviewed = true; + setOptionsOpen(false); + }, [event]); + + const onExport = useCallback(async () => { + const endTime = event.end_time + ? event.end_time + REVIEW_PADDING + : Date.now() / 1000; + + axios + .post( + `export/${event.camera}/start/${event.start_time + REVIEW_PADDING}/end/${endTime}`, + { playback: "realtime" }, + ) + .then((response) => { + if (response.status == 200) { + toast.success(t("export.toast.success"), { + position: "top-center", + action: ( + + + + ), + }); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || error.message || "Unknown error"; + toast.error(t("export.toast.error.failed", { error: errorMessage }), { + position: "top-center", + }); + }); + setOptionsOpen(false); + }, [event, t]); + + const onDelete = useCallback(async () => { + await axios.post(`reviews/delete`, { ids: [event.id] }); + event.id = ""; + setOptionsOpen(false); + }, [event]); + + useKeyboardListener(["Shift"], (_, modifiers) => { + bypassDialogRef.current = modifiers.shift; + return false; + }); + + const handleDelete = useCallback(() => { + if (bypassDialogRef.current) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialogRef, onDelete]); + + const content = ( +
    { + e.preventDefault(); + setOptionsOpen(true); + } + } + > + + { + onImgLoad(); + }} + /> +
    + + +
    + +
    + {event.data.objects.map((object, idx) => ( +
    + {getIconForLabel(object, "size-3 text-white")} +
    + ))} + {event.data.audio.map((audio, idx) => ( +
    + {getIconForLabel(audio, "size-3 text-white")} +
    + ))} +
    +
    {formattedDate}
    +
    +
    + + {[ + ...new Set([ + ...(event.data.objects || []), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => capitalizeFirstLetter(text)) + .sort() + .join(", ") + .replaceAll("-verified", "")} + +
    + +
    + {event.data.metadata?.title && ( +
    + + + {event.data.metadata.title} + +
    + )} +
    + ); + + if (event.id == "") { + return; + } + + if (isDesktop) { + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("recording.confirmDelete.title")} + + + + + recording.confirmDelete.title + + + + setOptionsOpen(false)}> + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + + {content} + + +
    + +
    + {t("recording.button.export")} +
    +
    +
    + {!event.has_been_reviewed && ( + +
    + +
    + {t("recording.button.markAsReviewed")} +
    +
    +
    + )} + +
    + +
    + {bypassDialogRef.current + ? t("recording.button.deleteNow") + : t("button.delete", { ns: "common" })} +
    +
    +
    +
    +
    + + ); + } + + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("recording.confirmDelete.title")} + + + + + recording.confirmDelete.desc.selected + + + + setOptionsOpen(false)}> + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + + {content} + +
    + +
    {t("recording.button.export")}
    +
    + {!event.has_been_reviewed && ( +
    + +
    + {t("recording.button.markAsReviewed")} +
    +
    + )} +
    + +
    + {bypassDialogRef.current + ? t("recording.button.deleteNow") + : t("button.delete", { ns: "common" })} +
    +
    +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnail.tsx b/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnail.tsx new file mode 100644 index 0000000..0b82475 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnail.tsx @@ -0,0 +1,168 @@ +import { useCallback, useMemo } from "react"; +import { useApiHost } from "@/api"; +import { getIconForLabel } from "@/utils/iconUtil"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { isIOS, isSafari } from "react-device-detect"; +import Chip from "@/components/indicators/Chip"; +import useImageLoaded from "@/hooks/use-image-loaded"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; +import { SearchResult } from "@/types/search"; +import { cn } from "@/lib/utils"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import useContextMenu from "@/hooks/use-contextmenu"; +import { getTranslatedLabel } from "@/utils/i18n"; + +type SearchThumbnailProps = { + searchResult: SearchResult; + onClick: (searchResult: SearchResult, ctrl: boolean, detail: boolean) => void; +}; + +export default function SearchThumbnail({ + searchResult, + onClick, +}: SearchThumbnailProps) { + const apiHost = useApiHost(); + const { data: config } = useSWR("config"); + const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + + // interactions + + useContextMenu(imgRef, () => { + onClick(searchResult, true, false); + }); + + const handleOnClick = useCallback( + (e: React.MouseEvent) => { + if (e.metaKey) { + e.stopPropagation(); + onClick(searchResult, true, false); + } + }, + [searchResult, onClick], + ); + + const hasRecognizedPlate = useMemo( + () => (searchResult.data.recognized_license_plate?.length || 0) > 0, + [searchResult], + ); + + const objectLabel = useMemo(() => { + if (!config) { + return searchResult.label; + } + + if (!searchResult.sub_label) { + return `${searchResult.label}${hasRecognizedPlate ? "-plate" : ""}`; + } + + if ( + config.model.attributes_map[searchResult.label]?.includes( + searchResult.sub_label, + ) + ) { + return searchResult.sub_label; + } + + return `${searchResult.label}-verified`; + }, [config, hasRecognizedPlate, searchResult]); + + const objectDetail = useMemo(() => { + if (!config) { + return undefined; + } + + if (!searchResult.sub_label) { + if (hasRecognizedPlate) { + return `(${searchResult.data.recognized_license_plate})`; + } + + return undefined; + } + + if ( + config.model.attributes_map[searchResult.label]?.includes( + searchResult.sub_label, + ) + ) { + return ""; + } + + return `(${searchResult.sub_label})`; + }, [config, hasRecognizedPlate, searchResult]); + + return ( +
    onClick(searchResult, false, true)} + > + +
    + { + onImgLoad(); + }} + /> + +
    + +
    + +
    + onClick(searchResult, false, true)} + > + {getIconForLabel(objectLabel, "size-3 text-white")} + {Math.floor( + (searchResult.data.score ?? + searchResult.data.top_score ?? + searchResult.top_score) * 100, + )} + % {objectDetail} + +
    +
    +
    + + + {[searchResult.sub_label ?? objectLabel] + .filter( + (item) => item !== undefined && !item.includes("-verified"), + ) + .map((text) => + getTranslatedLabel(text, searchResult.data.type), + ) + .sort() + .join(", ") + .replaceAll("-verified", "")} + + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnailFooter.tsx b/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnailFooter.tsx new file mode 100644 index 0000000..808ad28 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/card/SearchThumbnailFooter.tsx @@ -0,0 +1,68 @@ +import TimeAgo from "../dynamic/TimeAgo"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useFormattedTimestamp } from "@/hooks/use-date-utils"; +import { SearchResult } from "@/types/search"; +import ActivityIndicator from "../indicators/activity-indicator"; +import SearchResultActions from "../menu/SearchResultActions"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +type SearchThumbnailProps = { + searchResult: SearchResult; + columns: number; + findSimilar: () => void; + refreshResults: () => void; + showTrackingDetails: () => void; + addTrigger: () => void; +}; + +export default function SearchThumbnailFooter({ + searchResult, + columns, + findSimilar, + refreshResults, + showTrackingDetails, + addTrigger, +}: SearchThumbnailProps) { + const { t } = useTranslation(["views/search"]); + const { data: config } = useSWR("config"); + + // date + const formattedDate = useFormattedTimestamp( + searchResult.start_time, + config?.ui.time_format == "24hour" + ? t("time.formattedTimestampMonthDayHourMinute.24hour", { ns: "common" }) + : t("time.formattedTimestampMonthDayHourMinute.12hour", { ns: "common" }), + config?.ui.timezone, + ); + + return ( +
    4 && "items-start sm:flex-col lg:flex-row lg:items-center", + )} + > +
    + {searchResult.end_time ? ( + + ) : ( +
    + +
    + )} + {formattedDate} +
    +
    + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelEditDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelEditDialog.tsx new file mode 100644 index 0000000..a3ff2df --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelEditDialog.tsx @@ -0,0 +1,529 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + CustomClassificationModelConfig, + FrigateConfig, +} from "@/types/frigateConfig"; +import { ClassificationDatasetResponse } from "@/types/classification"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { LuPlus, LuX } from "react-icons/lu"; +import { toast } from "sonner"; +import useSWR, { mutate } from "swr"; +import { z } from "zod"; + +type ClassificationModelEditDialogProps = { + open: boolean; + model: CustomClassificationModelConfig; + onClose: () => void; + onSuccess: () => void; +}; + +type ObjectClassificationType = "sub_label" | "attribute"; + +type ObjectFormData = { + objectLabel: string; + objectType: ObjectClassificationType; +}; + +type StateFormData = { + classes: string[]; +}; + +export default function ClassificationModelEditDialog({ + open, + model, + onClose, + onSuccess, +}: ClassificationModelEditDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const [isSaving, setIsSaving] = useState(false); + + const isStateModel = model.state_config !== undefined; + const isObjectModel = model.object_config !== undefined; + + const objectLabels = useMemo(() => { + if (!config) return []; + + const labels = new Set(); + + Object.values(config.cameras).forEach((cameraConfig) => { + if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) { + return; + } + + cameraConfig.objects.track.forEach((label) => { + if (!config.model.all_attributes.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels].sort(); + }, [config]); + + // Define form schema based on model type + const formSchema = useMemo(() => { + if (isObjectModel) { + return z.object({ + objectLabel: z + .string() + .min(1, t("wizard.step1.errors.objectLabelRequired")), + objectType: z.enum(["sub_label", "attribute"]), + }); + } else { + // State model + return z.object({ + classes: z + .array(z.string()) + .min(1, t("wizard.step1.errors.classRequired")) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 2; + }, + { message: t("wizard.step1.errors.stateRequiresTwoClasses") }, + ) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + const unique = new Set(nonEmpty.map((c) => c.toLowerCase())); + return unique.size === nonEmpty.length; + }, + { message: t("wizard.step1.errors.classesUnique") }, + ), + }); + } + }, [isObjectModel, t]); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: isObjectModel + ? ({ + objectLabel: model.object_config?.objects?.[0] || "", + objectType: + (model.object_config + ?.classification_type as ObjectClassificationType) || "sub_label", + } as ObjectFormData) + : ({ + classes: [""], // Will be populated from dataset + } as StateFormData), + mode: "onChange", + }); + + // Fetch dataset to get current classes for state models + const { data: dataset } = useSWR( + isStateModel ? `classification/${model.name}/dataset` : null, + { + revalidateOnFocus: false, + }, + ); + + // Update form with classes from dataset when loaded + useEffect(() => { + if (isStateModel && dataset?.categories) { + const classes = Object.keys(dataset.categories).filter( + (key) => key !== "none", + ); + if (classes.length > 0) { + (form as ReturnType>).setValue( + "classes", + classes, + ); + } + } + }, [dataset, isStateModel, form]); + + const watchedClasses = isStateModel + ? (form as ReturnType>).watch("classes") + : undefined; + const watchedObjectType = isObjectModel + ? (form as ReturnType>).watch("objectType") + : undefined; + + const handleAddClass = useCallback(() => { + const currentClasses = ( + form as ReturnType> + ).getValues("classes"); + (form as ReturnType>).setValue( + "classes", + [...currentClasses, ""], + { + shouldValidate: true, + }, + ); + }, [form]); + + const handleRemoveClass = useCallback( + (index: number) => { + const currentClasses = ( + form as ReturnType> + ).getValues("classes"); + const newClasses = currentClasses.filter((_, i) => i !== index); + + // Ensure at least one field remains (even if empty) + if (newClasses.length === 0) { + (form as ReturnType>).setValue( + "classes", + [""], + { shouldValidate: true }, + ); + } else { + (form as ReturnType>).setValue( + "classes", + newClasses, + { shouldValidate: true }, + ); + } + }, + [form], + ); + + const onSubmit = useCallback( + async (data: ObjectFormData | StateFormData) => { + setIsSaving(true); + try { + if (isObjectModel) { + const objectData = data as ObjectFormData; + + // Update the config + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${model.name}`, + config_data: { + classification: { + custom: { + [model.name]: { + enabled: model.enabled, + name: model.name, + threshold: model.threshold, + object_config: { + objects: [objectData.objectLabel], + classification_type: objectData.objectType, + }, + }, + }, + }, + }, + }); + + toast.success(t("toast.success.updatedModel"), { + position: "top-center", + }); + } else { + const stateData = data as StateFormData; + const newClasses = stateData.classes.filter( + (c) => c.trim().length > 0, + ); + const oldClasses = dataset?.categories + ? Object.keys(dataset.categories).filter((key) => key !== "none") + : []; + + const renameMap = new Map(); + const maxLength = Math.max(oldClasses.length, newClasses.length); + + for (let i = 0; i < maxLength; i++) { + const oldClass = oldClasses[i]; + const newClass = newClasses[i]; + + if (oldClass && newClass && oldClass !== newClass) { + renameMap.set(oldClass, newClass); + } + } + + const renamePromises = Array.from(renameMap.entries()).map( + async ([oldName, newName]) => { + try { + await axios.put( + `/classification/${model.name}/dataset/${oldName}/rename`, + { + new_category: newName, + }, + ); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + }; + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + throw new Error( + `Failed to rename ${oldName} to ${newName}: ${errorMessage}`, + ); + } + }, + ); + + if (renamePromises.length > 0) { + await Promise.all(renamePromises); + await mutate(`classification/${model.name}/dataset`); + toast.success(t("toast.success.updatedModel"), { + position: "top-center", + }); + } else { + toast.info(t("edit.stateClassesInfo"), { + position: "top-center", + }); + } + } + + onSuccess(); + onClose(); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + error.message || + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.updateModelFailed", { errorMessage }), { + position: "top-center", + }); + } finally { + setIsSaving(false); + } + }, + [isObjectModel, model, dataset, t, onSuccess, onClose], + ); + + const handleCancel = useCallback(() => { + form.reset(); + onClose(); + }, [form, onClose]); + + return ( + !open && handleCancel()}> + + + {t("edit.title")} + + {isStateModel + ? t("edit.descriptionState") + : t("edit.descriptionObject")} + + + +
    +
    + + {isObjectModel && ( + <> + ( + + + {t("wizard.step1.objectLabel")} + + + + + )} + /> + + ( + + + {t("wizard.step1.classificationType")} + + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + )} + + {isStateModel && ( +
    +
    + + {t("wizard.step1.states")} + + +
    +
    + {watchedClasses?.map((_: string, index: number) => ( + >) + .control + } + name={`classes.${index}` as const} + render={({ field }) => ( + + +
    + + {watchedClasses && + watchedClasses.length > 1 && ( + + )} +
    +
    +
    + )} + /> + ))} +
    + {isStateModel && + "classes" in form.formState.errors && + form.formState.errors.classes && ( +

    + {form.formState.errors.classes.message} +

    + )} +
    + )} + +
    + + +
    + + +
    +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelWizardDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelWizardDialog.tsx new file mode 100644 index 0000000..06bf1f8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/classification/ClassificationModelWizardDialog.tsx @@ -0,0 +1,218 @@ +import { useTranslation } from "react-i18next"; +import StepIndicator from "../indicators/StepIndicator"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { useReducer, useMemo } from "react"; +import Step1NameAndDefine, { Step1FormData } from "./wizard/Step1NameAndDefine"; +import Step2StateArea, { Step2FormData } from "./wizard/Step2StateArea"; +import Step3ChooseExamples, { + Step3FormData, +} from "./wizard/Step3ChooseExamples"; +import { cn } from "@/lib/utils"; +import { isDesktop } from "react-device-detect"; +import axios from "axios"; + +const OBJECT_STEPS = [ + "wizard.steps.nameAndDefine", + "wizard.steps.chooseExamples", +]; + +const STATE_STEPS = [ + "wizard.steps.nameAndDefine", + "wizard.steps.stateArea", + "wizard.steps.chooseExamples", +]; + +type ClassificationModelWizardDialogProps = { + open: boolean; + onClose: () => void; + defaultModelType?: "state" | "object"; +}; + +type WizardState = { + currentStep: number; + step1Data?: Step1FormData; + step2Data?: Step2FormData; + step3Data?: Step3FormData; +}; + +type WizardAction = + | { type: "NEXT_STEP"; payload?: Partial } + | { type: "PREVIOUS_STEP" } + | { type: "SET_STEP_1"; payload: Step1FormData } + | { type: "SET_STEP_2"; payload: Step2FormData } + | { type: "SET_STEP_3"; payload: Step3FormData } + | { type: "RESET" }; + +const initialState: WizardState = { + currentStep: 0, +}; + +function wizardReducer(state: WizardState, action: WizardAction): WizardState { + switch (action.type) { + case "SET_STEP_1": + return { + ...state, + step1Data: action.payload, + currentStep: 1, + }; + case "SET_STEP_2": + return { + ...state, + step2Data: action.payload, + currentStep: 2, + }; + case "SET_STEP_3": + return { + ...state, + step3Data: action.payload, + currentStep: 3, + }; + case "NEXT_STEP": + return { + ...state, + ...action.payload, + currentStep: state.currentStep + 1, + }; + case "PREVIOUS_STEP": + return { + ...state, + currentStep: Math.max(0, state.currentStep - 1), + }; + case "RESET": + return initialState; + default: + return state; + } +} + +export default function ClassificationModelWizardDialog({ + open, + onClose, + defaultModelType, +}: ClassificationModelWizardDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + + const [wizardState, dispatch] = useReducer(wizardReducer, initialState); + + const steps = useMemo(() => { + if (!wizardState.step1Data) { + return OBJECT_STEPS; + } + return wizardState.step1Data.modelType === "state" + ? STATE_STEPS + : OBJECT_STEPS; + }, [wizardState.step1Data]); + + const handleStep1Next = (data: Step1FormData) => { + dispatch({ type: "SET_STEP_1", payload: data }); + }; + + const handleStep2Next = (data: Step2FormData) => { + dispatch({ type: "SET_STEP_2", payload: data }); + }; + + const handleBack = () => { + dispatch({ type: "PREVIOUS_STEP" }); + }; + + const handleCancel = async () => { + // Clean up any generated training images if we're cancelling from Step 3 + if (wizardState.step1Data && wizardState.step3Data?.examplesGenerated) { + try { + await axios.delete( + `/classification/${wizardState.step1Data.modelName}`, + ); + } catch (error) { + // Silently fail - user is already cancelling + } + } + + dispatch({ type: "RESET" }); + onClose(); + }; + + return ( + { + if (!open) { + handleCancel(); + } + }} + > + 0 && + "max-h-[90%] max-w-[70%] overflow-y-auto xl:max-h-[80%]", + )} + onInteractOutside={(e) => { + e.preventDefault(); + }} + > + + + {t("wizard.title")} + {wizardState.currentStep === 0 && ( + + {t("wizard.step1.description")} + + )} + {wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "state" && ( + + {t("wizard.step2.description")} + + )} + + +
    + {wizardState.currentStep === 0 && ( + + )} + {wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "state" && ( + + )} + {((wizardState.currentStep === 2 && + wizardState.step1Data?.modelType === "state") || + (wizardState.currentStep === 1 && + wizardState.step1Data?.modelType === "object")) && + wizardState.step1Data && ( + + )} +
    +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step1NameAndDefine.tsx b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step1NameAndDefine.tsx new file mode 100644 index 0000000..d5ee430 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step1NameAndDefine.tsx @@ -0,0 +1,500 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useMemo } from "react"; +import { LuX, LuPlus, LuInfo, LuExternalLink } from "react-icons/lu"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export type ModelType = "state" | "object"; +export type ObjectClassificationType = "sub_label" | "attribute"; + +export type Step1FormData = { + modelName: string; + modelType: ModelType; + objectLabel?: string; + objectType?: ObjectClassificationType; + classes: string[]; +}; + +type Step1NameAndDefineProps = { + initialData?: Partial; + defaultModelType?: "state" | "object"; + onNext: (data: Step1FormData) => void; + onCancel: () => void; +}; + +export default function Step1NameAndDefine({ + initialData, + defaultModelType, + onNext, + onCancel, +}: Step1NameAndDefineProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const { getLocaleDocUrl } = useDocDomain(); + + const objectLabels = useMemo(() => { + if (!config) return []; + + const labels = new Set(); + + Object.values(config.cameras).forEach((cameraConfig) => { + if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) { + return; + } + + cameraConfig.objects.track.forEach((label) => { + if (!config.model.all_attributes.includes(label)) { + labels.add(label); + } + }); + }); + + return [...labels].sort(); + }, [config]); + + const step1FormData = z + .object({ + modelName: z + .string() + .min(1, t("wizard.step1.errors.nameRequired")) + .max(64, t("wizard.step1.errors.nameLength")) + .refine((value) => !/^\d+$/.test(value), { + message: t("wizard.step1.errors.nameOnlyNumbers"), + }), + modelType: z.enum(["state", "object"]), + objectLabel: z.string().optional(), + objectType: z.enum(["sub_label", "attribute"]).optional(), + classes: z + .array(z.string()) + .min(1, t("wizard.step1.errors.classRequired")) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 1; + }, + { message: t("wizard.step1.errors.classRequired") }, + ) + .refine( + (classes) => { + const nonEmpty = classes.filter((c) => c.trim().length > 0); + const unique = new Set(nonEmpty.map((c) => c.toLowerCase())); + return unique.size === nonEmpty.length; + }, + { message: t("wizard.step1.errors.classesUnique") }, + ), + }) + .refine( + (data) => { + // State models require at least 2 classes + if (data.modelType === "state") { + const nonEmpty = data.classes.filter((c) => c.trim().length > 0); + return nonEmpty.length >= 2; + } + return true; + }, + { + message: t("wizard.step1.errors.stateRequiresTwoClasses"), + path: ["classes"], + }, + ) + .refine( + (data) => { + if (data.modelType === "object") { + return data.objectLabel !== undefined && data.objectLabel !== ""; + } + return true; + }, + { + message: t("wizard.step1.errors.objectLabelRequired"), + path: ["objectLabel"], + }, + ) + .refine( + (data) => { + if (data.modelType === "object") { + return data.objectType !== undefined; + } + return true; + }, + { + message: t("wizard.step1.errors.objectTypeRequired"), + path: ["objectType"], + }, + ); + + const form = useForm>({ + resolver: zodResolver(step1FormData), + defaultValues: { + modelName: initialData?.modelName || "", + modelType: initialData?.modelType || defaultModelType || "state", + objectLabel: initialData?.objectLabel, + objectType: initialData?.objectType || "sub_label", + classes: initialData?.classes?.length ? initialData.classes : [""], + }, + mode: "onChange", + }); + + const watchedClasses = form.watch("classes"); + const watchedModelType = form.watch("modelType"); + const watchedObjectType = form.watch("objectType"); + + const handleAddClass = () => { + const currentClasses = form.getValues("classes"); + form.setValue("classes", [...currentClasses, ""], { shouldValidate: true }); + }; + + const handleRemoveClass = (index: number) => { + const currentClasses = form.getValues("classes"); + const newClasses = currentClasses.filter((_, i) => i !== index); + + // Ensure at least one field remains (even if empty) + if (newClasses.length === 0) { + form.setValue("classes", [""], { shouldValidate: true }); + } else { + form.setValue("classes", newClasses, { shouldValidate: true }); + } + }; + + const onSubmit = (data: z.infer) => { + // Filter out empty classes + const filteredClasses = data.classes.filter((c) => c.trim().length > 0); + onNext({ + ...data, + classes: filteredClasses, + }); + }; + + return ( +
    +
    + + ( + + + {t("wizard.step1.name")} + + + + + + + )} + /> + + ( + + + {t("wizard.step1.type")} + + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + {watchedModelType === "object" && ( + <> + ( + + + {t("wizard.step1.objectLabel")} + + + + + )} + /> + + ( + +
    + + {t("wizard.step1.classificationType")} + + + + + + +
    +
    + {t("wizard.step1.classificationTypeDesc")} +
    + +
    +
    +
    +
    + + +
    + + +
    +
    + + +
    +
    +
    + +
    + )} + /> + + )} + +
    +
    +
    + + {watchedModelType === "state" + ? t("wizard.step1.states") + : t("wizard.step1.classes")} + + + + + + +
    +
    + {watchedModelType === "state" + ? t("wizard.step1.classesStateDesc") + : t("wizard.step1.classesObjectDesc")} +
    + +
    +
    +
    +
    + +
    +
    + {watchedClasses.map((_, index) => ( + ( + + +
    + + {watchedClasses.length > 1 && ( + + )} +
    +
    +
    + )} + /> + ))} +
    + {form.formState.errors.classes && ( +

    + {form.formState.errors.classes.message} +

    + )} +
    + + + +
    + + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step2StateArea.tsx b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step2StateArea.tsx new file mode 100644 index 0000000..38c2fca --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step2StateArea.tsx @@ -0,0 +1,479 @@ +import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; +import { useState, useMemo, useRef, useCallback, useEffect } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { LuX, LuPlus } from "react-icons/lu"; +import { Stage, Layer, Rect, Transformer } from "react-konva"; +import Konva from "konva"; +import { useResizeObserver } from "@/hooks/resize-observer"; +import { useApiHost } from "@/api"; +import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; +import Heading from "@/components/ui/heading"; +import { isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; + +export type CameraAreaConfig = { + camera: string; + crop: [number, number, number, number]; +}; + +export type Step2FormData = { + cameraAreas: CameraAreaConfig[]; +}; + +type Step2StateAreaProps = { + initialData?: Partial; + onNext: (data: Step2FormData) => void; + onBack: () => void; +}; + +export default function Step2StateArea({ + initialData, + onNext, + onBack, +}: Step2StateAreaProps) { + const { t } = useTranslation(["views/classificationModel"]); + const { data: config } = useSWR("config"); + const apiHost = useApiHost(); + + const [cameraAreas, setCameraAreas] = useState( + initialData?.cameraAreas || [], + ); + const [selectedCameraIndex, setSelectedCameraIndex] = useState(0); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [imageLoaded, setImageLoaded] = useState(false); + + const containerRef = useRef(null); + const imageRef = useRef(null); + const stageRef = useRef(null); + const rectRef = useRef(null); + const transformerRef = useRef(null); + + const [{ width: containerWidth }] = useResizeObserver(containerRef); + + const availableCameras = useMemo(() => { + if (!config) return []; + + const selectedCameraNames = cameraAreas.map((ca) => ca.camera); + return Object.entries(config.cameras) + .sort() + .filter( + ([name, cam]) => + cam.enabled && + cam.enabled_in_config && + !selectedCameraNames.includes(name), + ) + .map(([name]) => ({ + name, + displayName: resolveCameraName(config, name), + })); + }, [config, cameraAreas]); + + const selectedCamera = useMemo(() => { + if (cameraAreas.length === 0) return null; + return cameraAreas[selectedCameraIndex]; + }, [cameraAreas, selectedCameraIndex]); + + const selectedCameraConfig = useMemo(() => { + if (!config || !selectedCamera) return null; + return config.cameras[selectedCamera.camera]; + }, [config, selectedCamera]); + + const imageSize = useMemo(() => { + if (!containerWidth || !selectedCameraConfig) { + return { width: 0, height: 0 }; + } + + const containerAspectRatio = 16 / 9; + const containerHeight = containerWidth / containerAspectRatio; + + const cameraAspectRatio = + selectedCameraConfig.detect.width / selectedCameraConfig.detect.height; + + // Fit camera within 16:9 container + let imageWidth, imageHeight; + if (cameraAspectRatio > containerAspectRatio) { + imageWidth = containerWidth; + imageHeight = imageWidth / cameraAspectRatio; + } else { + imageHeight = containerHeight; + imageWidth = imageHeight * cameraAspectRatio; + } + + return { width: imageWidth, height: imageHeight }; + }, [containerWidth, selectedCameraConfig]); + + const handleAddCamera = useCallback( + (cameraName: string) => { + // Calculate a square crop in pixel space + const camera = config?.cameras[cameraName]; + if (!camera) return; + + const cameraAspect = camera.detect.width / camera.detect.height; + const cropSize = 0.3; + let x1, y1, x2, y2; + + if (cameraAspect >= 1) { + const pixelSize = cropSize * camera.detect.height; + const normalizedWidth = pixelSize / camera.detect.width; + x1 = (1 - normalizedWidth) / 2; + y1 = (1 - cropSize) / 2; + x2 = x1 + normalizedWidth; + y2 = y1 + cropSize; + } else { + const pixelSize = cropSize * camera.detect.width; + const normalizedHeight = pixelSize / camera.detect.height; + x1 = (1 - cropSize) / 2; + y1 = (1 - normalizedHeight) / 2; + x2 = x1 + cropSize; + y2 = y1 + normalizedHeight; + } + + const newArea: CameraAreaConfig = { + camera: cameraName, + crop: [x1, y1, x2, y2], + }; + setCameraAreas([...cameraAreas, newArea]); + setSelectedCameraIndex(cameraAreas.length); + setIsPopoverOpen(false); + }, + [cameraAreas, config], + ); + + const handleRemoveCamera = useCallback( + (index: number) => { + const newAreas = cameraAreas.filter((_, i) => i !== index); + setCameraAreas(newAreas); + if (selectedCameraIndex >= newAreas.length) { + setSelectedCameraIndex(Math.max(0, newAreas.length - 1)); + } + }, + [cameraAreas, selectedCameraIndex], + ); + + const handleCropChange = useCallback( + (crop: [number, number, number, number]) => { + const newAreas = [...cameraAreas]; + newAreas[selectedCameraIndex] = { + ...newAreas[selectedCameraIndex], + crop, + }; + setCameraAreas(newAreas); + }, + [cameraAreas, selectedCameraIndex], + ); + + useEffect(() => { + setImageLoaded(false); + }, [selectedCamera]); + + useEffect(() => { + const rect = rectRef.current; + const transformer = transformerRef.current; + + if ( + rect && + transformer && + selectedCamera && + imageSize.width > 0 && + imageLoaded + ) { + rect.scaleX(1); + rect.scaleY(1); + transformer.nodes([rect]); + transformer.getLayer()?.batchDraw(); + } + }, [selectedCamera, imageSize, imageLoaded]); + + const handleRectChange = useCallback(() => { + const rect = rectRef.current; + + if (rect && imageSize.width > 0) { + const actualWidth = rect.width() * rect.scaleX(); + const actualHeight = rect.height() * rect.scaleY(); + + // Average dimensions to maintain perfect square + const size = (actualWidth + actualHeight) / 2; + + rect.width(size); + rect.height(size); + rect.scaleX(1); + rect.scaleY(1); + + const x1 = rect.x() / imageSize.width; + const y1 = rect.y() / imageSize.height; + const x2 = (rect.x() + size) / imageSize.width; + const y2 = (rect.y() + size) / imageSize.height; + + handleCropChange([x1, y1, x2, y2]); + } + }, [imageSize, handleCropChange]); + + const handleContinue = useCallback(() => { + onNext({ cameraAreas }); + }, [cameraAreas, onNext]); + + const canContinue = cameraAreas.length > 0; + + return ( +
    +
    +
    +
    +

    {t("wizard.step2.cameras")}

    + {availableCameras.length > 0 ? ( + + + + + e.preventDefault()} + > +
    + + {t("wizard.step2.selectCamera")} + +
    + {availableCameras.map((cam) => ( + + ))} +
    +
    +
    +
    + ) : ( + + )} +
    + +
    + {cameraAreas.map((area, index) => { + const isSelected = index === selectedCameraIndex; + const displayName = resolveCameraName(config, area.camera); + + return ( +
    setSelectedCameraIndex(index)} + > + {displayName} + +
    + ); + })} +
    + + {cameraAreas.length === 0 && ( +
    + {t("wizard.step2.noCameras")} +
    + )} +
    + +
    +
    + {selectedCamera && selectedCameraConfig && imageSize.width > 0 ? ( +
    + {resolveCameraName(config, setImageLoaded(true)} + /> + + + { + const rect = rectRef.current; + if (!rect) return pos; + + const size = rect.width(); + const x = Math.max( + 0, + Math.min(pos.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(pos.y, imageSize.height - size), + ); + + return { x, y }; + }} + onDragEnd={handleRectChange} + onTransformEnd={handleRectChange} + /> + { + const minSize = 50; + const maxSize = Math.min( + imageSize.width, + imageSize.height, + ); + + // Clamp dimensions to stage bounds first + const clampedWidth = Math.max( + minSize, + Math.min(newBox.width, maxSize), + ); + const clampedHeight = Math.max( + minSize, + Math.min(newBox.height, maxSize), + ); + + // Enforce square using average + const size = (clampedWidth + clampedHeight) / 2; + + // Clamp position to keep square within bounds + const x = Math.max( + 0, + Math.min(newBox.x, imageSize.width - size), + ); + const y = Math.max( + 0, + Math.min(newBox.y, imageSize.height - size), + ); + + return { + ...newBox, + x, + y, + width: size, + height: size, + }; + }} + /> + + +
    + ) : ( +
    + {t("wizard.step2.selectCameraPrompt")} +
    + )} +
    +
    +
    + +
    + + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step3ChooseExamples.tsx b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step3ChooseExamples.tsx new file mode 100644 index 0000000..d15e45b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -0,0 +1,612 @@ +import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; +import { useState, useEffect, useCallback, useMemo } from "react"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import axios from "axios"; +import { toast } from "sonner"; +import { Step1FormData } from "./Step1NameAndDefine"; +import { Step2FormData } from "./Step2StateArea"; +import useSWR from "swr"; +import { baseUrl } from "@/api/baseUrl"; +import { isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { IoIosWarning } from "react-icons/io"; + +export type Step3FormData = { + examplesGenerated: boolean; + imageClassifications?: { [imageName: string]: string }; +}; + +type Step3ChooseExamplesProps = { + step1Data: Step1FormData; + step2Data?: Step2FormData; + initialData?: Partial; + onClose: () => void; + onBack: () => void; +}; + +export default function Step3ChooseExamples({ + step1Data, + step2Data, + initialData, + onClose, + onBack, +}: Step3ChooseExamplesProps) { + const { t } = useTranslation(["views/classificationModel"]); + const [isGenerating, setIsGenerating] = useState(false); + const [hasGenerated, setHasGenerated] = useState( + initialData?.examplesGenerated || false, + ); + const [imageClassifications, setImageClassifications] = useState<{ + [imageName: string]: string; + }>(initialData?.imageClassifications || {}); + const [isTraining, setIsTraining] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [currentClassIndex, setCurrentClassIndex] = useState(0); + const [selectedImages, setSelectedImages] = useState>(new Set()); + + const { data: trainImages, mutate: refreshTrainImages } = useSWR( + hasGenerated ? `classification/${step1Data.modelName}/train` : null, + ); + + const unknownImages = useMemo(() => { + if (!trainImages) return []; + return trainImages; + }, [trainImages]); + + const toggleImageSelection = useCallback((imageName: string) => { + setSelectedImages((prev) => { + const newSet = new Set(prev); + if (newSet.has(imageName)) { + newSet.delete(imageName); + } else { + newSet.add(imageName); + } + return newSet; + }); + }, []); + + // Get all classes (excluding "none" - it will be auto-assigned) + const allClasses = useMemo(() => { + return [...step1Data.classes]; + }, [step1Data.classes]); + + const currentClass = allClasses[currentClassIndex]; + + const processClassificationsAndTrain = useCallback( + async (classifications: { [imageName: string]: string }) => { + // Step 1: Create config for the new model + const modelConfig: { + enabled: boolean; + name: string; + threshold: number; + state_config?: { + cameras: Record; + motion: boolean; + }; + object_config?: { objects: string[]; classification_type: string }; + } = { + enabled: true, + name: step1Data.modelName, + threshold: 0.8, + }; + + if (step1Data.modelType === "state") { + // State model config + const cameras: Record = {}; + step2Data?.cameraAreas.forEach((area) => { + cameras[area.camera] = { + crop: area.crop, + }; + }); + + modelConfig.state_config = { + cameras, + motion: true, + }; + } else { + // Object model config + modelConfig.object_config = { + objects: step1Data.objectLabel ? [step1Data.objectLabel] : [], + classification_type: step1Data.objectType || "sub_label", + } as { objects: string[]; classification_type: string }; + } + + // Update config via config API + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${step1Data.modelName}`, + config_data: { + classification: { + custom: { + [step1Data.modelName]: modelConfig, + }, + }, + }, + }); + + // Step 2: Classify each image by moving it to the correct category folder + const categorizePromises = Object.entries(classifications).map( + ([imageName, className]) => { + if (!className) return Promise.resolve(); + return axios.post( + `/classification/${step1Data.modelName}/dataset/categorize`, + { + training_file: imageName, + category: className === "none" ? "none" : className, + }, + ); + }, + ); + await Promise.all(categorizePromises); + + // Step 2.5: Delete any unselected images from train folder + // For state models, all images must be classified, so unselected images should be removed + // For object models, unselected images are assigned to "none" so they're already categorized + if (step1Data.modelType === "state") { + try { + // Fetch current train images to see what's left after categorization + const trainImagesResponse = await axios.get( + `/classification/${step1Data.modelName}/train`, + ); + const remainingTrainImages = trainImagesResponse.data || []; + + const categorizedImageNames = new Set(Object.keys(classifications)); + const unselectedImages = remainingTrainImages.filter( + (imageName) => !categorizedImageNames.has(imageName), + ); + + if (unselectedImages.length > 0) { + await axios.post( + `/classification/${step1Data.modelName}/train/delete`, + { + ids: unselectedImages, + }, + ); + } + } catch (error) { + // Silently fail - unselected images will remain but won't cause issues + // since the frontend filters out images that don't match expected format + } + } + + // Step 2.6: Create empty folders for classes that don't have any images + // This ensures all classes are available in the dataset view later + const classesWithImages = new Set( + Object.values(classifications).filter((c) => c && c !== "none"), + ); + const emptyFolderPromises = step1Data.classes + .filter((className) => !classesWithImages.has(className)) + .map((className) => + axios.post( + `/classification/${step1Data.modelName}/dataset/${className}/create`, + ), + ); + await Promise.all(emptyFolderPromises); + + // Step 3: Determine if we should train + // For state models, we need ALL states to have examples + // For object models, we need at least 2 classes with images + const allStatesHaveExamplesForTraining = + step1Data.modelType !== "state" || + step1Data.classes.every((className) => + classesWithImages.has(className), + ); + const shouldTrain = + allStatesHaveExamplesForTraining && classesWithImages.size >= 2; + + // Step 4: Kick off training only if we have enough classes with images + if (shouldTrain) { + await axios.post(`/classification/${step1Data.modelName}/train`); + + toast.success(t("wizard.step3.trainingStarted"), { + closeButton: true, + }); + setIsTraining(true); + } else { + // Don't train - not all states have examples + toast.success(t("wizard.step3.modelCreated"), { + closeButton: true, + }); + setIsTraining(false); + onClose(); + } + }, + [step1Data, step2Data, t, onClose], + ); + + const handleContinueClassification = useCallback(async () => { + // Mark selected images with current class + const newClassifications = { ...imageClassifications }; + + // Handle user going back and de-selecting images + const imagesToCheck = unknownImages.slice(0, 24); + imagesToCheck.forEach((imageName) => { + if ( + newClassifications[imageName] === currentClass && + !selectedImages.has(imageName) + ) { + delete newClassifications[imageName]; + } + }); + + // Then, add all currently selected images to the current class + selectedImages.forEach((imageName) => { + newClassifications[imageName] = currentClass; + }); + + // Check if we're on the last class to select + const isLastClass = currentClassIndex === allClasses.length - 1; + + if (isLastClass) { + // For object models, assign remaining unclassified images to "none" + // For state models, this should never happen since we require all images to be classified + if (step1Data.modelType !== "state") { + unknownImages.slice(0, 24).forEach((imageName) => { + if (!newClassifications[imageName]) { + newClassifications[imageName] = "none"; + } + }); + } + + // All done, trigger training immediately + setImageClassifications(newClassifications); + setIsProcessing(true); + + try { + await processClassificationsAndTrain(newClassifications); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to classify images"; + + toast.error( + t("wizard.step3.errors.classifyFailed", { error: errorMessage }), + ); + setIsProcessing(false); + } + } else { + // Move to next class + setImageClassifications(newClassifications); + setCurrentClassIndex((prev) => prev + 1); + setSelectedImages(new Set()); + } + }, [ + selectedImages, + currentClass, + currentClassIndex, + allClasses, + imageClassifications, + unknownImages, + step1Data, + processClassificationsAndTrain, + t, + ]); + + const generateExamples = useCallback(async () => { + setIsGenerating(true); + + try { + if (step1Data.modelType === "state") { + // For state models, use cameras and crop areas + if (!step2Data?.cameraAreas || step2Data.cameraAreas.length === 0) { + toast.error(t("wizard.step3.errors.noCameras")); + setIsGenerating(false); + return; + } + + const cameras: { [key: string]: [number, number, number, number] } = {}; + step2Data.cameraAreas.forEach((area) => { + cameras[area.camera] = area.crop; + }); + + await axios.post("/classification/generate_examples/state", { + model_name: step1Data.modelName, + cameras, + }); + } else { + // For object models, use label + if (!step1Data.objectLabel) { + toast.error(t("wizard.step3.errors.noObjectLabel")); + setIsGenerating(false); + return; + } + + // For now, use all enabled cameras + // TODO: In the future, we might want to let users select specific cameras + await axios.post("/classification/generate_examples/object", { + model_name: step1Data.modelName, + label: step1Data.objectLabel, + }); + } + + setHasGenerated(true); + toast.success(t("wizard.step3.generateSuccess")); + + await refreshTrainImages(); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to generate examples"; + + toast.error( + t("wizard.step3.errors.generateFailed", { error: errorMessage }), + ); + } finally { + setIsGenerating(false); + } + }, [step1Data, step2Data, t, refreshTrainImages]); + + useEffect(() => { + if (!hasGenerated && !isGenerating) { + generateExamples(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleContinue = useCallback(async () => { + setIsProcessing(true); + try { + await processClassificationsAndTrain(imageClassifications); + } catch (error) { + const axiosError = error as { + response?: { data?: { message?: string; detail?: string } }; + message?: string; + }; + const errorMessage = + axiosError.response?.data?.message || + axiosError.response?.data?.detail || + axiosError.message || + "Failed to classify images"; + + toast.error( + t("wizard.step3.errors.classifyFailed", { error: errorMessage }), + ); + setIsProcessing(false); + } + }, [imageClassifications, processClassificationsAndTrain, t]); + + const unclassifiedImages = useMemo(() => { + if (!unknownImages) return []; + const images = unknownImages.slice(0, 24); + + // Only filter if we have any classifications + if (Object.keys(imageClassifications).length === 0) { + return images; + } + + // If we're viewing a previous class (going back), show images for that class + // Otherwise show only unclassified images + const currentClassInView = allClasses[currentClassIndex]; + return images.filter((img) => { + const imgClass = imageClassifications[img]; + // Show if: unclassified OR classified with current class we're viewing + return !imgClass || imgClass === currentClassInView; + }); + }, [unknownImages, imageClassifications, allClasses, currentClassIndex]); + + const allImagesClassified = useMemo(() => { + return unclassifiedImages.length === 0; + }, [unclassifiedImages]); + + const isLastClass = currentClassIndex === allClasses.length - 1; + const statesWithExamples = useMemo(() => { + if (step1Data.modelType !== "state") return new Set(); + + const states = new Set(); + const allImages = unknownImages.slice(0, 24); + + // Check which states have at least one image classified + allImages.forEach((img) => { + let className: string | undefined; + if (selectedImages.has(img)) { + className = currentClass; + } else { + className = imageClassifications[img]; + } + if (className && allClasses.includes(className)) { + states.add(className); + } + }); + + return states; + }, [ + step1Data.modelType, + unknownImages, + imageClassifications, + selectedImages, + currentClass, + allClasses, + ]); + + const allStatesHaveExamples = useMemo(() => { + if (step1Data.modelType !== "state") return true; + return allClasses.every((className) => statesWithExamples.has(className)); + }, [step1Data.modelType, allClasses, statesWithExamples]); + + const hasUnclassifiedImages = useMemo(() => { + if (!unknownImages) return false; + const allImages = unknownImages.slice(0, 24); + return allImages.some((img) => !imageClassifications[img]); + }, [unknownImages, imageClassifications]); + + const showMissingStatesWarning = useMemo(() => { + return ( + step1Data.modelType === "state" && + isLastClass && + !allStatesHaveExamples && + !hasUnclassifiedImages && + hasGenerated + ); + }, [ + step1Data.modelType, + isLastClass, + allStatesHaveExamples, + hasUnclassifiedImages, + hasGenerated, + ]); + + const handleBack = useCallback(() => { + if (currentClassIndex > 0) { + const previousClass = allClasses[currentClassIndex - 1]; + setCurrentClassIndex((prev) => prev - 1); + + // Restore selections for the previous class + const previousSelections = Object.entries(imageClassifications) + .filter(([_, className]) => className === previousClass) + .map(([imageName, _]) => imageName); + setSelectedImages(new Set(previousSelections)); + } else { + onBack(); + } + }, [currentClassIndex, allClasses, imageClassifications, onBack]); + + return ( +
    + {isTraining ? ( +
    + +
    +

    + {t("wizard.step3.training.title")} +

    +

    + {t("wizard.step3.training.description")} +

    +
    + +
    + ) : isGenerating ? ( +
    + +
    +

    + {t("wizard.step3.generating.title")} +

    +

    + {t("wizard.step3.generating.description")} +

    +
    +
    + ) : hasGenerated ? ( +
    + {showMissingStatesWarning && ( + + + + {t("wizard.step3.missingStatesWarning.title")} + + + {t("wizard.step3.missingStatesWarning.description")} + + + )} + {!allImagesClassified && ( +
    +

    + {t("wizard.step3.selectImagesPrompt", { + className: currentClass, + })} +

    +

    + {t("wizard.step3.selectImagesDescription")} +

    +
    + )} +
    + {!unknownImages || unknownImages.length === 0 ? ( +
    +

    + {t("wizard.step3.noImages")} +

    + +
    + ) : allImagesClassified && isProcessing ? ( +
    + +

    + {t("wizard.step3.classifying")} +

    +
    + ) : ( +
    + {unclassifiedImages.map((imageName, index) => { + const isSelected = selectedImages.has(imageName); + return ( +
    toggleImageSelection(imageName)} + > + {`Example +
    + ); + })} +
    + )} +
    +
    + ) : ( +
    +

    + {t("wizard.step3.errors.generationFailed")} +

    + +
    + )} + + {!isTraining && ( +
    + + +
    + )} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/dynamic/CameraFeatureToggle.tsx b/sam2-cpu/frigate-dev/web/src/components/dynamic/CameraFeatureToggle.tsx new file mode 100644 index 0000000..5479e42 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/dynamic/CameraFeatureToggle.tsx @@ -0,0 +1,88 @@ +import { IconType } from "react-icons"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import ActivityIndicator from "../indicators/activity-indicator"; + +const variants = { + primary: { + active: "font-bold text-white bg-selected rounded-lg", + inactive: "text-secondary-foreground bg-secondary rounded-lg", + disabled: + "text-secondary-foreground bg-secondary rounded-lg cursor-not-allowed opacity-50", + }, + overlay: { + active: "font-bold text-white bg-selected rounded-full", + inactive: + "text-primary rounded-full bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500", + disabled: + "bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-full cursor-not-allowed opacity-50", + }, +}; + +type CameraFeatureToggleProps = { + className?: string; + variant?: "primary" | "overlay"; + isActive: boolean; + Icon: IconType; + title: string; + onClick?: () => void; + disabled?: boolean; + loading?: boolean; +}; + +export default function CameraFeatureToggle({ + className = "", + variant = "primary", + isActive, + Icon, + title, + onClick, + disabled = false, + loading = false, +}: CameraFeatureToggleProps) { + const content = ( +
    + {loading ? ( + + ) : ( + + )} +
    + ); + + if (isDesktop) { + return ( + + {content} + +

    {title}

    +
    +
    + ); + } + + return content; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/dynamic/EnhancedScrollFollow.tsx b/sam2-cpu/frigate-dev/web/src/components/dynamic/EnhancedScrollFollow.tsx new file mode 100644 index 0000000..35673c8 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/dynamic/EnhancedScrollFollow.tsx @@ -0,0 +1,91 @@ +import { useRef, useCallback, useEffect, type ReactNode } from "react"; +import { ScrollFollow } from "@melloware/react-logviewer"; + +export type ScrollFollowProps = { + startFollowing?: boolean; + render: (renderProps: ScrollFollowRenderProps) => ReactNode; + onCustomScroll?: ( + scrollTop: number, + scrollHeight: number, + clientHeight: number, + ) => void; +}; + +export type ScrollFollowRenderProps = { + follow: boolean; + onScroll: (args: { + scrollTop: number; + scrollHeight: number; + clientHeight: number; + }) => void; + startFollowing: () => void; + stopFollowing: () => void; + onCustomScroll?: ( + scrollTop: number, + scrollHeight: number, + clientHeight: number, + ) => void; +}; + +const SCROLL_BUFFER = 5; + +export default function EnhancedScrollFollow(props: ScrollFollowProps) { + const followRef = useRef(props.startFollowing || false); + const prevScrollTopRef = useRef(undefined); + + useEffect(() => { + prevScrollTopRef.current = undefined; + }, []); + + const wrappedRender = useCallback( + (renderProps: ScrollFollowRenderProps) => { + const wrappedOnScroll = (args: { + scrollTop: number; + scrollHeight: number; + clientHeight: number; + }) => { + // Check if scrolling up and immediately stop following + if ( + prevScrollTopRef.current !== undefined && + args.scrollTop < prevScrollTopRef.current + ) { + if (followRef.current) { + renderProps.stopFollowing(); + followRef.current = false; + } + } + + const bottomThreshold = + args.scrollHeight - args.clientHeight - SCROLL_BUFFER; + const isNearBottom = args.scrollTop >= bottomThreshold; + + if (isNearBottom && !followRef.current) { + renderProps.startFollowing(); + followRef.current = true; + } else if (!isNearBottom && followRef.current) { + renderProps.stopFollowing(); + followRef.current = false; + } + + prevScrollTopRef.current = args.scrollTop; + renderProps.onScroll(args); + if (props.onCustomScroll) { + props.onCustomScroll( + args.scrollTop, + args.scrollHeight, + args.clientHeight, + ); + } + }; + + return props.render({ + ...renderProps, + onScroll: wrappedOnScroll, + follow: followRef.current, + }); + }, + [props], + ); + + return ; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/dynamic/NewReviewData.tsx b/sam2-cpu/frigate-dev/web/src/components/dynamic/NewReviewData.tsx new file mode 100644 index 0000000..b0fd747 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/dynamic/NewReviewData.tsx @@ -0,0 +1,58 @@ +import { ReviewSegment } from "@/types/review"; +import { Button } from "../ui/button"; +import { LuRefreshCcw } from "react-icons/lu"; +import { MutableRefObject, useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +type NewReviewDataProps = { + className: string; + contentRef: MutableRefObject; + reviewItems?: ReviewSegment[] | null; + itemsToReview?: number; + pullLatestData: () => void; +}; +export default function NewReviewData({ + className, + contentRef, + reviewItems, + itemsToReview, + pullLatestData, +}: NewReviewDataProps) { + const { t } = useTranslation(["views/events"]); + const hasUpdate = useMemo(() => { + if (!reviewItems || !itemsToReview) { + return false; + } + + return reviewItems.length < itemsToReview; + }, [reviewItems, itemsToReview]); + + return ( +
    +
    + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/dynamic/TimeAgo.tsx b/sam2-cpu/frigate-dev/web/src/components/dynamic/TimeAgo.tsx new file mode 100644 index 0000000..1573147 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/dynamic/TimeAgo.tsx @@ -0,0 +1,124 @@ +import { t } from "i18next"; +import { FunctionComponent, useEffect, useMemo, useState } from "react"; + +interface IProp { + /** OPTIONAL: classname */ + className?: string; + /** The time to calculate time-ago from */ + time: number; + /** OPTIONAL: overwrite current time */ + currentTime?: Date; + /** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */ + dense?: boolean; + /** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */ + manualRefreshInterval?: number; +} + +type TimeUnit = { + unit: string; + full: string; + value: number; +}; + +const timeAgo = ({ + time, + currentTime = new Date(), + dense = false, +}: IProp): string => { + if (typeof time !== "number" || time < 0) return "Invalid Time Provided"; + + const pastTime: Date = new Date(time); + const elapsedTime: number = currentTime.getTime() - pastTime.getTime(); + + const timeUnits: TimeUnit[] = [ + { unit: "yr", full: "year", value: 31536000 }, + { unit: "mo", full: "month", value: 0 }, + { unit: "d", full: "day", value: 86400 }, + { unit: "h", full: "hour", value: 3600 }, + { unit: "m", full: "minute", value: 60 }, + { unit: "s", full: "second", value: 1 }, + ]; + + const elapsed: number = elapsedTime / 1000; + if (elapsed < 10) { + return t("time.justNow", { ns: "common" }); + } + + for (let i = 0; i < timeUnits.length; i++) { + // if months + if (i === 1) { + // Get the month and year for the time provided + const pastMonth = pastTime.getUTCMonth(); + const pastYear = pastTime.getUTCFullYear(); + + // get current month and year + const currentMonth = currentTime.getUTCMonth(); + const currentYear = currentTime.getUTCFullYear(); + + let monthDiff = + (currentYear - pastYear) * 12 + (currentMonth - pastMonth); + + // check if the time provided is the previous month but not exceeded 1 month ago. + if (currentTime.getUTCDate() < pastTime.getUTCDate()) { + monthDiff--; + } + + if (monthDiff > 0) { + const unitAmount = monthDiff; + return t("time.ago", { + ns: "common", + timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, { + time: unitAmount, + }), + }); + } + } else if (elapsed >= timeUnits[i].value) { + const unitAmount: number = Math.floor(elapsed / timeUnits[i].value); + return t("time.ago", { + ns: "common", + timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, { + time: unitAmount, + }), + }); + } + } + return "Invalid Time"; +}; + +const TimeAgo: FunctionComponent = ({ + className, + time, + manualRefreshInterval, + ...rest +}): JSX.Element => { + const [currentTime, setCurrentTime] = useState(new Date()); + const refreshInterval = useMemo(() => { + if (manualRefreshInterval) { + return manualRefreshInterval; + } + + const currentTs = currentTime.getTime() / 1000; + if (currentTs - time < 60) { + return 1000; // refresh every second + } else if (currentTs - time < 3600) { + return 60000; // refresh every minute + } else { + return 3600000; // refresh every hour + } + }, [currentTime, manualRefreshInterval, time]); + + useEffect(() => { + const intervalId: NodeJS.Timeout = setInterval(() => { + setCurrentTime(new Date()); + }, refreshInterval); + return () => clearInterval(intervalId); + }, [refreshInterval]); + + const timeAgoValue = useMemo( + () => timeAgo({ time, currentTime, ...rest }), + [currentTime, rest, time], + ); + + return {timeAgoValue}; +}; +export default TimeAgo; diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/CalendarFilterButton.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/CalendarFilterButton.tsx new file mode 100644 index 0000000..876eb9a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/CalendarFilterButton.tsx @@ -0,0 +1,165 @@ +import { + useFormattedRange, + useFormattedTimestamp, + useTimezone, +} from "@/hooks/use-date-utils"; +import { RecordingsSummary, ReviewSummary } from "@/types/review"; +import { Button } from "../ui/button"; +import { FaCalendarAlt } from "react-icons/fa"; +import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { isMobile } from "react-device-detect"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { DateRangePicker } from "../ui/calendar-range"; +import { DateRange } from "react-day-picker"; +import { useState } from "react"; +import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; + +type CalendarFilterButtonProps = { + reviewSummary?: ReviewSummary; + recordingsSummary?: RecordingsSummary; + day?: Date; + updateSelectedDay: (day?: Date) => void; +}; +export default function CalendarFilterButton({ + reviewSummary, + recordingsSummary, + day, + updateSelectedDay, +}: CalendarFilterButtonProps) { + const { t } = useTranslation(["components/filter", "views/events"]); + const { data: config } = useSWR("config"); + const [open, setOpen] = useState(false); + const selectedDate = useFormattedTimestamp( + day == undefined ? 0 : day?.getTime() / 1000 + 1, + t("time.formattedTimestampMonthDay", { ns: "common" }), + config?.ui.timezone, + ); + + const trigger = ( + + ); + const content = ( + <> + + +
    + +
    + + ); + + return ( + + ); +} + +type CalendarRangeFilterButtonProps = { + range?: DateRange; + defaultText: string; + updateSelectedRange: (range?: DateRange) => void; +}; +export function CalendarRangeFilterButton({ + range, + defaultText, + updateSelectedRange, +}: CalendarRangeFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config"); + const timezone = useTimezone(config); + const [open, setOpen] = useState(false); + + const selectedDate = useFormattedRange( + range?.from == undefined ? 0 : range.from.getTime() / 1000 + 1, + range?.to == undefined ? 0 : range.to.getTime() / 1000 - 1, + t("time.formattedTimestampMonthDay", { ns: "common" }), + config?.ui.timezone, + ); + + const trigger = ( + + ); + const content = ( + <> + { + updateSelectedRange(range.range); + setOpen(false); + }} + onReset={() => updateSelectedRange(undefined)} + /> + + ); + + if (isMobile) { + return ( + + {trigger} + {content} + + ); + } + + return ( + + {trigger} + {content} + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/CameraGroupSelector.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/CameraGroupSelector.tsx new file mode 100644 index 0000000..14845fd --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/CameraGroupSelector.tsx @@ -0,0 +1,1004 @@ +import { + AllGroupsStreamingSettings, + CameraGroupConfig, + FrigateConfig, + GroupStreamingSettings, +} from "@/types/frigateConfig"; +import { isDesktop, isMobile } from "react-device-detect"; +import useSWR from "swr"; +import { MdHome } from "react-icons/md"; +import { Button, buttonVariants } from "../ui/button"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { LuPencil, LuPlus } from "react-icons/lu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { Separator } from "../ui/separator"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import axios from "axios"; +import { HiOutlineDotsVertical, HiTrash } from "react-icons/hi"; +import IconWrapper from "../ui/icon-wrapper"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { ScrollArea, ScrollBar } from "../ui/scroll-area"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { cn } from "@/lib/utils"; +import * as LuIcons from "react-icons/lu"; +import IconPicker, { IconName, IconRenderer } from "../icons/IconPicker"; +import { isValidIconName } from "@/utils/iconUtil"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "../mobile/MobilePage"; + +import { Switch } from "../ui/switch"; +import { CameraStreamingDialog } from "../settings/CameraStreamingDialog"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { useStreamingSettings } from "@/context/streaming-settings-provider"; +import { Trans, useTranslation } from "react-i18next"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state"; + +type CameraGroupSelectorProps = { + className?: string; +}; + +export function CameraGroupSelector({ className }: CameraGroupSelectorProps) { + const { t } = useTranslation(["components/camera"]); + const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); + const isAdmin = useIsAdmin(); + + // tooltip + + const [tooltip, setTooltip] = useState(); + const [timeoutId, setTimeoutId] = useState(); + const showTooltip = useCallback( + (newTooltip: string | undefined) => { + if (!newTooltip) { + setTooltip(newTooltip); + + if (timeoutId) { + clearTimeout(timeoutId); + } + } else { + setTimeoutId(setTimeout(() => setTooltip(newTooltip), 500)); + } + }, + [timeoutId], + ); + + // groups - use user-namespaced key for persistence to avoid cross-user conflicts + + const [group, setGroup, , deleteGroup] = useUserPersistedOverlayState( + "cameraGroup", + "default" as string, + ); + + const groups = useMemo(() => { + if (!config) { + return []; + } + + const allGroups = Object.entries(config.camera_groups); + + // If custom role, filter out groups where user has no accessible cameras + if (!isAdmin) { + return allGroups + .filter(([, groupConfig]) => { + // Check if user has access to at least one camera in this group + return groupConfig.cameras.some((cameraName) => + allowedCameras.includes(cameraName), + ); + }) + .sort((a, b) => a[1].order - b[1].order); + } + + return allGroups.sort((a, b) => a[1].order - b[1].order); + }, [config, allowedCameras, isAdmin]); + + // add group + + const [addGroup, setAddGroup] = useState(false); + + const Scroller = isMobile ? ScrollArea : "div"; + + return ( + <> + + +
    + + + + + + + {t("menu.live.allCameras", { ns: "common" })} + + + + {groups.map(([name, config]) => { + return ( + + + + + + + {name} + + + + ); + })} + + {isAdmin && ( + + )} + {isMobile && } +
    +
    + + ); +} + +type NewGroupDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + currentGroups: [string, CameraGroupConfig][]; + activeGroup?: string; + setGroup: (value: string | undefined, replace?: boolean | undefined) => void; + deleteGroup: () => void; + isAdmin?: boolean; +}; +function NewGroupDialog({ + open, + setOpen, + currentGroups, + activeGroup, + setGroup, + deleteGroup, + isAdmin, +}: NewGroupDialogProps) { + const { t } = useTranslation(["components/camera"]); + const { mutate: updateConfig } = useSWR("config"); + + // editing group and state + + const [editingGroupName, setEditingGroupName] = useState(""); + + const editingGroup = useMemo(() => { + if (currentGroups && editingGroupName !== undefined) { + return currentGroups.find( + ([groupName]) => groupName === editingGroupName, + ); + } else { + return undefined; + } + }, [currentGroups, editingGroupName]); + + const [editState, setEditState] = useState<"none" | "add" | "edit">("none"); + const [isLoading, setIsLoading] = useState(false); + + const [, , , deleteGridLayout] = useUserPersistence( + `${activeGroup}-draggable-layout`, + ); + + useEffect(() => { + if (!open) { + setEditState("none"); + } + }, [open]); + + // callbacks + + const onDeleteGroup = useCallback( + async (name: string) => { + deleteGridLayout(); + deleteGroup(); + + await axios + .put(`config/set?camera_groups.${name}`, { requires_restart: 0 }) + .then((res) => { + if (res.status === 200) { + if (activeGroup == name) { + // deleting current group + setGroup("default"); + } + updateConfig(); + } else { + setOpen(false); + setEditState("none"); + toast.error( + t("toast.save.error.title", { + errorMessage: res.statusText, + ns: "common", + }), + { + position: "top-center", + }, + ); + } + }) + .catch((error) => { + setOpen(false); + setEditState("none"); + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("toast.save.error.title", { errorMessage, ns: "common" }), + { + position: "top-center", + }, + ); + }) + .finally(() => { + setIsLoading(false); + }); + }, + [ + updateConfig, + activeGroup, + setGroup, + setOpen, + deleteGroup, + deleteGridLayout, + t, + ], + ); + + const onSave = () => { + setOpen(false); + setEditState("none"); + setEditingGroupName(""); + }; + + const onCancel = () => { + setEditingGroupName(""); + setEditState("none"); + }; + + const onEditGroup = useCallback((group: [string, CameraGroupConfig]) => { + setEditingGroupName(group[0]); + setEditState("edit"); + }, []); + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + return ( + <> + + + + {editState === "none" && ( + <> +
    setOpen(false)} + > + {t("group.label")} + {t("group.edit")} + {isAdmin && ( +
    + +
    + )} +
    +
    + {currentGroups.map((group) => ( + onDeleteGroup(group[0])} + onEditGroup={() => onEditGroup(group)} + isReadOnly={!isAdmin} + /> + ))} +
    + + )} + + {editState != "none" && ( + <> +
    { + setEditState("none"); + setEditingGroupName(""); + }} + > + + {editState == "add" ? t("group.add") : t("group.edit")} + + {t("group.edit")} +
    + + + )} +
    +
    + + ); +} + +type EditGroupDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + currentGroups: [string, CameraGroupConfig][]; + activeGroup?: string; +}; +export function EditGroupDialog({ + open, + setOpen, + currentGroups, + activeGroup, +}: EditGroupDialogProps) { + const { t } = useTranslation(["components/camera"]); + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + // editing group and state + + const editingGroup = useMemo(() => { + if (currentGroups && activeGroup) { + return currentGroups.find(([groupName]) => groupName === activeGroup); + } else { + return undefined; + } + }, [currentGroups, activeGroup]); + + const [isLoading, setIsLoading] = useState(false); + + return ( + <> + + { + setOpen(open); + }} + > + +
    +
    setOpen(false)}> + {t("group.edit")} + {t("group.edit")} +
    + + setOpen(false)} + onCancel={() => setOpen(false)} + /> +
    +
    +
    + + ); +} + +type CameraGroupRowProps = { + group: [string, CameraGroupConfig]; + onDeleteGroup: () => void; + onEditGroup: () => void; + isReadOnly?: boolean; +}; + +export function CameraGroupRow({ + group, + onDeleteGroup, + onEditGroup, + isReadOnly, +}: CameraGroupRowProps) { + const { t } = useTranslation(["components/camera"]); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + if (!group) { + return; + } + + return ( + <> +
    +
    +

    {group[0]}

    +
    + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("group.delete.confirm.title")} + + + + + group.delete.confirm.desc + + + + + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + + {isMobile && !isReadOnly && ( + <> + + + + + + + + {t("button.edit", { ns: "common" })} + + setDeleteDialogOpen(true)} + > + {t("button.delete", { ns: "common" })} + + + + + + )} + {!isMobile && !isReadOnly && ( +
    + + + + + + {t("button.edit", { ns: "common" })} + + + + + + setDeleteDialogOpen(true)} + /> + + + {t("button.delete", { ns: "common" })} + + +
    + )} +
    + + ); +} + +type CameraGroupEditProps = { + currentGroups: [string, CameraGroupConfig][]; + editingGroup?: [string, CameraGroupConfig]; + isLoading: boolean; + setIsLoading: React.Dispatch>; + onSave?: () => void; + onCancel?: () => void; +}; + +export function CameraGroupEdit({ + currentGroups, + editingGroup, + isLoading, + setIsLoading, + onSave, + onCancel, +}: CameraGroupEditProps) { + const { t } = useTranslation(["components/camera"]); + const { data: config, mutate: updateConfig } = + useSWR("config"); + + const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } = + useStreamingSettings(); + + const [groupStreamingSettings, setGroupStreamingSettings] = + useState( + allGroupsStreamingSettings[editingGroup?.[0] ?? ""], + ); + + const allowedCameras = useAllowedCameras(); + const isAdmin = useIsAdmin(); + + const [openCamera, setOpenCamera] = useState(); + + const birdseyeConfig = useMemo(() => config?.birdseye, [config]); + + const formSchema = z.object({ + name: z + .string() + .trim() + .min(2, { + message: t("group.name.errorMessage.mustLeastCharacters"), + }) + .transform((val: string) => val.replace(/\s+/g, "_")) + .refine( + (value: string) => { + return ( + editingGroup !== undefined || + !currentGroups.map((group) => group[0]).includes(value) + ); + }, + { + message: t("group.name.errorMessage.exists"), + }, + ) + .refine( + (value: string) => { + return !value.includes("."); + }, + { + message: t("group.name.errorMessage.nameMustNotPeriod"), + }, + ) + .refine((value: string) => value.toLowerCase() !== "default", { + message: t("group.name.errorMessage.invalid"), + }), + + cameras: z.array(z.string()), + icon: z + .string() + .min(1, { message: "You must select an icon." }) + .refine((value) => Object.keys(LuIcons).includes(value), { + message: "Invalid icon", + }), + }); + + const onSubmit = useCallback( + async (values: z.infer) => { + if (!values) { + return; + } + + setIsLoading(true); + + // update streaming settings + const updatedSettings: AllGroupsStreamingSettings = { + ...Object.fromEntries( + Object.entries(allGroupsStreamingSettings || {}).filter( + ([key]) => key !== editingGroup?.[0], + ), + ), + [values.name]: groupStreamingSettings, + }; + + let renamingQuery = ""; + if (editingGroup && editingGroup[0] !== values.name) { + renamingQuery = `camera_groups.${editingGroup[0]}&`; + } + + const order = + editingGroup === undefined + ? currentGroups.length + 1 + : editingGroup[1].order; + + const orderQuery = `camera_groups.${values.name}.order=${+order}`; + const iconQuery = `camera_groups.${values.name}.icon=${values.icon}`; + const cameraQueries = values.cameras + .map((cam) => `&camera_groups.${values.name}.cameras=${cam}`) + .join(""); + + axios + .put( + `config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}`, + { + requires_restart: 0, + }, + ) + .then(async (res) => { + if (res.status === 200) { + toast.success( + t("group.success", { + name: values.name, + }), + { + position: "top-center", + }, + ); + updateConfig(); + if (onSave) { + onSave(); + } + setAllGroupsStreamingSettings(updatedSettings); + } else { + toast.error( + t("toast.save.error.title", { + errorMessage: res.statusText, + ns: "common", + }), + { + position: "top-center", + }, + ); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("toast.save.error.title", { + errorMessage, + ns: "common", + }), + { position: "top-center" }, + ); + }) + .finally(() => { + setIsLoading(false); + }); + }, + [ + currentGroups, + setIsLoading, + onSave, + updateConfig, + editingGroup, + groupStreamingSettings, + allGroupsStreamingSettings, + setAllGroupsStreamingSettings, + t, + ], + ); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onSubmit", + defaultValues: { + name: (editingGroup && editingGroup[0]) ?? "", + icon: editingGroup && (editingGroup[1].icon as IconName), + cameras: editingGroup && editingGroup[1].cameras, + }, + }); + + return ( +
    + + ( + + {t("group.name.label")} + + + + + + )} + /> + + +
    + ( + + {t("group.cameras.label")} + {t("group.cameras.desc")} + + {[ + ...(birdseyeConfig?.enabled && + (isAdmin || "birdseye" in allowedCameras) + ? ["birdseye"] + : []), + ...Object.keys(config?.cameras ?? {}) + .filter((camera) => allowedCameras.includes(camera)) + .sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), + ].map((camera) => ( + +
    + + +
    + {camera !== "birdseye" && ( + + setOpenCamera(isOpen ? camera : null) + } + > + + + + + setOpenCamera(isOpen ? camera : null) + } + /> + + )} + { + const updatedCameras = checked + ? [...(field.value || []), camera] + : (field.value || []).filter((c) => c !== camera); + form.setValue("cameras", updatedCameras); + }} + /> +
    +
    +
    + ))} +
    + )} + /> +
    + + + ( + + {t("group.icon")} + + { + field.onChange(newIcon?.name ?? undefined); + }} + /> + + + + )} + /> + + + +
    + + +
    + + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/CamerasFilterButton.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/CamerasFilterButton.tsx new file mode 100644 index 0000000..baeccf0 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/CamerasFilterButton.tsx @@ -0,0 +1,258 @@ +import { Button } from "../ui/button"; +import { CameraGroupConfig } from "@/types/frigateConfig"; +import { useEffect, useMemo, useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { isMobile } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import FilterSwitch from "./FilterSwitch"; +import { FaVideo } from "react-icons/fa"; +import { useTranslation } from "react-i18next"; + +type CameraFilterButtonProps = { + allCameras: string[]; + groups: [string, CameraGroupConfig][]; + selectedCameras: string[] | undefined; + hideText?: boolean; + mainCamera?: string; + updateCameraFilter: (cameras: string[] | undefined) => void; +}; +export function CamerasFilterButton({ + allCameras, + groups, + selectedCameras, + hideText = isMobile, + mainCamera, + updateCameraFilter, +}: CameraFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); + const [open, setOpen] = useState(false); + const [currentCameras, setCurrentCameras] = useState( + selectedCameras, + ); + + const buttonText = useMemo(() => { + if (isMobile) { + return t("menu.live.cameras.title", { ns: "common" }); + } + + if (!selectedCameras || selectedCameras.length == 0) { + return t("menu.live.allCameras", { ns: "common" }); + } + return t("menu.live.cameras.count", { + ns: "common", + count: selectedCameras.includes("birdseye") + ? selectedCameras.length - 1 + : selectedCameras.length, + }); + }, [selectedCameras, t]); + + // ui + + useEffect(() => { + setCurrentCameras(selectedCameras); + // only refresh when state changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedCameras]); + + const trigger = ( + + ); + const content = ( + + ); + + if (isMobile) { + return ( + { + if (!open) { + setCurrentCameras(selectedCameras); + } + + setOpen(open); + }} + > + {trigger} + + {content} + + + ); + } + + return ( + { + if (!open) { + setCurrentCameras(selectedCameras); + } + setOpen(open); + }} + > + {trigger} + {content} + + ); +} + +type CamerasFilterContentProps = { + allCameras: string[]; + currentCameras: string[] | undefined; + mainCamera?: string; + groups: [string, CameraGroupConfig][]; + setCurrentCameras: (cameras: string[] | undefined) => void; + setOpen: (open: boolean) => void; + updateCameraFilter: (cameras: string[] | undefined) => void; +}; +export function CamerasFilterContent({ + allCameras, + currentCameras, + mainCamera, + groups, + setCurrentCameras, + setOpen, + updateCameraFilter, +}: CamerasFilterContentProps) { + const { t } = useTranslation(["components/filter"]); + return ( + <> + {isMobile && ( + <> + + {t("cameras.all.short")} + + + + )} +
    + { + if (isChecked) { + setCurrentCameras(undefined); + } + }} + /> + {groups.length > 0 && ( + <> + + {groups.map(([name, conf]) => { + return ( +
    { + setCurrentCameras([...conf.cameras]); + }} + > + {name} +
    + ); + })} + + )} + +
    + {allCameras.map((item) => ( + { + if ( + mainCamera !== undefined && // Only enforce if mainCamera is defined + item === mainCamera && + !isChecked && + currentCameras !== undefined + ) { + return; // Prevent deselecting mainCamera when filtered and mainCamera is defined + } + if (isChecked) { + const updatedCameras = currentCameras + ? [...currentCameras] + : mainCamera !== undefined && item !== mainCamera // If mainCamera exists and this isn’t it + ? [mainCamera] // Start with mainCamera when transitioning from undefined + : []; // Otherwise start empty + if (!updatedCameras.includes(item)) { + updatedCameras.push(item); + } + setCurrentCameras(updatedCameras); + } else { + const updatedCameras = currentCameras + ? [...currentCameras] + : []; + // can not deselect the last item + if (updatedCameras.length > 1) { + updatedCameras.splice(updatedCameras.indexOf(item), 1); + setCurrentCameras(updatedCameras); + } + } + }} + /> + ))} +
    +
    + +
    + + +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/FilterSwitch.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/FilterSwitch.tsx new file mode 100644 index 0000000..d282b9e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/FilterSwitch.tsx @@ -0,0 +1,53 @@ +import { Switch } from "../ui/switch"; +import { Label } from "../ui/label"; +import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel"; + +type FilterSwitchProps = { + label: string; + disabled?: boolean; + isChecked: boolean; + isCameraName?: boolean; + type?: string; + extraValue?: string; + onCheckedChange: (checked: boolean) => void; +}; +export default function FilterSwitch({ + label, + disabled = false, + isChecked, + type = "", + extraValue = "", + onCheckedChange, +}: FilterSwitchProps) { + return ( +
    + {type === "camera" ? ( + + ) : type === "zone" ? ( + + ) : ( + + )} + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/LogSettingsButton.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/LogSettingsButton.tsx new file mode 100644 index 0000000..024f04c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/LogSettingsButton.tsx @@ -0,0 +1,169 @@ +import { Button } from "../ui/button"; +import { FaCog } from "react-icons/fa"; +import { isMobile } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { LogSettingsType, LogSeverity } from "@/types/log"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import FilterSwitch from "./FilterSwitch"; +import { useTranslation } from "react-i18next"; + +type LogSettingsButtonProps = { + selectedLabels?: LogSeverity[]; + updateLabelFilter: (labels: LogSeverity[] | undefined) => void; + logSettings?: LogSettingsType; + setLogSettings: (logSettings: LogSettingsType) => void; +}; +export function LogSettingsButton({ + selectedLabels, + updateLabelFilter, + logSettings, + setLogSettings, +}: LogSettingsButtonProps) { + const { t } = useTranslation(["components/filter"]); + const trigger = ( + + ); + const content = ( +
    +
    +
    +
    {t("filter")}
    +
    + {t("logSettings.filterBySeverity")} +
    +
    + +
    + +
    +
    +
    {t("logSettings.loading.title")}
    +
    +
    + {t("logSettings.loading.desc")} +
    + { + setLogSettings({ + disableStreaming: isChecked, + }); + }} + /> +
    +
    +
    +
    + ); + + if (isMobile) { + return ( + + {trigger} + + {content} + + + ); + } + + return ( + + {trigger} + {content} + + ); +} + +type GeneralFilterContentProps = { + selectedLabels: LogSeverity[] | undefined; + updateLabelFilter: (labels: LogSeverity[] | undefined) => void; +}; +export function GeneralFilterContent({ + selectedLabels, + updateLabelFilter, +}: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter"]); + return ( + <> +
    +
    + + { + if (isChecked) { + updateLabelFilter(undefined); + } + }} + /> +
    +
    + {["debug", "info", "warning", "error"].map((item) => ( +
    + + { + if (isChecked) { + const updatedLabels = selectedLabels + ? [...selectedLabels] + : []; + + updatedLabels.push(item as LogSeverity); + updateLabelFilter(updatedLabels); + } else { + const updatedLabels = selectedLabels + ? [...selectedLabels] + : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice( + updatedLabels.indexOf(item as LogSeverity), + 1, + ); + updateLabelFilter(updatedLabels); + } + } + }} + /> +
    + ))} +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/ReviewActionGroup.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/ReviewActionGroup.tsx new file mode 100644 index 0000000..31c5a56 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/ReviewActionGroup.tsx @@ -0,0 +1,211 @@ +import { FaCircleCheck, FaCircleXmark } from "react-icons/fa6"; +import { useCallback, useState } from "react"; +import axios from "axios"; +import { Button, buttonVariants } from "../ui/button"; +import { isDesktop } from "react-device-detect"; +import { FaCompactDisc } from "react-icons/fa"; +import { HiTrash } from "react-icons/hi"; +import { ReviewSegment } from "@/types/review"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { Trans, useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { useIsAdmin } from "@/hooks/use-is-admin"; + +type ReviewActionGroupProps = { + selectedReviews: ReviewSegment[]; + setSelectedReviews: (reviews: ReviewSegment[]) => void; + onExport: (id: string) => void; + pullLatestData: () => void; +}; +export default function ReviewActionGroup({ + selectedReviews, + setSelectedReviews, + onExport, + pullLatestData, +}: ReviewActionGroupProps) { + const { t } = useTranslation(["components/dialog"]); + const isAdmin = useIsAdmin(); + const onClearSelected = useCallback(() => { + setSelectedReviews([]); + }, [setSelectedReviews]); + + const allReviewed = selectedReviews.every( + (review) => review.has_been_reviewed, + ); + + const onToggleReviewed = useCallback(async () => { + const ids = selectedReviews.map((review) => review.id); + await axios.post(`reviews/viewed`, { + ids, + reviewed: !allReviewed, + }); + setSelectedReviews([]); + pullLatestData(); + }, [selectedReviews, setSelectedReviews, pullLatestData, allReviewed]); + + const onDelete = useCallback(() => { + const ids = selectedReviews.map((review) => review.id); + axios + .post(`reviews/delete`, { ids }) + .then((resp) => { + if (resp.status === 200) { + toast.success(t("recording.confirmDelete.toast.success"), { + position: "top-center", + }); + setSelectedReviews([]); + pullLatestData(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("recording.confirmDelete.toast.error", { + error: errorMessage, + }), + { + position: "top-center", + }, + ); + }); + }, [selectedReviews, setSelectedReviews, pullLatestData, t]); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [bypassDialog, setBypassDialog] = useState(false); + + useKeyboardListener(["Shift"], (_, modifiers) => { + setBypassDialog(modifiers.shift); + return false; + }); + + const handleDelete = useCallback(() => { + if (bypassDialog) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialog, onDelete]); + + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("recording.confirmDelete.title")} + + + + + recording.confirmDelete.desc.selected + + + + + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + +
    +
    +
    + {t("selected", { + ns: "views/events", + count: selectedReviews.length, + })} +
    +
    {"|"}
    +
    + {t("button.unselect", { ns: "common" })} +
    +
    +
    + {selectedReviews.length == 1 && ( + + )} + + {isAdmin && ( + + )} +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/ReviewFilterGroup.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/ReviewFilterGroup.tsx new file mode 100644 index 0000000..76274ec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/ReviewFilterGroup.tsx @@ -0,0 +1,672 @@ +import { Button } from "../ui/button"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; +import { + RecordingsSummary, + ReviewFilter, + ReviewSeverity, + ReviewSummary, +} from "@/types/review"; +import { getEndOfDayTimestamp } from "@/utils/dateUtil"; +import { FaCheckCircle, FaFilter, FaRunning } from "react-icons/fa"; +import { isDesktop, isMobile } from "react-device-detect"; +import { Switch } from "../ui/switch"; +import { Label } from "../ui/label"; +import MobileReviewSettingsDrawer, { + DrawerFeatures, +} from "../overlay/MobileReviewSettingsDrawer"; +import useOptimisticState from "@/hooks/use-optimistic-state"; +import FilterSwitch from "./FilterSwitch"; +import { FilterList, GeneralFilter } from "@/types/filter"; +import CalendarFilterButton from "./CalendarFilterButton"; +import { CamerasFilterButton } from "./CamerasFilterButton"; +import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; +import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; + +const REVIEW_FILTERS = [ + "cameras", + "reviewed", + "date", + "general", + "motionOnly", +] as const; +type ReviewFilters = (typeof REVIEW_FILTERS)[number]; +const DEFAULT_REVIEW_FILTERS: ReviewFilters[] = [ + "cameras", + "reviewed", + "date", + "general", + "motionOnly", +]; + +type ReviewFilterGroupProps = { + filters?: ReviewFilters[]; + currentSeverity?: ReviewSeverity; + reviewSummary?: ReviewSummary; + recordingsSummary?: RecordingsSummary; + filter?: ReviewFilter; + motionOnly: boolean; + filterList?: FilterList; + showReviewed: boolean; + mainCamera?: string; + setShowReviewed: (show: boolean) => void; + onUpdateFilter: (filter: ReviewFilter) => void; + setMotionOnly: React.Dispatch>; +}; + +export default function ReviewFilterGroup({ + filters = DEFAULT_REVIEW_FILTERS, + currentSeverity, + reviewSummary, + recordingsSummary, + filter, + motionOnly, + filterList, + showReviewed, + mainCamera, + setShowReviewed, + onUpdateFilter, + setMotionOnly, +}: ReviewFilterGroupProps) { + const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); + + const allLabels = useMemo(() => { + if (filterList?.labels) { + return filterList.labels; + } + + if (!config) { + return []; + } + + const labels = new Set(); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); + + cameras.forEach((camera) => { + if (camera == "birdseye") { + return; + } + const cameraConfig = config.cameras[camera]; + cameraConfig.objects.track.forEach((label) => { + labels.add(label); + }); + + if (cameraConfig.type == "lpr") { + labels.add("license_plate"); + } + + if (cameraConfig.audio.enabled_in_config) { + cameraConfig.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + + return [...labels].sort(); + }, [config, filterList, filter, allowedCameras]); + + const allZones = useMemo(() => { + if (filterList?.zones) { + return filterList.zones; + } + + if (!config) { + return []; + } + + const zones = new Set(); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); + + cameras.forEach((camera) => { + if (camera == "birdseye") { + return; + } + const cameraConfig = config.cameras[camera]; + cameraConfig.review.alerts.required_zones.forEach((zone) => { + zones.add(zone); + }); + cameraConfig.review.detections.required_zones.forEach((zone) => { + zones.add(zone); + }); + }); + + return [...zones].sort(); + }, [config, filterList, filter, allowedCameras]); + + const filterValues = useMemo( + () => ({ + cameras: allowedCameras.sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), + labels: Object.values(allLabels || {}), + zones: Object.values(allZones || {}), + }), + [config, allLabels, allZones, allowedCameras], + ); + + const groups = useMemo(() => { + if (!config) { + return []; + } + + return Object.entries(config.camera_groups).sort( + (a, b) => a[1].order - b[1].order, + ); + }, [config]); + + const mobileSettingsFeatures = useMemo(() => { + const features: DrawerFeatures[] = []; + + if (filters.includes("date")) { + features.push("calendar"); + } + + if (filters.includes("general")) { + features.push("filter"); + } + + return features; + }, [filters]); + + // handle updating filters + + const onUpdateSelectedDay = useCallback( + (day?: Date) => { + onUpdateFilter({ + ...filter, + after: day == undefined ? undefined : day.getTime() / 1000, + before: day == undefined ? undefined : getEndOfDayTimestamp(day), + }); + }, + [filter, onUpdateFilter], + ); + + return ( +
    + {filters.includes("cameras") && ( + { + onUpdateFilter({ ...filter, cameras: newCameras }); + }} + /> + )} + {filters.includes("reviewed") && ( + + )} + {isDesktop && filters.includes("date") && ( + + )} + {filters.includes("motionOnly") && ( + + )} + {isDesktop && filters.includes("general") && ( + { + onUpdateFilter({ ...filter, ...general }); + }} + /> + )} + {isMobile && mobileSettingsFeatures.length > 0 && ( + {}} + setRange={() => {}} + showExportPreview={false} + setShowExportPreview={() => {}} + /> + )} +
    + ); +} + +type ShowReviewedFilterProps = { + showReviewed: boolean; + setShowReviewed: (reviewed: boolean) => void; +}; +function ShowReviewFilter({ + showReviewed, + setShowReviewed, +}: ShowReviewedFilterProps) { + const { t } = useTranslation(["components/filter"]); + const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState( + showReviewed, + setShowReviewed, + ); + return ( + <> +
    + + setShowReviewedSwitch(showReviewedSwitch == false ? true : false) + } + /> + +
    + + + + ); +} + +type GeneralFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + currentSeverity?: ReviewSeverity; + showAll: boolean; + allZones: string[]; + selectedZones?: string[]; + filter?: GeneralFilter; + onUpdateFilter: (filter: GeneralFilter) => void; +}; + +function GeneralFilterButton({ + allLabels, + selectedLabels, + filter, + currentSeverity, + showAll, + allZones, + selectedZones, + onUpdateFilter, +}: GeneralFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); + const [open, setOpen] = useState(false); + const [currentFilter, setCurrentFilter] = useState({ + labels: selectedLabels, + zones: selectedZones, + showAll: showAll, + ...filter, + }); + + // Update local state when props change + + useEffect(() => { + setCurrentFilter({ + labels: selectedLabels, + zones: selectedZones, + showAll: showAll, + ...filter, + }); + // only refresh when state changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedLabels, selectedZones, showAll, filter]); + + const trigger = ( + + ); + const content = ( + { + if (currentFilter !== filter) { + onUpdateFilter(currentFilter); + } + setOpen(false); + }} + onReset={() => { + const resetFilter: GeneralFilter = { + labels: undefined, + zones: undefined, + showAll: false, + }; + setCurrentFilter(resetFilter); + onUpdateFilter(resetFilter); + }} + onClose={() => setOpen(false)} + /> + ); + + return ( + { + if (!open) { + setCurrentFilter({ + labels: selectedLabels, + zones: selectedZones, + showAll: showAll, + ...filter, + }); + } + + setOpen(open); + }} + /> + ); +} + +type GeneralFilterContentProps = { + allLabels: string[]; + allZones: string[]; + currentSeverity?: ReviewSeverity; + filter: GeneralFilter; + selectedLabels?: string[]; + selectedZones?: string[]; + onUpdateFilter: (filter: GeneralFilter) => void; + onApply: () => void; + onReset: () => void; + onClose: () => void; +}; +export function GeneralFilterContent({ + allLabels, + allZones, + currentSeverity, + filter, + onUpdateFilter, + onApply, + onReset, + onClose, +}: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter", "views/events"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const allAudioListenLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return [...labels].sort(); + }, [config]); + return ( + <> +
    + {currentSeverity && ( +
    + + onUpdateFilter({ ...filter, showAll: checked }) + } + /> + + onUpdateFilter({ ...filter, showAll: checked }) + } + /> + +
    + )} +
    + + { + if (isChecked) { + onUpdateFilter({ ...filter, labels: undefined }); + } + }} + /> +
    +
    + {allLabels.map((item) => ( + { + if (isChecked) { + const updatedLabels = filter.labels ? [...filter.labels] : []; + updatedLabels.push(item); + onUpdateFilter({ ...filter, labels: updatedLabels }); + } else { + const updatedLabels = filter.labels ? [...filter.labels] : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice(updatedLabels.indexOf(item), 1); + onUpdateFilter({ ...filter, labels: updatedLabels }); + } + } + }} + /> + ))} +
    + + {allZones && ( + <> + +
    + + { + if (isChecked) { + onUpdateFilter({ ...filter, zones: undefined }); + } + }} + /> +
    +
    + {allZones.map((item) => ( + { + if (isChecked) { + const updatedZones = filter.zones + ? [...filter.zones] + : []; + + updatedZones.push(item); + onUpdateFilter({ ...filter, zones: updatedZones }); + } else { + const updatedZones = filter.zones + ? [...filter.zones] + : []; + + // can not deselect the last item + if (updatedZones.length > 1) { + updatedZones.splice(updatedZones.indexOf(item), 1); + onUpdateFilter({ ...filter, zones: updatedZones }); + } + } + }} + /> + ))} +
    + + )} +
    + +
    + + +
    + + ); +} + +type ShowMotionOnlyButtonProps = { + motionOnly: boolean; + setMotionOnly: React.Dispatch>; +}; +function ShowMotionOnlyButton({ + motionOnly, + setMotionOnly, +}: ShowMotionOnlyButtonProps) { + const { t } = useTranslation(["views/events", "components/filter"]); + const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState( + motionOnly, + setMotionOnly, + ); + + return ( + <> +
    + + +
    + +
    + +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/SearchActionGroup.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/SearchActionGroup.tsx new file mode 100644 index 0000000..62a3dc6 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/SearchActionGroup.tsx @@ -0,0 +1,165 @@ +import { useCallback, useState } from "react"; +import axios from "axios"; +import { Button, buttonVariants } from "../ui/button"; +import { isDesktop } from "react-device-detect"; +import { HiTrash } from "react-icons/hi"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { toast } from "sonner"; +import { Trans, useTranslation } from "react-i18next"; +import { useIsAdmin } from "@/hooks/use-is-admin"; + +type SearchActionGroupProps = { + selectedObjects: string[]; + setSelectedObjects: (ids: string[]) => void; + pullLatestData: () => void; + onSelectAllObjects: () => void; + totalItems: number; +}; +export default function SearchActionGroup({ + selectedObjects, + setSelectedObjects, + pullLatestData, + onSelectAllObjects, + totalItems, +}: SearchActionGroupProps) { + const { t } = useTranslation(["components/filter"]); + const isAdmin = useIsAdmin(); + const onClearSelected = useCallback(() => { + setSelectedObjects([]); + }, [setSelectedObjects]); + + const onDelete = useCallback(async () => { + await axios + .delete(`events/`, { + data: { event_ids: selectedObjects }, + }) + .then((resp) => { + if (resp.status == 200) { + toast.success(t("trackedObjectDelete.toast.success"), { + position: "top-center", + }); + setSelectedObjects([]); + pullLatestData(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("trackedObjectDelete.toast.error", { errorMessage }), { + position: "top-center", + }); + }); + }, [selectedObjects, setSelectedObjects, pullLatestData, t]); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [bypassDialog, setBypassDialog] = useState(false); + + useKeyboardListener(["Shift"], (_, modifiers) => { + setBypassDialog(modifiers.shift); + return false; + }); + + const handleDelete = useCallback(() => { + if (bypassDialog) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialog, onDelete]); + + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("trackedObjectDelete.title")} + + + + + trackedObjectDelete.desc + + + + + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + +
    +
    +
    + {t("selected", { + ns: "views/events", + count: selectedObjects.length, + })} +
    +
    {"|"}
    +
    + {t("button.unselect", { ns: "common" })} +
    + {selectedObjects.length < totalItems && ( + <> +
    {"|"}
    +
    + {t("select_all", { ns: "views/events" })} +
    + + )} +
    + {isAdmin && ( +
    + +
    + )} +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/SearchFilterGroup.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/SearchFilterGroup.tsx new file mode 100644 index 0000000..3c44cad --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/SearchFilterGroup.tsx @@ -0,0 +1,621 @@ +import { Button } from "../ui/button"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; +import { getEndOfDayTimestamp } from "@/utils/dateUtil"; +import { isDesktop, isMobile } from "react-device-detect"; +import { Switch } from "../ui/switch"; +import { Label } from "../ui/label"; +import FilterSwitch from "./FilterSwitch"; +import { FilterList } from "@/types/filter"; +import { CamerasFilterButton } from "./CamerasFilterButton"; +import { + DEFAULT_SEARCH_FILTERS, + SearchFilter, + SearchFilters, + SearchSource, + SearchSortType, +} from "@/types/search"; +import { DateRange } from "react-day-picker"; +import { cn } from "@/lib/utils"; +import { MdLabel, MdSort } from "react-icons/md"; +import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; +import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog"; +import { CalendarRangeFilterButton } from "./CalendarFilterButton"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; + +type SearchFilterGroupProps = { + className: string; + filters?: SearchFilters[]; + filter?: SearchFilter; + filterList?: FilterList; + onUpdateFilter: (filter: SearchFilter) => void; +}; +export default function SearchFilterGroup({ + className, + filters = DEFAULT_SEARCH_FILTERS, + filter, + filterList, + onUpdateFilter, +}: SearchFilterGroupProps) { + const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const allowedCameras = useAllowedCameras(); + + const allLabels = useMemo(() => { + if (filterList?.labels) { + return filterList.labels; + } + + if (!config) { + return []; + } + + const labels = new Set(); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); + + cameras.forEach((camera) => { + if (camera == "birdseye") { + return; + } + const cameraConfig = config.cameras[camera]; + + if (!cameraConfig) { + return; + } + + cameraConfig.objects.track.forEach((label) => { + if (!config.model.all_attributes.includes(label)) { + labels.add(label); + } + }); + + if (cameraConfig.type == "lpr") { + labels.add("license_plate"); + } + + if (cameraConfig.audio.enabled_in_config) { + cameraConfig.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + + return [...labels].sort(); + }, [config, filterList, filter, allowedCameras]); + + const allZones = useMemo(() => { + if (filterList?.zones) { + return filterList.zones; + } + + if (!config) { + return []; + } + + const zones = new Set(); + const cameras = (filter?.cameras || allowedCameras).filter((camera) => + allowedCameras.includes(camera), + ); + + cameras.forEach((camera) => { + if (camera == "birdseye") { + return; + } + + const cameraConfig = config.cameras[camera]; + + if (!cameraConfig) { + return; + } + + Object.entries(cameraConfig.zones).map(([name, _]) => { + zones.add(name); + }); + }); + + return [...zones].sort(); + }, [config, filterList, filter, allowedCameras]); + + const filterValues = useMemo( + () => ({ + cameras: allowedCameras, + labels: Object.values(allLabels || {}), + zones: Object.values(allZones || {}), + search_type: ["thumbnail", "description"] as SearchSource[], + }), + [allLabels, allZones, allowedCameras], + ); + + const availableSortTypes = useMemo(() => { + const sortTypes = ["date_asc", "date_desc", "score_desc", "score_asc"]; + if (filter?.min_speed || filter?.max_speed) { + sortTypes.push("speed_desc", "speed_asc"); + } + if (filter?.event_id || filter?.query) { + sortTypes.push("relevance"); + } + return sortTypes as SearchSortType[]; + }, [filter]); + + const defaultSortType = useMemo(() => { + if (filter?.query || filter?.event_id) { + return "relevance"; + } else { + return "date_desc"; + } + }, [filter]); + + const groups = useMemo(() => { + if (!config) { + return []; + } + + return Object.entries(config.camera_groups).sort( + (a, b) => a[1].order - b[1].order, + ); + }, [config]); + + // handle updating filters + + const onUpdateSelectedRange = useCallback( + (range?: DateRange) => { + onUpdateFilter({ + ...filter, + after: + range?.from == undefined ? undefined : range.from.getTime() / 1000, + before: + range?.to == undefined ? undefined : getEndOfDayTimestamp(range.to), + }); + }, + [filter, onUpdateFilter], + ); + + return ( +
    + {filters.includes("cameras") && ( + { + onUpdateFilter({ ...filter, cameras: newCameras }); + }} + /> + )} + {filters.includes("general") && ( + { + onUpdateFilter({ ...filter, labels: newLabels }); + }} + /> + )} + {filters.includes("date") && ( + + )} + + {filters.includes("sort") && Object.keys(filter ?? {}).length > 0 && ( + { + onUpdateFilter({ ...filter, sort: newSort }); + }} + /> + )} +
    + ); +} + +type GeneralFilterButtonProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; +}; +function GeneralFilterButton({ + allLabels, + selectedLabels, + updateLabelFilter, +}: GeneralFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); + const [open, setOpen] = useState(false); + const [currentLabels, setCurrentLabels] = useState( + selectedLabels, + ); + + const buttonText = useMemo(() => { + if (isMobile) { + return t("labels.all.short"); + } + + if (!selectedLabels || selectedLabels.length == 0) { + return t("labels.all.title"); + } + + if (selectedLabels.length == 1) { + return getTranslatedLabel(selectedLabels[0]); + } + + return t("labels.count", { + count: selectedLabels.length, + }); + }, [selectedLabels, t]); + + // ui + + useEffect(() => { + setCurrentLabels(selectedLabels); + // only refresh when state changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedLabels]); + + const trigger = ( + + ); + const content = ( + setOpen(false)} + /> + ); + + return ( + { + if (!open) { + setCurrentLabels(selectedLabels); + } + + setOpen(open); + }} + /> + ); +} + +type GeneralFilterContentProps = { + allLabels: string[]; + selectedLabels: string[] | undefined; + currentLabels: string[] | undefined; + updateLabelFilter: (labels: string[] | undefined) => void; + setCurrentLabels: (labels: string[] | undefined) => void; + onClose: () => void; +}; +export function GeneralFilterContent({ + allLabels, + selectedLabels, + currentLabels, + updateLabelFilter, + setCurrentLabels, + onClose, +}: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const allAudioListenLabels = useMemo(() => { + if (!config) { + return []; + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return [...labels].sort(); + }, [config]); + + return ( + <> +
    +
    + + { + if (isChecked) { + setCurrentLabels(undefined); + } + }} + /> +
    +
    + {allLabels.map((item) => ( + { + if (isChecked) { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + updatedLabels.push(item); + setCurrentLabels(updatedLabels); + } else { + const updatedLabels = currentLabels ? [...currentLabels] : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice(updatedLabels.indexOf(item), 1); + setCurrentLabels(updatedLabels); + } + } + }} + /> + ))} +
    +
    + +
    + + +
    + + ); +} + +type SortTypeButtonProps = { + availableSortTypes: SearchSortType[]; + defaultSortType: SearchSortType; + selectedSortType: SearchSortType | undefined; + updateSortType: (sortType: SearchSortType | undefined) => void; +}; +function SortTypeButton({ + availableSortTypes, + defaultSortType, + selectedSortType, + updateSortType, +}: SortTypeButtonProps) { + const { t } = useTranslation(["components/filter"]); + const [open, setOpen] = useState(false); + const [currentSortType, setCurrentSortType] = useState< + SearchSortType | undefined + >(selectedSortType as SearchSortType); + + // ui + + useEffect(() => { + setCurrentSortType(selectedSortType); + // only refresh when state changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSortType]); + + const trigger = ( + + ); + const content = ( + setOpen(false)} + /> + ); + + return ( + { + if (!open) { + setCurrentSortType(selectedSortType); + } + + setOpen(open); + }} + /> + ); +} + +type SortTypeContentProps = { + availableSortTypes: SearchSortType[]; + defaultSortType: SearchSortType; + selectedSortType: SearchSortType | undefined; + currentSortType: SearchSortType | undefined; + updateSortType: (sort_type: SearchSortType | undefined) => void; + setCurrentSortType: (sort_type: SearchSortType | undefined) => void; + onClose: () => void; +}; +export function SortTypeContent({ + availableSortTypes, + defaultSortType, + selectedSortType, + currentSortType, + updateSortType, + setCurrentSortType, + onClose, +}: SortTypeContentProps) { + const { t } = useTranslation(["components/filter"]); + const sortLabels = { + date_asc: t("sort.dateAsc"), + date_desc: t("sort.dateDesc"), + score_asc: t("sort.scoreAsc"), + score_desc: t("sort.scoreDesc"), + speed_asc: t("sort.speedAsc"), + speed_desc: t("sort.speedDesc"), + relevance: t("sort.relevance"), + }; + return ( + <> +
    +
    + + setCurrentSortType(value as SearchSortType) + } + className="w-full space-y-1" + > + {availableSortTypes.map((value) => ( +
    + + +
    + ))} +
    +
    +
    + +
    + + +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/filter/ZoneMaskFilter.tsx b/sam2-cpu/frigate-dev/web/src/components/filter/ZoneMaskFilter.tsx new file mode 100644 index 0000000..6512346 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/filter/ZoneMaskFilter.tsx @@ -0,0 +1,148 @@ +import { Button } from "../ui/button"; +import { FaFilter } from "react-icons/fa"; +import { isMobile } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { PolygonType } from "@/types/canvas"; +import { Label } from "../ui/label"; +import { Switch } from "../ui/switch"; +import { DropdownMenuSeparator } from "../ui/dropdown-menu"; +import { useTranslation } from "react-i18next"; + +type ZoneMaskFilterButtonProps = { + selectedZoneMask?: PolygonType[]; + updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void; +}; +export function ZoneMaskFilterButton({ + selectedZoneMask, + updateZoneMaskFilter, +}: ZoneMaskFilterButtonProps) { + const { t } = useTranslation(["components/filter"]); + const trigger = ( + + ); + const content = ( + + ); + + if (isMobile) { + return ( + + {trigger} + + {content} + + + ); + } + + return ( + + {trigger} + {content} + + ); +} + +type GeneralFilterContentProps = { + selectedZoneMask: PolygonType[] | undefined; + updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void; +}; +export function GeneralFilterContent({ + selectedZoneMask, + updateZoneMaskFilter, +}: GeneralFilterContentProps) { + const { t } = useTranslation(["components/filter"]); + return ( + <> +
    +
    + + { + if (isChecked) { + updateZoneMaskFilter(undefined); + } + }} + /> +
    + +
    + {["zone", "motion_mask", "object_mask"].map((item) => ( +
    + + { + if (isChecked) { + const updatedLabels = selectedZoneMask + ? [...selectedZoneMask] + : []; + + updatedLabels.push(item as PolygonType); + updateZoneMaskFilter(updatedLabels); + } else { + const updatedLabels = selectedZoneMask + ? [...selectedZoneMask] + : []; + + // can not deselect the last item + if (updatedLabels.length > 1) { + updatedLabels.splice( + updatedLabels.indexOf(item as PolygonType), + 1, + ); + updateZoneMaskFilter(updatedLabels); + } + } + }} + /> +
    + ))} +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/graph/CombinedStorageGraph.tsx b/sam2-cpu/frigate-dev/web/src/components/graph/CombinedStorageGraph.tsx new file mode 100644 index 0000000..4279e73 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/graph/CombinedStorageGraph.tsx @@ -0,0 +1,265 @@ +import { useTheme } from "@/context/theme-provider"; +import { generateColors } from "@/utils/colorUtil"; +import { useCallback, useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { getUnitSize } from "@/utils/storageUtil"; + +import { CiCircleAlert } from "react-icons/ci"; +import { useTranslation } from "react-i18next"; + +type CameraStorage = { + [key: string]: { + bandwidth: number; + usage: number; + usage_percent: number; + }; +}; + +type TotalStorage = { + used: number; + camera: number; + total: number; +}; + +type CombinedStorageGraphProps = { + graphId: string; + cameraStorage: CameraStorage; + totalStorage: TotalStorage; +}; +export function CombinedStorageGraph({ + graphId, + cameraStorage, + totalStorage, +}: CombinedStorageGraphProps) { + const { t } = useTranslation(["views/system"]); + + const { theme, systemTheme } = useTheme(); + + const entities = Object.keys(cameraStorage); + const colors = generateColors(entities.length); + + const series = entities.map((entity, index) => ({ + name: entity, + data: [(cameraStorage[entity].usage / totalStorage.total) * 100], + usage: cameraStorage[entity].usage, + bandwidth: cameraStorage[entity].bandwidth, + color: colors[index], // Assign the corresponding color + })); + + // Add the unused percentage to the series + series.push({ + name: "Other", + data: [ + ((totalStorage.used - totalStorage.camera) / totalStorage.total) * 100, + ], + usage: totalStorage.used - totalStorage.camera, + bandwidth: 0, + color: (systemTheme || theme) == "dark" ? "#606060" : "#D5D5D5", + }); + series.push({ + name: "Unused", + data: [ + ((totalStorage.total - totalStorage.used) / totalStorage.total) * 100, + ], + usage: totalStorage.total - totalStorage.used, + bandwidth: 0, + color: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + }); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + stacked: true, + stackType: "100%", + }, + grid: { + show: false, + padding: { + bottom: -45, + top: -40, + left: -20, + right: -20, + }, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + plotOptions: { + bar: { + horizontal: true, + }, + }, + states: { + active: { + filter: { + type: "none", + }, + }, + hover: { + filter: { + type: "none", + }, + }, + }, + tooltip: { + enabled: false, + x: { + show: false, + }, + y: { + formatter: function (val, { seriesIndex }) { + if (series[seriesIndex]) { + const usage = series[seriesIndex].usage; + return `${getUnitSize(usage)} (${val.toFixed(2)}%)`; + } + }, + }, + theme: systemTheme || theme, + }, + xaxis: { + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + labels: { + formatter: function (val) { + return val + "%"; + }, + }, + min: 0, + max: 100, + }, + yaxis: { + show: false, + min: 0, + max: 100, + }, + } as ApexCharts.ApexOptions; + }, [graphId, systemTheme, theme, series]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + // convenience + + const getItemTitle = useCallback( + (name: string) => { + if (name == "Unused") { + return t("storage.cameraStorage.unused.title"); + } else if (name == "Other") { + return t("label.other", { ns: "common" }); + } else { + return name.replaceAll("_", " "); + } + }, + [t], + ); + + return ( +
    +
    +
    +
    + {getUnitSize(totalStorage.camera)} +
    +
    /
    +
    + {getUnitSize(totalStorage.total)} +
    +
    +
    +
    + +
    +
    + + + + {t("storage.cameraStorage.camera")} + {t("storage.cameraStorage.storageUsed")} + + {t("storage.cameraStorage.percentageOfTotalUsed")} + + {t("storage.cameraStorage.bandwidth")} + + + + {series.map((item) => ( + + + {" "} +
    + {getItemTitle(item.name)} + {(item.name === "Unused" || item.name == "Other") && ( + + + + + +
    + {t("storage.cameraStorage.unused.tips")} +
    +
    +
    + )} +
    + {getUnitSize(item.usage ?? 0)} + {item.data[0].toFixed(2)}% + + {item.name === "Unused" || item.name == "Other" + ? "—" + : `${getUnitSize(item.bandwidth)} / hour`} + +
    + ))} +
    +
    +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/graph/LineGraph.tsx b/sam2-cpu/frigate-dev/web/src/components/graph/LineGraph.tsx new file mode 100644 index 0000000..ad841de --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/graph/LineGraph.tsx @@ -0,0 +1,292 @@ +import { useTheme } from "@/context/theme-provider"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { useCallback, useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { isMobileOnly } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import { MdCircle } from "react-icons/md"; +import useSWR from "swr"; + +const GRAPH_COLORS = ["#5C7CFA", "#ED5CFA", "#FAD75C"]; + +type CameraLineGraphProps = { + graphId: string; + unit: string; + dataLabels: string[]; + updateTimes: number[]; + data: ApexAxisChartSeries; +}; +export function CameraLineGraph({ + graphId, + unit, + dataLabels, + updateTimes, + data, +}: CameraLineGraphProps) { + const { t } = useTranslation(["views/system", "common"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const lastValues = useMemo(() => { + if (!dataLabels || !data || data.length == 0) { + return undefined; + } + + return dataLabels.map( + (_, labelIdx) => + // @ts-expect-error y is valid + data[labelIdx].data[data[labelIdx].data.length - 1]?.y ?? 0, + ) as number[]; + }, [data, dataLabels]); + + const { theme, systemTheme } = useTheme(); + + const locale = useDateLocale(); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const format = useMemo(() => { + return t(`time.formattedTimestampHourMinute.${timeFormat}`, { + ns: "common", + }); + }, [t, timeFormat]); + + const formatTime = useCallback( + (val: unknown) => { + return formatUnixTimestampToDateTime( + updateTimes[Math.round(val as number)], + { + timezone: config?.ui.timezone, + date_format: format, + locale, + }, + ); + }, + [config?.ui.timezone, format, locale, updateTimes], + ); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + colors: GRAPH_COLORS, + grid: { + show: false, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + stroke: { + width: 1, + }, + tooltip: { + theme: systemTheme || theme, + }, + markers: { + size: 0, + }, + xaxis: { + tickAmount: isMobileOnly ? 2 : 3, + tickPlacement: "on", + labels: { + rotate: 0, + formatter: formatTime, + style: { + colors: "#6B6B6B", + }, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.ceil(val).toString(), + style: { + colors: "#6B6B6B", + }, + }, + min: 0, + }, + } as ApexCharts.ApexOptions; + }, [graphId, systemTheme, theme, formatTime]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + return ( +
    + {lastValues && ( +
    + {dataLabels.map((label, labelIdx) => ( +
    + +
    + {t("cameras.label." + label)} +
    +
    + {lastValues[labelIdx]} + {unit} +
    +
    + ))} +
    + )} + +
    + ); +} + +type EventsPerSecondLineGraphProps = { + graphId: string; + unit: string; + name: string; + updateTimes: number[]; + data: ApexAxisChartSeries; +}; +export function EventsPerSecondsLineGraph({ + graphId, + unit, + name, + updateTimes, + data, +}: EventsPerSecondLineGraphProps) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const { theme, systemTheme } = useTheme(); + + const lastValue = useMemo( + // @ts-expect-error y is valid + () => data[0].data[data[0].data.length - 1]?.y ?? 0, + [data], + ); + + const locale = useDateLocale(); + const { t } = useTranslation(["common"]); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const format = useMemo(() => { + return t(`time.formattedTimestampHourMinute.${timeFormat}`, { + ns: "common", + }); + }, [t, timeFormat]); + + const formatTime = useCallback( + (val: unknown) => { + return formatUnixTimestampToDateTime( + updateTimes[Math.round(val as number) - 1], + { + timezone: config?.ui.timezone, + date_format: format, + locale, + }, + ); + }, + [config?.ui.timezone, format, locale, updateTimes], + ); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + colors: GRAPH_COLORS, + grid: { + show: false, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + stroke: { + width: 1, + }, + tooltip: { + theme: systemTheme || theme, + }, + markers: { + size: 0, + }, + xaxis: { + tickAmount: isMobileOnly ? 2 : 3, + tickPlacement: "on", + labels: { + rotate: 0, + formatter: formatTime, + style: { + colors: "#6B6B6B", + }, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.ceil(val).toString(), + style: { + colors: "#6B6B6B", + }, + }, + min: 0, + }, + } as ApexCharts.ApexOptions; + }, [graphId, systemTheme, theme, formatTime]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + return ( +
    +
    +
    {name}
    +
    + {lastValue} + {unit} +
    +
    + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/graph/StorageGraph.tsx b/sam2-cpu/frigate-dev/web/src/components/graph/StorageGraph.tsx new file mode 100644 index 0000000..a021273 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/graph/StorageGraph.tsx @@ -0,0 +1,113 @@ +import { useTheme } from "@/context/theme-provider"; +import { useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { getUnitSize } from "@/utils/storageUtil"; + +type StorageGraphProps = { + graphId: string; + used: number; + total: number; +}; +export function StorageGraph({ graphId, used, total }: StorageGraphProps) { + const { theme, systemTheme } = useTheme(); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + grid: { + show: false, + padding: { + bottom: -40, + top: -60, + left: -20, + right: 0, + }, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + plotOptions: { + bar: { + horizontal: true, + }, + }, + states: { + active: { + filter: { + type: "none", + }, + }, + hover: { + filter: { + type: "none", + }, + }, + }, + tooltip: { + enabled: false, + }, + xaxis: { + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + labels: { + show: false, + }, + }, + yaxis: { + show: false, + min: 0, + max: 100, + }, + } as ApexCharts.ApexOptions; + }, [graphId, systemTheme, theme]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + return ( +
    +
    +
    +
    {getUnitSize(used)}
    +
    /
    +
    + {getUnitSize(total)} +
    +
    +
    + {Math.round((used / total) * 100)}% +
    +
    +
    + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/graph/SystemGraph.tsx b/sam2-cpu/frigate-dev/web/src/components/graph/SystemGraph.tsx new file mode 100644 index 0000000..fcafeda --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/graph/SystemGraph.tsx @@ -0,0 +1,198 @@ +import { useTheme } from "@/context/theme-provider"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { Threshold } from "@/types/graph"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { useCallback, useEffect, useMemo } from "react"; +import Chart from "react-apexcharts"; +import { isMobileOnly } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; + +type ThresholdBarGraphProps = { + graphId: string; + name: string; + unit: string; + threshold: Threshold; + updateTimes: number[]; + data: ApexAxisChartSeries; +}; +export function ThresholdBarGraph({ + graphId, + name, + unit, + threshold, + updateTimes, + data, +}: ThresholdBarGraphProps) { + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const lastValue = useMemo( + // @ts-expect-error y is valid + () => data[0].data[data[0].data.length - 1]?.y ?? 0, + [data], + ); + + const yMax = useMemo(() => { + if (unit != "%") { + return undefined; + } + + // @ts-expect-error y is valid + const yValues: number[] = data[0].data.map((point) => point?.y); + return Math.max(threshold.warning, ...yValues); + }, [data, threshold, unit]); + + const { theme, systemTheme } = useTheme(); + + const locale = useDateLocale(); + const { t } = useTranslation(["common"]); + + const timeFormat = config?.ui.time_format === "24hour" ? "24hour" : "12hour"; + const format = useMemo(() => { + return t(`time.formattedTimestampHourMinute.${timeFormat}`, { + ns: "common", + }); + }, [t, timeFormat]); + + const formatTime = useCallback( + (val: unknown) => { + const dateIndex = Math.round(val as number); + + let timeOffset = 0; + if (dateIndex < 0) { + timeOffset = 5 * Math.abs(dateIndex); + } + return formatUnixTimestampToDateTime( + updateTimes[Math.max(1, dateIndex) - 1] - timeOffset, + { + timezone: config?.ui.timezone, + date_format: format, + locale, + }, + ); + }, + [config?.ui.timezone, format, locale, updateTimes], + ); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + colors: [ + ({ value }: { value: number }) => { + if (value >= threshold.error) { + return "#FA5252"; + } else if (value >= threshold.warning) { + return "#FF9966"; + } else { + return "#217930"; + } + }, + ], + grid: { + show: false, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + plotOptions: { + bar: { + distributed: true, + }, + }, + states: { + active: { + filter: { + type: "none", + }, + }, + }, + tooltip: { + theme: systemTheme || theme, + y: { + formatter: (val) => `${val}${unit}`, + }, + }, + markers: { + size: 0, + }, + xaxis: { + tickAmount: isMobileOnly ? 2 : 3, + tickPlacement: "on", + labels: { + rotate: 0, + formatter: formatTime, + style: { + colors: "#6B6B6B", + }, + }, + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + }, + yaxis: { + show: true, + labels: { + formatter: (val: number) => Math.ceil(val).toString(), + style: { + colors: "#6B6B6B", + }, + }, + min: 0, + max: yMax, + }, + } as ApexCharts.ApexOptions; + }, [graphId, threshold, unit, yMax, systemTheme, theme, formatTime]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + const chartData = useMemo(() => { + if (data.length > 0 && data[0].data.length >= 30) { + return data; + } + + const copiedData = [...data]; + const fakeData = []; + for (let i = data.length; i < 30; i++) { + fakeData.push({ x: i - 30, y: 0 }); + } + + // @ts-expect-error data types are not obvious + copiedData[0].data = [...fakeData, ...data[0].data]; + return copiedData; + }, [data]); + + return ( +
    +
    +
    {name}
    +
    + {lastValue} + {unit} +
    +
    + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/AddFaceIcon.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/AddFaceIcon.tsx new file mode 100644 index 0000000..ce06120 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/AddFaceIcon.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from "react"; +import { LuPlus, LuScanFace } from "react-icons/lu"; +import { cn } from "@/lib/utils"; + +type AddFaceIconProps = { + className?: string; + onClick?: () => void; +}; + +const AddFaceIcon = forwardRef( + ({ className, onClick }, ref) => { + return ( +
    + + +
    + ); + }, +); + +export default AddFaceIcon; diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/FrigatePlusIcon.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/FrigatePlusIcon.tsx new file mode 100644 index 0000000..15e196c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/FrigatePlusIcon.tsx @@ -0,0 +1,26 @@ +import { forwardRef } from "react"; +import { LuPlus } from "react-icons/lu"; +import Logo from "../Logo"; +import { cn } from "@/lib/utils"; + +type FrigatePlusIconProps = { + className?: string; + onClick?: () => void; +}; + +const FrigatePlusIcon = forwardRef( + ({ className, onClick }, ref) => { + return ( +
    + + +
    + ); + }, +); + +export default FrigatePlusIcon; diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/IconPicker.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/IconPicker.tsx new file mode 100644 index 0000000..b74029e --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/IconPicker.tsx @@ -0,0 +1,163 @@ +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { IconType } from "react-icons"; +import * as LuIcons from "react-icons/lu"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { IoClose } from "react-icons/io5"; +import Heading from "../ui/heading"; +import { cn } from "@/lib/utils"; +import { Button } from "../ui/button"; + +import { useTranslation } from "react-i18next"; + +export type IconName = keyof typeof LuIcons; + +export type IconElement = { + name?: string; + Icon?: IconType; +}; + +type IconPickerProps = { + selectedIcon?: IconElement; + setSelectedIcon?: React.Dispatch< + React.SetStateAction + >; +}; + +export default function IconPicker({ + selectedIcon, + setSelectedIcon, +}: IconPickerProps) { + const { t } = useTranslation(["components/icons"]); + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const [searchTerm, setSearchTerm] = useState(""); + + const iconSets = useMemo(() => [...Object.entries(LuIcons)], []); + + const icons = useMemo( + () => + iconSets.filter( + ([name]) => + name.toLowerCase().includes(searchTerm.toLowerCase()) || + searchTerm === "", + ), + [iconSets, searchTerm], + ); + + const handleIconSelect = useCallback( + ({ name, Icon }: IconElement) => { + if (setSelectedIcon) { + setSelectedIcon({ name, Icon }); + } + setSearchTerm(""); + }, + [setSelectedIcon], + ); + + return ( +
    + { + setOpen(open); + }} + > + + {!selectedIcon?.name || !selectedIcon?.Icon ? ( + + ) : ( +
    +
    +
    + +
    + {selectedIcon.name + .replace(/^Lu/, "") + .replace(/([A-Z])/g, " $1")} +
    +
    + + { + handleIconSelect({ name: undefined, Icon: undefined }); + }} + /> +
    +
    + )} +
    + +
    + {t("iconPicker.selectIcon")} + + { + setOpen(false); + }} + /> +
    + setSearchTerm(e.target.value)} + /> +
    +
    + {icons.map(([name, Icon]) => ( +
    + { + handleIconSelect({ name, Icon }); + setOpen(false); + }} + /> +
    + ))} +
    +
    +
    +
    +
    + ); +} + +type IconRendererProps = { + icon: IconType; + size?: number; + className?: string; +}; + +export function IconRenderer({ icon, size, className }: IconRendererProps) { + return <>{React.createElement(icon, { size, className })}; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/LiveIcons.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/LiveIcons.tsx new file mode 100644 index 0000000..9a8d58f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/LiveIcons.tsx @@ -0,0 +1,42 @@ +type LiveIconProps = { + layout?: "list" | "grid"; +}; + +export function LiveGridIcon({ layout }: LiveIconProps) { + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); +} + +export function LiveListIcon({ layout }: LiveIconProps) { + return ( +
    +
    +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/SearchSourceIcon.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/SearchSourceIcon.tsx new file mode 100644 index 0000000..e96ac51 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/SearchSourceIcon.tsx @@ -0,0 +1,26 @@ +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; +import { FaImage } from "react-icons/fa"; +import { LuText } from "react-icons/lu"; + +type SearchSourceIconProps = { + className?: string; + onClick?: () => void; +}; + +const SearchSourceIcon = forwardRef( + ({ className, onClick }, ref) => { + return ( +
    + + +
    + ); + }, +); + +export default SearchSourceIcon; diff --git a/sam2-cpu/frigate-dev/web/src/components/icons/SubFilterIcon.tsx b/sam2-cpu/frigate-dev/web/src/components/icons/SubFilterIcon.tsx new file mode 100644 index 0000000..4860aec --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/icons/SubFilterIcon.tsx @@ -0,0 +1,26 @@ +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; +import { FaCog } from "react-icons/fa"; +import { MdLabelOutline } from "react-icons/md"; + +type SubFilterIconProps = { + className?: string; + onClick?: () => void; +}; + +const SubFilterIcon = forwardRef( + ({ className, onClick }, ref) => { + return ( +
    + + +
    + ); + }, +); + +export default SubFilterIcon; diff --git a/sam2-cpu/frigate-dev/web/src/components/indicators/CameraActivityIndicator.tsx b/sam2-cpu/frigate-dev/web/src/components/indicators/CameraActivityIndicator.tsx new file mode 100644 index 0000000..efeaa6a --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/indicators/CameraActivityIndicator.tsx @@ -0,0 +1,33 @@ +function CameraActivityIndicator() { + return ( +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + +
    + ); +} + +export default CameraActivityIndicator; diff --git a/sam2-cpu/frigate-dev/web/src/components/indicators/Chip.tsx b/sam2-cpu/frigate-dev/web/src/components/indicators/Chip.tsx new file mode 100644 index 0000000..06e905c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/indicators/Chip.tsx @@ -0,0 +1,88 @@ +import { cn } from "@/lib/utils"; +import { LogSeverity } from "@/types/log"; +import { ReactNode, useMemo, useRef } from "react"; +import { isIOS } from "react-device-detect"; +import { CSSTransition } from "react-transition-group"; + +type ChipProps = { + className?: string; + children?: ReactNode | ReactNode[]; + in?: boolean; + onClick?: () => void; +}; + +export default function Chip({ + className, + children, + in: inProp = true, + onClick, +}: ChipProps) { + const nodeRef = useRef(null); + + return ( + +
    { + e.stopPropagation(); + + if (onClick) { + onClick(); + } + }} + > + {children} +
    +
    + ); +} + +type LogChipProps = { + severity: LogSeverity; + onClickSeverity?: () => void; +}; +export function LogChip({ severity, onClickSeverity }: LogChipProps) { + const severityClassName = useMemo(() => { + switch (severity) { + case "info": + return "text-primary/60 bg-secondary hover:bg-secondary/60"; + case "warning": + return "text-warning-foreground bg-warning hover:bg-warning/80"; + case "error": + return "text-destructive-foreground bg-destructive hover:bg-destructive/80"; + } + }, [severity]); + + return ( +
    + { + e.stopPropagation(); + + if (onClickSeverity) { + onClickSeverity(); + } + }} + > + {severity} + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/indicators/ImageLoadingIndicator.tsx b/sam2-cpu/frigate-dev/web/src/components/indicators/ImageLoadingIndicator.tsx new file mode 100644 index 0000000..74002e9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/indicators/ImageLoadingIndicator.tsx @@ -0,0 +1,21 @@ +import { isSafari } from "react-device-detect"; +import { Skeleton } from "../ui/skeleton"; +import { cn } from "@/lib/utils"; + +export default function ImageLoadingIndicator({ + className, + imgLoaded, +}: { + className?: string; + imgLoaded: boolean; +}) { + if (imgLoaded) { + return; + } + + return isSafari ? ( +
    + ) : ( + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/indicators/StepIndicator.tsx b/sam2-cpu/frigate-dev/web/src/components/indicators/StepIndicator.tsx new file mode 100644 index 0000000..282527f --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/indicators/StepIndicator.tsx @@ -0,0 +1,59 @@ +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +type StepIndicatorProps = { + steps: string[]; + currentStep: number; + variant?: "default" | "dots"; + translationNameSpace?: string; + className?: string; +}; + +export default function StepIndicator({ + steps, + currentStep, + variant = "default", + translationNameSpace, + className, +}: StepIndicatorProps) { + const { t } = useTranslation(translationNameSpace); + + if (variant == "dots") { + return ( +
    + {steps.map((_, idx) => ( +
    idx + ? "bg-muted-foreground" + : "bg-muted", + )} + /> + ))} +
    + ); + } + + // Default variant (original behavior) + return ( +
    + {steps.map((name, idx) => ( +
    +
    + {idx + 1} +
    +
    {t(name)}
    +
    + ))} +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/indicators/activity-indicator.tsx b/sam2-cpu/frigate-dev/web/src/components/indicators/activity-indicator.tsx new file mode 100644 index 0000000..677a815 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/indicators/activity-indicator.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; + +export default function ActivityIndicator({ className = "w-full", size = 30 }) { + return ( +
    + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/DeleteSearchDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/input/DeleteSearchDialog.tsx new file mode 100644 index 0000000..735f52b --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/DeleteSearchDialog.tsx @@ -0,0 +1,47 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { buttonVariants } from "../ui/button"; + +type DeleteSearchDialogProps = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + searchName: string; +}; + +export function DeleteSearchDialog({ + isOpen, + onClose, + onConfirm, + searchName, +}: DeleteSearchDialogProps) { + return ( + + + + Are you sure? + + This will permanently delete the saved search "{searchName}". + + + + Cancel + + Delete + + + + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/ImageEntry.tsx b/sam2-cpu/frigate-dev/web/src/components/input/ImageEntry.tsx new file mode 100644 index 0000000..493d107 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/ImageEntry.tsx @@ -0,0 +1,189 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useDropzone } from "react-dropzone"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { LuUpload, LuX } from "react-icons/lu"; +import { z } from "zod"; + +type ImageEntryProps = { + onSave: (file: File) => void; + children?: React.ReactNode; + maxSize?: number; + accept?: Record; +}; + +export default function ImageEntry({ + onSave, + children, + maxSize = 20 * 1024 * 1024, // 20MB default + accept = { "image/*": [".jpeg", ".jpg", ".png", ".gif", ".webp"] }, +}: ImageEntryProps) { + const { t } = useTranslation(["views/faceLibrary"]); + const [preview, setPreview] = useState(null); + const dropzoneRef = useRef(null); + + // Auto focus the dropzone + useEffect(() => { + if (dropzoneRef.current && !preview) { + dropzoneRef.current.focus(); + } + }, [preview]); + + // Clean up preview URL on unmount or preview change + useEffect(() => { + return () => { + if (preview) { + URL.revokeObjectURL(preview); + } + }; + }, [preview]); + + const formSchema = z.object({ + file: z + .instanceof(File, { message: t("imageEntry.validation.selectImage") }) + .refine((file) => + accept["image/*"].includes(`.${file.type.split("/")[1]}`), + ), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + const onDrop = useCallback( + (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + const file = acceptedFiles[0]; + form.setValue("file", file, { shouldValidate: true }); + + // Create preview + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }, + [form], + ); + + const { getRootProps, getInputProps, isDragActive, isDragReject } = + useDropzone({ + onDrop, + maxSize, + accept, + multiple: false, + }); + + const handlePaste = useCallback( + (event: React.ClipboardEvent) => { + event.preventDefault(); + const clipboardItems = Array.from(event.clipboardData.items); + for (const item of clipboardItems) { + if (item.type.startsWith("image/")) { + const blob = item.getAsFile(); + if (blob && blob.size <= maxSize) { + const mimeType = blob.type.split("/")[1]; + const extension = `.${mimeType}`; + if (accept["image/*"].includes(extension)) { + const fileName = blob.name || `pasted-image.${mimeType}`; + const file = new File([blob], fileName, { type: blob.type }); + form.setValue("file", file, { shouldValidate: true }); + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + return; // Take the first valid image + } + } + } + } + }, + [form, maxSize, accept], + ); + + const onSubmit = useCallback( + (data: z.infer) => { + if (!data.file) return; + onSave(data.file); + }, + [onSave], + ); + + const clearSelection = () => { + form.reset(); + setPreview(null); + }; + + return ( +
    + + ( + + +
    + {!preview ? ( +
    + + +

    + {isDragActive + ? t("imageEntry.dropActive") + : t("imageEntry.dropInstructions")} +

    +

    + {t("imageEntry.maxSize", { + size: Math.round(maxSize / (1024 * 1024)), + })} +

    +
    + ) : ( +
    + Preview + +
    + )} +
    +
    + +
    + )} + /> +
    {children}
    + + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/InputWithTags.tsx b/sam2-cpu/frigate-dev/web/src/components/input/InputWithTags.tsx new file mode 100755 index 0000000..2985371 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/InputWithTags.tsx @@ -0,0 +1,1020 @@ +import React, { + useState, + useRef, + useEffect, + useCallback, + useMemo, +} from "react"; +import { + LuX, + LuFilter, + LuChevronDown, + LuChevronUp, + LuTrash2, + LuStar, + LuSearch, +} from "react-icons/lu"; +import { + FilterType, + SavedSearchQuery, + SearchFilter, + SearchSortType, + SearchSource, +} from "@/types/search"; +import useSuggestions from "@/hooks/use-suggestions"; +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { useUserPersistence } from "@/hooks/use-user-persistence"; +import { SaveSearchDialog } from "./SaveSearchDialog"; +import { DeleteSearchDialog } from "./DeleteSearchDialog"; +import { + convertLocalDateToTimestamp, + convertTo12Hour, + getIntlDateFormat, + isValidTimeRange, + to24Hour, +} from "@/utils/dateUtil"; +import { toast } from "sonner"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { MdImageSearch } from "react-icons/md"; +import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { CameraNameLabel, ZoneNameLabel } from "../camera/FriendlyNameLabel"; + +type InputWithTagsProps = { + inputFocused: boolean; + setInputFocused: React.Dispatch>; + filters: SearchFilter; + setFilters: (filter: SearchFilter) => void; + search: string; + setSearch: (search: string) => void; + allSuggestions: { + [K in keyof SearchFilter]: string[]; + }; +}; + +export default function InputWithTags({ + inputFocused, + setInputFocused, + filters, + setFilters, + search, + setSearch, + allSuggestions, +}: InputWithTagsProps) { + const { t, i18n } = useTranslation(["views/search"]); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + + const allAudioListenLabels = useMemo>(() => { + if (!config) { + return new Set(); + } + + const labels = new Set(); + Object.values(config.cameras).forEach((camera) => { + if (camera?.audio?.enabled) { + camera.audio.listen.forEach((label) => { + labels.add(label); + }); + } + }); + return labels; + }, [config]); + + const translatedAudioLabelMap = useMemo>(() => { + const map = new Map(); + if (!config) return map; + + allAudioListenLabels.forEach((label) => { + // getTranslatedLabel likely depends on i18n internally; including `lang` + // in deps ensures this map is rebuilt when language changes + map.set(label, getTranslatedLabel(label, "audio")); + }); + return map; + }, [allAudioListenLabels, config]); + + function resolveLabel(value: string) { + const mapped = translatedAudioLabelMap.get(value); + if (mapped) return mapped; + return getTranslatedLabel( + value, + allAudioListenLabels.has(value) ? "audio" : "object", + ); + } + + const [inputValue, setInputValue] = useState(search || ""); + const [currentFilterType, setCurrentFilterType] = useState( + null, + ); + const [isSimilaritySearch, setIsSimilaritySearch] = useState(false); + const inputRef = useRef(null); + const commandRef = useRef(null); + + // TODO: search history from browser storage + + const [searchHistory, setSearchHistory, searchHistoryLoaded] = + useUserPersistence("frigate-search-history"); + + const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [searchToDelete, setSearchToDelete] = useState(null); + + const searchHistoryNames = useMemo( + () => searchHistory?.map((item) => item.name) ?? [], + [searchHistory], + ); + + const handleSetSearchHistory = useCallback(() => { + setIsSaveDialogOpen(true); + }, []); + + const handleSaveSearch = useCallback( + (name: string) => { + if (searchHistoryLoaded) { + setSearchHistory([ + ...(searchHistory ?? []).filter((item) => item.name !== name), + { name, search, filter: filters }, + ]); + } + }, + [search, filters, searchHistory, setSearchHistory, searchHistoryLoaded], + ); + + const handleLoadSavedSearch = useCallback( + (name: string) => { + if (searchHistoryLoaded) { + const savedSearchEntry = searchHistory?.find( + (entry) => entry.name === name, + ); + if (savedSearchEntry) { + setFilters(savedSearchEntry.filter!); + setSearch(savedSearchEntry.search); + } + } + }, + [searchHistory, searchHistoryLoaded, setFilters, setSearch], + ); + + const handleDeleteSearch = useCallback((name: string) => { + setSearchToDelete(name); + setIsDeleteDialogOpen(true); + }, []); + + const confirmDeleteSearch = useCallback(() => { + if (searchToDelete && searchHistory) { + setSearchHistory( + searchHistory.filter((item) => item.name !== searchToDelete) ?? [], + ); + setSearchToDelete(null); + setIsDeleteDialogOpen(false); + } + }, [searchToDelete, searchHistory, setSearchHistory]); + + // suggestions + + const { suggestions, updateSuggestions } = useSuggestions( + filters, + allSuggestions, + searchHistory, + ); + + const resetSuggestions = useCallback( + (value: string) => { + setCurrentFilterType(null); + updateSuggestions(value, null); + }, + [updateSuggestions], + ); + + const filterSuggestions = useCallback( + (current_suggestions: string[]) => { + if (!inputValue || currentFilterType) return suggestions; + const words = inputValue.split(/[\s,]+/); + const lastNonEmptyWordIndex = words + .map((word) => word.trim()) + .lastIndexOf(words.filter((word) => word.trim() !== "").pop() || ""); + const currentWord = words[lastNonEmptyWordIndex]; + if (words.at(-1) === "") { + return current_suggestions; + } + + return current_suggestions.filter((suggestion) => + suggestion.toLowerCase().startsWith(currentWord), + ); + }, + [inputValue, suggestions, currentFilterType], + ); + + const removeFilter = useCallback( + (filterType: FilterType, filterValue: string | number) => { + const newFilters = { ...filters }; + if (Array.isArray(newFilters[filterType])) { + (newFilters[filterType] as string[]) = ( + newFilters[filterType] as string[] + ).filter((v) => v !== filterValue); + if ((newFilters[filterType] as string[]).length === 0) { + delete newFilters[filterType]; + } + } else if (filterType === "before" || filterType === "after") { + if (newFilters[filterType] === filterValue) { + delete newFilters[filterType]; + } + } else if (filterType === "has_snapshot") { + if (newFilters[filterType] === filterValue) { + delete newFilters[filterType]; + delete newFilters["is_submitted"]; + } + } else { + delete newFilters[filterType]; + } + setFilters(newFilters as SearchFilter); + }, + [filters, setFilters], + ); + + const createFilter = useCallback( + (type: FilterType, value: string) => { + if ( + allSuggestions[type as FilterType]?.includes(value) || + type == "before" || + type == "after" || + type == "time_range" || + type == "min_score" || + type == "max_score" || + type == "min_speed" || + type == "max_speed" + ) { + const newFilters = { ...filters }; + let timestamp = 0; + let score = 0; + let speed = 0; + + switch (type) { + case "before": + case "after": + timestamp = convertLocalDateToTimestamp(value); + if (timestamp > 0) { + // Check for conflicts with existing before/after filters + if ( + type === "before" && + filters.after && + timestamp <= filters.after * 1000 + ) { + toast.error(t("filter.toast.error.beforeDateBeLaterAfter"), { + position: "top-center", + }); + return; + } + if ( + type === "after" && + filters.before && + timestamp >= filters.before * 1000 + ) { + toast.error(t("filter.toast.error.afterDatebeEarlierBefore"), { + position: "top-center", + }); + return; + } + if (type === "before") { + timestamp -= 1; + } + newFilters[type] = timestamp / 1000; + } + break; + case "min_score": + case "max_score": + score = parseInt(value); + if (score >= 0) { + // Check for conflicts between min_score and max_score + if ( + type === "min_score" && + filters.max_score !== undefined && + score > filters.max_score * 100 + ) { + toast.error( + t("filter.toast.error.minScoreMustBeLessOrEqualMaxScore"), + { + position: "top-center", + }, + ); + return; + } + if ( + type === "max_score" && + filters.min_score !== undefined && + score < filters.min_score * 100 + ) { + toast.error( + t("filter.toast.error.maxScoreMustBeGreaterOrEqualMinScore"), + { + position: "top-center", + }, + ); + return; + } + newFilters[type] = score / 100; + } + break; + case "min_speed": + case "max_speed": + speed = parseFloat(value); + if (score >= 0) { + // Check for conflicts between min_speed and max_speed + if ( + type === "min_speed" && + filters.max_speed !== undefined && + speed > filters.max_speed + ) { + toast.error( + t("filter.toast.error.minSpeedMustBeLessOrEqualMaxSpeed"), + { + position: "top-center", + }, + ); + return; + } + if ( + type === "max_speed" && + filters.min_speed !== undefined && + speed < filters.min_speed + ) { + toast.error( + t("filter.toast.error.maxSpeedMustBeGreaterOrEqualMinSpeed"), + { + position: "top-center", + }, + ); + return; + } + newFilters[type] = speed; + } + break; + case "time_range": + newFilters[type] = value; + break; + case "search_type": + if (!newFilters.search_type) newFilters.search_type = []; + if ( + !(newFilters.search_type as SearchSource[]).includes( + value as SearchSource, + ) + ) { + (newFilters.search_type as SearchSource[]).push( + value as SearchSource, + ); + } + break; + case "has_snapshot": + if (!newFilters.has_snapshot) newFilters.has_snapshot = undefined; + newFilters.has_snapshot = value == "yes" ? 1 : 0; + break; + case "is_submitted": + if (!newFilters.is_submitted) newFilters.is_submitted = undefined; + newFilters.is_submitted = value == "yes" ? 1 : 0; + break; + case "has_clip": + if (!newFilters.has_clip) newFilters.has_clip = undefined; + newFilters.has_clip = value == "yes" ? 1 : 0; + break; + case "event_id": + newFilters.event_id = value; + break; + case "sort": + newFilters.sort = value as SearchSortType; + break; + default: + // Handle array types (cameras, labels, subLabels, zones) + if (!newFilters[type]) newFilters[type] = []; + if (Array.isArray(newFilters[type])) { + if (!(newFilters[type] as string[]).includes(value)) { + (newFilters[type] as string[]).push(value); + } + } + break; + } + + setFilters(newFilters); + setInputValue((prev) => prev.replace(`${type}:${value}`, "").trim()); + setCurrentFilterType(null); + } + }, + [filters, setFilters, allSuggestions, t], + ); + + function formatFilterValues( + filterType: string, + filterValues: number | string, + ): string { + if (filterType === "before" || filterType === "after") { + return new Date( + (filterType === "before" + ? (filterValues as number) + 1 + : (filterValues as number)) * 1000, + ).toLocaleDateString(window.navigator?.language || "en-US"); + } else if (filterType === "time_range") { + const [startTime, endTime] = (filterValues as string) + .replace("-", ",") + .split(","); + return `${ + config?.ui.time_format === "24hour" + ? startTime + : convertTo12Hour(startTime) + } - ${ + config?.ui.time_format === "24hour" ? endTime : convertTo12Hour(endTime) + }`; + } else if (filterType === "min_score" || filterType === "max_score") { + return Math.round(Number(filterValues) * 100).toString() + "%"; + } else if (filterType === "min_speed" || filterType === "max_speed") { + return ( + filterValues + + " " + + (config?.ui.unit_system == "metric" + ? t("unit.speed.kph", { ns: "common" }) + : t("unit.speed.mph", { ns: "common" })) + ); + } else if ( + filterType === "has_clip" || + filterType === "has_snapshot" || + filterType === "is_submitted" + ) { + return filterValues + ? t("button.yes", { ns: "common" }) + : t("button.no", { ns: "common" }); + } else if (filterType === "labels") { + const value = String(filterValues); + return resolveLabel(value); + } else if (filterType === "search_type") { + return t("filter.searchType." + String(filterValues)); + } else { + return String(filterValues).replaceAll("_", " "); + } + } + + // handlers + + const handleFilterCreation = useCallback( + (filterType: FilterType, filterValue: string) => { + const trimmedValue = filterValue.trim(); + if ( + allSuggestions[filterType]?.includes(trimmedValue) || + ((filterType === "before" || filterType === "after") && + trimmedValue.match(/^\d{8}$/)) || + (filterType === "time_range" && + isValidTimeRange( + trimmedValue.replace("-", ","), + config?.ui.time_format, + )) || + ((filterType === "min_score" || filterType === "max_score") && + !isNaN(Number(trimmedValue)) && + Number(trimmedValue) >= 50 && + Number(trimmedValue) <= 100) || + ((filterType === "min_speed" || filterType === "max_speed") && + !isNaN(Number(trimmedValue)) && + Number(trimmedValue) >= 1 && + Number(trimmedValue) <= 150) + ) { + createFilter( + filterType, + filterType === "time_range" + ? trimmedValue + .replace("-", ",") + .split(",") + .map((time) => to24Hour(time.trim(), config?.ui.time_format)) + .join(",") + : trimmedValue, + ); + setInputValue((prev) => { + const regex = new RegExp( + `${filterType}:${filterValue.trim()}[,\\s]*`, + ); + const newValue = prev.replace(regex, "").trim(); + return newValue.endsWith(",") + ? newValue.slice(0, -1).trim() + : newValue; + }); + setCurrentFilterType(null); + } + }, + [allSuggestions, createFilter, config], + ); + + const handleInputChange = useCallback( + (value: string) => { + setInputValue(value); + + const words = value.split(/[\s,]+/); + const lastNonEmptyWordIndex = words + .map((word) => word.trim()) + .lastIndexOf(words.filter((word) => word.trim() !== "").pop() || ""); + const currentWord = words[lastNonEmptyWordIndex]; + const isLastCharSpaceOrComma = value.endsWith(" ") || value.endsWith(","); + + // Check if the current word is a filter type + const filterTypeMatch = currentWord.match(/^(\w+):(.*)$/); + if (filterTypeMatch) { + const [_, filterType, filterValue] = filterTypeMatch as [ + string, + FilterType, + string, + ]; + + // Check if filter type is valid + if (filterType in allSuggestions) { + setCurrentFilterType(filterType); + + updateSuggestions(filterValue, filterType); + + // Check if the last character is a space or comma + if (isLastCharSpaceOrComma) { + handleFilterCreation(filterType, filterValue); + } + } else { + resetSuggestions(value); + } + } else { + resetSuggestions(value); + } + }, + [updateSuggestions, resetSuggestions, allSuggestions, handleFilterCreation], + ); + + const handleInputFocus = useCallback(() => { + setInputFocused(true); + }, [setInputFocused]); + + const handleClearInput = useCallback(() => { + setInputFocused(false); + setInputValue(""); + resetSuggestions(""); + setSearch(""); + inputRef?.current?.blur(); + setFilters({}); + setCurrentFilterType(null); + setIsSimilaritySearch(false); + }, [setFilters, resetSuggestions, setSearch, setInputFocused]); + + const handleClearSimilarity = useCallback(() => { + const newFilters = { ...filters }; + if (newFilters.event_id === filters.event_id) { + delete newFilters.event_id; + } + delete newFilters.search_type; + setFilters(newFilters); + }, [setFilters, filters]); + + const handleInputBlur = useCallback( + (e: React.FocusEvent) => { + if ( + commandRef.current && + !commandRef.current.contains(e.relatedTarget as Node) + ) { + setInputFocused(false); + } + }, + [setInputFocused], + ); + + const handleSuggestionClick = useCallback( + (suggestion: string) => { + if (currentFilterType) { + // Apply the selected suggestion to the current filter type + if (currentFilterType == "time_range") { + suggestion = suggestion + .replace("-", ",") + .split(",") + .map((time) => to24Hour(time.trim(), config?.ui.time_format)) + .join(","); + } + createFilter(currentFilterType, suggestion); + setInputValue((prev) => { + const regex = new RegExp(`${currentFilterType}:[^\\s,]*`, "g"); + return prev.replace(regex, "").trim(); + }); + } else if (suggestion in allSuggestions) { + // Set the suggestion as a new filter type + setCurrentFilterType(suggestion as FilterType); + setInputValue((prev) => { + // Remove any partial match of the filter type, including incomplete matches + const words = prev.split(/\s+/); + const lastWord = words[words.length - 1]; + if (lastWord && suggestion.startsWith(lastWord.toLowerCase())) { + words[words.length - 1] = suggestion + ":"; + } else { + words.push(suggestion + ":"); + } + return words.join(" ").trim(); + }); + } else { + // Add the suggestion as a standalone word + setInputValue((prev) => `${prev}${suggestion} `); + } + + inputRef.current?.focus(); + }, + [createFilter, currentFilterType, allSuggestions, config], + ); + + const handleSearch = useCallback( + (value: string) => { + setSearch(value); + setInputFocused(false); + inputRef?.current?.blur(); + }, + [setSearch, setInputFocused], + ); + + const handleInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const event = e.target as HTMLInputElement; + + if (!currentFilterType && (e.key === "Home" || e.key === "End")) { + const position = e.key === "Home" ? 0 : event.value.length; + event.setSelectionRange(position, position); + } + + if ( + e.key === "Enter" && + inputValue.trim() !== "" && + filterSuggestions(suggestions).length == 0 + ) { + e.preventDefault(); + handleSearch(inputValue); + } + }, + [ + inputValue, + handleSearch, + filterSuggestions, + suggestions, + currentFilterType, + ], + ); + + // effects + + useEffect(() => { + updateSuggestions(inputValue, currentFilterType); + }, [currentFilterType, inputValue, updateSuggestions]); + + useEffect(() => { + if (filters?.search_type && filters?.search_type.includes("similarity")) { + setIsSimilaritySearch(true); + setInputValue(""); + } else { + setIsSimilaritySearch(false); + setInputValue(search || ""); + } + }, [filters, search]); + + return ( + <> + +
    + +
    + {(search || Object.keys(filters).length > 0) && ( + + + + + + {t("button.clear")} + + + )} + + {(search || Object.keys(filters).length > 0) && ( + + + + + + {t("button.save")} + + + )} + + {isSimilaritySearch && ( + + + + + + + {t("similaritySearch.active")} + + + + )} + + + + + + +
    +

    {t("filter.tips.title")}

    +

    + {t("filter.tips.desc.text")} +

    +
      +
    • {t("filter.tips.desc.step1")}
    • +
    • {t("filter.tips.desc.step2")}
    • +
    • {t("filter.tips.desc.step3")}
    • +
    • + {t("filter.tips.desc.step4", { + DateFormat: getIntlDateFormat(), + })} +
    • +
    • + {t("filter.tips.desc.step5", { + exampleTime: + config?.ui.time_format == "24hour" + ? "15:00-16:00" + : "3:00PM-4:00PM", + })} +
    • +
    • {t("filter.tips.desc.step6")}
    • +
    +

    + {t("filter.tips.desc.exampleLabel")}{" "} + + cameras:front_door label:person before:01012024 + time_range:3:00PM-4:00PM + +

    +
    +
    +
    + + {inputFocused ? ( + { + setInputFocused(false); + inputRef.current?.blur(); + }} + className="size-4 cursor-pointer text-secondary-foreground" + /> + ) : ( + { + setInputFocused(true); + inputRef.current?.focus(); + }} + className="size-4 cursor-pointer text-secondary-foreground" + /> + )} +
    +
    + + + {!currentFilterType && inputValue && ( + + handleSearch(inputValue)} + > + + {t("searchFor", { inputValue })} + + + )} + {(Object.keys(filters).filter((key) => key !== "query").length > 0 || + isSimilaritySearch) && ( + +
    + {isSimilaritySearch && ( + + {t("similaritySearch.title")} + + + )} + {Object.entries(filters).map(([filterType, filterValues]) => + Array.isArray(filterValues) + ? filterValues + .filter(() => filterType !== "query") + .filter(() => !filterValues.includes("similarity")) + .map((value, index) => ( + + {t("filter.label." + filterType)}:{" "} + {filterType === "labels" ? ( + resolveLabel(value) + ) : filterType === "cameras" ? ( + + ) : filterType === "zones" ? ( + + ) : ( + value.replaceAll("_", " ") + )} + + + )) + : !(filterType == "event_id" && isSimilaritySearch) && ( + + {filterType === "event_id" + ? t("trackedObjectId") + : filterType === "is_submitted" + ? t("features.submittedToFrigatePlus.label", { + ns: "components/filter", + }) + : t("filter.label." + filterType)} + : {formatFilterValues(filterType, filterValues)} + + + ), + )} +
    +
    + )} + + {!currentFilterType && + !inputValue && + searchHistoryLoaded && + (searchHistory?.length ?? 0) > 0 && ( + + {searchHistory?.map((suggestion, index) => ( + handleLoadSavedSearch(suggestion.name)} + > + {suggestion.name} + + + + + + {t("button.delete")} + + + + ))} + + )} + + {filterSuggestions(suggestions) + .filter( + (item) => + !searchHistory?.some((history) => history.name === item), + ) + .map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + > + {i18n.language === "en" ? ( + currentFilterType && currentFilterType === "cameras" ? ( + <> + {suggestion} {" ("}{" "} + + {")"} + + ) : currentFilterType === "zones" ? ( + <> + {suggestion} {" ("} + {")"} + + ) : ( + suggestion + ) + ) : ( + <> + {suggestion} {" ("} + {currentFilterType ? ( + currentFilterType === "cameras" ? ( + + ) : currentFilterType === "zones" ? ( + + ) : ( + formatFilterValues(currentFilterType, suggestion) + ) + ) : ( + t("filter.label." + suggestion) + )} + {")"} + + )} + + ))} + +
    +
    + setIsSaveDialogOpen(false)} + onSave={handleSaveSearch} + /> + setIsDeleteDialogOpen(false)} + onConfirm={confirmDeleteSearch} + searchName={searchToDelete || ""} + /> + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/NameAndIdFields.tsx b/sam2-cpu/frigate-dev/web/src/components/input/NameAndIdFields.tsx new file mode 100644 index 0000000..c78a291 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/NameAndIdFields.tsx @@ -0,0 +1,139 @@ +import { Control, FieldValues, Path, PathValue } from "react-hook-form"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useState, useEffect, useRef } from "react"; +import { useFormContext } from "react-hook-form"; +import { generateFixedHash, isValidId } from "@/utils/stringUtil"; +import { useTranslation } from "react-i18next"; + +type NameAndIdFieldsProps = { + control: Control; + type?: string; + nameField: Path; + idField: Path; + nameLabel: string; + nameDescription?: string; + idLabel?: string; + idDescription?: string; + processId?: (name: string) => string; + placeholderName?: string; + placeholderId?: string; + idVisible?: boolean; +}; + +export default function NameAndIdFields({ + control, + type, + nameField, + idField, + nameLabel, + nameDescription, + idLabel, + idDescription, + processId, + placeholderName, + placeholderId, + idVisible, +}: NameAndIdFieldsProps) { + const { t } = useTranslation(["common"]); + const { watch, setValue, trigger, formState } = useFormContext(); + const [isIdVisible, setIsIdVisible] = useState(idVisible ?? false); + const hasUserTypedRef = useRef(false); + + const defaultProcessId = (name: string) => { + const normalized = name.replace(/\s+/g, "_").toLowerCase(); + if (isValidId(normalized)) { + return normalized; + } else { + return generateFixedHash(name, type); + } + }; + + const effectiveProcessId = processId || defaultProcessId; + + useEffect(() => { + const subscription = watch((value, { name }) => { + if (name === nameField) { + hasUserTypedRef.current = true; + const processedId = effectiveProcessId(value[nameField] || ""); + setValue(idField, processedId as PathValue>); + trigger(idField); + } + }); + return () => subscription.unsubscribe(); + }, [watch, setValue, trigger, nameField, idField, effectiveProcessId]); + + // Auto-expand if there's an error on the ID field after user has typed + useEffect(() => { + const idError = formState.errors[idField]; + if (idError && hasUserTypedRef.current && !isIdVisible) { + setIsIdVisible(true); + } + }, [formState.errors, idField, isIdVisible]); + + return ( + <> + ( + +
    + {nameLabel} + setIsIdVisible(!isIdVisible)} + > + {isIdVisible + ? t("label.hide", { item: idLabel ?? t("label.ID") }) + : t("label.show", { + item: idLabel ?? t("label.ID"), + })} + +
    + + + + {nameDescription && ( + {nameDescription} + )} + +
    + )} + /> + {isIdVisible && ( + ( + + {idLabel ?? t("label.ID")} + + + + + {idDescription ?? t("field.internalID")} + + + + )} + /> + )} + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/SaveSearchDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/input/SaveSearchDialog.tsx new file mode 100644 index 0000000..5eebdda --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/SaveSearchDialog.tsx @@ -0,0 +1,100 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useMemo, useState } from "react"; +import { isMobile } from "react-device-detect"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; + +type SaveSearchDialogProps = { + existingNames: string[]; + isOpen: boolean; + onClose: () => void; + onSave: (name: string) => void; +}; + +export function SaveSearchDialog({ + existingNames, + isOpen, + onClose, + onSave, +}: SaveSearchDialogProps) { + const { t } = useTranslation(["components/dialog"]); + + const [searchName, setSearchName] = useState(""); + + const handleSave = () => { + if (searchName.trim()) { + onSave(searchName.trim()); + setSearchName(""); + toast.success( + t("search.saveSearch.success", { + searchName: searchName.trim(), + }), + { + position: "top-center", + }, + ); + onClose(); + } + }; + + const overwrite = useMemo( + () => existingNames.includes(searchName), + [existingNames, searchName], + ); + + return ( + + { + if (isMobile) { + e.preventDefault(); + } + }} + > + + {t("search.saveSearch.label")} + + {t("search.saveSearch.desc")} + + + setSearchName(e.target.value)} + placeholder={t("search.saveSearch.placeholder")} + /> + {overwrite && ( +
    + {t("search.saveSearch.overwrite", { searchName })} +
    + )} + + + + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/input/TextEntry.tsx b/sam2-cpu/frigate-dev/web/src/components/input/TextEntry.tsx new file mode 100644 index 0000000..e266444 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/input/TextEntry.tsx @@ -0,0 +1,86 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import React, { useCallback } from "react"; +import { useForm } from "react-hook-form"; + +import { z } from "zod"; + +type TextEntryProps = { + defaultValue?: string; + placeholder?: string; + allowEmpty?: boolean; + onSave: (text: string) => void; + children?: React.ReactNode; + regexPattern?: RegExp; + regexErrorMessage?: string; +}; + +export default function TextEntry({ + defaultValue = "", + placeholder, + allowEmpty = false, + onSave, + children, + regexPattern, + regexErrorMessage = "Input does not match the required format", +}: TextEntryProps) { + const formSchema = z.object({ + text: z + .string() + .optional() + .refine( + (val) => { + if (!allowEmpty && !val) return false; + if (val && regexPattern) return regexPattern.test(val); + return true; + }, + { + message: regexPattern ? regexErrorMessage : "Field is required", + }, + ), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { text: defaultValue }, + }); + + const onSubmit = useCallback( + (data: z.infer) => { + onSave(data.text || ""); + }, + [onSave], + ); + + return ( +
    + + ( + + + + + + + )} + /> + {children} + + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/menu/AccountSettings.tsx b/sam2-cpu/frigate-dev/web/src/components/menu/AccountSettings.tsx new file mode 100644 index 0000000..e5a3673 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/menu/AccountSettings.tsx @@ -0,0 +1,171 @@ +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { baseUrl } from "../../api/baseUrl"; +import { cn } from "@/lib/utils"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { isDesktop } from "react-device-detect"; +import { VscAccount } from "react-icons/vsc"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Drawer, + DrawerContent, + DrawerTrigger, + DrawerClose, +} from "@/components/ui/drawer"; +import { LuLogOut, LuSquarePen } from "react-icons/lu"; +import useSWR from "swr"; + +import { useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; +import SetPasswordDialog from "../overlay/SetPasswordDialog"; +import { useTranslation } from "react-i18next"; + +type AccountSettingsProps = { + className?: string; +}; + +export default function AccountSettings({ className }: AccountSettingsProps) { + const { t } = useTranslation(["views/settings", "common"]); + const { data: profile } = useSWR("profile"); + const { data: config } = useSWR("config"); + const logoutUrl = config?.proxy?.logout_url || `${baseUrl}api/logout`; + + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + const [passwordError, setPasswordError] = useState(null); + const [isPasswordLoading, setIsPasswordLoading] = useState(false); + + const Container = isDesktop ? DropdownMenu : Drawer; + const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; + const Content = isDesktop ? DropdownMenuContent : DrawerContent; + const MenuItem = isDesktop ? DropdownMenuItem : DrawerClose; + + const handlePasswordSave = async (password: string, oldPassword?: string) => { + if (!profile?.username || profile.username === "anonymous") return; + setIsPasswordLoading(true); + axios + .put(`users/${profile.username}/password`, { + password, + old_password: oldPassword, + }) + .then((response) => { + if (response.status === 200) { + setPasswordDialogOpen(false); + setPasswordError(null); + setIsPasswordLoading(false); + toast.success(t("users.toast.success.updatePassword"), { + position: "top-center", + }); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + // Keep dialog open and show error + setPasswordError(errorMessage); + setIsPasswordLoading(false); + }); + }; + + return ( + + + + +
    + +
    +
    +
    + + +

    {t("menu.user.account", { ns: "common" })}

    +
    +
    +
    + + +
    + +
    + {t("menu.user.current", { + ns: "common", + user: + profile?.username || + t("menu.user.anonymous", { ns: "common" }), + })}{" "} + {t("role." + profile?.role) && + `(${t("role." + profile?.role, { ns: "common" })})`} +
    +
    + + + + {profile?.username && profile.username !== "anonymous" && ( + setPasswordDialogOpen(true)} + > + + {t("menu.user.setPassword", { ns: "common" })} + + )} + + + + + {t("menu.user.logout", { ns: "common" })} + + +
    +
    + { + setPasswordDialogOpen(false); + setPasswordError(null); + }} + initialError={passwordError} + username={profile?.username} + isLoading={isPasswordLoading} + /> +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/menu/GeneralSettings.tsx b/sam2-cpu/frigate-dev/web/src/components/menu/GeneralSettings.tsx new file mode 100644 index 0000000..1788bce --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/menu/GeneralSettings.tsx @@ -0,0 +1,571 @@ +import { + LuActivity, + LuGithub, + LuLanguages, + LuLifeBuoy, + LuList, + LuLogOut, + LuMoon, + LuSquarePen, + LuScanFace, + LuRotateCw, + LuSettings, + LuSun, + LuSunMoon, +} from "react-icons/lu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { Link } from "react-router-dom"; +import { CgDarkMode } from "react-icons/cg"; +import { + colorSchemes, + friendlyColorSchemeName, + useTheme, +} from "@/context/theme-provider"; +import { IoColorPalette } from "react-icons/io5"; +import { useMemo, useState } from "react"; +import { useRestart } from "@/api/ws"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop, isMobile } from "react-device-detect"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import { + Dialog, + DialogClose, + DialogContent, + DialogPortal, + DialogTrigger, +} from "../ui/dialog"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { cn } from "@/lib/utils"; +import useSWR from "swr"; +import RestartDialog from "../overlay/dialog/RestartDialog"; + +import { useLanguage } from "@/context/language-provider"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import SetPasswordDialog from "../overlay/SetPasswordDialog"; +import { toast } from "sonner"; +import axios from "axios"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { useTranslation } from "react-i18next"; +import { supportedLanguageKeys } from "@/lib/const"; + +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { MdCategory } from "react-icons/md"; + +type GeneralSettingsProps = { + className?: string; +}; + +export default function GeneralSettings({ className }: GeneralSettingsProps) { + const { t } = useTranslation(["common", "views/settings"]); + const { getLocaleDocUrl } = useDocDomain(); + const { data: profile } = useSWR("profile"); + const { data: config } = useSWR("config"); + const logoutUrl = config?.proxy?.logout_url || "/api/logout"; + + // languages + + const languages = useMemo(() => { + // Handle language keys that aren't directly used for translation key + const specialKeyMap: { [key: string]: string } = { + "nb-NO": "nb", + "yue-Hant": "yue", + "zh-CN": "zhCN", + "pt-BR": "ptBR", + }; + + return supportedLanguageKeys.map((key) => { + return { + code: key, + label: t(`menu.language.${specialKeyMap[key] || key}`), + }; + }); + }, [t]); + + // settings + + const { language, setLanguage } = useLanguage(); + const { theme, colorScheme, setTheme, setColorScheme } = useTheme(); + const [restartDialogOpen, setRestartDialogOpen] = useState(false); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + const { send: sendRestart } = useRestart(); + + const isAdmin = useIsAdmin(); + + const Container = isDesktop ? DropdownMenu : Drawer; + const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; + const Content = isDesktop ? DropdownMenuContent : DrawerContent; + const MenuItem = isDesktop ? DropdownMenuItem : DialogClose; + const SubItem = isDesktop ? DropdownMenuSub : Dialog; + const SubItemTrigger = isDesktop ? DropdownMenuSubTrigger : DialogTrigger; + const SubItemContent = isDesktop ? DropdownMenuSubContent : DialogContent; + const Portal = isDesktop ? DropdownMenuPortal : DialogPortal; + + const [passwordError, setPasswordError] = useState(null); + const [isPasswordLoading, setIsPasswordLoading] = useState(false); + + const handlePasswordSave = async (password: string, oldPassword?: string) => { + if (!profile?.username || profile.username === "anonymous") return; + setIsPasswordLoading(true); + axios + .put(`users/${profile.username}/password`, { + password, + old_password: oldPassword, + }) + .then((response) => { + if (response.status === 200) { + setPasswordDialogOpen(false); + setPasswordError(null); + setIsPasswordLoading(false); + toast.success( + t("users.toast.success.updatePassword", { + ns: "views/settings", + }), + { + position: "top-center", + }, + ); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + + // Keep dialog open and show error + setPasswordError(errorMessage); + setIsPasswordLoading(false); + }); + }; + + return ( + <> + + + + +
    + +
    +
    + + +

    {t("menu.settings")}

    +
    +
    +
    +
    + +
    + {isMobile && ( +
    + + {t("menu.user.current", { + user: profile?.username || t("menu.user.anonymous"), + })}{" "} + {t("role." + profile?.role) && + `(${t("role." + profile?.role)})`} + + + {profile?.username && profile.username !== "anonymous" && ( + setPasswordDialogOpen(true)} + > + + {t("menu.user.setPassword", { ns: "common" })} + + )} + + + + {t("menu.user.logout", { ns: "common" })} + + +
    + )} + {isAdmin && ( + <> + {t("menu.system")} + + + + + + {t("menu.systemMetrics")} + + + + + + {t("menu.systemLogs")} + + + + + )} + + {t("menu.configuration")} + + + + + + + {t("menu.settings")} + + + {isAdmin && ( + <> + + + + {t("menu.configurationEditor")} + + + + )} + {isAdmin && isMobile && config?.face_recognition.enabled && ( + <> + + + + {t("menu.faceLibrary")} + + + + )} + {isAdmin && isMobile && ( + <> + + + + {t("menu.classification")} + + + + )} + + + {t("menu.appearance")} + + + + + + {t("menu.languages")} + + + + + {languages.map(({ code, label }) => ( + setLanguage(code)} + > + {language.trim() === code ? ( + <> + + {label} + + ) : ( + {label} + )} + + ))} + + + + + + + {t("menu.darkMode.label")} + + + + + setTheme("light")} + > + {theme === "light" ? ( + <> + + {t("menu.darkMode.light")} + + ) : ( + + {t("menu.darkMode.light")} + + )} + + setTheme("dark")} + > + {theme === "dark" ? ( + <> + + {t("menu.darkMode.dark")} + + ) : ( + + {t("menu.darkMode.dark")} + + )} + + setTheme("system")} + > + {theme === "system" ? ( + <> + + {t("menu.withSystem")} + + ) : ( + {t("menu.withSystem")} + )} + + + + + + + + {t("menu.theme.label")} + + + + + {colorSchemes.map((scheme) => ( + setColorScheme(scheme)} + > + {scheme === colorScheme ? ( + <> + + {t(friendlyColorSchemeName(scheme))} + + ) : ( + + {t(friendlyColorSchemeName(scheme))} + + )} + + ))} + + + + + {t("menu.help")} + + + + + + {t("menu.documentation.title")} + + + + + + GitHub + + + {isAdmin && ( + <> + + setRestartDialogOpen(true)} + > + + {t("menu.restart")} + + + )} +
    +
    +
    + setRestartDialogOpen(false)} + onRestart={() => sendRestart("restart")} + /> + { + setPasswordDialogOpen(false); + setPasswordError(null); + }} + initialError={passwordError} + username={profile?.username} + isLoading={isPasswordLoading} + /> + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/menu/LiveContextMenu.tsx b/sam2-cpu/frigate-dev/web/src/components/menu/LiveContextMenu.tsx new file mode 100644 index 0000000..6715067 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/menu/LiveContextMenu.tsx @@ -0,0 +1,569 @@ +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + MdVolumeDown, + MdVolumeMute, + MdVolumeOff, + MdVolumeUp, +} from "react-icons/md"; +import { Dialog } from "@/components/ui/dialog"; +import { VolumeSlider } from "@/components/ui/slider"; +import { CameraStreamingDialog } from "../settings/CameraStreamingDialog"; +import { + AllGroupsStreamingSettings, + FrigateConfig, + GroupStreamingSettings, +} from "@/types/frigateConfig"; +import { useStreamingSettings } from "@/context/streaming-settings-provider"; +import { + IoIosNotifications, + IoIosNotificationsOff, + IoIosWarning, +} from "react-icons/io"; +import { cn } from "@/lib/utils"; +import { useNavigate } from "react-router-dom"; +import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { + useEnabledState, + useNotifications, + useNotificationSuspend, +} from "@/api/ws"; +import { useTranslation } from "react-i18next"; +import { useDateLocale } from "@/hooks/use-date-locale"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { LiveStreamMetadata } from "@/types/live"; + +type LiveContextMenuProps = { + className?: string; + camera: string; + streamName: string; + cameraGroup?: string; + preferredLiveMode: string; + isRestreamed: boolean; + supportsAudio: boolean; + audioState: boolean; + toggleAudio: () => void; + volumeState?: number; + setVolumeState: (volumeState: number) => void; + muteAll: () => void; + unmuteAll: () => void; + statsState: boolean; + toggleStats: () => void; + resetPreferredLiveMode: () => void; + config?: FrigateConfig; + children?: ReactNode; + streamMetadata?: { [key: string]: LiveStreamMetadata }; +}; +export default function LiveContextMenu({ + className, + camera, + streamName, + cameraGroup, + preferredLiveMode, + isRestreamed, + supportsAudio, + audioState, + toggleAudio, + volumeState, + setVolumeState, + muteAll, + unmuteAll, + statsState, + toggleStats, + resetPreferredLiveMode, + config, + children, + streamMetadata, +}: LiveContextMenuProps) { + const { t } = useTranslation("views/live"); + const [showSettings, setShowSettings] = useState(false); + + // roles + + const isAdmin = useIsAdmin(); + + // camera enabled + + const { payload: enabledState, send: sendEnabled } = useEnabledState(camera); + const isEnabled = enabledState === "ON"; + + // streaming settings + + const { allGroupsStreamingSettings, setAllGroupsStreamingSettings } = + useStreamingSettings(); + + const [groupStreamingSettings, setGroupStreamingSettings] = + useState( + allGroupsStreamingSettings[cameraGroup ?? ""], + ); + + useEffect(() => { + if (cameraGroup && cameraGroup != "default") { + setGroupStreamingSettings(allGroupsStreamingSettings[cameraGroup]); + } + }, [allGroupsStreamingSettings, cameraGroup]); + + const onSave = useCallback( + (settings: GroupStreamingSettings) => { + if ( + !cameraGroup || + !allGroupsStreamingSettings || + cameraGroup == "default" || + !settings + ) { + return; + } + + const updatedSettings: AllGroupsStreamingSettings = { + ...Object.fromEntries( + Object.entries(allGroupsStreamingSettings || {}).filter( + ([key]) => key !== cameraGroup, + ), + ), + [cameraGroup]: { + ...Object.fromEntries( + Object.entries(settings).map(([cameraName, cameraSettings]) => [ + cameraName, + cameraName === camera + ? { + ...cameraSettings, + playAudio: audioState ?? cameraSettings.playAudio ?? false, + volume: volumeState ?? cameraSettings.volume ?? 1, + } + : cameraSettings, + ]), + ), + // Add the current camera if it doesn't exist + ...(!settings[camera] + ? { + [camera]: { + streamName: streamName, + streamType: "smart", + compatibilityMode: false, + playAudio: audioState, + volume: volumeState ?? 1, + }, + } + : {}), + }, + }; + + setAllGroupsStreamingSettings?.(updatedSettings); + }, + [ + camera, + streamName, + cameraGroup, + allGroupsStreamingSettings, + setAllGroupsStreamingSettings, + audioState, + volumeState, + ], + ); + + // ui + + const audioControlsUsed = useRef(false); + + const VolumeIcon = useMemo(() => { + if (!volumeState || volumeState == 0.0 || !audioState) { + return MdVolumeOff; + } else if (volumeState <= 0.33) { + return MdVolumeMute; + } else if (volumeState <= 0.67) { + return MdVolumeDown; + } else { + return MdVolumeUp; + } + // only update when specific fields change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [volumeState, audioState]); + + const handleVolumeIconClick = (e: React.MouseEvent) => { + e.stopPropagation(); + audioControlsUsed.current = true; + toggleAudio(); + }; + + const handleVolumeChange = (value: number[]) => { + audioControlsUsed.current = true; + setVolumeState(value[0]); + }; + + const handleOpenChange = (open: boolean) => { + if (!open && audioControlsUsed.current) { + onSave(groupStreamingSettings); + audioControlsUsed.current = false; + } + }; + + // navigate for debug view + + const navigate = useNavigate(); + + // notifications + + const notificationsEnabledInConfig = + config?.cameras[camera]?.notifications?.enabled_in_config; + + const { payload: notificationState, send: sendNotification } = + useNotifications(camera); + const { payload: notificationSuspendUntil, send: sendNotificationSuspend } = + useNotificationSuspend(camera); + const [isSuspended, setIsSuspended] = useState(false); + + useEffect(() => { + if (notificationSuspendUntil) { + setIsSuspended( + notificationSuspendUntil !== "0" || notificationState === "OFF", + ); + } + }, [notificationSuspendUntil, notificationState]); + + const handleSuspend = (duration: string) => { + if (duration === "off") { + sendNotification("OFF"); + } else { + sendNotificationSuspend(Number.parseInt(duration)); + } + }; + + const locale = useDateLocale(); + + const formatSuspendedUntil = (timestamp: string) => { + // Some languages require a change in word order + if (timestamp === "0") return t("time.untilForRestart", { ns: "common" }); + + const time = formatUnixTimestampToDateTime(parseInt(timestamp), { + time_style: "medium", + date_style: "medium", + timezone: config?.ui.timezone, + date_format: + config?.ui.time_format == "24hour" + ? t("time.formattedTimestampMonthDayHourMinute.24hour", { + ns: "common", + }) + : t("time.formattedTimestampMonthDayHourMinute.12hour", { + ns: "common", + }), + locale: locale, + }); + return t("time.untilForTime", { ns: "common", time }); + }; + + return ( +
    + + {children} + +
    +
    + +
    + {preferredLiveMode == "jsmpeg" && isRestreamed && ( +
    + +

    {t("lowBandwidthMode")}

    +
    + )} +
    + {preferredLiveMode != "jsmpeg" && isRestreamed && supportsAudio && ( + <> + +
    +
    +

    {t("audio")}

    +
    + + +
    +
    +
    + + )} + + {isAdmin && ( + <> + +
    sendEnabled(isEnabled ? "OFF" : "ON")} + > +
    + {isEnabled ? t("camera.disable") : t("camera.enable")} +
    +
    +
    + + + )} + +
    +
    {t("muteCameras.enable")}
    +
    +
    + +
    +
    {t("muteCameras.disable")}
    +
    +
    + + +
    +
    + {statsState + ? t("streamStats.disable") + : t("streamStats.enable")} +
    +
    +
    + +
    navigate(`?debug=true#${camera}`) : undefined + } + > +
    + {t("streaming.debugView", { + ns: "components/dialog", + })} +
    +
    +
    + {cameraGroup && cameraGroup !== "default" && ( + <> + + +
    setShowSettings(true) : undefined} + > +
    {t("streamingSettings")}
    +
    +
    + + )} + {preferredLiveMode == "jsmpeg" && isRestreamed && ( + <> + + +
    +
    + {t("button.reset", { ns: "common" })} +
    +
    +
    + + )} + {notificationsEnabledInConfig && isEnabled && ( + <> + + + +
    + {t("notifications")} +
    +
    + +
    +
    + {notificationState === "ON" ? ( + <> + {isSuspended ? ( + <> + + + {t("button.suspended", { ns: "common" })} + + + ) : ( + <> + + + {t("button.enabled", { ns: "common" })} + + + )} + + ) : ( + <> + + {t("button.disabled", { ns: "common" })} + + )} +
    + {isSuspended && ( + + {formatSuspendedUntil(notificationSuspendUntil)} + + )} +
    + + {isSuspended ? ( + <> + + { + sendNotification("ON"); + sendNotificationSuspend(0); + } + : undefined + } + > +
    + {notificationState === "ON" ? ( + + {t("button.unsuspended", { ns: "common" })} + + ) : ( + {t("button.enable", { ns: "common" })} + )} +
    +
    + + ) : ( + notificationState === "ON" && ( + <> + +
    +

    + {t("suspend.forTime")} +

    +
    + handleSuspend("5") : undefined + } + > + {t("time.5minutes", { ns: "common" })} + + handleSuspend("10") + : undefined + } + > + {t("time.10minutes", { ns: "common" })} + + handleSuspend("30") + : undefined + } + > + {t("time.30minutes", { ns: "common" })} + + handleSuspend("60") + : undefined + } + > + {t("time.1hour", { ns: "common" })} + + handleSuspend("840") + : undefined + } + > + {t("time.12hours", { ns: "common" })} + + handleSuspend("1440") + : undefined + } + > + {t("time.24hours", { ns: "common" })} + + handleSuspend("off") + : undefined + } + > + {t("time.untilRestart", { ns: "common" })} + +
    +
    + + ) + )} +
    +
    + + )} +
    +
    + + + + +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/menu/SearchResultActions.tsx b/sam2-cpu/frigate-dev/web/src/components/menu/SearchResultActions.tsx new file mode 100644 index 0000000..3116ae4 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/menu/SearchResultActions.tsx @@ -0,0 +1,210 @@ +import { useState, ReactNode } from "react"; +import { SearchResult } from "@/types/search"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { baseUrl } from "@/api/baseUrl"; +import { toast } from "sonner"; +import axios from "axios"; +import { FiMoreVertical } from "react-icons/fi"; +import { buttonVariants } from "@/components/ui/button"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import useSWR from "swr"; +import { Trans, useTranslation } from "react-i18next"; +import BlurredIconButton from "../button/BlurredIconButton"; +import { useIsAdmin } from "@/hooks/use-is-admin"; + +type SearchResultActionsProps = { + searchResult: SearchResult; + findSimilar: () => void; + refreshResults: () => void; + showTrackingDetails: () => void; + addTrigger: () => void; + isContextMenu?: boolean; + children?: ReactNode; +}; + +export default function SearchResultActions({ + searchResult, + findSimilar, + refreshResults, + showTrackingDetails, + addTrigger, + isContextMenu = false, + children, +}: SearchResultActionsProps) { + const { t } = useTranslation(["views/explore"]); + const isAdmin = useIsAdmin(); + + const { data: config } = useSWR("config"); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const handleDelete = () => { + axios + .delete(`events/${searchResult.id}`) + .then((resp) => { + if (resp.status == 200) { + toast.success(t("searchResult.deleteTrackedObject.toast.success"), { + position: "top-center", + }); + refreshResults(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("searchResult.deleteTrackedObject.toast.error", { errorMessage }), + { + position: "top-center", + }, + ); + }); + }; + + const MenuItem = isContextMenu ? ContextMenuItem : DropdownMenuItem; + + const menuItems = ( + <> + {searchResult.has_clip && ( + + + {t("itemMenu.downloadVideo.label")} + + + )} + {searchResult.has_snapshot && ( + + + {t("itemMenu.downloadSnapshot.label")} + + + )} + {searchResult.has_snapshot && + config?.cameras[searchResult.camera].snapshots.clean_copy && ( + + + {t("itemMenu.downloadCleanSnapshot.label")} + + + )} + {searchResult.data.type == "object" && ( + + {t("itemMenu.viewTrackingDetails.label")} + + )} + {config?.semantic_search?.enabled && + searchResult.data.type == "object" && ( + + {t("itemMenu.findSimilar.label")} + + )} + {isAdmin && + config?.semantic_search?.enabled && + searchResult.data.type == "object" && ( + + {t("itemMenu.addTrigger.label")} + + )} + {isAdmin && ( + setDeleteDialogOpen(true)} + > + {t("button.delete", { ns: "common" })} + + )} + + ); + + return ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + + {t("dialog.confirmDelete.title")} + + + + dialog.confirmDelete.desc + + + + {t("button.cancel", { ns: "common" })} + + + {t("button.delete", { ns: "common" })} + + + + + {isContextMenu ? ( + + {children} + {menuItems} + + ) : ( + <> + + + + + + + {menuItems} + + + )} + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/mobile/MobilePage.tsx b/sam2-cpu/frigate-dev/web/src/components/mobile/MobilePage.tsx new file mode 100644 index 0000000..4b6c41c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/mobile/MobilePage.tsx @@ -0,0 +1,233 @@ +import { + createContext, + useContext, + useEffect, + useState, + useCallback, + useRef, +} from "react"; +import { createPortal } from "react-dom"; +import { motion, AnimatePresence } from "framer-motion"; +import { IoMdArrowRoundBack } from "react-icons/io"; +import { cn } from "@/lib/utils"; +import { isPWA } from "@/utils/isPWA"; +import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; +import { useHistoryBack } from "@/hooks/use-history-back"; + +const MobilePageContext = createContext<{ + open: boolean; + onOpenChange: (open: boolean) => void; +} | null>(null); + +type MobilePageProps = { + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + enableHistoryBack?: boolean; +}; + +export function MobilePage({ + children, + open: controlledOpen, + onOpenChange, + enableHistoryBack = true, +}: MobilePageProps) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + + const open = controlledOpen ?? uncontrolledOpen; + const setOpen = useCallback( + (value: boolean) => { + if (onOpenChange) { + onOpenChange(value); + } else { + setUncontrolledOpen(value); + } + }, + [onOpenChange, setUncontrolledOpen], + ); + + // Handle browser back button to close mobile page + useHistoryBack({ + enabled: enableHistoryBack, + open, + onClose: () => setOpen(false), + }); + + return ( + + {children} + + ); +} + +type MobilePageTriggerProps = React.HTMLAttributes; + +export function MobilePageTrigger({ + children, + ...props +}: MobilePageTriggerProps) { + const context = useContext(MobilePageContext); + if (!context) + throw new Error("MobilePageTrigger must be used within MobilePage"); + + return ( +
    context.onOpenChange(true)} {...props}> + {children} +
    + ); +} + +type MobilePagePortalProps = { + children: React.ReactNode; + container?: HTMLElement; +}; + +export function MobilePagePortal({ + children, + container, +}: MobilePagePortalProps) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + + if (!mounted) return null; + + return createPortal(children, container || document.body); +} + +type MobilePageContentProps = { + children: React.ReactNode; + className?: string; + scrollerRef?: React.RefObject; +}; + +export function MobilePageContent({ + children, + className, + scrollerRef, +}: MobilePageContentProps) { + const context = useContext(MobilePageContext); + if (!context) + throw new Error("MobilePageContent must be used within MobilePage"); + + const [isVisible, setIsVisible] = useState(context.open); + const containerRef = useRef(null); + + useEffect(() => { + if (context.open) { + setIsVisible(true); + } + }, [context.open]); + + const handleAnimationComplete = () => { + if (context.open) { + // After opening animation completes, ensure scroller is at the top + if (scrollerRef?.current) { + scrollerRef.current.scrollTop = 0; + } + } else { + setIsVisible(false); + } + }; + + useEffect(() => { + if (context.open && scrollerRef?.current) { + scrollerRef.current.scrollTop = 0; + } + }, [context.open, scrollerRef]); + + return ( + + {isVisible && ( + + {children} + + )} + + ); +} + +interface MobilePageHeaderProps extends React.HTMLAttributes { + onClose?: () => void; + actions?: React.ReactNode; +} + +export function MobilePageHeader({ + children, + className, + onClose, + actions, + ...props +}: MobilePageHeaderProps) { + const { t } = useTranslation(["common"]); + const context = useContext(MobilePageContext); + if (!context) + throw new Error("MobilePageHeader must be used within MobilePage"); + + const handleClose = () => { + if (onClose) { + onClose(); + } else { + context.onOpenChange(false); + } + }; + + return ( +
    + +
    {children}
    + {actions && ( +
    + {actions} +
    + )} +
    + ); +} + +type MobilePageTitleProps = React.HTMLAttributes; + +export function MobilePageTitle({ className, ...props }: MobilePageTitleProps) { + return

    ; +} + +type MobilePageDescriptionProps = React.HTMLAttributes; + +export function MobilePageDescription({ + className, + ...props +}: MobilePageDescriptionProps) { + return ( +

    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/navigation/Bottombar.tsx b/sam2-cpu/frigate-dev/web/src/components/navigation/Bottombar.tsx new file mode 100644 index 0000000..0c34436 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/navigation/Bottombar.tsx @@ -0,0 +1,149 @@ +import NavItem from "./NavItem"; +import { IoIosWarning } from "react-icons/io"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import useSWR from "swr"; +import { FrigateStats } from "@/types/stats"; +import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws"; +import { useContext, useEffect, useMemo } from "react"; +import useStats from "@/hooks/use-stats"; +import GeneralSettings from "../menu/GeneralSettings"; +import useNavigation from "@/hooks/use-navigation"; +import { + StatusBarMessagesContext, + StatusMessage, +} from "@/context/statusbar-provider"; +import { Link } from "react-router-dom"; +import { cn } from "@/lib/utils"; +import { isIOS, isMobile } from "react-device-detect"; +import { isPWA } from "@/utils/isPWA"; +import { useTranslation } from "react-i18next"; + +function Bottombar() { + const navItems = useNavigation("secondary"); + + return ( +

    + {navItems.map((item) => ( + + ))} + + +
    + ); +} + +type StatusAlertNavProps = { + className?: string; +}; +function StatusAlertNav({ className }: StatusAlertNavProps) { + const { t } = useTranslation(["views/system"]); + const { data: initialStats } = useSWR("stats", { + revalidateOnFocus: false, + }); + const latestStats = useFrigateStats(); + + const { messages, addMessage, clearMessages } = useContext( + StatusBarMessagesContext, + )!; + + const stats = useMemo(() => { + if (latestStats) { + return latestStats; + } + + return initialStats; + }, [initialStats, latestStats]); + const { potentialProblems } = useStats(stats); + + useEffect(() => { + clearMessages("stats"); + potentialProblems.forEach((problem) => { + addMessage( + "stats", + problem.text, + problem.color, + undefined, + problem.relevantLink, + ); + }); + }, [potentialProblems, addMessage, clearMessages]); + + const { payload: reindexState } = useEmbeddingsReindexProgress(); + + useEffect(() => { + if (reindexState) { + if (reindexState.status == "indexing") { + clearMessages("embeddings-reindex"); + addMessage( + "embeddings-reindex", + t("stats.reindexingEmbeddings", { + processed: Math.floor( + (reindexState.processed_objects / reindexState.total_objects) * + 100, + ), + }), + ); + } + if (reindexState.status === "completed") { + clearMessages("embeddings-reindex"); + } + } + }, [reindexState, addMessage, clearMessages, t]); + + if (!messages || Object.keys(messages).length === 0) { + return; + } + + return ( + + +
    + +
    +
    + +
    + {Object.entries(messages).map(([key, messageArray]) => ( +
    + {messageArray.map(({ id, text, color, link }: StatusMessage) => { + const message = ( +
    + + {text} +
    + ); + + if (link) { + return ( + + {message} + + ); + } else { + return message; + } + })} +
    + ))} +
    +
    +
    + ); +} + +export default Bottombar; diff --git a/sam2-cpu/frigate-dev/web/src/components/navigation/NavItem.tsx b/sam2-cpu/frigate-dev/web/src/components/navigation/NavItem.tsx new file mode 100644 index 0000000..204e7a9 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/navigation/NavItem.tsx @@ -0,0 +1,73 @@ +import { NavLink } from "react-router-dom"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop } from "react-device-detect"; +import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { NavData } from "@/types/navigation"; +import { IconType } from "react-icons"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; + +const variants = { + primary: { + active: "font-bold text-white bg-selected hover:bg-selected/80", + inactive: "text-secondary-foreground bg-secondary hover:bg-muted", + }, + secondary: { + active: "font-bold text-selected", + inactive: "text-secondary-foreground", + }, +}; + +type NavItemProps = { + className?: string; + item: NavData; + Icon: IconType; + onClick?: () => void; +}; + +export default function NavItem({ + className, + item, + Icon, + onClick, +}: NavItemProps) { + const { t } = useTranslation(["common"]); + if (item.enabled == false) { + return; + } + + const content = ( + + cn( + "flex flex-col items-center justify-center rounded-lg p-[6px]", + className, + variants[item.variant ?? "primary"][isActive ? "active" : "inactive"], + ) + } + > + + + ); + + if (isDesktop) { + return ( + + {content} + + +

    {t(item.title)}

    +
    +
    +
    + ); + } + + return content; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/navigation/Redirect.tsx b/sam2-cpu/frigate-dev/web/src/components/navigation/Redirect.tsx new file mode 100644 index 0000000..d905666 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/navigation/Redirect.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; + +type RedirectProps = { + to: string; +}; +export function Redirect({ to }: RedirectProps) { + const navigate = useNavigate(); + + useEffect(() => { + navigate(to); + }, [to, navigate]); + return
    ; +} diff --git a/sam2-cpu/frigate-dev/web/src/components/navigation/Sidebar.tsx b/sam2-cpu/frigate-dev/web/src/components/navigation/Sidebar.tsx new file mode 100644 index 0000000..af9a3bc --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/navigation/Sidebar.tsx @@ -0,0 +1,50 @@ +import Logo from "../Logo"; +import NavItem from "./NavItem"; +import { CameraGroupSelector } from "../filter/CameraGroupSelector"; +import { Link, useMatch } from "react-router-dom"; +import GeneralSettings from "../menu/GeneralSettings"; +import AccountSettings from "../menu/AccountSettings"; +import useNavigation from "@/hooks/use-navigation"; +import { baseUrl } from "@/api/baseUrl"; +import { useMemo } from "react"; + +function Sidebar() { + const basePath = useMemo(() => new URL(baseUrl).pathname, []); + + const isRootMatch = useMatch("/"); + const isBasePathMatch = useMatch(basePath); + + const navbarLinks = useNavigation(); + + return ( + + ); +} + +export default Sidebar; diff --git a/sam2-cpu/frigate-dev/web/src/components/overlay/CameraInfoDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/overlay/CameraInfoDialog.tsx new file mode 100644 index 0000000..a15d9c5 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/overlay/CameraInfoDialog.tsx @@ -0,0 +1,207 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import ActivityIndicator from "../indicators/activity-indicator"; +import { Ffprobe } from "@/types/stats"; +import { Button } from "../ui/button"; +import copy from "copy-to-clipboard"; +import { CameraConfig } from "@/types/frigateConfig"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import { toast } from "sonner"; +import { Toaster } from "../ui/sonner"; +import { Trans, useTranslation } from "react-i18next"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; + +type CameraInfoDialogProps = { + camera: CameraConfig; + showCameraInfoDialog: boolean; + setShowCameraInfoDialog: React.Dispatch>; +}; +export default function CameraInfoDialog({ + camera, + showCameraInfoDialog, + setShowCameraInfoDialog, +}: CameraInfoDialogProps) { + const { t } = useTranslation(["views/system"]); + const [ffprobeInfo, setFfprobeInfo] = useState(); + + useEffect(() => { + axios + .get("ffprobe", { + params: { + paths: `camera:${camera.name}`, + }, + }) + .then((res) => { + if (res.status === 200) { + setFfprobeInfo(res.data); + } else { + toast.error( + t("cameras.toast.success.copyToClipboard", { + errorMessage: res.statusText, + }), + { + position: "top-center", + }, + ); + } + }) + .catch((error) => { + toast.error( + t("cameras.toast.success.copyToClipboard", { + errorMessage: error.response.data.message, + }), + { + position: "top-center", + }, + ); + }); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onCopyFfprobe = async () => { + copy(JSON.stringify(ffprobeInfo)); + toast.success(t("cameras.toast.success.copyToClipboard")); + }; + + function gcd(a: number, b: number): number { + return b === 0 ? a : gcd(b, a % b); + } + + const cameraName = useCameraFriendlyName(camera); + + return ( + <> + + + + + + {t("cameras.info.cameraProbeInfo", { + camera: cameraName, + })} + + + + cameras.info.streamDataFromFFPROBE + + +
    + {ffprobeInfo ? ( +
    + {ffprobeInfo.map((stream, idx) => ( +
    +
    + {t("cameras.info.stream", { + idx: idx + 1, + })} +
    + {stream.return_code == 0 ? ( +
    + {stream.stdout.streams.map((codec, idx) => ( +
    + {codec.width ? ( +
    +
    + {t("cameras.info.video")} +
    +
    +
    + {t("cameras.info.codec")} + + {" "} + {codec.codec_long_name} + +
    +
    + {codec.width && codec.height ? ( + <> + {t("cameras.info.resolution")}{" "} + + {" "} + {codec.width}x{codec.height} ( + {codec.width / + gcd(codec.width, codec.height)} + / + {codec.height / + gcd(codec.width, codec.height)}{" "} + {t("cameras.info.aspectRatio")}) + + + ) : ( + + {t("cameras.info.resolution")}{" "} + + t("cameras.info.unknown") + + + )} +
    +
    + {t("cameras.info.fps")}{" "} + + {codec.avg_frame_rate == "0/0" + ? t("cameras.info.unknown") + : codec.avg_frame_rate} + +
    +
    +
    + ) : ( +
    +
    Audio:
    +
    + {t("cameras.info.codec")}{" "} + + {codec.codec_long_name} + +
    +
    + )} +
    + ))} +
    + ) : ( +
    +
    + {t("cameras.info.error", { + error: stream.stderr, + })} +
    +
    + )} +
    + ))} +
    + ) : ( +
    + +
    {t("cameras.info.fetching")}
    +
    + )} +
    + + + + +
    +
    + + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/overlay/ClassificationSelectionDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/overlay/ClassificationSelectionDialog.tsx new file mode 100644 index 0000000..ab35ac3 --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/overlay/ClassificationSelectionDialog.tsx @@ -0,0 +1,153 @@ +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { isDesktop, isMobile } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; +import React, { ReactNode, useCallback, useMemo, useState } from "react"; +import TextEntryDialog from "./dialog/TextEntryDialog"; +import { Button } from "../ui/button"; +import axios from "axios"; +import { toast } from "sonner"; +import { Separator } from "../ui/separator"; + +type ClassificationSelectionDialogProps = { + className?: string; + classes: string[]; + modelName: string; + image: string; + onRefresh: () => void; + children: ReactNode; +}; +export default function ClassificationSelectionDialog({ + className, + classes, + modelName, + image, + onRefresh, + children, +}: ClassificationSelectionDialogProps) { + const { t } = useTranslation(["views/classificationModel"]); + + const onCategorizeImage = useCallback( + (category: string) => { + axios + .post(`/classification/${modelName}/dataset/categorize`, { + category, + training_file: image, + }) + .then((resp) => { + if (resp.status == 200) { + toast.success(t("toast.success.categorizedImage"), { + position: "top-center", + }); + onRefresh(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.categorizeFailed", { errorMessage }), { + position: "top-center", + }); + }); + }, + [modelName, image, onRefresh, t], + ); + + const isChildButton = useMemo( + () => React.isValidElement(children) && children.type === Button, + [children], + ); + + // control + const [newClass, setNewClass] = useState(false); + + // components + const Selector = isDesktop ? DropdownMenu : Drawer; + const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger; + const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent; + const SelectorItem = isDesktop + ? DropdownMenuItem + : (props: React.HTMLAttributes) => ( + +
    + + ); + + return ( +
    + onCategorizeImage(newCat)} + /> + + + + + {children} + + + {isMobile && ( + + Details + Details + + )} + {t("categorizeImageAs")} +
    + {classes.sort().map((category) => ( + onCategorizeImage(category)} + > + {category === "none" + ? t("none") + : category.replaceAll("_", " ")} + + ))} + + setNewClass(true)} + > + {t("createCategory.new")} + +
    +
    +
    + {t("categorizeImage")} +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/overlay/CreateRoleDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/overlay/CreateRoleDialog.tsx new file mode 100644 index 0000000..0b10f1c --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/overlay/CreateRoleDialog.tsx @@ -0,0 +1,249 @@ +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Switch } from "@/components/ui/switch"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useTranslation } from "react-i18next"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { isDesktop, isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "../mobile/MobilePage"; + +type CreateRoleOverlayProps = { + show: boolean; + config: FrigateConfig; + onCreate: (role: string, cameras: string[]) => void; + onCancel: () => void; +}; + +export default function CreateRoleDialog({ + show, + config, + onCreate, + onCancel, +}: CreateRoleOverlayProps) { + const { t } = useTranslation(["views/settings"]); + const [isLoading, setIsLoading] = useState(false); + + const cameras = Object.keys(config.cameras || {}); + + const existingRoles = Object.keys(config.auth?.roles || {}); + + const formSchema = z.object({ + role: z + .string() + .min(1, t("roles.dialog.form.role.roleIsRequired")) + .regex(/^[A-Za-z0-9._]+$/, { + message: t("roles.dialog.form.role.roleOnlyInclude"), + }) + .refine((role) => !existingRoles.includes(role), { + message: t("roles.dialog.form.role.roleExists"), + }), + cameras: z + .array(z.string()) + .min(1, t("roles.dialog.form.cameras.required")), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + role: "", + cameras: [], + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + await onCreate(values.role, values.cameras); + form.reset(); + } catch (error) { + // Error handled in parent + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!show) { + form.reset({ + role: "", + cameras: [], + }); + } + }, [show, form]); + + const handleCancel = () => { + form.reset({ + role: "", + cameras: [], + }); + onCancel(); + }; + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + return ( + + +
    + {t("roles.dialog.createRole.title")} + + {t("roles.dialog.createRole.desc")} + +
    + +
    + + ( + + + {t("roles.dialog.form.role.title")} + + + + + + {t("roles.dialog.form.role.desc")} + + + + )} + /> + +
    + {t("roles.dialog.form.cameras.title")} + + {t("roles.dialog.form.cameras.desc")} + +
    + {cameras.map((camera) => ( + { + return ( + +
    + + + +
    + + { + return checked + ? field.onChange([ + ...(field.value as string[]), + camera, + ]) + : field.onChange( + (field.value as string[])?.filter( + (value: string) => value !== camera, + ) || [], + ); + }} + /> + +
    + ); + }} + /> + ))} +
    + +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + ); +} diff --git a/sam2-cpu/frigate-dev/web/src/components/overlay/CreateTriggerDialog.tsx b/sam2-cpu/frigate-dev/web/src/components/overlay/CreateTriggerDialog.tsx new file mode 100644 index 0000000..11734ac --- /dev/null +++ b/sam2-cpu/frigate-dev/web/src/components/overlay/CreateTriggerDialog.tsx @@ -0,0 +1,469 @@ +import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import useSWR from "swr"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { FrigateConfig } from "@/types/frigateConfig"; +import ImagePicker from "@/components/overlay/ImagePicker"; +import { Trigger, TriggerAction, TriggerType } from "@/types/trigger"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "../ui/textarea"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; +import { isDesktop, isMobile } from "react-device-detect"; +import { cn } from "@/lib/utils"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "../mobile/MobilePage"; +import NameAndIdFields from "@/components/input/NameAndIdFields"; + +type CreateTriggerDialogProps = { + show: boolean; + trigger: Trigger | null; + selectedCamera: string; + isLoading: boolean; + onCreate: ( + enabled: boolean, + name: string, + type: TriggerType, + data: string, + threshold: number, + actions: TriggerAction[], + friendly_name: string, + ) => void; + onEdit: (trigger: Trigger) => void; + onCancel: () => void; +}; + +export default function CreateTriggerDialog({ + show, + trigger, + selectedCamera, + isLoading, + onCreate, + onEdit, + onCancel, +}: CreateTriggerDialogProps) { + const { t } = useTranslation("views/settings"); + const { data: config } = useSWR("config"); + + const availableActions = useMemo(() => { + if (!config) return []; + + if (config.cameras[selectedCamera].notifications.enabled_in_config) { + return ["notification", "sub_label", "attribute"]; + } + return ["sub_label", "attribute"]; + }, [config, selectedCamera]); + + const existingTriggerNames = useMemo(() => { + if ( + !config || + !selectedCamera || + !config.cameras[selectedCamera]?.semantic_search?.triggers + ) { + return []; + } + return Object.keys(config.cameras[selectedCamera].semantic_search.triggers); + }, [config, selectedCamera]); + + const existingTriggerFriendlyNames = useMemo(() => { + if ( + !config || + !selectedCamera || + !config.cameras[selectedCamera]?.semantic_search?.triggers + ) { + return []; + } + return Object.values( + config.cameras[selectedCamera].semantic_search.triggers, + ).map((trigger) => trigger.friendly_name); + }, [config, selectedCamera]); + + const formSchema = z.object({ + enabled: z.boolean(), + name: z + .string() + .min(2, t("triggers.dialog.form.name.error.minLength")) + .regex( + /^[a-zA-Z0-9_-]+$/, + t("triggers.dialog.form.name.error.invalidCharacters"), + ) + .refine( + (value) => + !existingTriggerNames.includes(value) || value === trigger?.name, + t("triggers.dialog.form.name.error.alreadyExists"), + ), + friendly_name: z + .string() + .min(2, t("triggers.dialog.form.name.error.minLength")) + .refine( + (value) => + !existingTriggerFriendlyNames.includes(value) || + value === trigger?.friendly_name, + t("triggers.dialog.form.name.error.alreadyExists"), + ), + type: z.enum(["thumbnail", "description"]), + data: z.string().min(1, t("triggers.dialog.form.content.error.required")), + threshold: z + .number() + .min(0, t("triggers.dialog.form.threshold.error.min")) + .max(1, t("triggers.dialog.form.threshold.error.max")), + actions: z.array(z.enum(["notification", "sub_label", "attribute"])), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + enabled: trigger?.enabled ?? true, + name: trigger?.name ?? "", + friendly_name: trigger?.friendly_name ?? "", + type: trigger?.type ?? "description", + data: trigger?.data ?? "", + threshold: trigger?.threshold ?? 0.5, + actions: trigger?.actions ?? [], + }, + }); + + const onSubmit = async (values: z.infer) => { + if (trigger && existingTriggerNames.includes(trigger.name)) { + onEdit({ ...values }); + } else { + onCreate( + values.enabled, + values.name, + values.type, + values.data, + values.threshold, + values.actions, + values.friendly_name, + ); + } + }; + + useEffect(() => { + if (!show) { + form.reset({ + enabled: true, + name: "", + friendly_name: "", + type: "description", + data: "", + threshold: 0.5, + actions: [], + }); + } else if (trigger) { + form.reset( + { + enabled: trigger.enabled, + name: trigger.name, + friendly_name: trigger.friendly_name ?? trigger.name, + type: trigger.type, + data: trigger.data, + threshold: trigger.threshold, + actions: trigger.actions, + }, + { keepDirty: false, keepTouched: false }, // Reset validation state + ); + // Trigger validation to ensure isValid updates + // form.trigger(); + } + }, [show, trigger, form]); + + const handleCancel = () => { + form.reset(); + onCancel(); + }; + + const cameraName = useCameraFriendlyName(selectedCamera); + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + + return ( + + +
    + + {t( + trigger + ? "triggers.dialog.editTrigger.title" + : "triggers.dialog.createTrigger.title", + )} + + + {t( + trigger + ? "triggers.dialog.editTrigger.desc" + : "triggers.dialog.createTrigger.desc", + { + camera: cameraName, + }, + )} + +
    + +
    + + + + ( + +
    + + {t("enabled", { ns: "common" })} + +
    + {t("triggers.dialog.form.enabled.description")} +
    +
    + + + +
    + )} + /> + + ( + + {t("triggers.dialog.form.type.title")} + + + + )} + /> + + ( + + + {t("triggers.dialog.form.content.title")} + + {form.watch("type") === "thumbnail" ? ( + <> + + + + + ) : ( + <> + +