• 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:
mike
2026-07-01 12:09:53 +02:00
parent ec4fa1aa72
commit 5a8acece09
4 changed files with 559 additions and 64 deletions

View File

@@ -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")