update poses

This commit is contained in:
mike
2026-06-22 19:00:19 +02:00
parent c0d863ee09
commit 9e99c85134
6 changed files with 691 additions and 77 deletions

View File

@@ -460,7 +460,7 @@ def _make_video_poster(video_path: str) -> str | None:
return None
def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance: bool = True):
def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance: bool = True, preview_scale: float = 1.0):
"""Frame-by-frame face swap: model face → every face in template video."""
output_dir = _load_output_dir()
wireframe_dir = _load_wireframe_dir()
@@ -502,8 +502,12 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
raise ValueError(f"Cannot open video: {video_name}")
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
scale = max(0.1, min(1.0, preview_scale))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) * scale)
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) * scale)
# Ensure even dimensions for codec compatibility
width = width if width % 2 == 0 else width - 1
height = height if height % 2 == 0 else height - 1
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
jobs[job_id]["total"] = max(total, 1)
@@ -511,8 +515,9 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
ts = time.strftime("%Y%m%d_%H%M%S")
vid_stem = os.path.splitext(video_name)[0]
base_name = naming.get_base_name(model_filename)
tmp_name = f"{ts}_fs_tmp_{vid_stem}_{base_name}.mp4"
out_name = f"{ts}_fs_{vid_stem}_{base_name}.mp4"
prev_tag = f"_prev{int(scale*100)}" if scale < 1.0 else ""
tmp_name = f"{ts}_fs_tmp_{vid_stem}_{base_name}{prev_tag}.mp4"
out_name = f"{ts}_fs_{vid_stem}_{base_name}{prev_tag}.mp4"
tmp_path = os.path.join(output_dir, tmp_name)
out_path = os.path.join(output_dir, out_name)
@@ -547,6 +552,8 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
)
except Exception:
pass
if scale < 1.0:
result = cv2.resize(result, (width, height), interpolation=cv2.INTER_AREA)
writer.write(result)
frame_idx += 1
jobs[job_id]["done"] = frame_idx
@@ -598,7 +605,7 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
hair: bool = True, enhance: bool = True):
hair: bool = True, enhance: bool = True, preview_scale: float = 1.0):
"""High-quality faceswap via FaceFusion CLI (supports hair_swapper + ghost model)."""
import subprocess as sp
import sys
@@ -630,7 +637,9 @@ def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
ts = time.strftime('%Y%m%d_%H%M%S')
vid_stem = os.path.splitext(video_name)[0]
base_name = naming.get_base_name(model_filename)
out_name = f'{ts}_fs_{vid_stem}_{base_name}.mp4'
scale = max(0.1, min(1.0, preview_scale))
prev_tag = f'_prev{int(scale*100)}' if scale < 1.0 else ''
out_name = f'{ts}_fs_{vid_stem}_{base_name}{prev_tag}.mp4'
out_path = os.path.join(output_dir, out_name)
processors = ['face_swapper']
@@ -662,6 +671,19 @@ def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
]
if enhance:
cmd += ['--face-enhancer-model', 'gfpgan_1.4']
if scale < 1.0:
# Determine native video width to compute preview width
try:
import cv2 as _cv2
_cap = _cv2.VideoCapture(video_path)
_native_w = int(_cap.get(_cv2.CAP_PROP_FRAME_WIDTH))
_cap.release()
_preview_w = max(2, int(_native_w * scale))
if _preview_w % 2 != 0:
_preview_w -= 1
cmd += ['--output-video-width', str(_preview_w)]
except Exception:
pass
jobs[job_id]['total'] = 100
jobs[job_id]['done'] = 0
@@ -1120,15 +1142,15 @@ def get_batch(job_id: str):
@app.get("/images")
def list_images():
def list_images(archived: bool = False):
output_dir = _load_output_dir()
all_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg') + VIDEO_EXTENSIONS
try:
try:
persons = database.list_persons()
persons = database.list_persons(include_archived=archived)
# list_persons cols: filename, name, group_id, clip_description,
# prompt, pose, sort_order, group_name, hidden, has_background,
# source_refs, has_clothing, content_type, faceswap_source_video
# source_refs, has_clothing, content_type, faceswap_source_video, archived
db_images = []
for p in persons:
fpath = os.path.join(output_dir, p[0])
@@ -1149,6 +1171,7 @@ def list_images():
"has_clothing": p[11],
"content_type": p[12] or "image",
"faceswap_source_video":p[13],
"archived": bool(p[14]) if p[14] else False,
})
db_images.sort(
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
@@ -1274,6 +1297,7 @@ class FaceswapRequest(BaseModel):
video_name: str # filename of template video in wireframe_dir
enhance: bool = True # GFPGAN face restoration after each frame swap
hair: bool = False # use FaceFusion with hair_swapper (requires FaceFusion install)
preview_scale: float = 1.0 # 0.251.0; <1.0 produces a smaller preview video
@app.get("/faceswap/check")
@@ -1318,14 +1342,14 @@ def start_faceswap(req: FaceswapRequest):
t = threading.Thread(
target=_faceswap_worker_ff,
args=(job_id, req.model_filename, req.video_name),
kwargs={'hair': True, 'enhance': req.enhance},
kwargs={'hair': True, 'enhance': req.enhance, 'preview_scale': req.preview_scale},
daemon=True,
)
else:
t = threading.Thread(
target=_faceswap_worker,
args=(job_id, req.model_filename, req.video_name),
kwargs={'enhance': req.enhance},
kwargs={'enhance': req.enhance, 'preview_scale': req.preview_scale},
daemon=True,
)
t.start()
@@ -1674,6 +1698,24 @@ def set_image_hidden(filename: str, body: dict):
return {"filename": filename, "hidden": hidden}
@app.post("/images/{filename}/archive")
def archive_image(filename: str):
try:
database.set_archived(filename, True)
except Exception as e:
raise HTTPException(500, str(e))
return {"filename": filename, "archived": True}
@app.post("/images/{filename}/unarchive")
def unarchive_image(filename: str):
try:
database.set_archived(filename, False)
except Exception as e:
raise HTTPException(500, str(e))
return {"filename": filename, "archived": False}
@app.post("/images/{filename}/set-preferred")
def set_image_preferred(filename: str):
"""Make this image sort_order=0 within its group, shifting others to 1,2,..."""
@@ -2029,6 +2071,34 @@ def autocrop_image(filename: str):
return {"status": "success", "filename": filename, "box": [int(cmin), int(rmin), int(cmax+1), int(rmax+1)]}
class CropRequest(BaseModel):
x1: int
y1: int
x2: int
y2: int
@app.post("/images/{filename}/crop")
def manual_crop_image(filename: str, req: CropRequest):
"""Crop the image to the given pixel rectangle (in original image coordinates) in-place."""
person = database.get_person(filename)
if not person or not person[5] or not os.path.exists(person[5]):
raise HTTPException(404, "Image file not found")
path = person[5]
img = Image.open(path)
w, h = img.size
x1 = max(0, min(req.x1, w))
y1 = max(0, min(req.y1, h))
x2 = max(0, min(req.x2, w))
y2 = max(0, min(req.y2, h))
if x2 <= x1 or y2 <= y1:
raise HTTPException(400, "Invalid crop rectangle")
cropped = img.crop((x1, y1, x2, y2))
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
cropped.save(path, format=fmt)
return {"status": "success", "filename": filename, "box": [x1, y1, x2, y2]}
@app.post("/images/{filename}/duplicate")
def duplicate_image(filename: str):
"""Copy an image into the same group with a fresh timestamp-based filename."""
@@ -2046,7 +2116,15 @@ def duplicate_image(filename: str):
new_path = os.path.join(output_dir, new_filename)
_shutil.copy2(path, new_path)
group_id = person[1]
database.upsert_person(new_filename, filepath=new_path, group_id=group_id)
# person tuple: (name, group_id, tags, embedding, clip_description, filepath,
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing)
database.upsert_person(
new_filename, filepath=new_path, group_id=group_id,
prompt=person[6], pose=person[7],
has_background=person[11],
has_clothing=person[13],
source_refs=json.dumps([filename]), # original is the reference
)
return {"status": "success", "new_filename": new_filename, "new_url": f"/output/{new_filename}"}