171 lines
5.7 KiB
Python
171 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Iterable, List, Sequence, Tuple
|
|
|
|
try:
|
|
from natsort import natsorted
|
|
except Exception: # pragma: no cover - optional dependency fallback
|
|
natsorted = sorted
|
|
|
|
|
|
DEFAULT_IMAGE_EXTENSIONS = ".png,.jpg,.jpeg,.webp,.bmp"
|
|
PLUGIN_DIR = Path(__file__).resolve().parent
|
|
WORKSPACE_DIR = PLUGIN_DIR.parent
|
|
LEGACY_WORKSPACE_PREFIX = "/workspace"
|
|
|
|
|
|
def resolve_container_path(value: str) -> Path:
|
|
raw = os.path.expandvars(str(value or "").strip())
|
|
if raw == LEGACY_WORKSPACE_PREFIX:
|
|
raw = str(WORKSPACE_DIR)
|
|
elif raw.startswith(f"{LEGACY_WORKSPACE_PREFIX}/"):
|
|
raw = str(WORKSPACE_DIR / raw.removeprefix(f"{LEGACY_WORKSPACE_PREFIX}/"))
|
|
return Path(os.path.expanduser(raw)).resolve()
|
|
|
|
|
|
def parse_extensions(value: str | Sequence[str] | None) -> Tuple[str, ...]:
|
|
if value is None:
|
|
value = DEFAULT_IMAGE_EXTENSIONS
|
|
if isinstance(value, str):
|
|
raw_items = re.split(r"[,;\s]+", value)
|
|
else:
|
|
raw_items = list(value)
|
|
|
|
extensions: List[str] = []
|
|
for item in raw_items:
|
|
item = str(item).strip().lower()
|
|
if not item:
|
|
continue
|
|
if not item.startswith("."):
|
|
item = f".{item}"
|
|
extensions.append(item)
|
|
return tuple(dict.fromkeys(extensions)) or tuple(DEFAULT_IMAGE_EXTENSIONS.split(","))
|
|
|
|
|
|
def is_image_file(path: Path, extensions: Iterable[str]) -> bool:
|
|
return path.is_file() and path.suffix.lower() in set(extensions)
|
|
|
|
|
|
def list_images(folder: Path, extensions: Iterable[str], max_images: int = 0) -> List[str]:
|
|
images = [path for path in folder.iterdir() if is_image_file(path, extensions)]
|
|
images = natsorted(images, key=lambda p: p.name.lower())
|
|
if max_images and max_images > 0:
|
|
images = images[:max_images]
|
|
return [str(path) for path in images]
|
|
|
|
|
|
def normalize_trigger_word(name: str) -> str:
|
|
trigger = name.strip()
|
|
trigger = re.sub(r"\s+", "_", trigger)
|
|
trigger = re.sub(r"[^0-9A-Za-z_\-]+", "_", trigger)
|
|
trigger = re.sub(r"_+", "_", trigger).strip("_")
|
|
return trigger or "anima_subject"
|
|
|
|
|
|
def matches_folder_filter(folder_name: str, folder_filter: str) -> bool:
|
|
folder_filter = (folder_filter or "").strip()
|
|
if not folder_filter:
|
|
return True
|
|
|
|
items = [item.strip() for item in re.split(r"[,;\n]+", folder_filter) if item.strip()]
|
|
if not items:
|
|
return True
|
|
|
|
for item in items:
|
|
if item == folder_name:
|
|
return True
|
|
try:
|
|
if re.search(item, folder_name):
|
|
return True
|
|
except re.error:
|
|
if item.lower() in folder_name.lower():
|
|
return True
|
|
return False
|
|
|
|
|
|
def count_existing_captions(images: Sequence[str]) -> int:
|
|
count = 0
|
|
for image in images:
|
|
caption_path = Path(image).with_suffix(".txt")
|
|
if caption_path.exists() and caption_path.read_text(encoding="utf-8", errors="ignore").strip():
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def scan_task_folders(
|
|
dataset_root: str,
|
|
image_extensions: str = DEFAULT_IMAGE_EXTENSIONS,
|
|
folder_filter: str = "",
|
|
include_root_images: bool = False,
|
|
max_images_per_folder: int = 0,
|
|
) -> Tuple[List[Dict[str, Any]], str]:
|
|
root = resolve_container_path(dataset_root)
|
|
logs: List[str] = []
|
|
if not dataset_root.strip():
|
|
return [], "ERROR: dataset_root is empty."
|
|
if not root.exists():
|
|
return [], f"ERROR: dataset_root does not exist: {root}"
|
|
if not root.is_dir():
|
|
return [], f"ERROR: dataset_root is not a directory: {root}"
|
|
|
|
extensions = parse_extensions(image_extensions)
|
|
candidate_folders: List[Path] = []
|
|
child_folders = [path for path in root.iterdir() if path.is_dir()]
|
|
child_folders = natsorted(child_folders, key=lambda p: p.name.lower())
|
|
|
|
for folder in child_folders:
|
|
if matches_folder_filter(folder.name, folder_filter):
|
|
candidate_folders.append(folder)
|
|
|
|
if include_root_images:
|
|
candidate_folders.insert(0, root)
|
|
|
|
tasks: List[Dict[str, Any]] = []
|
|
for folder in candidate_folders:
|
|
images = list_images(folder, extensions, max_images_per_folder)
|
|
if not images:
|
|
logs.append(f"SKIP: no images found in {folder}")
|
|
continue
|
|
|
|
trigger_word = normalize_trigger_word(folder.name if folder != root else root.name)
|
|
caption_count = count_existing_captions(images)
|
|
task = {
|
|
"dataset_root": str(root),
|
|
"task_dir": str(folder),
|
|
"folder_name": folder.name,
|
|
"trigger_word": trigger_word,
|
|
"images": images,
|
|
"image_count": len(images),
|
|
"caption_count": caption_count,
|
|
}
|
|
tasks.append(task)
|
|
logs.append(
|
|
f"OK: {folder.name} -> trigger={trigger_word}, images={len(images)}, captions={caption_count}"
|
|
)
|
|
|
|
if not tasks:
|
|
logs.append("ERROR: no valid LoRA task folders were found.")
|
|
else:
|
|
logs.insert(0, f"Found {len(tasks)} task folder(s) under {root}.")
|
|
return tasks, "\n".join(logs)
|
|
|
|
|
|
def iter_task_images(task_list: Sequence[Dict[str, Any]]):
|
|
for task in task_list or []:
|
|
trigger_word = normalize_trigger_word(str(task.get("trigger_word") or task.get("folder_name") or ""))
|
|
for image_path in task.get("images") or []:
|
|
yield task, Path(image_path), trigger_word
|
|
|
|
|
|
def refresh_task_caption_counts(task_list: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
refreshed: List[Dict[str, Any]] = []
|
|
for task in task_list or []:
|
|
task_copy = dict(task)
|
|
images = list(task_copy.get("images") or [])
|
|
task_copy["caption_count"] = count_existing_captions(images)
|
|
refreshed.append(task_copy)
|
|
return refreshed
|