diff --git a/frigate/api/app.py b/frigate/api/app.py index 328e4be60..fa34b3dcd 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -403,12 +403,13 @@ def config_set(request: Request, body: AppConfigSetBody): settings, ) else: - # Handle nested config updates (e.g., config/classification/custom/{name}) + # Generic handling for global config updates settings = config.get_nested_object(body.update_topic) - if settings: - request.app.config_publisher.publisher.publish( - body.update_topic, settings - ) + + # Publish None for removal, actual config for add/update + request.app.config_publisher.publisher.publish( + body.update_topic, settings + ) return JSONResponse( content=( diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 975370d59..90f6391c0 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -34,12 +34,10 @@ 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.builtin import update_yaml_file_bulk from frigate.util.classification import ( collect_object_classification_examples, collect_state_classification_examples, ) -from frigate.util.config import find_config_file from frigate.util.path import get_event_snapshot logger = logging.getLogger(__name__) @@ -840,24 +838,6 @@ def delete_classification_model(request: Request, name: str): if os.path.exists(model_dir): shutil.rmtree(model_dir) - # Remove the model from the config file - config_file = find_config_file() - try: - # Setting value to empty string deletes the key - updates = {f"classification.custom.{name}": None} - update_yaml_file_bulk(config_file, updates) - except Exception as e: - logger.error(f"Error updating config file: {e}") - return JSONResponse( - content=( - { - "success": False, - "message": "Failed to update config file.", - } - ), - status_code=500, - ) - return JSONResponse( content=( { diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index d3ebc5397..6148e8c05 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -330,6 +330,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): 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), diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 10895d592..4ab8132c1 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -283,44 +283,65 @@ class EmbeddingMaintainer(threading.Thread): logger.info("Exiting embeddings maintenance...") def _check_classification_config_updates(self) -> None: - """Check for classification config updates and add new processors.""" + """Check for classification config updates and add/remove processors.""" topic, model_config = self.classification_config_subscriber.check_for_update() - if topic and model_config: + if topic: model_name = topic.split("/")[-1] - 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" + if model_config is None: + self.realtime_processors = [ + processor + for processor in self.realtime_processors + if not ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), ) - return + and processor.model_config.name == model_name + ) + ] - if model_config.state_config is not None: - processor = CustomStateClassificationProcessor( - self.config, model_config, self.requestor, self.metrics + logger.info( + f"Successfully removed classification processor for model: {model_name}" ) else: - processor = CustomObjectClassificationProcessor( - self.config, - model_config, - self.event_metadata_publisher, - self.metrics, - ) + self.config.classification.custom[model_name] = model_config - self.realtime_processors.append(processor) - logger.info( - f"Added classification processor for model: {model_name} (type: {type(processor).__name__})" - ) + # 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.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""" diff --git a/web/src/views/classification/ModelSelectionView.tsx b/web/src/views/classification/ModelSelectionView.tsx index f0bd3193a..b353be65f 100644 --- a/web/src/views/classification/ModelSelectionView.tsx +++ b/web/src/views/classification/ModelSelectionView.tsx @@ -213,25 +213,39 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const handleDelete = useCallback(async () => { - await axios - .delete(`classification/${config.name}`) - .then((resp) => { - if (resp.status == 200) { - toast.success(t("toast.success.deletedModel", { count: 1 }), { - position: "top-center", - }); - onDelete(); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error(t("toast.error.deleteModelFailed", { errorMessage }), { - position: "top-center", - }); + try { + // First, remove from config to stop the processor + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${config.name}`, + config_data: { + classification: { + custom: { + [config.name]: "", + }, + }, + }, }); + + // Then, delete the model data and files + await axios.delete(`classification/${config.name}`); + + toast.success(t("toast.success.deletedModel", { count: 1 }), { + position: "top-center", + }); + onDelete(); + } catch (err) { + const error = err as { + response?: { data?: { message?: string; detail?: string } }; + }; + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.deleteModelFailed", { errorMessage }), { + position: "top-center", + }); + } }, [config, onDelete, t]); const handleDeleteClick = useCallback((e: React.MouseEvent) => {