88
99from simpletuner .helpers .publishing .metadata import save_model_card , save_training_config
1010from simpletuner .helpers .training .state_tracker import StateTracker
11+ from simpletuner .helpers .training .validation_images import save_image_with_validation_format
1112
1213logger = logging .getLogger (__name__ )
1314from simpletuner .helpers .training .multi_process import should_log
2122LORA_SAFETENSORS_FILENAME = "pytorch_lora_weights.safetensors"
2223EMA_SAFETENSORS_FILENAME = "ema_model.safetensors"
2324SLA_ATTENTION_FILENAME = "sla_attention.pt"
25+ VALIDATION_IMAGE_SUFFIXES = {".png" , ".jpg" , ".jpeg" , ".webp" , ".gif" }
26+ VALIDATION_VIDEO_SUFFIXES = {".mp4" , ".avi" , ".mov" , ".webm" }
27+ VALIDATION_AUDIO_SUFFIXES = {".wav" , ".flac" , ".mp3" , ".ogg" , ".m4a" }
28+ VALIDATION_MEDIA_SUFFIXES = VALIDATION_IMAGE_SUFFIXES | VALIDATION_VIDEO_SUFFIXES | VALIDATION_AUDIO_SUFFIXES
2429
2530
2631class HubManager :
@@ -409,6 +414,59 @@ def find_latest_checkpoint(self):
409414
410415 return highest_checkpoint
411416
417+ @staticmethod
418+ def _validation_media_shortname (filename : str , checkpoint_step : int ) -> str | None :
419+ stem = Path (filename ).stem
420+ prefix = f"step_{ checkpoint_step } _"
421+ if not stem .startswith (prefix ):
422+ return None
423+
424+ remainder = stem [len (prefix ) :]
425+ image_or_video_parts = remainder .rsplit ("_" , 2 )
426+ if len (image_or_video_parts ) == 3 and image_or_video_parts [1 ].isdigit ():
427+ return image_or_video_parts [0 ]
428+
429+ audio_parts = remainder .rsplit ("_" , 1 )
430+ if len (audio_parts ) == 2 and audio_parts [1 ].isdigit ():
431+ return audio_parts [0 ]
432+
433+ return None
434+
435+ def _filter_checkpoint_validation_media (
436+ self ,
437+ checkpoint_step : int ,
438+ validation_images : dict | None ,
439+ validation_audios : dict | None ,
440+ ) -> tuple [dict , dict ]:
441+ filtered_images = {}
442+ filtered_audios = {}
443+ validation_dir = Path (self .config .output_dir ) / "validation_images"
444+ if not validation_dir .exists ():
445+ return filtered_images , filtered_audios
446+
447+ image_shortnames = set (validation_images .keys ()) if validation_images else set ()
448+ audio_shortnames = set (validation_audios .keys ()) if validation_audios else set ()
449+ shortnames = image_shortnames | audio_shortnames
450+ if not shortnames :
451+ return filtered_images , filtered_audios
452+
453+ for media_path in sorted (validation_dir .iterdir (), key = lambda path : path .name ):
454+ if not media_path .is_file () or media_path .suffix .lower () not in VALIDATION_MEDIA_SUFFIXES :
455+ continue
456+ shortname = self ._validation_media_shortname (media_path .name , checkpoint_step )
457+ if shortname not in shortnames :
458+ continue
459+
460+ suffix = media_path .suffix .lower ()
461+ if suffix in VALIDATION_AUDIO_SUFFIXES :
462+ if shortname in audio_shortnames :
463+ filtered_audios .setdefault (shortname , []).append (str (media_path ))
464+ continue
465+ if shortname in image_shortnames :
466+ filtered_images .setdefault (shortname , []).append (str (media_path ))
467+
468+ return filtered_images , filtered_audios
469+
412470 def upload_latest_checkpoint (
413471 self ,
414472 validation_images : dict ,
@@ -435,30 +493,11 @@ def upload_latest_checkpoint(
435493 filtered_images = {}
436494 filtered_audios = {}
437495 if (validation_images or validation_audios ) and checkpoint_step is not None :
438- validation_dir = os .path .join (self .config .output_dir , "validation_images" )
439- if os .path .exists (validation_dir ):
440- shortnames = set ()
441- if validation_images :
442- shortnames .update (validation_images .keys ())
443- if validation_audios :
444- shortnames .update (validation_audios .keys ())
445- for shortname in shortnames :
446- step_pattern = f"step_{ checkpoint_step } _"
447- for img_file in os .listdir (validation_dir ):
448- if step_pattern in img_file and shortname in img_file :
449- img_path = os .path .join (validation_dir , img_file )
450- try :
451- if img_path .endswith ((".mp4" , ".avi" , ".mov" , ".webm" )):
452- filtered_images .setdefault (shortname , []).append (img_path )
453- elif img_path .endswith ((".wav" , ".flac" , ".mp3" , ".ogg" , ".m4a" )):
454- filtered_audios .setdefault (shortname , []).append (img_path )
455- else :
456- from PIL import Image
457-
458- img = Image .open (img_path )
459- filtered_images .setdefault (shortname , []).append (img )
460- except Exception as e :
461- logger .warning (f"Could not load validation asset { img_path } : { e } " )
496+ filtered_images , filtered_audios = self ._filter_checkpoint_validation_media (
497+ checkpoint_step ,
498+ validation_images ,
499+ validation_audios ,
500+ )
462501
463502 # Only use media generated at this checkpoint step. Previous-step benchmark samples
464503 # should not be associated with the checkpoint model card.
@@ -503,12 +542,13 @@ def upload_validation_images(self, validation_images, webhook_handler=None, over
503542 images = [images ]
504543 sub_idx = 0
505544 for image in images :
506- image_path = os .path .join (
507- override_path or self .config .output_dir ,
508- "assets" ,
509- f"image_{ idx } _{ sub_idx } .png" ,
545+ assets_dir = os .path .join (override_path or self .config .output_dir , "assets" )
546+ os .makedirs (assets_dir , exist_ok = True )
547+ image_path , image_extension = save_image_with_validation_format (
548+ image ,
549+ os .path .join (assets_dir , f"image_{ idx } _{ sub_idx } " ),
550+ self .config ,
510551 )
511- image .save (image_path , format = "PNG" )
512552 if not self .config .push_to_hub :
513553 continue
514554 attempt = 0
@@ -517,7 +557,7 @@ def upload_validation_images(self, validation_images, webhook_handler=None, over
517557 try :
518558 self ._hub_api .upload_file (
519559 repo_id = self ._repo_id ,
520- path_in_repo = f"/assets/image_{ idx } _{ sub_idx } .png " ,
560+ path_in_repo = f"/assets/image_{ idx } _{ sub_idx } .{ image_extension } " ,
521561 path_or_fileobj = image_path ,
522562 commit_message = "Validation image auto-generated by SimpleTuner" ,
523563 token = self .hub_token ,
0 commit comments