Compare commits

..

No commits in common. "4dd999dc74ebc60b72d71a9ef87012b8657c0012" and "9160c5168e7d04c7f905bf899bf75265f6de456e" have entirely different histories.

6 changed files with 71 additions and 100 deletions

View File

@ -387,28 +387,27 @@ def config_set(request: Request, body: AppConfigSetBody):
old_config: FrigateConfig = request.app.frigate_config old_config: FrigateConfig = request.app.frigate_config
request.app.frigate_config = config request.app.frigate_config = config
if body.update_topic: if body.update_topic and body.update_topic.startswith("config/cameras/"):
if body.update_topic.startswith("config/cameras/"): _, _, camera, field = body.update_topic.split("/")
_, _, camera, field = body.update_topic.split("/")
if field == "add": if field == "add":
settings = config.cameras[camera] settings = config.cameras[camera]
elif field == "remove": elif field == "remove":
settings = old_config.cameras[camera] settings = old_config.cameras[camera]
else:
settings = config.get_nested_object(body.update_topic)
request.app.config_publisher.publish_update(
CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera),
settings,
)
else: else:
# Handle nested config updates (e.g., config/classification/custom/{name})
settings = config.get_nested_object(body.update_topic) settings = config.get_nested_object(body.update_topic)
if settings:
request.app.config_publisher.publisher.publish( request.app.config_publisher.publish_update(
body.update_topic, settings CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera),
) settings,
)
elif body.update_topic and "/config/" in body.update_topic[1:]:
# Handle nested config updates (e.g., config/classification/custom/{name})
settings = config.get_nested_object(body.update_topic)
if settings:
request.app.config_publisher.publisher.publish(
body.update_topic, settings
)
return JSONResponse( return JSONResponse(
content=( content=(

View File

@ -3,9 +3,7 @@
import datetime import datetime
import logging import logging
import os import os
import random
import shutil import shutil
import string
from typing import Any from typing import Any
import cv2 import cv2
@ -709,9 +707,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
status_code=404, status_code=404,
) )
random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) new_name = f"{category}-{datetime.datetime.now().timestamp()}.png"
timestamp = datetime.datetime.now().timestamp()
new_name = f"{category}-{timestamp}-{random_id}.png"
new_file_folder = os.path.join( new_file_folder = os.path.join(
CLIPS_DIR, sanitize_filename(name), "dataset", category CLIPS_DIR, sanitize_filename(name), "dataset", category
) )

View File

@ -286,11 +286,16 @@ class EmbeddingMaintainer(threading.Thread):
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 and model_config:
# Extract model name from topic: config/classification/custom/{model_name}
model_name = topic.split("/")[-1] model_name = topic.split("/")[-1]
self.config.classification.custom[model_name] = model_config logger.info(
f"Received classification config update for model: {model_name}"
)
# Check if processor already exists self.config.classification.custom[model_name] = model_config
for processor in self.realtime_processors: existing_processor_index = None
for i, processor in enumerate(self.realtime_processors):
if isinstance( if isinstance(
processor, processor,
( (
@ -299,10 +304,14 @@ class EmbeddingMaintainer(threading.Thread):
), ),
): ):
if processor.model_config.name == model_name: if processor.model_config.name == model_name:
logger.debug( existing_processor_index = i
f"Classification processor for model {model_name} already exists, skipping" break
)
return if existing_processor_index is not None:
logger.info(
f"Removing existing classification processor for model: {model_name}"
)
self.realtime_processors.pop(existing_processor_index)
if model_config.state_config is not None: if model_config.state_config is not None:
processor = CustomStateClassificationProcessor( processor = CustomStateClassificationProcessor(
@ -317,9 +326,7 @@ class EmbeddingMaintainer(threading.Thread):
) )
self.realtime_processors.append(processor) self.realtime_processors.append(processor)
logger.info( logger.info(f"Added classification processor for model: {model_name}")
f"Added classification processor for model: {model_name} (type: {type(processor).__name__})"
)
def _process_requests(self) -> None: def _process_requests(self) -> None:
"""Process embeddings requests""" """Process embeddings requests"""

View File

@ -101,10 +101,6 @@
"title": "Generating Sample Images", "title": "Generating Sample Images",
"description": "We're pulling representative images from your recordings. This may take a moment..." "description": "We're pulling representative images from your recordings. This may take a moment..."
}, },
"training": {
"title": "Training Model",
"description": "Your model is being trained in the background. You can close this wizard and the training will continue."
},
"retryGenerate": "Retry Generation", "retryGenerate": "Retry Generation",
"selectClass": "Select class...", "selectClass": "Select class...",
"none": "None", "none": "None",

View File

