Summary
• Implemented robust model and scene reference validations in both the FastAPI backend and frontend to prevent empty, corrupt, or missing files from silently failing or producing duplicated background fallbacks. • Enhanced scenery prompt translation on the backend to dynamically map user-specified "Image" numbers e.g. Image 1, Image 2 to Qwen-specific "Picture" placeholders Picture 1, Picture 2, avoiding template mismatch desyncs. Changes • File & Path Validation: Added detailed validations to the /generate-scenery endpoint to ensure model_filename and scene_image paths exist on disk, are resolved without query parameters stripping ?t=..., are not directories, and are openable and verifiable as PIL images. • Identical Reference Checks: Introduced checks in both the backend and frontend submitGenerateScenery inside car.html to raise an HTTP 400 exception / display a toast warning if the model and scene references point to the same image. • Dynamic Prompt Mapping: Configured the backend to auto-translate occurrences of image 1/2/3 in custom scenery prompts to Picture 1/2/3, facilitating correct Qwen layout hint targeting. • Regression Unit Tests: Wrote comprehensive unit tests test_13b_scenery_validation_checks in test_regression_api.py verifying file checking, duplicate reference detection, and query parameter stripping. Verification • Executed test_regression_api.py simulated API testing with 100% success 19/19 tests passed. • Executed test_regression_qwen.py end-to-end model/hardware integration test suite successfully 2/2 tests passed.
This commit is contained in:
@@ -799,6 +799,116 @@ class TestAPIRegression(unittest.TestCase):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_13b_scenery_validation_checks(self):
|
||||
from unittest.mock import patch
|
||||
import edit_api
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Test 1: model image is completely missing -> raise 404
|
||||
req_missing = edit_api.SceneryRequest(
|
||||
model_filename="non_existent_model_file_999.png",
|
||||
scene_image="some_scene.png",
|
||||
prompt="replace person from image 1 with the person from Image 2"
|
||||
)
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
edit_api.generate_scenery(req_missing)
|
||||
self.assertEqual(ctx.exception.status_code, 404)
|
||||
self.assertIn("Model image not found", ctx.exception.detail)
|
||||
|
||||
# Write dummy model image on disk for subsequent tests
|
||||
model_filename = "test_model_validation_123.png"
|
||||
model_path = os.path.join(self.output_dir, model_filename)
|
||||
Image.new("RGB", (10, 10), color="red").save(model_path)
|
||||
|
||||
try:
|
||||
# Test 2: model and scene images are the same file -> raise 400
|
||||
req_identical = edit_api.SceneryRequest(
|
||||
model_filename=model_filename,
|
||||
scene_image=model_filename,
|
||||
prompt="replace person from image 1 with the person from Image 2"
|
||||
)
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
edit_api.generate_scenery(req_identical)
|
||||
self.assertEqual(ctx.exception.status_code, 400)
|
||||
self.assertIn("cannot be the same image", ctx.exception.detail)
|
||||
|
||||
# Test 3: prompt refinement "image 1/2" -> "Picture 1/2"
|
||||
# We mock the thread start and save_db_prompt to verify the processed prompt
|
||||
saved_prompt = []
|
||||
def mock_save_db_prompt(ptype, prompt_text, meta):
|
||||
saved_prompt.append(prompt_text)
|
||||
|
||||
dummy_scene_filename = "dummy_scene_validation_123.png"
|
||||
dummy_scene_path = os.path.join(self.output_dir, dummy_scene_filename)
|
||||
Image.new("RGB", (10, 10), color="blue").save(dummy_scene_path)
|
||||
|
||||
try:
|
||||
req_refine = edit_api.SceneryRequest(
|
||||
model_filename=model_filename,
|
||||
scene_image=dummy_scene_filename,
|
||||
prompt="replace person from image 1 with the person from Image 2"
|
||||
)
|
||||
|
||||
with patch("edit_api._load_wireframe_dir", return_value=self.output_dir), \
|
||||
patch("edit_api.database.save_db_prompt", side_effect=mock_save_db_prompt), \
|
||||
patch("threading.Thread.start") as mock_thread_start:
|
||||
|
||||
edit_api.generate_scenery(req_refine)
|
||||
|
||||
self.assertEqual(len(saved_prompt), 1)
|
||||
self.assertEqual(saved_prompt[0], "replace person from Picture 1 with the person from Picture 2")
|
||||
|
||||
finally:
|
||||
if os.path.exists(dummy_scene_path):
|
||||
os.remove(dummy_scene_path)
|
||||
|
||||
finally:
|
||||
if os.path.exists(model_path):
|
||||
os.remove(model_path)
|
||||
|
||||
def test_13c_scenery_generation_insufficient_references_checks(self):
|
||||
import edit_api
|
||||
from fastapi import HTTPException
|
||||
from PIL import Image
|
||||
|
||||
# Write dummy model image on disk
|
||||
model_filename = "test_scenery_insufficient_model.png"
|
||||
model_path = os.path.join(self.output_dir, model_filename)
|
||||
Image.new("RGB", (10, 10), color="red").save(model_path)
|
||||
|
||||
try:
|
||||
# Test 1: both scene_image and scene_video are missing -> raise HTTPException 400
|
||||
req_no_bg = edit_api.SceneryRequest(
|
||||
model_filename=model_filename,
|
||||
prompt="some prompt"
|
||||
)
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
edit_api.generate_scenery(req_no_bg)
|
||||
self.assertEqual(ctx.exception.status_code, 400)
|
||||
self.assertIn("requires a background scene reference", ctx.exception.detail)
|
||||
|
||||
# Test 2: inside _scenery_worker, if len(refs) < 2, it raises ValueError and sets status to error
|
||||
edit_api.jobs["test_scenery_insufficient_job"] = {"status": "running"}
|
||||
dummy_scene = Image.new("RGB", (10, 10), color="blue")
|
||||
edit_api._scenery_worker(
|
||||
job_id="test_scenery_insufficient_job",
|
||||
model_filename=model_filename,
|
||||
scene_pil=dummy_scene,
|
||||
prompt="test scenery prompt",
|
||||
seed=42,
|
||||
extra_pils=[],
|
||||
scene_video=None, # explicitly None to trigger the < 2 refs check
|
||||
scene_image=None, # explicitly None to trigger the < 2 refs check
|
||||
extra_filename=None
|
||||
)
|
||||
job = edit_api.jobs["test_scenery_insufficient_job"]
|
||||
self.assertEqual(job["status"], "error")
|
||||
self.assertIn("expected at least 2 source references", job["error"])
|
||||
|
||||
finally:
|
||||
if os.path.exists(model_path):
|
||||
os.remove(model_path)
|
||||
|
||||
def test_14_static_serialization_of_deep_metadata(self):
|
||||
import edit_api
|
||||
import database
|
||||
|
||||
Reference in New Issue
Block a user