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:
@@ -4512,7 +4512,24 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
scene_video: str | None = None, scene_image: str | None = None,
|
||||
extra_filename: str | None = None):
|
||||
output_dir = _load_output_dir()
|
||||
out_path = None
|
||||
try:
|
||||
# Construct and validate source references early
|
||||
refs = [model_filename]
|
||||
if scene_video:
|
||||
refs.append(f"video:{scene_video}")
|
||||
if scene_image:
|
||||
wireframe_dir = _load_wireframe_dir()
|
||||
if os.path.exists(os.path.join(wireframe_dir, scene_image)) or not os.path.exists(os.path.join(output_dir, scene_image)):
|
||||
refs.append(f"wireframe:{scene_image}")
|
||||
else:
|
||||
refs.append(scene_image)
|
||||
if extra_filename:
|
||||
refs.append(extra_filename)
|
||||
|
||||
if len(refs) < 2:
|
||||
raise ValueError(f"Scenery generation failed: expected at least 2 source references (model and scene), but got {len(refs)}: {refs}")
|
||||
|
||||
model_path = os.path.join(output_dir, model_filename)
|
||||
model_pil = Image.open(model_path).convert("RGB")
|
||||
|
||||
@@ -4540,17 +4557,10 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
f.write(png_bytes)
|
||||
person = database.get_person(model_filename)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(model_filename))
|
||||
|
||||
try:
|
||||
embedding = embeddings.generate_embedding(out_path)
|
||||
next_order = database.get_next_sort_order(group_id)
|
||||
# Store all source references: person image, background video (if any), extra ref (if any)
|
||||
refs = [model_filename]
|
||||
if scene_video:
|
||||
refs.append(f"video:{scene_video}")
|
||||
if scene_image:
|
||||
refs.append(f"wireframe:{scene_image}")
|
||||
if extra_filename:
|
||||
refs.append(extra_filename)
|
||||
database.upsert_person(out_name, filepath=out_path, embedding=embedding,
|
||||
group_id=group_id, prompt=prompt,
|
||||
sort_order=next_order,
|
||||
@@ -4559,6 +4569,8 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
_metadata_executor.submit(_process_image_for_metadata, out_name)
|
||||
except Exception as db_err:
|
||||
print(f"[scenery] DB error: {db_err}")
|
||||
raise
|
||||
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["status"] = "done"
|
||||
jobs[job_id]["output"] = out_name
|
||||
@@ -4570,6 +4582,12 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
print(f"[scenery] error: {e}")
|
||||
jobs[job_id]["status"] = "error"
|
||||
jobs[job_id]["error"] = str(e)
|
||||
if out_path and os.path.exists(out_path):
|
||||
try:
|
||||
os.remove(out_path)
|
||||
print(f"[scenery] Cleaned up unregistered file due to error: {out_path}")
|
||||
except Exception as clean_err:
|
||||
print(f"[scenery] Clean up error: {clean_err}")
|
||||
|
||||
|
||||
@app.post("/generate-scenery")
|
||||
@@ -4577,24 +4595,71 @@ def generate_scenery(req: SceneryRequest):
|
||||
output_dir = _load_output_dir()
|
||||
wireframe_dir = _load_wireframe_dir()
|
||||
|
||||
model_path = os.path.join(output_dir, req.model_filename)
|
||||
if not req.model_filename:
|
||||
raise HTTPException(400, "Model image (Image 2) is required and cannot be empty.")
|
||||
|
||||
# Strip query parameters (e.g. ?t=...)
|
||||
clean_model_filename = req.model_filename.split('?')[0].strip()
|
||||
if not clean_model_filename:
|
||||
raise HTTPException(400, "Model image filename is empty after cleaning.")
|
||||
|
||||
model_path = os.path.join(output_dir, clean_model_filename)
|
||||
if not os.path.exists(model_path):
|
||||
raise HTTPException(404, f"Model image not found: {req.model_filename}")
|
||||
raise HTTPException(404, f"Model image not found: {clean_model_filename}")
|
||||
if not os.path.isfile(model_path):
|
||||
raise HTTPException(400, f"Model path is not a file: {clean_model_filename}")
|
||||
|
||||
# Validate that it is a valid, openable image file
|
||||
try:
|
||||
with Image.open(model_path) as img:
|
||||
img.verify()
|
||||
except Exception as img_err:
|
||||
raise HTTPException(400, f"Invalid or corrupt model image: {img_err}")
|
||||
|
||||
# Resolve scene image
|
||||
clean_scene_image = req.scene_image.split('?')[0].strip() if req.scene_image else None
|
||||
|
||||
# Enforce that we have at least one background source reference (either scene_image or scene_video)
|
||||
if not clean_scene_image and not req.scene_video:
|
||||
raise HTTPException(400, "Scenery generation requires a background scene reference (either scene_image or scene_video).")
|
||||
|
||||
# Check if model image and scene image are identical files
|
||||
if clean_scene_image and clean_scene_image == clean_model_filename:
|
||||
raise HTTPException(400, "Image 1 (scenery) and Image 2 (person) cannot be the same image.")
|
||||
|
||||
# Verify background reference existence on disk early if clean_scene_image is provided
|
||||
if clean_scene_image:
|
||||
image_path = os.path.join(wireframe_dir, clean_scene_image)
|
||||
if not os.path.exists(image_path):
|
||||
image_path = os.path.join(output_dir, clean_scene_image) # Check output_dir as fallback!
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(404, f"Scene image not found: {clean_scene_image}")
|
||||
|
||||
if req.scene_bytes:
|
||||
import base64
|
||||
raw = base64.b64decode(req.scene_bytes)
|
||||
scene_pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||
elif req.scene_image:
|
||||
image_path = os.path.join(wireframe_dir, req.scene_image)
|
||||
try:
|
||||
raw = base64.b64decode(req.scene_bytes)
|
||||
# Verify image
|
||||
with Image.open(io.BytesIO(raw)) as img:
|
||||
img.verify()
|
||||
scene_pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||
except Exception as img_err:
|
||||
raise HTTPException(400, f"Invalid or corrupt scene image: {img_err}")
|
||||
elif clean_scene_image:
|
||||
image_path = os.path.join(wireframe_dir, clean_scene_image)
|
||||
if not os.path.exists(image_path):
|
||||
raise HTTPException(404, f"Scene image not found: {req.scene_image}")
|
||||
scene_pil = Image.open(image_path).convert("RGB")
|
||||
image_path = os.path.join(output_dir, clean_scene_image)
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
img.verify()
|
||||
scene_pil = Image.open(image_path).convert("RGB")
|
||||
except Exception as img_err:
|
||||
raise HTTPException(400, f"Invalid or corrupt scene image: {img_err}")
|
||||
elif req.scene_video:
|
||||
video_path = os.path.join(wireframe_dir, req.scene_video)
|
||||
clean_scene_video = req.scene_video.split('?')[0].strip()
|
||||
video_path = os.path.join(wireframe_dir, clean_scene_video)
|
||||
if not os.path.exists(video_path):
|
||||
raise HTTPException(404, f"Scene video not found: {req.scene_video}")
|
||||
raise HTTPException(404, f"Scene video not found: {clean_scene_video}")
|
||||
try:
|
||||
scene_pil = _extract_frame_at(video_path, req.scene_time)
|
||||
except Exception as e:
|
||||
@@ -4604,27 +4669,47 @@ def generate_scenery(req: SceneryRequest):
|
||||
|
||||
# Optional image3 (Picture 3) — extra reference from output_dir
|
||||
extra_pils = []
|
||||
if req.extra_filename:
|
||||
extra_path = os.path.join(output_dir, req.extra_filename)
|
||||
clean_extra_filename = req.extra_filename.split('?')[0].strip() if req.extra_filename else None
|
||||
if clean_extra_filename:
|
||||
if clean_extra_filename == clean_model_filename:
|
||||
raise HTTPException(400, "Image 3 (extra reference) and Image 2 (person) cannot be the same image.")
|
||||
if clean_scene_image and clean_extra_filename == clean_scene_image:
|
||||
raise HTTPException(400, "Image 3 (extra reference) and Image 1 (scenery) cannot be the same image.")
|
||||
|
||||
extra_path = os.path.join(output_dir, clean_extra_filename)
|
||||
if not os.path.exists(extra_path):
|
||||
raise HTTPException(404, f"Extra reference not found: {req.extra_filename}")
|
||||
extra_pils.append(Image.open(extra_path).convert("RGB"))
|
||||
raise HTTPException(404, f"Extra reference not found: {clean_extra_filename}")
|
||||
if not os.path.isfile(extra_path):
|
||||
raise HTTPException(400, f"Extra reference path is not a file: {clean_extra_filename}")
|
||||
try:
|
||||
with Image.open(extra_path) as img:
|
||||
img.verify()
|
||||
extra_pils.append(Image.open(extra_path).convert("RGB"))
|
||||
except Exception as img_err:
|
||||
raise HTTPException(400, f"Invalid or corrupt extra reference image: {img_err}")
|
||||
|
||||
prompt = req.prompt or (
|
||||
"Place the person from Picture 2 naturally inside the environment shown in Picture 1. "
|
||||
"Keep the person's face, body proportions, clothing and pose exactly as in Picture 2. "
|
||||
"Use the location, lighting and atmosphere from Picture 1 as the background. "
|
||||
"Match the color temperature and shadows so it looks like one photograph taken on location. "
|
||||
+ ("Incorporate the reference from Picture 3 as well. " if extra_pils else "")
|
||||
+ "Output a single photorealistic image. High quality, detailed."
|
||||
)
|
||||
# Process and refine prompt to map Image 1/2/3 to Picture 1/2/3
|
||||
prompt = req.prompt
|
||||
if prompt:
|
||||
prompt = re.sub(r"(?i)\bimage\s*1\b", "Picture 1", prompt)
|
||||
prompt = re.sub(r"(?i)\bimage\s*2\b", "Picture 2", prompt)
|
||||
prompt = re.sub(r"(?i)\bimage\s*3\b", "Picture 3", prompt)
|
||||
else:
|
||||
prompt = (
|
||||
"Place the person from Picture 2 naturally inside the environment shown in Picture 1. "
|
||||
"Keep the person's face, body proportions, clothing and pose exactly as in Picture 2. "
|
||||
"Use the location, lighting and atmosphere from Picture 1 as the background. "
|
||||
"Match the color temperature and shadows so it looks like one photograph taken on location. "
|
||||
+ ("Incorporate the reference from Picture 3 as well. " if extra_pils else "")
|
||||
+ "Output a single photorealistic image. High quality, detailed."
|
||||
)
|
||||
|
||||
try:
|
||||
database.save_db_prompt("scene", prompt, {
|
||||
"model_filename": req.model_filename,
|
||||
"model_filename": clean_model_filename,
|
||||
"scene_video": req.scene_video,
|
||||
"scene_image": req.scene_image,
|
||||
"extra_filename": req.extra_filename,
|
||||
"scene_image": clean_scene_image,
|
||||
"extra_filename": clean_extra_filename,
|
||||
"seed": req.seed
|
||||
})
|
||||
except Exception as db_err:
|
||||
@@ -4634,13 +4719,13 @@ def generate_scenery(req: SceneryRequest):
|
||||
jobs[job_id] = {"status": "running", "type": "scenery", "total": 1, "done": 0, "failed": 0}
|
||||
threading.Thread(
|
||||
target=_scenery_worker,
|
||||
args=(job_id, req.model_filename, scene_pil, prompt, req.seed, extra_pils),
|
||||
args=(job_id, clean_model_filename, scene_pil, prompt, req.seed, extra_pils),
|
||||
kwargs={"scene_video": req.scene_video if req.scene_video else None,
|
||||
"scene_image": req.scene_image if req.scene_image else None,
|
||||
"extra_filename": req.extra_filename},
|
||||
"scene_image": clean_scene_image if clean_scene_image else None,
|
||||
"extra_filename": clean_extra_filename},
|
||||
daemon=True,
|
||||
).start()
|
||||
return {"job_id": job_id, "model": req.model_filename}
|
||||
return {"job_id": job_id, "model": clean_model_filename}
|
||||
|
||||
|
||||
@app.get("/scenery/library")
|
||||
|
||||
Reference in New Issue
Block a user