diff --git a/frigate/api/classification.py b/frigate/api/classification.py index 48b26e355..84c47ddaa 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -680,6 +680,97 @@ def delete_classification_dataset_images( ) +@router.put( + "/classification/{name}/dataset/{old_category}/rename", + response_model=GenericResponse, + dependencies=[Depends(require_role(["admin"]))], + summary="Rename a classification category", + description="""Renames a classification category for a given classification model. + The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid.""", +) +def rename_classification_category( + request: Request, name: str, old_category: str, body: dict = None +): + config: FrigateConfig = request.app.frigate_config + + if name not in config.classification.custom: + return JSONResponse( + content=( + { + "success": False, + "message": f"{name} is not a known classification model.", + } + ), + status_code=404, + ) + + json: dict[str, Any] = body or {} + new_category = sanitize_filename(json.get("new_category", "")) + + if not new_category: + return JSONResponse( + content=( + { + "success": False, + "message": "New category name is required.", + } + ), + status_code=400, + ) + + old_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", sanitize_filename(old_category) + ) + new_folder = os.path.join( + CLIPS_DIR, sanitize_filename(name), "dataset", new_category + ) + + if not os.path.exists(old_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {old_category} does not exist.", + } + ), + status_code=404, + ) + + if os.path.exists(new_folder): + return JSONResponse( + content=( + { + "success": False, + "message": f"Category {new_category} already exists.", + } + ), + status_code=400, + ) + + try: + os.rename(old_folder, new_folder) + return JSONResponse( + content=( + { + "success": True, + "message": f"Successfully renamed category to {new_category}.", + } + ), + status_code=200, + ) + except Exception as e: + logger.error(f"Error renaming category: {e}") + return JSONResponse( + content=( + { + "success": False, + "message": f"Failed to rename category: {str(e)}", + } + ), + status_code=500, + ) + + @router.post( "/classification/{name}/dataset/categorize", response_model=GenericResponse, diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index 803c579f1..65118f227 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -22,7 +22,8 @@ "categorizedImage": "Successfully Classified Image", "trainedModel": "Successfully trained model.", "trainingModel": "Successfully started model training.", - "updatedModel": "Successfully updated model configuration" + "updatedModel": "Successfully updated model configuration", + "renamedCategory": "Successfully renamed class to {{name}}" }, "error": { "deleteImageFailed": "Failed to delete: {{errorMessage}}", @@ -30,7 +31,8 @@ "deleteModelFailed": "Failed to delete model: {{errorMessage}}", "categorizeFailed": "Failed to categorize image: {{errorMessage}}", "trainingFailed": "Failed to start model training: {{errorMessage}}", - "updateModelFailed": "Failed to update model: {{errorMessage}}" + "updateModelFailed": "Failed to update model: {{errorMessage}}", + "renameCategoryFailed": "Failed to rename class: {{errorMessage}}" } }, "deleteCategory": { diff --git a/web/src/views/classification/ModelTrainingView.tsx b/web/src/views/classification/ModelTrainingView.tsx index 5fe614145..697483897 100644 --- a/web/src/views/classification/ModelTrainingView.tsx +++ b/web/src/views/classification/ModelTrainingView.tsx @@ -187,6 +187,37 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) { null, ); + const onRename = useCallback( + (old_name: string, new_name: string) => { + axios + .put(`/classification/${model.name}/dataset/${old_name}/rename`, { + new_category: new_name, + }) + .then((resp) => { + if (resp.status == 200) { + toast.success( + t("toast.success.renamedCategory", { name: new_name }), + { + position: "top-center", + }, + ); + setPageToggle(new_name); + refreshDataset(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.renameCategoryFailed", { errorMessage }), { + position: "top-center", + }); + }); + }, + [model, setPageToggle, refreshDataset, t], + ); + const onDelete = useCallback( (ids: string[], isName: boolean = false, category?: string) => { const targetCategory = category || pageToggle; @@ -354,7 +385,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) { trainImages={trainImages || []} setPageToggle={setPageToggle} onDelete={onDelete} - onRename={() => {}} + onRename={onRename} /> )}