[bugfix] Preserve image output formats (#1915)

The OpenAI image endpoint defaults to a .jpg output path, but the shared
output-path helper treated every non-PNG image path as a directory, so JPEG and
WebP requests were written somewhere other than where the endpoint read them.
Recognized PNG/JPEG/WebP extensions are now preserved, and existing directories
are checked first so a directory named e.g. renders.jpg still receives its
output inside it. Collision handling and filename sanitization are unchanged.
This commit is contained in:
Boyang ZHONG
2026-10-07 14:23:15 -07:00
committed by GitHub
parent d1416b5930
commit 3907a69727
3 changed files with 67 additions and 11 deletions
+8 -8
View File
@@ -747,9 +747,8 @@ class VideoGenerator:
) -> str:
"""Build a unique, sanitized output file path.
The file extension is chosen automatically based on the workload type:
``.png`` for image workloads (``t2i``, ``i2i``, …) and ``.mp4`` for
video workloads.
Image workloads preserve explicit PNG, JPEG, or WebP extensions and
otherwise default to ``.png``. Video workloads use ``.mp4``.
- If ``output_path`` already carries the correct extension, treat it
as a file path.
@@ -759,8 +758,12 @@ class VideoGenerator:
warning is logged.
- If the target path already exists, a numeric suffix is appended.
"""
base_path, extension = os.path.splitext(output_path)
extension_lower = extension.lower()
is_directory = os.path.isdir(output_path)
if self._is_image_workload():
target_ext = ".png"
target_ext = extension_lower if not is_directory and extension_lower in {".png", ".jpg", ".jpeg", ".webp"
} else ".png"
elif self._is_audio_workload():
target_ext = ".wav"
else:
@@ -773,10 +776,7 @@ class VideoGenerator:
sanitized = re.sub(r'\s+', ' ', sanitized)
return sanitized or "output"
base_path, extension = os.path.splitext(output_path)
extension_lower = extension.lower()
if extension_lower == target_ext:
if extension_lower == target_ext and not is_directory:
output_dir = os.path.dirname(output_path)
base_name = os.path.basename(base_path) # filename without extension
sanitized_base = _sanitize_filename_component(base_name)
@@ -10,6 +10,8 @@ import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.fastvideo_args import WorkloadType
from fastvideo.entrypoints.openai import image_api
from fastvideo.entrypoints.openai.stores import AsyncDictStore
@@ -22,9 +24,17 @@ from fastvideo.entrypoints.openai.stores import AsyncDictStore
pytest.param("b64_json", id="b64-json"),
pytest.param("url", id="url"),
])
def test_image_generation_routes(path, response_format, monkeypatch, tmp_path):
@pytest.mark.parametrize("output_format", [None, "png", "jpeg", "webp"])
def test_image_generation_routes(path, response_format, output_format, monkeypatch, tmp_path):
image_bytes = b"test-image-content"
generate = Mock(side_effect=lambda **kwargs: Path(kwargs["output_path"]).write_bytes(image_bytes))
generator = VideoGenerator.__new__(VideoGenerator)
generator.fastvideo_args = SimpleNamespace(workload_type=WorkloadType.from_string("t2i"))
def save_image(**kwargs):
output_path = generator._prepare_output_path(kwargs["output_path"], kwargs["prompt"])
Path(output_path).write_bytes(image_bytes)
generate = Mock(side_effect=save_image)
async def run_serialized(fn, **kwargs):
return fn(**kwargs)
@@ -40,7 +50,10 @@ def test_image_generation_routes(path, response_format, monkeypatch, tmp_path):
app.include_router(image_api.router)
with TestClient(app) as client:
response = client.post(path, json={"prompt": "a cat", "response_format": response_format})
request = {"prompt": "a cat", "response_format": response_format}
if output_format is not None:
request["output_format"] = output_format
response = client.post(path, json=request)
assert response.status_code == 200, response.text
body = response.json()
assert body["data"][0]["revised_prompt"] == "a cat"
@@ -1228,3 +1228,46 @@ def test_ffmpeg_pipe_preserves_video_duration(tmp_path, env_overrides, audio_sec
assert int(video["nb_read_frames"]) == frame_count
assert float(video["duration"]) == pytest.approx(frame_count / fps, abs=1 / fps)
assert float(sound["duration"]) == pytest.approx(frame_count / fps, abs=1 / fps)
@pytest.mark.parametrize("extension,format_name", [(".png", "PNG"), (".jpg", "JPEG"), (".jpeg", "JPEG"), (".webp", "WEBP")])
def test_image_output_path_preserves_requested_format(tmp_path, extension, format_name):
from PIL import Image
args = _single_video_args()
args.workload_type = WorkloadType.from_string("t2i")
generator = _single_video_generator(_single_video_output_batch(torch.zeros(1, 3, 1, 16, 16)), args)
target = tmp_path / ("image" + extension)
output_path = generator._prepare_output_path(str(target), "a cat")
assert output_path == str(target)
sampling = _small_sampling_param(save_video=True)
sampling.num_frames = 1
result = generator._generate_single_video("a cat", sampling, output_path=output_path)
assert result["video_path"] == str(target)
with Image.open(target) as image:
assert image.format == format_name
assert image.size == (16, 16)
def test_image_output_directory_defaults_to_png(tmp_path):
generator = _new_video_generator()
generator.fastvideo_args = SimpleNamespace(workload_type=WorkloadType.from_string("t2i"))
assert generator._prepare_output_path(str(tmp_path), "a cat") == str(tmp_path / "a cat.png")
@pytest.mark.parametrize("extension,target_extension,workload", [
(".png", ".png", "t2i"), (".jpg", ".png", "t2i"), (".jpeg", ".png", "t2i"),
(".webp", ".png", "t2i"), (".JPEG", ".png", "t2i"),
(".mp4", ".mp4", "t2v"), (".wav", ".wav", "t2a"),
])
def test_existing_image_extension_directory_receives_output(tmp_path, extension, target_extension, workload):
generator = _new_video_generator()
generator.fastvideo_args = SimpleNamespace(workload_type=WorkloadType.from_string("t2i"))
generator.fastvideo_args.workload_type = WorkloadType.from_string(workload)
directory = tmp_path / ("renders" + extension)
directory.mkdir()
existing = directory / ("a cat" + target_extension)
existing.write_bytes(b"existing image")
result = generator._prepare_output_path(str(directory), "a cat")
assert result == str(directory / ("a cat_1" + target_extension))
assert existing.read_bytes() == b"existing image"