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,
)
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:
# Publish None for removal, actual config for add/update
request.app.config_publisher.publisher.publish(
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.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=(
{

View File

@ -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),

View File

@ -283,11 +283,32 @@ 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]
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

View File

@ -213,17 +213,31 @@ 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) {
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((error) => {
} catch (err) {
const error = err as {
response?: { data?: { message?: string; detail?: string } };
};
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
@ -231,7 +245,7 @@ function ModelCard({ config, onClick, onDelete }: ModelCardProps) {
toast.error(t("toast.error.deleteModelFailed", { errorMessage }), {
position: "top-center",
});
});
}
}, [config, onDelete, t]);
const handleDeleteClick = useCallback((e: React.MouseEvent) => {