Correctly remove classification model from config (#20772)

* Correctly remove classification model from config

* Undo

* fix

* Use existing config update API and dynamically remove models that were running

* Set update message for face
This commit is contained in:
Nicolas Mowen 2025-11-03 08:01:30 -07:00 committed by GitHub
parent 740c618240
commit 31fa87ce73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 72 deletions

View File

@ -403,9 +403,10 @@ def config_set(request: Request, body: AppConfigSetBody):
settings, settings,
) )
else: 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) settings = config.get_nested_object(body.update_topic)
if settings:
# Publish None for removal, actual config for add/update
request.app.config_publisher.publisher.publish( request.app.config_publisher.publisher.publish(
body.update_topic, settings body.update_topic, settings
) )

View File

@ -34,12 +34,10 @@ from frigate.config.camera import DetectConfig
from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR from frigate.const import CLIPS_DIR, FACE_DIR, MODEL_CACHE_DIR
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.models import Event from frigate.models import Event
from frigate.util.builtin import update_yaml_file_bulk
from frigate.util.classification import ( from frigate.util.classification import (
collect_object_classification_examples, collect_object_classification_examples,
collect_state_classification_examples, collect_state_classification_examples,
) )
from frigate.util.config import find_config_file
from frigate.util.path import get_event_snapshot from frigate.util.path import get_event_snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -840,24 +838,6 @@ def delete_classification_model(request: Request, name: str):
if os.path.exists(model_dir): if os.path.exists(model_dir):
shutil.rmtree(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( return JSONResponse(
content=( content=(
{ {

View File

@ -330,6 +330,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data) -> dict[str, Any] | None: def handle_request(self, topic, request_data) -> dict[str, Any] | None:
if topic == EmbeddingsRequestEnum.clear_face_classifier.value: if topic == EmbeddingsRequestEnum.clear_face_classifier.value:
self.recognizer.clear() self.recognizer.clear()
return {"success": True, "message": "Face classifier cleared."}
elif topic == EmbeddingsRequestEnum.recognize_face.value: elif topic == EmbeddingsRequestEnum.recognize_face.value:
img = cv2.imdecode( img = cv2.imdecode(
np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8), np.frombuffer(base64.b64decode(request_data["image"]), dtype=np.uint8),

View File

@ -283,11 +283,32 @@ class EmbeddingMaintainer(threading.Thread):
logger.info("Exiting embeddings maintenance...") logger.info("Exiting embeddings maintenance...")
def _check_classification_config_updates(self) -> None: 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() topic, model_config = self.classification_config_subscriber.check_for_update()
if topic and model_config: if topic:
model_name = topic.split("/")[-1] 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 self.config.classification.custom[model_name] = model_config
# Check if processor already exists # Check if processor already exists

View File

@ -213,17 +213,31 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const handleDelete = useCallback(async () => { const handleDelete = useCallback(async () => {
await axios try {
.delete(`classification/${config.name}`) // First, remove from config to stop the processor
.then((resp) => { await axios.put("/config/set", {
if (resp.status == 200) { 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 }), { toast.success(t("toast.success.deletedModel", { count: 1 }), {
position: "top-center", position: "top-center",
}); });
onDelete(); onDelete();
} } catch (err) {
}) const error = err as {
.catch((error) => { response?: { data?: { message?: string; detail?: string } };
};
const errorMessage = const errorMessage =
error.response?.data?.message || error.response?.data?.message ||
error.response?.data?.detail || error.response?.data?.detail ||
@ -231,7 +245,7 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
toast.error(t("toast.error.deleteModelFailed", { errorMessage }), { toast.error(t("toast.error.deleteModelFailed", { errorMessage }), {
position: "top-center", position: "top-center",
}); });
}); }
}, [config, onDelete, t]); }, [config, onDelete, t]);
const handleDeleteClick = useCallback((e: React.MouseEvent) => { const handleDeleteClick = useCallback((e: React.MouseEvent) => {