From 39cc075452ebeff2123c8cd431a6951c542e6133 Mon Sep 17 00:00:00 2001 From: SolitaryThinker Date: Sun, 5 Jul 2026 13:44:57 -0700 Subject: [PATCH] [bugfix]: reserve batch output paths so duplicate prompts do not collide (#1453) Batch mode resolves every output path via _prepare_output_path before any file is written, so the os.path.exists dedupe never fired and duplicate prompts (or prompts equal in their first 100 sanitized chars) overwrote each other. Track paths reserved within the batch and suffix duplicates, matching what the legacy sequential path produces for the same inputs. --- fastvideo/entrypoints/video_generator.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/fastvideo/entrypoints/video_generator.py b/fastvideo/entrypoints/video_generator.py index a3fa21ecb..db8ae2c96 100644 --- a/fastvideo/entrypoints/video_generator.py +++ b/fastvideo/entrypoints/video_generator.py @@ -461,6 +461,7 @@ class VideoGenerator: def generate_video_batch(self, request_kwargs: list[dict[str, Any]]) -> list[dict[str, Any]]: """Generate multiple legacy video requests, batching compatible items.""" work_items: list[_GenerationWorkItem] = [] + reserved_output_paths: set[str] = set() fastvideo_args_by_pipeline_override: dict[tuple[tuple[str, str], ...], FastVideoArgs] = { (): self.fastvideo_args } @@ -508,7 +509,8 @@ class VideoGenerator: request, model_path=self.fastvideo_args.model_path, ) - output_path = self._prepare_output_path(resolved_sampling_param.output_path, request.prompt) + output_path = self._prepare_output_path(resolved_sampling_param.output_path, request.prompt, + reserved_output_paths) work_items.append( self._prepare_generation_work_item( prompt=request.prompt, @@ -618,9 +620,11 @@ class VideoGenerator: if self._dynamic_batching_enabled(fastvideo_args): work_items: list[_GenerationWorkItem] = [] + reserved_output_paths: set[str] = set() for batch_prompt in prompts: item_kwargs = dict(kwargs) - item_kwargs["output_path"] = self._prepare_output_path(sampling_param.output_path, batch_prompt) + item_kwargs["output_path"] = self._prepare_output_path(sampling_param.output_path, batch_prompt, + reserved_output_paths) work_items.append( self._prepare_generation_work_item( prompt=batch_prompt, @@ -689,6 +693,7 @@ class VideoGenerator: self, output_path: str, prompt: str, + reserved_paths: set[str] | None = None, ) -> str: """Build a unique, sanitized output file path. @@ -703,6 +708,9 @@ class VideoGenerator: - Invalid filename characters are removed; if the name changes, a warning is logged. - If the target path already exists, a numeric suffix is appended. + - ``reserved_paths`` lets batch callers resolve every path before any + file is written: paths in the set are treated as taken, and the + chosen path is added to the set. """ target_ext = ".png" if self._is_image_workload() else ".mp4" @@ -747,13 +755,18 @@ class VideoGenerator: if output_dir: os.makedirs(output_dir, exist_ok=True) + def _is_taken(path: str) -> bool: + return os.path.exists(path) or (reserved_paths is not None and path in reserved_paths) + new_output_path = os.path.join(output_dir, out_name) counter = 1 - while os.path.exists(new_output_path): + while _is_taken(new_output_path): name_part, ext_part = os.path.splitext(out_name) new_name = f"{name_part}_{counter}{ext_part}" new_output_path = os.path.join(output_dir, new_name) counter += 1 + if reserved_paths is not None: + reserved_paths.add(new_output_path) return new_output_path def _dynamic_batching_enabled(self, fastvideo_args: FastVideoArgs) -> bool: