• 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:
mike
2026-06-28 16:56:07 +02:00
parent da1fd4a26c
commit 3b9b9e829b
6 changed files with 881 additions and 239 deletions

View File

@@ -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}