From 3b6814fbc9a09100c29c6cb974f8ffbf9472c307 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:30:21 -0600 Subject: [PATCH] Revert "Miscellaneous fixes (0.17 beta) (#21764)" (#21825) This reverts commit 50ac5a1483c94c86fca3a84c2a33a9e898a40de9. --- .github/copilot-instructions.md | 388 +----------------- docs/docs/frigate/hardware.md | 2 +- frigate/genai/__init__.py | 21 +- frigate/genai/azure-openai.py | 55 +-- frigate/genai/gemini.py | 10 +- frigate/genai/ollama.py | 8 +- frigate/genai/openai.py | 35 +- web/src/components/card/AnimatedEventCard.tsx | 41 +- 8 files changed, 60 insertions(+), 500 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f053abe3f..89acd8a9b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,385 +1,3 @@ -# 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
{t("camera_not_found")}
; - } - ``` - -- **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 -
Camera not found
# ❌ 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(); -
{t("camera_not_found")}
# ✅ 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 +- 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`. diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index 48ab2c647..f7294042a 100644 --- a/docs/docs/frigate/hardware.md +++ b/docs/docs/frigate/hardware.md @@ -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 s-640: 30 ms | 320: ~ 14 ms 640: ~ 34 ms | 320-n: 40 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 | | | diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 821b449de..7f0192912 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -140,12 +140,7 @@ Each line represents a detection state, not necessarily unique individuals. Pare ) as f: f.write(context_prompt) - json_schema = { - "name": "review_metadata", - "schema": ReviewMetadata.model_json_schema(), - "strict": True, - } - response = self._send(context_prompt, thumbnails, json_schema=json_schema) + response = self._send(context_prompt, thumbnails) if debug_save and response: with open( @@ -157,8 +152,6 @@ Each line represents a detection state, not necessarily unique individuals. Pare f.write(response) if response: - # With JSON schema, response should already be valid JSON - # But keep regex cleanup as fallback for providers without schema support clean_json = re.sub( r"\n?```$", "", re.sub(r"^```[a-zA-Z0-9]*\n?", "", response) ) @@ -291,16 +284,8 @@ Guidelines: """Initialize the client.""" return None - def _send( - self, prompt: str, images: list[bytes], json_schema: Optional[dict] = None - ) -> Optional[str]: - """Submit a request to the provider. - - Args: - prompt: The text prompt to send - images: List of image bytes to include - json_schema: Optional JSON schema for structured output (provider-specific support) - """ + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: + """Submit a request to the provider.""" return None def get_context_size(self) -> int: diff --git a/frigate/genai/azure-openai.py b/frigate/genai/azure-openai.py index 9cc03fe75..eb08f7786 100644 --- a/frigate/genai/azure-openai.py +++ b/frigate/genai/azure-openai.py @@ -41,46 +41,29 @@ class OpenAIClient(GenAIClient): azure_endpoint=azure_endpoint, ) - def _send( - self, prompt: str, images: list[bytes], json_schema: Optional[dict] = None - ) -> Optional[str]: + 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] - - request_params = { - "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, - } - - if json_schema: - request_params["response_format"] = { - "type": "json_schema", - "json_schema": { - "name": json_schema.get("name", "response"), - "schema": json_schema.get("schema", {}), - "strict": json_schema.get("strict", True), - }, - } - try: result = self.provider.chat.completions.create( - **request_params, + 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, **self.genai_config.runtime_options, ) except Exception as e: diff --git a/frigate/genai/gemini.py b/frigate/genai/gemini.py index 1212f15ad..b700c33a4 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/gemini.py @@ -41,9 +41,7 @@ class GeminiClient(GenAIClient): http_options=types.HttpOptions(**http_options_dict), ) - def _send( - self, prompt: str, images: list[bytes], json_schema: Optional[dict] = None - ) -> Optional[str]: + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: """Submit a request to Gemini.""" contents = [ types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images @@ -53,12 +51,6 @@ class GeminiClient(GenAIClient): generation_config_dict = {"candidate_count": 1} generation_config_dict.update(self.genai_config.runtime_options) - if json_schema and "schema" in json_schema: - generation_config_dict["response_mime_type"] = "application/json" - generation_config_dict["response_schema"] = types.Schema( - json_schema=json_schema["schema"] - ) - response = self.provider.models.generate_content( model=self.genai_config.model, contents=contents, diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index f798cbd19..ab6d3c0b3 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -50,9 +50,7 @@ class OllamaClient(GenAIClient): logger.warning("Error initializing Ollama: %s", str(e)) return None - def _send( - self, prompt: str, images: list[bytes], json_schema: Optional[dict] = None - ) -> Optional[str]: + def _send(self, prompt: str, images: list[bytes]) -> Optional[str]: """Submit a request to Ollama""" if self.provider is None: logger.warning( @@ -64,10 +62,6 @@ class OllamaClient(GenAIClient): **self.provider_options, **self.genai_config.runtime_options, } - - if json_schema and "schema" in json_schema: - ollama_options["format"] = json_schema["schema"] - result = self.provider.generate( self.genai_config.model, prompt, diff --git a/frigate/genai/openai.py b/frigate/genai/openai.py index a5ee6455e..1fb0dd852 100644 --- a/frigate/genai/openai.py +++ b/frigate/genai/openai.py @@ -31,9 +31,7 @@ class OpenAIClient(GenAIClient): } return OpenAI(api_key=self.genai_config.api_key, **provider_opts) - def _send( - self, prompt: str, images: list[bytes], json_schema: Optional[dict] = None - ) -> Optional[str]: + 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 = [] @@ -53,31 +51,16 @@ class OpenAIClient(GenAIClient): "text": prompt, } ) - - request_params = { - "model": self.genai_config.model, - "messages": [ - { - "role": "user", - "content": messages_content, - }, - ], - "timeout": self.timeout, - } - - if json_schema: - request_params["response_format"] = { - "type": "json_schema", - "json_schema": { - "name": json_schema.get("name", "response"), - "schema": json_schema.get("schema", {}), - "strict": json_schema.get("strict", True), - }, - } - try: result = self.provider.chat.completions.create( - **request_params, + model=self.genai_config.model, + messages=[ + { + "role": "user", + "content": messages_content, + }, + ], + timeout=self.timeout, **self.genai_config.runtime_options, ) if ( diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index c5da99aa2..a67dd8305 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -12,7 +12,7 @@ import { useCameraPreviews } from "@/hooks/use-camera-previews"; import { baseUrl } from "@/api/baseUrl"; import { VideoPreview } from "../preview/ScrubbablePreview"; import { useApiHost } from "@/api"; -import { isSafari } from "react-device-detect"; +import { isDesktop, isSafari } from "react-device-detect"; import { useUserPersistence } from "@/hooks/use-user-persistence"; import { Skeleton } from "../ui/skeleton"; import { Button } from "../ui/button"; @@ -87,6 +87,7 @@ export function AnimatedEventCard({ }, [visibilityListener]); const [isLoaded, setIsLoaded] = useState(false); + const [isHovered, setIsHovered] = useState(false); // interaction @@ -133,27 +134,31 @@ export function AnimatedEventCard({
setIsHovered(true) : undefined} + onMouseLeave={isDesktop ? () => setIsHovered(false) : undefined} > - - - - - {t("markAsReviewed")} - + {isHovered && ( + + + + + {t("markAsReviewed")} + + )} {previews != undefined && alertVideosLoaded && (