dphn/Dolphin3.0-Mistral-24B is the ungated mirror of the Dolphin 3.0 Mistral 24B — exactly what you asked for. It's ~48GB fp16, which needs GPU+CPU split (device_map="auto" with 32GB on GPU, ~16GB in RAM). Let me kick off the download and update the service in parallel.
This commit is contained in:
@@ -1486,6 +1486,7 @@ def list_videos():
|
||||
@app.get("/wireframe/frame/{video_name}")
|
||||
def wireframe_frame(video_name: str, t: float = 0.5):
|
||||
"""Extract a single frame at normalized time t (0–1) from a wireframe video. Returns PNG."""
|
||||
import cv2
|
||||
wireframe_dir = _load_wireframe_dir()
|
||||
video_path = os.path.join(wireframe_dir, video_name)
|
||||
if not os.path.exists(video_path):
|
||||
@@ -1916,10 +1917,12 @@ def _extract_face_bg(filename: str, fpath: str):
|
||||
face_fname = f"{gid_tag}_face.png"
|
||||
face_path = os.path.join(os.path.dirname(fpath), face_fname)
|
||||
cropped.save(face_path)
|
||||
face_embed = face.normed_embedding.tolist() if hasattr(face, 'normed_embedding') and face.normed_embedding is not None else None
|
||||
database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
|
||||
name=person[0] if person else None,
|
||||
source_refs=json.dumps([filename]))
|
||||
print(f"[extract-face] saved {face_fname}")
|
||||
source_refs=json.dumps([filename]),
|
||||
face_embedding=face_embed)
|
||||
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
|
||||
except Exception as e:
|
||||
print(f"[extract-face] error for {filename}: {e}")
|
||||
|
||||
@@ -2120,6 +2123,88 @@ def extract_face_endpoint(filename: str):
|
||||
return {"status": "queued", "filename": filename}
|
||||
|
||||
|
||||
class FaceSimilarRequest(BaseModel):
|
||||
group_id: str
|
||||
limit: int = 12
|
||||
|
||||
|
||||
@app.post("/faces/similar")
|
||||
def face_similar(req: FaceSimilarRequest):
|
||||
"""Find groups with visually similar faces using insightface embeddings.
|
||||
|
||||
Looks up the face embedding stored for {group_id}_face.png and returns
|
||||
the top-N closest matches from other groups.
|
||||
"""
|
||||
face_fname = f"{req.group_id.replace('/', '_')}_face.png"
|
||||
embedding = database.get_face_embedding(face_fname)
|
||||
if embedding is None:
|
||||
raise HTTPException(404, "No face embedding found for this group — set a preferred image first")
|
||||
|
||||
rows = database.search_similar_face(embedding, limit=req.limit, exclude_group_id=req.group_id)
|
||||
# Each row is (filename, group_id, distance). Return the group thumbnail filename
|
||||
# (the _face.png itself) so the frontend can render it directly.
|
||||
results = [
|
||||
{"filename": r[0], "group_id": r[1], "distance": round(float(r[2]), 4)}
|
||||
for r in rows
|
||||
]
|
||||
return {"similar": results}
|
||||
|
||||
|
||||
_face_index_status: dict = {"running": False, "done": 0, "total": 0, "indexed": 0}
|
||||
|
||||
|
||||
def _face_index_worker():
|
||||
"""Backfill face embeddings for all *_face.png files that lack one."""
|
||||
global _face_index_status
|
||||
output_dir = _load_output_dir()
|
||||
face_files = [f for f in os.listdir(output_dir) if f.endswith("_face.png")]
|
||||
_face_index_status.update({"running": True, "done": 0, "total": len(face_files), "indexed": 0})
|
||||
try:
|
||||
import cv2
|
||||
app_fa, _ = _load_faceswapper()
|
||||
except Exception as e:
|
||||
print(f"[face-index] failed to load insightface: {e}")
|
||||
_face_index_status["running"] = False
|
||||
return
|
||||
indexed = 0
|
||||
for i, fname in enumerate(face_files):
|
||||
existing = database.get_face_embedding(fname)
|
||||
if existing is not None:
|
||||
_face_index_status["done"] = i + 1
|
||||
continue
|
||||
fpath = os.path.join(output_dir, fname)
|
||||
try:
|
||||
bgr = cv2.imread(fpath)
|
||||
if bgr is None:
|
||||
continue
|
||||
faces = app_fa.get(bgr)
|
||||
if not faces:
|
||||
continue
|
||||
face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
|
||||
if not hasattr(face, 'normed_embedding') or face.normed_embedding is None:
|
||||
continue
|
||||
database.upsert_person(fname, face_embedding=face.normed_embedding.tolist())
|
||||
indexed += 1
|
||||
except Exception as e:
|
||||
print(f"[face-index] {fname}: {e}")
|
||||
_face_index_status.update({"done": i + 1, "indexed": indexed})
|
||||
_face_index_status["running"] = False
|
||||
print(f"[face-index] done: {indexed}/{len(face_files)} embeddings stored")
|
||||
|
||||
|
||||
@app.post("/faces/index")
|
||||
def build_face_index():
|
||||
if _face_index_status.get("running"):
|
||||
return {"status": "already_running", **_face_index_status}
|
||||
threading.Thread(target=_face_index_worker, daemon=True).start()
|
||||
return {"status": "started"}
|
||||
|
||||
|
||||
@app.get("/faces/index/status")
|
||||
def face_index_status():
|
||||
return _face_index_status
|
||||
|
||||
|
||||
@app.get("/faces/{group_id}")
|
||||
def face_status(group_id: str):
|
||||
"""Report whether a face crop exists for a group.
|
||||
|
||||
Reference in New Issue
Block a user