update poses
This commit is contained in:
@@ -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.25–1.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}"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user