mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-29 07:31:44 -05:00
Compare commits
3 Commits
dependabot
...
misc-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0df44e8bc | ||
|
|
789a7d12a2 | ||
|
|
a2e2b70c61 |
388
.github/copilot-instructions.md
vendored
388
.github/copilot-instructions.md
vendored
@@ -1,3 +1,385 @@
|
||||
- For Frigate NVR, never write strings in the frontend directly. Since the project uses `react-i18next`, use `t()` and write the English string in the relevant translations file in `web/public/locales/en`.
|
||||
- Always conform new and refactored code to the existing coding style in the project.
|
||||
- Always have a way to test your work and confirm your changes. When running backend tests, use `python3 -u -m unittest`.
|
||||
# GitHub Copilot Instructions for Frigate NVR
|
||||
|
||||
This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Frigate NVR is a realtime object detection system for IP cameras that uses:
|
||||
|
||||
- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX
|
||||
- **Frontend**: React with TypeScript, Vite, TailwindCSS
|
||||
- **Architecture**: Multiprocessing design with ZMQ and MQTT communication
|
||||
- **Focus**: Minimal resource usage with maximum performance
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
When reviewing code, do NOT comment on:
|
||||
|
||||
- Missing imports - Static analysis tooling catches these
|
||||
- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting
|
||||
- Minor style inconsistencies already enforced by linters
|
||||
|
||||
## Python Backend Standards
|
||||
|
||||
### Python Requirements
|
||||
|
||||
- **Compatibility**: Python 3.13+
|
||||
- **Language Features**: Use modern Python features:
|
||||
- Pattern matching
|
||||
- Type hints (comprehensive typing preferred)
|
||||
- f-strings (preferred over `%` or `.format()`)
|
||||
- Dataclasses
|
||||
- Async/await patterns
|
||||
|
||||
### Code Quality Standards
|
||||
|
||||
- **Formatting**: Ruff (configured in `pyproject.toml`)
|
||||
- **Linting**: Ruff with rules defined in project config
|
||||
- **Type Checking**: Use type hints consistently
|
||||
- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
|
||||
- **Language**: American English for all code, comments, and documentation
|
||||
|
||||
### Logging Standards
|
||||
|
||||
- **Logger Pattern**: Use module-level logger
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
- **Format Guidelines**:
|
||||
- No periods at end of log messages
|
||||
- No sensitive data (keys, tokens, passwords)
|
||||
- Use lazy logging: `logger.debug("Message with %s", variable)`
|
||||
- **Log Levels**:
|
||||
- `debug`: Development and troubleshooting information
|
||||
- `info`: Important runtime events (startup, shutdown, state changes)
|
||||
- `warning`: Recoverable issues that should be addressed
|
||||
- `error`: Errors that affect functionality but don't crash the app
|
||||
- `exception`: Use in except blocks to include traceback
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Exception Types**: Choose most specific exception available
|
||||
- **Try/Catch Best Practices**:
|
||||
- Only wrap code that can throw exceptions
|
||||
- Keep try blocks minimal - process data after the try/except
|
||||
- Avoid bare exceptions except in background tasks
|
||||
|
||||
Bad pattern:
|
||||
|
||||
```python
|
||||
try:
|
||||
data = await device.get_data() # Can throw
|
||||
# ❌ Don't process data inside try block
|
||||
processed = data.get("value", 0) * 100
|
||||
result = processed
|
||||
except DeviceError:
|
||||
logger.error("Failed to get data")
|
||||
```
|
||||
|
||||
Good pattern:
|
||||
|
||||
```python
|
||||
try:
|
||||
data = await device.get_data() # Can throw
|
||||
except DeviceError:
|
||||
logger.error("Failed to get data")
|
||||
return
|
||||
|
||||
# ✅ Process data outside try block
|
||||
processed = data.get("value", 0) * 100
|
||||
result = processed
|
||||
```
|
||||
|
||||
### Async Programming
|
||||
|
||||
- **External I/O**: All external I/O operations must be async
|
||||
- **Best Practices**:
|
||||
- Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()`
|
||||
- Avoid awaiting in loops - use `asyncio.gather()` instead
|
||||
- No blocking calls in async functions
|
||||
- Use `asyncio.create_task()` for background operations
|
||||
- **Thread Safety**: Use proper synchronization for shared state
|
||||
|
||||
### Documentation Standards
|
||||
|
||||
- **Module Docstrings**: Concise descriptions at top of files
|
||||
```python
|
||||
"""Utilities for motion detection and analysis."""
|
||||
```
|
||||
- **Function Docstrings**: Required for public functions and methods
|
||||
|
||||
```python
|
||||
async def process_frame(frame: ndarray, config: Config) -> Detection:
|
||||
"""Process a video frame for object detection.
|
||||
|
||||
Args:
|
||||
frame: The video frame as numpy array
|
||||
config: Detection configuration
|
||||
|
||||
Returns:
|
||||
Detection results with bounding boxes
|
||||
"""
|
||||
```
|
||||
|
||||
- **Comment Style**:
|
||||
- Explain the "why" not just the "what"
|
||||
- Keep lines under 88 characters when possible
|
||||
- Use clear, descriptive comments
|
||||
|
||||
### File Organization
|
||||
|
||||
- **API Endpoints**: `frigate/api/` - FastAPI route handlers
|
||||
- **Configuration**: `frigate/config/` - Configuration parsing and validation
|
||||
- **Detectors**: `frigate/detectors/` - Object detection backends
|
||||
- **Events**: `frigate/events/` - Event management and storage
|
||||
- **Utilities**: `frigate/util/` - Shared utility functions
|
||||
|
||||
## Frontend (React/TypeScript) Standards
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
- **CRITICAL**: Never write user-facing strings directly in components
|
||||
- **Always use react-i18next**: Import and use the `t()` function
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
return <div>{t("camera_not_found")}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en`
|
||||
- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`)
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
|
||||
- **Formatting**: Prettier with Tailwind CSS plugin
|
||||
- **Type Safety**: TypeScript strict mode enabled
|
||||
- **Testing**: Vitest for unit tests
|
||||
|
||||
### Component Patterns
|
||||
|
||||
- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`)
|
||||
- **Styling**: TailwindCSS with `cn()` utility for class merging
|
||||
- **State Management**: React hooks (useState, useEffect, useCallback, useMemo)
|
||||
- **Data Fetching**: Custom hooks with proper loading and error states
|
||||
|
||||
### ESLint Rules
|
||||
|
||||
Key rules enforced:
|
||||
|
||||
- `react-hooks/rules-of-hooks`: error
|
||||
- `react-hooks/exhaustive-deps`: error
|
||||
- `no-console`: error (use proper logging or remove)
|
||||
- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`)
|
||||
- Unused variables must be prefixed with `_`
|
||||
- Comma dangles required for multiline objects/arrays
|
||||
|
||||
### File Organization
|
||||
|
||||
- **Pages**: `web/src/pages/` - Route components
|
||||
- **Views**: `web/src/views/` - Complex view components
|
||||
- **Components**: `web/src/components/` - Reusable components
|
||||
- **Hooks**: `web/src/hooks/` - Custom React hooks
|
||||
- **API**: `web/src/api/` - API client functions
|
||||
- **Types**: `web/src/types/` - TypeScript type definitions
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Backend Testing
|
||||
|
||||
- **Framework**: Python unittest
|
||||
- **Run Command**: `python3 -u -m unittest`
|
||||
- **Location**: `frigate/test/`
|
||||
- **Coverage**: Aim for comprehensive test coverage of core functionality
|
||||
- **Pattern**: Use `TestCase` classes with descriptive test method names
|
||||
```python
|
||||
class TestMotionDetection(unittest.TestCase):
|
||||
def test_detects_motion_above_threshold(self):
|
||||
# Test implementation
|
||||
```
|
||||
|
||||
### Test Best Practices
|
||||
|
||||
- Always have a way to test your work and confirm your changes
|
||||
- Write tests for bug fixes to prevent regressions
|
||||
- Test edge cases and error conditions
|
||||
- Mock external dependencies (cameras, APIs, hardware)
|
||||
- Use fixtures for test data
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Python Backend
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
python3 -u -m unittest
|
||||
|
||||
# Run specific test file
|
||||
python3 -u -m unittest frigate.test.test_ffmpeg_presets
|
||||
|
||||
# Check formatting (Ruff)
|
||||
ruff format --check frigate/
|
||||
|
||||
# Apply formatting
|
||||
ruff format frigate/
|
||||
|
||||
# Run linter
|
||||
ruff check frigate/
|
||||
```
|
||||
|
||||
### Frontend (from web/ directory)
|
||||
|
||||
```bash
|
||||
# Start dev server (AI agents should never run this directly unless asked)
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Run linter
|
||||
npm run lint
|
||||
|
||||
# Fix linting issues
|
||||
npm run lint:fix
|
||||
|
||||
# Format code
|
||||
npm run prettier:write
|
||||
```
|
||||
|
||||
### Docker Development
|
||||
|
||||
AI agents should never run these commands directly unless instructed.
|
||||
|
||||
```bash
|
||||
# Build local image
|
||||
make local
|
||||
|
||||
# Build debug image
|
||||
make debug
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### API Endpoint Pattern
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Request
|
||||
from frigate.api.defs.tags import Tags
|
||||
|
||||
router = APIRouter(tags=[Tags.Events])
|
||||
|
||||
@router.get("/events")
|
||||
async def get_events(request: Request, limit: int = 100):
|
||||
"""Retrieve events from the database."""
|
||||
# Implementation
|
||||
```
|
||||
|
||||
### Configuration Access
|
||||
|
||||
```python
|
||||
# Access Frigate configuration
|
||||
config: FrigateConfig = request.app.frigate_config
|
||||
camera_config = config.cameras["front_door"]
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
```python
|
||||
from frigate.models import Event
|
||||
|
||||
# Use Peewee ORM for database access
|
||||
events = (
|
||||
Event.select()
|
||||
.where(Event.camera == camera_name)
|
||||
.order_by(Event.start_time.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Avoid These
|
||||
|
||||
```python
|
||||
# Blocking operations in async functions
|
||||
data = requests.get(url) # ❌ Use async HTTP client
|
||||
time.sleep(5) # ❌ Use asyncio.sleep()
|
||||
|
||||
# Hardcoded strings in React components
|
||||
<div>Camera not found</div> # ❌ Use t("camera_not_found")
|
||||
|
||||
# Missing error handling
|
||||
data = await api.get_data() # ❌ No exception handling
|
||||
|
||||
# Bare exceptions in regular code
|
||||
try:
|
||||
value = await sensor.read()
|
||||
except Exception: # ❌ Too broad
|
||||
logger.error("Failed")
|
||||
```
|
||||
|
||||
### ✅ Use These Instead
|
||||
|
||||
```python
|
||||
# Async operations
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
data = await response.json()
|
||||
|
||||
await asyncio.sleep(5) # ✅ Non-blocking
|
||||
|
||||
# Translatable strings in React
|
||||
const { t } = useTranslation();
|
||||
<div>{t("camera_not_found")}</div> # ✅ Translatable
|
||||
|
||||
# Proper error handling
|
||||
try:
|
||||
data = await api.get_data()
|
||||
except ApiException as err:
|
||||
logger.error("API error: %s", err)
|
||||
raise
|
||||
|
||||
# Specific exceptions
|
||||
try:
|
||||
value = await sensor.read()
|
||||
except SensorException as err: # ✅ Specific
|
||||
logger.exception("Failed to read sensor")
|
||||
```
|
||||
|
||||
## Project-Specific Conventions
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- Main config: `config/config.yml`
|
||||
|
||||
### Directory Structure
|
||||
|
||||
- Backend code: `frigate/`
|
||||
- Frontend code: `web/`
|
||||
- Docker files: `docker/`
|
||||
- Documentation: `docs/`
|
||||
- Database migrations: `migrations/`
|
||||
|
||||
### Code Style Conformance
|
||||
|
||||
Always conform new and refactored code to the existing coding style in the project:
|
||||
|
||||
- Follow established patterns in similar files
|
||||
- Match indentation and formatting of surrounding code
|
||||
- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript)
|
||||
- Maintain the same level of verbosity in comments and docstrings
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Documentation: https://docs.frigate.video
|
||||
- Main Repository: https://github.com/blakeblackshear/frigate
|
||||
- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration
|
||||
|
||||
@@ -167,7 +167,7 @@ Inference speeds vary greatly depending on the CPU or GPU used, some known examp
|
||||
| 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 NPU | ~ 6 ms | s-320: 11 ms s-640: 30 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 | | |
|
||||
|
||||
@@ -419,14 +419,19 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
||||
"""
|
||||
if object_id not in self.classification_history:
|
||||
self.classification_history[object_id] = []
|
||||
logger.debug(f"Created new classification history for {object_id}")
|
||||
|
||||
self.classification_history[object_id].append(
|
||||
(current_label, current_score, current_time)
|
||||
)
|
||||
|
||||
history = self.classification_history[object_id]
|
||||
logger.debug(
|
||||
f"History for {object_id}: {len(history)} entries, latest=({current_label}, {current_score})"
|
||||
)
|
||||
|
||||
if len(history) < 3:
|
||||
logger.debug(f"History for {object_id} has {len(history)} entries, need at least 3")
|
||||
return None, 0.0
|
||||
|
||||
label_counts = {}
|
||||
@@ -445,14 +450,25 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
||||
best_count = label_counts[best_label]
|
||||
|
||||
consensus_threshold = total_attempts * 0.6
|
||||
logger.debug(
|
||||
f"Consensus calc for {object_id}: label_counts={label_counts}, "
|
||||
f"best_label={best_label}, best_count={best_count}, "
|
||||
f"total={total_attempts}, threshold={consensus_threshold}"
|
||||
)
|
||||
|
||||
if best_count < consensus_threshold:
|
||||
logger.debug(
|
||||
f"No consensus for {object_id}: {best_count} < {consensus_threshold}"
|
||||
)
|
||||
return None, 0.0
|
||||
|
||||
avg_score = sum(label_scores[best_label]) / len(label_scores[best_label])
|
||||
|
||||
if best_label == "none":
|
||||
logger.debug(f"Filtering 'none' label for {object_id}")
|
||||
return None, 0.0
|
||||
|
||||
logger.debug(f"Consensus reached for {object_id}: {best_label} with avg_score={avg_score}")
|
||||
return best_label, avg_score
|
||||
|
||||
def process_frame(self, obj_data, frame):
|
||||
@@ -560,17 +576,30 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
|
||||
)
|
||||
|
||||
if score < self.model_config.threshold:
|
||||
logger.debug(f"Score {score} is less than threshold.")
|
||||
logger.debug(
|
||||
f"{self.model_config.name}: Score {score} < threshold {self.model_config.threshold} for {object_id}, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
sub_label = self.labelmap[best_id]
|
||||
|
||||
logger.debug(
|
||||
f"{self.model_config.name}: Object {object_id} (label={obj_data['label']}) passed threshold with sub_label={sub_label}, score={score}"
|
||||
)
|
||||
|
||||
consensus_label, consensus_score = self.get_weighted_score(
|
||||
object_id, sub_label, score, now
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"{self.model_config.name}: get_weighted_score returned consensus_label={consensus_label}, consensus_score={consensus_score} for {object_id}"
|
||||
)
|
||||
|
||||
if consensus_label is not None:
|
||||
camera = obj_data["camera"]
|
||||
logger.info(
|
||||
f"{self.model_config.name}: Publishing sub_label={consensus_label} for {obj_data['label']} object {object_id} on {camera}"
|
||||
)
|
||||
|
||||
if (
|
||||
self.model_config.object_config.classification_type
|
||||
|
||||
@@ -173,9 +173,9 @@ function getVerifiedIcon(
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="flex items-center">
|
||||
<div key={label} className="relative flex items-center">
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
|
||||
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -188,9 +188,9 @@ function getRecognizedPlateIcon(
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="flex items-center">
|
||||
<div key={label} className="relative inline-flex items-center">
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
|
||||
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="scrollbar-container flex w-full flex-col overflow-y-auto">
|
||||
<div className="scrollbar-container relative flex w-full flex-col overflow-y-auto">
|
||||
{objects && objects.length > 0 ? (
|
||||
objects.map((obj: ObjectType) => {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user