From an outside audit: - The scan wizard's offline mode still sent the greyed-out rename and overwrite options, and scraper.py fetched Civitai model pages and preview media for models whose .info held a Civitai id. Offline now implies no media and no model page; the folder scan and scan_all refuse a missing or non-object body instead of defaulting to an online scan. - The Prompt Workshop's snap dock referenced leftSearch from the source deck module (ReferenceError on click); the deck now exposes focusSearch(). - translate, delete_model, delete_gallery_image and the image-material routes answer malformed bodies with 400 instead of 500. - Removed six unused imports in ui_doctor.js and 16 duplicate locale keys per language (the later, effective entry is kept, so nothing changes on screen). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""Image inspection and asset storage for curated materials."""
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
|
|
from .recipe_images import _embedded_workflow_payload, _output_source_path, _recipe_cover_webp_bytes
|
|
from .recipe_schema import _build_model_references, _normalise_source_image
|
|
from .workflow_schema import _validate_workflow
|
|
from .material_schema import _node_blocks, _suggested_name
|
|
from .utils import require_filename, resolve_within
|
|
|
|
|
|
MAX_SOURCE_IMAGE_BYTES = 64 * 1024 * 1024
|
|
|
|
|
|
def _material_assets_dir(materials_dir, filename, create=False):
|
|
stem = os.path.splitext(require_filename(filename))[0]
|
|
assets_dir = resolve_within(materials_dir, ".assets", stem)
|
|
if create:
|
|
os.makedirs(assets_dir, exist_ok=True)
|
|
return assets_dir
|
|
|
|
|
|
def _inspect_source_image(source_image):
|
|
source = _normalise_source_image(source_image)
|
|
if source is None:
|
|
raise ValueError("Missing source image")
|
|
if not source["filename"].lower().endswith(".png"):
|
|
raise ValueError("Only PNG output images can contain reusable workflow metadata")
|
|
source_path = _output_source_path(source)
|
|
payload = _embedded_workflow_payload(source_path)
|
|
workflow = payload.get("workflow") if isinstance(payload, dict) else None
|
|
if not isinstance(workflow, dict):
|
|
raise ValueError("Image has no reusable UI workflow")
|
|
_validate_workflow(workflow)
|
|
blocks = _node_blocks(workflow, include_values=False)
|
|
references = _build_model_references({"workflow": workflow, "params": {}}, verify_identities=False)
|
|
suggested_name = _suggested_name(workflow, source_path, references)
|
|
return source, source_path, workflow, blocks, references, suggested_name
|
|
|
|
|
|
def _store_assets(materials_dir, filename, source_path):
|
|
source_size = os.path.getsize(source_path)
|
|
if source_size > MAX_SOURCE_IMAGE_BYTES:
|
|
raise ValueError("Source image is too large")
|
|
digest = hashlib.sha256()
|
|
with open(source_path, "rb") as source_file:
|
|
for block in iter(lambda: source_file.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
assets_dir = _material_assets_dir(materials_dir, filename, create=True)
|
|
source_asset = f"source-{digest.hexdigest()}.png"
|
|
source_target = resolve_within(assets_dir, source_asset)
|
|
if not os.path.exists(source_target):
|
|
fd, temp_path = tempfile.mkstemp(prefix=".source-", suffix=".tmp", dir=assets_dir)
|
|
os.close(fd)
|
|
try:
|
|
shutil.copyfile(source_path, temp_path)
|
|
os.replace(temp_path, source_target)
|
|
finally:
|
|
if os.path.exists(temp_path):
|
|
os.remove(temp_path)
|
|
|
|
preview = _recipe_cover_webp_bytes(source_path)
|
|
preview_asset = None
|
|
if preview:
|
|
data, width, height = preview
|
|
preview_asset = f"preview-{hashlib.sha256(data).hexdigest()}.webp"
|
|
preview_target = resolve_within(assets_dir, preview_asset)
|
|
if not os.path.exists(preview_target):
|
|
with open(preview_target, "wb") as preview_file:
|
|
preview_file.write(data)
|
|
else:
|
|
width = height = None
|
|
return {
|
|
"source_asset_id": source_asset,
|
|
"preview_asset_id": preview_asset,
|
|
"preview_width": width,
|
|
"preview_height": height,
|
|
"source_sha256": digest.hexdigest(),
|
|
"source_size": source_size,
|
|
}
|