Summary
• Implemented global offline mode for all HuggingFace-dependent modules to eliminate unauthenticated request warnings and prevent external connection attempts during startup and runtime. Changes • Global Offline Configuration: Added HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 to the environment variables at the top of edit_api.py, embeddings.py, watcher.py, and pose_llm/pose_llm_api.py. • Explicit Local Files Only: Updated all huggingface_hub.hf_hub_download calls in edit_api.py to include local_files_only=True, ensuring they never attempt to reach the Hub if models are already cached. • LLM Service Optimization: Updated AutoTokenizer and AutoModelForCausalLM calls in pose_llm/pose_llm_api.py to use local_files_only=True. • Consistency: Ensured that shared modules like embeddings.py which uses open_clip are also locked to offline mode to prevent background downloads.
This commit is contained in:
@@ -118,6 +118,43 @@ IDLE_THRESHOLD = 45 # seconds of inactivity before background gen starts
|
||||
IDLE_CHECK_INTERVAL = 4 # polling interval (seconds)
|
||||
|
||||
|
||||
# --- File Metadata In-Memory Cache (resolves performance bottlenecks on /mnt/zim) ---
|
||||
_file_meta_cache = {} # filename -> (exists, mtime, cache_time)
|
||||
_file_meta_cache_lock = threading.Lock()
|
||||
|
||||
def _get_cached_file_meta(filename: str, output_dir: str):
|
||||
now = time.time()
|
||||
with _file_meta_cache_lock:
|
||||
cached = _file_meta_cache.get(filename)
|
||||
if cached and (now - cached[2] < 5.0):
|
||||
return cached[0], cached[1]
|
||||
|
||||
fpath = os.path.join(output_dir, filename)
|
||||
exists = os.path.exists(fpath)
|
||||
mtime = 0.0
|
||||
if exists:
|
||||
try:
|
||||
mtime = os.path.getmtime(fpath)
|
||||
except Exception:
|
||||
pass
|
||||
with _file_meta_cache_lock:
|
||||
_file_meta_cache[filename] = (exists, mtime, now)
|
||||
return exists, mtime
|
||||
|
||||
def _update_cached_file_meta(filename: str, exists: bool = True, mtime: float = None):
|
||||
if mtime is None:
|
||||
mtime = time.time()
|
||||
with _file_meta_cache_lock:
|
||||
_file_meta_cache[filename] = (exists, mtime, time.time())
|
||||
|
||||
def _clear_cached_file_meta(filename: str):
|
||||
with _file_meta_cache_lock:
|
||||
if filename in _file_meta_cache:
|
||||
del _file_meta_cache[filename]
|
||||
|
||||
|
||||
_last_preloaded_images_set = None
|
||||
|
||||
@app.middleware("http")
|
||||
async def _track_activity(request, call_next):
|
||||
global _last_request_time
|
||||
@@ -126,6 +163,7 @@ async def _track_activity(request, call_next):
|
||||
|
||||
def _sync_preloaded_images():
|
||||
"""Update PRELOADED_IMAGES in car.html (both source and output) to match current DB state."""
|
||||
global _last_preloaded_images_set
|
||||
try:
|
||||
output_dir = _load_output_dir()
|
||||
paths = [
|
||||
@@ -134,11 +172,21 @@ def _sync_preloaded_images():
|
||||
]
|
||||
|
||||
persons = database.list_persons(include_archived=False)
|
||||
# Only include if file actually exists on disk
|
||||
db_images = [p[0] for p in persons if os.path.exists(os.path.join(output_dir, p[0]))]
|
||||
# Only include if file actually exists on disk, using the fast cache
|
||||
db_images = []
|
||||
for p in persons:
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if exists:
|
||||
db_images.append(p[0])
|
||||
|
||||
# Sort by mtime, newest first
|
||||
db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
db_images.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||
|
||||
# Avoid redundant HTML rewrites if the preloaded set is unchanged
|
||||
current_set = set(db_images)
|
||||
if _last_preloaded_images_set is not None and _last_preloaded_images_set == current_set:
|
||||
return
|
||||
_last_preloaded_images_set = current_set
|
||||
|
||||
images_json = json.dumps(db_images, indent=12).strip()
|
||||
# Format for JS insertion
|
||||
@@ -195,7 +243,8 @@ def _watch_frontend():
|
||||
def _load_wireframe_dir() -> str:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
conf = json.load(f)
|
||||
return conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||
d = conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||
return os.path.expanduser(d)
|
||||
|
||||
|
||||
def _load_faceswap_model_path() -> str:
|
||||
@@ -1055,9 +1104,9 @@ def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
|
||||
source_refs=json.dumps([model_filename]),
|
||||
sort_order=database.get_next_sort_order(group_id),
|
||||
)
|
||||
_invalidate_static()
|
||||
jobs[job_id]['status'] = 'done'
|
||||
jobs[job_id]['output'] = out_name
|
||||
_invalidate_static()
|
||||
|
||||
except Exception as e:
|
||||
print(f'[faceswap-ff] error: {e}')
|
||||
@@ -1270,7 +1319,7 @@ jobs: dict[str, dict] = {}
|
||||
def _load_output_dir() -> str:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
conf = json.load(f)
|
||||
d = conf["output_dir"]
|
||||
d = os.path.expanduser(conf["output_dir"])
|
||||
if not os.path.isabs(d):
|
||||
d = os.path.normpath(os.path.join(os.path.dirname(CONFIG_PATH), "..", d))
|
||||
return d
|
||||
@@ -1363,8 +1412,8 @@ def _write_all_static() -> None:
|
||||
db_images = []
|
||||
archived_count = 0
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if not exists:
|
||||
continue
|
||||
|
||||
is_archived = bool(p[14]) if p[14] else False
|
||||
@@ -1406,7 +1455,7 @@ def _write_all_static() -> None:
|
||||
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
|
||||
try:
|
||||
db_images.sort(
|
||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
@@ -1705,6 +1754,7 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
||||
except Exception as db_err:
|
||||
print(f"Database error in batch worker: {db_err}")
|
||||
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["done"] += 1
|
||||
# Regenerate static JSON so the frontend's polling picks up the
|
||||
# new image immediately (progressive refresh, not just at the end).
|
||||
@@ -1806,6 +1856,7 @@ def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], pos
|
||||
except Exception as db_err:
|
||||
print(f"DB error in multi-ref: {db_err}")
|
||||
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["done"] += 1
|
||||
# Regenerate static JSON so the frontend's polling picks up the new
|
||||
# image immediately (progressive refresh, matching _batch_worker).
|
||||
@@ -2044,8 +2095,8 @@ def list_images(archived: bool = False):
|
||||
# source_refs, has_clothing, content_type, faceswap_source_video, archived
|
||||
db_images = []
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||
if not exists:
|
||||
continue
|
||||
db_images.append({
|
||||
"filename": p[0],
|
||||
@@ -2065,7 +2116,7 @@ def list_images(archived: bool = False):
|
||||
"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"])),
|
||||
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||
reverse=True,
|
||||
)
|
||||
return {"images": db_images}
|
||||
@@ -2079,7 +2130,7 @@ def list_images(archived: bool = False):
|
||||
if f.lower().endswith(all_extensions)
|
||||
and not (f.lower().endswith('.jpg') and os.path.splitext(f)[0] in video_stems)
|
||||
]
|
||||
files.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
||||
files.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||
return {"images": [{"filename": f} for f in files]}
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
@@ -2568,7 +2619,7 @@ def merge_groups(req: MergeRequest):
|
||||
|
||||
# Write synchronously: the frontend reloads images.json immediately after this
|
||||
# returns, so an async rebuild would race and show the pre-merge grouping.
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"group_id": gid, "files": req.filenames}
|
||||
|
||||
|
||||
@@ -2584,7 +2635,7 @@ def extract_from_group(req: ExtractRequest):
|
||||
except Exception as db_err:
|
||||
print(f"Database error in extract: {db_err}")
|
||||
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"filename": req.filename}
|
||||
|
||||
|
||||
@@ -2762,7 +2813,7 @@ def set_privacy_lock():
|
||||
global _privacy_locked
|
||||
with _privacy_lock:
|
||||
_privacy_locked = True
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"status": "locked"}
|
||||
|
||||
|
||||
@@ -2771,7 +2822,7 @@ def set_privacy_unlock():
|
||||
global _privacy_locked
|
||||
with _privacy_lock:
|
||||
_privacy_locked = False
|
||||
_write_all_static()
|
||||
_invalidate_static()
|
||||
return {"status": "unlocked"}
|
||||
|
||||
|
||||
@@ -2827,7 +2878,8 @@ def _extract_face_bg(filename: str, fpath: str):
|
||||
group_id = person[1] if person else None
|
||||
gid_tag = (group_id or "face").replace("/", "_")
|
||||
face_fname = f"{gid_tag}_face.png"
|
||||
face_path = os.path.join(os.path.dirname(fpath), face_fname)
|
||||
output_dir = _load_output_dir()
|
||||
face_path = os.path.join(output_dir, 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,
|
||||
@@ -2837,6 +2889,7 @@ def _extract_face_bg(filename: str, fpath: str):
|
||||
hidden=True,
|
||||
tags=["FACE"])
|
||||
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
|
||||
_invalidate_static()
|
||||
except Exception as e:
|
||||
print(f"[extract-face] error for {filename}: {e}")
|
||||
|
||||
@@ -2933,6 +2986,9 @@ def upload_image(
|
||||
shutil.copyfileobj(image.file, f)
|
||||
|
||||
# Fast path: add to existing group without pose generation
|
||||
if not group_id:
|
||||
group_id = naming.get_base_name(filename)
|
||||
|
||||
if group_id and skip_poses:
|
||||
sort_order = database.get_next_sort_order(group_id)
|
||||
database.upsert_person(filename, filepath=file_path, group_id=group_id,
|
||||
@@ -3055,8 +3111,9 @@ def set_image_preferred(filename: str):
|
||||
others = [r[0] for r in rows if r[0] != filename]
|
||||
database.set_group_order(group_id, [filename] + others)
|
||||
_invalidate_static()
|
||||
fpath = os.path.join(_load_output_dir(), filename)
|
||||
if os.path.exists(fpath):
|
||||
# Use stored filepath if available (absolute), else resolve relative to output_dir
|
||||
fpath = person[5] if (len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||
if fpath and os.path.exists(fpath):
|
||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||
return {"filename": filename, "group_id": group_id}
|
||||
|
||||
@@ -3064,9 +3121,9 @@ def set_image_preferred(filename: str):
|
||||
@app.post("/images/{filename:path}/extract-face")
|
||||
def extract_face_endpoint(filename: str):
|
||||
"""Detect and crop the largest face from image; saves as {group_id}_face.png."""
|
||||
output_dir = _load_output_dir()
|
||||
fpath = os.path.join(output_dir, filename)
|
||||
if not os.path.exists(fpath):
|
||||
person = database.get_person(filename)
|
||||
fpath = person[5] if (person and len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||
if not fpath or not os.path.exists(fpath):
|
||||
raise HTTPException(404, "not found")
|
||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||
return {"status": "queued", "filename": filename}
|
||||
@@ -3506,6 +3563,7 @@ def delete_image(filename: str):
|
||||
_move_to_trash(person[5])
|
||||
|
||||
database.delete_person(filename)
|
||||
_update_cached_file_meta(filename, exists=False)
|
||||
_invalidate_static()
|
||||
return {"status": "deleted", "filename": filename}
|
||||
|
||||
@@ -3563,6 +3621,7 @@ def delete_group(group_id: str):
|
||||
for filename, filepath in files:
|
||||
if filepath and os.path.exists(filepath):
|
||||
_move_to_trash(filepath)
|
||||
_update_cached_file_meta(filename, exists=False)
|
||||
|
||||
database.delete_group(group_id)
|
||||
_invalidate_static()
|
||||
@@ -3748,6 +3807,7 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||
source_refs=json.dumps(refs))
|
||||
except Exception as db_err:
|
||||
print(f"[scenery] DB error: {db_err}")
|
||||
jobs[job_id]["latest_output"] = out_name
|
||||
jobs[job_id]["status"] = "done"
|
||||
jobs[job_id]["output"] = out_name
|
||||
# Regenerate the static JSON so the frontend's polling surfaces the new
|
||||
@@ -4182,11 +4242,27 @@ def remove_background_sam(filename: str):
|
||||
f.write(transparent_png)
|
||||
|
||||
# Register sidecar in DB so it appears in the same group
|
||||
group_id = person[1]
|
||||
database.upsert_person(nobg_filename, filepath=nobg_path,
|
||||
group_id=group_id, has_background=False,
|
||||
source_refs=json.dumps([filename]))
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
nobg_filename, filepath=nobg_path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=False,
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
|
||||
_invalidate_static()
|
||||
used_sam2 = _sam2_predictor is not False and _sam2_predictor is not None
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -4258,10 +4334,23 @@ def manual_crop_image(filename: str, req: CropRequest):
|
||||
new_filename = new_basename
|
||||
path = os.path.join(output_dir, new_basename)
|
||||
shutil.copy2(src_path, path)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
new_filename, filepath=path, group_id=person[1],
|
||||
prompt=person[6], pose=person[7],
|
||||
has_background=person[11], has_clothing=person[13],
|
||||
new_filename, filepath=path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
else:
|
||||
@@ -4376,10 +4465,23 @@ def pad_image(filename: str, req: PadRequest):
|
||||
new_filename = new_basename
|
||||
path = os.path.join(output_dir, new_basename)
|
||||
shutil.copy2(src_path, path)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
database.upsert_person(
|
||||
new_filename, filepath=path, group_id=person[1],
|
||||
prompt=person[6], pose=person[7],
|
||||
has_background=person[11], has_clothing=person[13],
|
||||
new_filename, filepath=path, group_id=group_id,
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]),
|
||||
)
|
||||
else:
|
||||
@@ -4393,7 +4495,20 @@ def pad_image(filename: str, req: PadRequest):
|
||||
|
||||
if req.outpaint:
|
||||
# Trigger Qwen outpainting
|
||||
out_instr = "Naturally outpaint and extend the borders of the image to complete the scene."
|
||||
# If the image is transparent, composite it onto a neutral background (black)
|
||||
# so the AI can see the area to be filled.
|
||||
qwen_input = padded
|
||||
fill_desc = ""
|
||||
if qwen_input.mode == "RGBA":
|
||||
# Composite onto black for maximal contrast in outpainting
|
||||
bg = Image.new("RGB", qwen_input.size, (0, 0, 0))
|
||||
bg.paste(qwen_input, mask=qwen_input.split()[3])
|
||||
qwen_input = bg
|
||||
fill_desc = " replacing the black background areas"
|
||||
elif req.fill in ["black", "white"]:
|
||||
fill_desc = f" replacing the {req.fill} background areas"
|
||||
|
||||
out_instr = f"Naturally outpaint and extend the borders of the image to complete the scene{fill_desc}."
|
||||
actual_prompt = req.prompt or out_instr
|
||||
if req.prompt and out_instr.lower() not in actual_prompt.lower():
|
||||
actual_prompt = f"{actual_prompt}. {out_instr}"
|
||||
@@ -4401,7 +4516,7 @@ def pad_image(filename: str, req: PadRequest):
|
||||
actual_prompt = f"{person[6]}. {out_instr}"
|
||||
|
||||
try:
|
||||
png_bytes = _run_pipeline(padded, actual_prompt)
|
||||
png_bytes = _run_pipeline(qwen_input, actual_prompt)
|
||||
padded = Image.open(io.BytesIO(png_bytes))
|
||||
# Register the outpaint prompt and set has_background=True in DB
|
||||
database.upsert_person(new_filename, prompt=actual_prompt, has_background=True)
|
||||
@@ -4474,16 +4589,29 @@ def duplicate_image(filename: str):
|
||||
|
||||
new_path = os.path.join(output_dir, new_basename)
|
||||
_shutil.copy2(path, new_path)
|
||||
group_id = person[1]
|
||||
# person tuple: (name, group_id, tags, embedding, clip_description, filepath,
|
||||
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing)
|
||||
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||
|
||||
# Parse tags list if present
|
||||
tags_list = None
|
||||
if person[2]:
|
||||
try:
|
||||
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||
except Exception:
|
||||
tags_list = None
|
||||
|
||||
database.upsert_person(
|
||||
new_filename, filepath=new_path, group_id=group_id,
|
||||
prompt=person[6], pose=person[7],
|
||||
name=person[0], tags=tags_list, embedding=person[3],
|
||||
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||
group_name=person[9], hidden=person[10],
|
||||
has_background=person[11],
|
||||
has_clothing=person[13],
|
||||
is_source=person[14],
|
||||
pose_description=person[15],
|
||||
pose_skeleton=person[16],
|
||||
source_refs=json.dumps([filename]), # original is the reference
|
||||
)
|
||||
_update_cached_file_meta(new_filename, exists=True)
|
||||
_invalidate_static()
|
||||
return {"status": "success", "new_filename": new_filename, "new_url": f"/output/{new_filename}"}
|
||||
|
||||
@@ -4503,6 +4631,7 @@ def restore_background(filename: str):
|
||||
bg.save(buf, format="PNG")
|
||||
with open(path, "wb") as f:
|
||||
f.write(buf.getvalue())
|
||||
_invalidate_static()
|
||||
return {"status": "success", "filename": filename}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user