@ -46,7 +46,6 @@ export default function Step3ChooseExamples({
const [imageClassifications, setImageClassifications] = useState<{ const [imageClassifications, setImageClassifications] = useState<{
[imageName: string]: string; [imageName: string]: string;
}>(initialData?.imageClassifications || {}); }>(initialData?.imageClassifications || {});
const [isTraining, setIsTraining] = useState(false);
const { data: trainImages, mutate: refreshTrainImages } = useSWR<string[]>( const { data: trainImages, mutate: refreshTrainImages } = useSWR<string[]>(
hasGenerated ? `classification/${step1Data.modelName}/train` : null, hasGenerated ? `classification/${step1Data.modelName}/train` : null,
@ -187,25 +186,25 @@ export default function Step3ChooseExamples({
}); });
// Step 2: Classify each image by moving it to the correct category folder // Step 2: Classify each image by moving it to the correct category folder
const categorizePromises = Object.entries(imageClassifications).map( for (const [imageName, className] of Object.entries(
([imageName, className]) => { imageClassifications,
if (!className) return Promise.resolve(); )) {
return axios.post( if (!className) continue;
`/classification/${step1Data.modelName}/dataset/categorize`,
{ await axios.post(
training_file: imageName, `/classification/${step1Data.modelName}/dataset/categorize`,
category: className === "none" ? "none" : className, {
}, training_file: imageName,
); category: className === "none" ? "none" : className,
}, },
); );
await Promise.all(categorizePromises); }
// Step 3: Kick off training // Step 3: Kick off training
await axios.post(`/classification/${step1Data.modelName}/train`); await axios.post(`/classification/${step1Data.modelName}/train`);
toast.success(t("wizard.step3.trainingStarted")); toast.success(t("wizard.step3.trainingStarted"));
setIsTraining(true); onClose();
} catch (error) { } catch (error) {
const axiosError = error as { const axiosError = error as {
response?: { data?: { message?: string; detail?: string } }; response?: { data?: { message?: string; detail?: string } };
@ -221,7 +220,7 @@ export default function Step3ChooseExamples({
t("wizard.step3.errors.classifyFailed", { error: errorMessage }), t("wizard.step3.errors.classifyFailed", { error: errorMessage }),
); );
} }
}, [imageClassifications, step1Data, step2Data, t]); }, [onClose, imageClassifications, step1Data, step2Data, t]);
const allImagesClassified = useMemo(() => { const allImagesClassified = useMemo(() => {
if (!unknownImages || unknownImages.length === 0) return false; if (!unknownImages || unknownImages.length === 0) return false;
@ -231,22 +230,7 @@ export default function Step3ChooseExamples({
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{isTraining ? ( {isGenerating ? (
<div className="flex flex-col items-center gap-6 py-12">
<ActivityIndicator className="size-12" />
<div className="text-center">
<h3 className="mb-2 text-lg font-medium">
{t("wizard.step3.training.title")}
</h3>
<p className="text-sm text-muted-foreground">
{t("wizard.step3.training.description")}
</p>
</div>
<Button onClick={onClose} variant="select" className="mt-4">
{t("button.close", { ns: "common" })}
</Button>
</div>
) : isGenerating ? (
<div className="flex h-[50vh] flex-col items-center justify-center gap-4"> <div className="flex h-[50vh] flex-col items-center justify-center gap-4">
<ActivityIndicator className="size-12" /> <ActivityIndicator className="size-12" />
<div className="text-center"> <div className="text-center">
@ -337,22 +321,20 @@ export default function Step3ChooseExamples({
</div> </div>
)} )}
{!isTraining && ( <div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4"> <Button type="button" onClick={onBack} className="sm:flex-1">
<Button type="button" onClick={onBack} className="sm:flex-1"> {t("button.back", { ns: "common" })}
{t("button.back", { ns: "common" })} </Button>
</Button> <Button
<Button type="button"
type="button" onClick={handleContinue}
onClick={handleContinue} variant="select"
variant="select" className="flex items-center justify-center gap-2 sm:flex-1"
className="flex items-center justify-center gap-2 sm:flex-1" disabled={!hasGenerated || isGenerating || !allImagesClassified}
disabled={!hasGenerated || isGenerating || !allImagesClassified} >
> {t("button.continue", { ns: "common" })}
{t("button.continue", { ns: "common" })} </Button>
</Button> </div>
</div>
)}
</div> </div>
); );
} }

View File

@ -30,12 +30,9 @@ export default function ModelSelectionView({
const { t } = useTranslation(["views/classificationModel"]); const { t } = useTranslation(["views/classificationModel"]);
const [page, setPage] = useState<ModelType>("objects"); const [page, setPage] = useState<ModelType>("objects");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const { data: config, mutate: refreshConfig } = useSWR<FrigateConfig>( const { data: config } = useSWR<FrigateConfig>("config", {
"config", revalidateOnFocus: false,
{ });
revalidateOnFocus: false,
},
);
// data // data
@ -74,10 +71,7 @@ export default function ModelSelectionView({
<> <>
<ClassificationModelWizardDialog <ClassificationModelWizardDialog
open={newModel} open={newModel}
onClose={() => { onClose={() => setNewModel(false)}
setNewModel(false);
refreshConfig();
}}
/> />
<NoModelsView onCreateModel={() => setNewModel(true)} />; <NoModelsView onCreateModel={() => setNewModel(true)} />;
</> </>
@ -88,10 +82,7 @@ export default function ModelSelectionView({
<div className="flex size-full flex-col p-2"> <div className="flex size-full flex-col p-2">
<ClassificationModelWizardDialog <ClassificationModelWizardDialog
open={newModel} open={newModel}
onClose={() => { onClose={() => setNewModel(false)}
setNewModel(false);
refreshConfig();
}}
/> />
<div className="flex h-12 w-full items-center justify-between"> <div className="flex h-12 w-full items-center justify-between">