+## 自然语言分类&搜索(实验性)
+
+这个功能用于把图片按**提示词语义相似度**自动分组(主题),并支持用一句自然语言做**语义检索**(类似 RAG 的召回阶段)。
+它是实验性功能:效果强依赖模型与提示词质量,适合快速找回/整理生成图片。
+
+### 使用方式(面向使用者)
+
+1. 打开首页「**自然语言分类&搜索(实验性)**」
+2. 点击「范围」选择要处理的文件夹(可多选,来源于 QuickMovePaths)
+3. **归类**:点「刷新」会在所选范围内生成主题列表(标题会按前端语言输出)
+4. **搜索**:输入一句话点「搜索」,会自动打开结果页(TopK 相似图片)
+
+> 选择的范围会持久化到后端 KV(`app_fe_setting["topic_search_scope"]`),下次打开会自动恢复并自动刷新一次结果。
+
+### 接口(给高级用户/二次开发)
+
+- **构建/刷新向量**:`POST /infinite_image_browsing/db/build_iib_output_embeddings`
+ - 入参:`folder`, `model`, `force`, `batch_size`, `max_chars`
+- **归类(聚类)**:`POST /infinite_image_browsing/db/cluster_iib_output`
+ - 入参:`folder_paths`(必填,数组)、`threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
+- **语义检索(RAG 召回)**:`POST /infinite_image_browsing/db/search_iib_output_by_prompt`
+ - 入参:`query`, `folder_paths`(必填,数组)、`top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
+
+### 原理(简单版)
+
+- **1)提示词抽取与清洗**
+ - 从 `image.exif` 中抽取提示词文本(只取 `Negative prompt:` 之前)
+ - 可选做“语义清洗”:去掉无意义的高频模板词(画质/摄影参数等),更聚焦主题语义(见 `IIB_PROMPT_NORMALIZE*`)
+- **2)向量化(Embedding)**
+ - 调用 OpenAI 兼容的 `/embeddings` 得到向量
+ - 写入 SQLite 表 `image_embedding`(增量更新,避免重复花费)
+- **3)主题聚类**
+ - 用“簇向量求和方向”的增量聚类(近似在线聚类),再把高相似簇做一次合并(减少同主题被切碎)
+ - 可选把小簇成员重新分配到最相近的大簇,降低噪声
+- **4)主题命名(LLM)**
+ - 对每个簇取代表提示词样本,调用 `/chat/completions` 生成短标题与关键词
+ - 通过 tool/function calling 强制结构化输出(JSON),并写入 `topic_title_cache`
+- **5)语义检索**
+ - 把用户 query 向量化,然后和范围内所有图片向量做余弦相似度排序,返回 TopK
+
+### 缓存与增量更新
+
+#### 1)向量缓存(`image_embedding`)
+
+- **存储位置**:SQLite 表 `image_embedding`(以 `image_id` 为主键)
+- **增量跳过条件**:满足以下条件则跳过重新向量化:
+ - `model` 相同
+ - `text_hash` 相同
+ - 已存在 `vec`
+- **“重新向量化”的缓存键**:`text_hash = sha256(f"{normalize_version}:{prompt_text}")`
+ - `prompt_text`:用于 embedding 的最终文本(抽取 + 可选清洗)
+ - `normalize_version`:由代码对清洗规则/模式计算出的**指纹**(不允许用户用环境变量手动覆盖)
+- **强制刷新**:在 `build_iib_output_embeddings` 传 `force=true`,或在 `cluster_iib_output` 传 `force_embed=true`
+
+#### 2)标题缓存(`topic_title_cache`)
+
+- **存储位置**:SQLite 表 `topic_title_cache`(主键 `cluster_hash`)
+- **命中条件**:`use_title_cache=true` 且 `force_title=false` 时复用历史标题/关键词
+- **缓存键 `cluster_hash` 包含**:
+ - 成员图片 id(排序后)
+ - embedding `model`、`threshold`、`min_cluster_size`
+ - `title_model`、输出语言 `lang`
+ - 语义清洗指纹(`normalize_version`)与清洗模式
+- **强制重新生成标题**:`force_title=true`
+
+### 配置(环境变量)
+
+所有 AI 调用都基于 **OpenAI 兼容** 的服务:
+
+- **`OPENAI_BASE_URL`**:例如 `https://your-host/v1`
+- **`OPENAI_API_KEY`**:你的 Key
+- **`EMBEDDING_MODEL`**:用于聚类的 embedding 模型
+- **`AI_MODEL`**:默认 chat 模型(兜底默认)
+- **`TOPIC_TITLE_MODEL`**:用于主题标题的 chat 模型(不配则回退到 `AI_MODEL`)
+- **`IIB_PROMPT_NORMALIZE`**:`1/0` 是否开启提示词清洗
+- **`IIB_PROMPT_NORMALIZE_MODE`**:`balanced`(推荐)/ `theme_only`(更激进)
+
+> 注意:AI 调用**没有 mock 兜底**。只要服务端/模型返回异常或不符合约束,就会直接报错,避免产生“看似能跑但其实不可信”的结果。
+
diff --git a/README.md b/README.md
index b154eae..ae45df1 100644
--- a/README.md
+++ b/README.md
@@ -176,3 +176,82 @@ https://user-images.githubusercontent.com/25872019/230768207-daab786b-d4ab-489f-
### Dark mode
+
+## Natural Language Categorization & Search (Experimental)
+
+This feature groups images by **semantic similarity of prompts** and supports **natural-language retrieval** (similar to the retrieval stage in RAG).
+It’s experimental: results depend on the embedding/chat models and the quality of prompt metadata.
+
+### How to Use (for end users)
+
+1. Open **“Natural Language Categorization & Search (Experimental)”** from the startup page
+2. Click **Scope** and select one or more folders (from QuickMovePaths)
+3. **Categorize**: click **Refresh** to generate topic cards for the selected scope
+4. **Search**: type a natural-language query and click **Search** (auto-opens the result grid)
+
+> The selected scope is persisted in backend KV: `app_fe_setting["topic_search_scope"]`. Next time it will auto-restore and auto-refresh once.
+
+### API Endpoints
+
+- **Build/refresh embeddings**: `POST /infinite_image_browsing/db/build_iib_output_embeddings`
+ - Request: `folder`, `model`, `force`, `batch_size`, `max_chars`
+- **Cluster (categorize)**: `POST /infinite_image_browsing/db/cluster_iib_output`
+ - Request: `folder_paths` (required, array), `threshold`, `min_cluster_size`, `force_embed`, `title_model`, `force_title`, `use_title_cache`, `assign_noise_threshold`, `lang`
+- **Prompt retrieval (RAG-like)**: `POST /infinite_image_browsing/db/search_iib_output_by_prompt`
+ - Request: `query`, `folder_paths` (required, array), `top_k`, `min_score`, `ensure_embed`, `model`, `max_chars`
+
+### How it Works (simple explanation)
+
+- **1) Prompt extraction & normalization**
+ - Reads `image.exif` and keeps content before `Negative prompt:`
+ - Optionally removes “boilerplate” terms (quality/photography parameters, etc.) to focus on topic semantics (`IIB_PROMPT_NORMALIZE*`)
+- **2) Embeddings**
+ - Calls OpenAI-compatible `/embeddings`
+ - Stores vectors in SQLite table `image_embedding` (incremental, to avoid repeated costs)
+- **3) Clustering**
+ - Online centroid-sum clustering, plus a post-merge step for highly similar clusters
+ - Optionally reassigns members of small clusters into the closest large cluster to reduce noise
+- **4) Title generation (LLM)**
+ - Calls `/chat/completions` with tool/function calling to force structured JSON output
+ - Stores titles/keywords in SQLite table `topic_title_cache`
+- **5) Retrieval**
+ - Embeds the query and ranks images in the selected scope by cosine similarity, returning TopK
+
+### Caching & Incremental Updates
+
+#### 1) Embedding cache (`image_embedding`)
+
+- **Where**: table `image_embedding` (keyed by `image_id`)
+- **Skip rule (incremental update)**: an image is skipped if:
+ - same `model`
+ - same `text_hash`
+ - existing `vec` is present
+- **Re-vectorization cache key**: `text_hash = sha256(f"{normalize_version}:{prompt_text}")`
+ - `prompt_text` is the extracted + (optionally) normalized text used for embeddings
+ - `normalize_version` is a **code-derived fingerprint** of normalization rules/mode (not user-configurable)
+- **Force rebuild**: pass `force=true` to `build_iib_output_embeddings` or `force_embed=true` to `cluster_iib_output`
+
+#### 2) Title cache (`topic_title_cache`)
+
+- **Where**: table `topic_title_cache` keyed by `cluster_hash`
+- **Hit rule**: when `use_title_cache=true` and `force_title=false`, titles/keywords are reused
+- **Cache key (`cluster_hash`) includes**:
+ - member image IDs (sorted)
+ - embedding `model`, `threshold`, `min_cluster_size`
+ - `title_model`, output `lang`
+ - normalization fingerprint (`normalize_version`) and mode
+- **Force title regeneration**: `force_title=true`
+
+### Configuration (Environment Variables)
+
+All calls use an **OpenAI-compatible** provider:
+
+- **`OPENAI_BASE_URL`**: e.g. `https://your-host/v1`
+- **`OPENAI_API_KEY`**: your API key
+- **`EMBEDDING_MODEL`**: embeddings model used for clustering
+- **`AI_MODEL`**: default chat model (fallback)
+- **`TOPIC_TITLE_MODEL`**: chat model used for cluster titles (falls back to `AI_MODEL`)
+- **`IIB_PROMPT_NORMALIZE`**: `1/0` enable prompt normalization
+- **`IIB_PROMPT_NORMALIZE_MODE`**: `balanced` (recommended) / `theme_only`
+
+> Note: There is **no mock fallback** for AI calls. If the provider/model fails or returns invalid output, the API will return an error directly.
diff --git a/requirements.txt b/requirements.txt
index 80232e7..8f337fc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,5 @@ pillow-avif-plugin
imageio
av>=14,<15
lxml
-filetype
\ No newline at end of file
+filetype
+requests
\ No newline at end of file
diff --git a/scripts/iib/api.py b/scripts/iib/api.py
index 2f1d43e..e186e19 100644
--- a/scripts/iib/api.py
+++ b/scripts/iib/api.py
@@ -6,7 +6,6 @@ from pathlib import Path
import shutil
import sqlite3
-
from scripts.iib.dir_cover_cache import get_top_4_media_info
from scripts.iib.tool import (
get_created_date_by_stat,
@@ -48,6 +47,7 @@ from PIL import Image
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
import hashlib
+from contextlib import closing
from scripts.iib.db.datamodel import (
DataBase,
ExtraPathType,
@@ -58,9 +58,10 @@ from scripts.iib.db.datamodel import (
ExtraPath,
FileInfoDict,
Cursor,
- GlobalSetting
+ GlobalSetting,
)
from scripts.iib.db.update_image_data import update_image_data, rebuild_image_index, add_image_data_single
+from scripts.iib.topic_cluster import mount_topic_cluster_routes
from scripts.iib.logger import logger
from scripts.iib.seq import seq
import urllib.parse
@@ -73,6 +74,13 @@ try:
except Exception as e:
logger.error(e)
+import requests
+import dotenv
+
+
+
+# 加载环境变量
+dotenv.load_dotenv()
index_html_path = get_data_file_path("vue/dist/index.html") if is_exe_ver else os.path.join(cwd, "vue/dist/index.html") # 在app.py也被使用
@@ -83,6 +91,17 @@ secret_key = os.getenv("IIB_SECRET_KEY")
if secret_key:
print("Secret key loaded successfully. ")
+# AI 配置
+OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
+OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
+AI_MODEL = os.getenv("AI_MODEL", "gpt-4o-mini")
+EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
+
+print(f"AI Model: {AI_MODEL or 'Not configured'}")
+print(f"OpenAI Base URL: {OPENAI_BASE_URL}")
+print(f"OpenAI API Key: {'Configured' if OPENAI_API_KEY else 'Not configured'}")
+print(f"Embedding Model: {EMBEDDING_MODEL or 'Not configured'}")
+
WRITEABLE_PERMISSIONS = ["read-write", "write-only"]
is_api_writeable = not (os.getenv("IIB_ACCESS_CONTROL_PERMISSION")) or (
@@ -376,6 +395,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
async def delete_files(req: DeleteFilesReq):
conn = DataBase.get_conn()
+
for path in req.file_paths:
check_path_trust(path)
try:
@@ -390,10 +410,12 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
shutil.rmtree(path)
else:
close_video_file_reader(path)
- os.remove(path)
txt_path = get_img_geninfo_txt_path(path)
+
+ os.remove(path)
if txt_path:
os.remove(txt_path)
+
img = DbImg.get(conn, os.path.normpath(path))
if img:
logger.info("delete file: %s", path)
@@ -409,6 +431,8 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
raise HTTPException(400, detail=error_msg)
+ return {"ok": True}
+
class CreateFoldersReq(BaseModel):
dest_folder: str
@@ -606,6 +630,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
img.thumbnail((int(w), int(h)))
os.makedirs(cache_dir, exist_ok=True)
img.save(cache_path, "webp")
+ # print(f"Image cache generated: {path}")
# 返回缓存文件
return FileResponse(
@@ -1216,6 +1241,17 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
ImageTag.remove(conn, image_id=req.img_id, tag_id=req.tag_id)
+ # ===== 主题聚类 / Embedding(拆分到独立模块,减少 api.py 体积)=====
+ mount_topic_cluster_routes(
+ app=app,
+ db_api_base=db_api_base,
+ verify_secret=verify_secret,
+ write_permission_required=write_permission_required,
+ openai_base_url=OPENAI_BASE_URL,
+ openai_api_key=OPENAI_API_KEY,
+ embedding_model=EMBEDDING_MODEL,
+ ai_model=AI_MODEL,
+ )
class ExtraPathModel(BaseModel):
@@ -1293,3 +1329,48 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
update_extra_paths(conn = DataBase.get_conn())
rebuild_image_index(search_dirs = get_img_search_dirs() + mem["extra_paths"])
+
+ # AI 相关路由
+ class AIChatRequest(BaseModel):
+ messages: List[dict]
+ temperature: Optional[float] = 0.7
+ max_tokens: Optional[int] = None
+ stream: Optional[bool] = False
+
+ @app.post(f"{api_base}/ai-chat", dependencies=[Depends(verify_secret), Depends(write_permission_required)])
+ async def ai_chat(req: AIChatRequest):
+ """通用AI聊天接口,转发到OpenAI兼容API"""
+ if not OPENAI_API_KEY:
+ raise HTTPException(status_code=500, detail="OpenAI API Key not configured")
+
+ try:
+ payload = {
+ "model": AI_MODEL,
+ "messages": req.messages,
+ "temperature": req.temperature,
+ "stream": req.stream
+ }
+ if req.max_tokens:
+ payload["max_tokens"] = req.max_tokens
+
+ headers = {
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
+ "Content-Type": "application/json"
+ }
+
+ response = requests.post(
+ f"{OPENAI_BASE_URL}/chat/completions",
+ json=payload,
+ headers=headers,
+ timeout=60
+ )
+
+ if response.status_code != 200:
+ raise HTTPException(status_code=response.status_code, detail=response.text)
+
+ return response.json()
+
+ except requests.RequestException as e:
+ logger.error(f"AI API request failed: {e}")
+ raise HTTPException(status_code=500, detail=f"AI API request failed: {str(e)}")
+
diff --git a/scripts/iib/db/datamodel.py b/scripts/iib/db/datamodel.py
index 2e1194e..e950f12 100644
--- a/scripts/iib/db/datamodel.py
+++ b/scripts/iib/db/datamodel.py
@@ -18,6 +18,7 @@ from contextlib import closing
import os
import threading
import re
+import hashlib
class FileInfoDict(TypedDict):
@@ -80,6 +81,8 @@ class DataBase:
ExtraPath.create_table(conn)
DirCoverCache.create_table(conn)
GlobalSetting.create_table(conn)
+ ImageEmbedding.create_table(conn)
+ TopicTitleCache.create_table(conn)
finally:
conn.commit()
clz.num += 1
@@ -301,6 +304,151 @@ class Image:
return images
+class ImageEmbedding:
+ """
+ Store embeddings for image prompt text.
+
+ Notes:
+ - vec is stored as float32 bytes (little-endian), compatible with Python's array('f').
+ - text_hash is used to skip recomputation when prompt text doesn't change.
+ """
+
+ @classmethod
+ def create_table(cls, conn: Connection):
+ with closing(conn.cursor()) as cur:
+ cur.execute(
+ """CREATE TABLE IF NOT EXISTS image_embedding (
+ image_id INTEGER PRIMARY KEY,
+ model TEXT NOT NULL,
+ dim INTEGER NOT NULL,
+ text_hash TEXT NOT NULL,
+ vec BLOB NOT NULL,
+ updated_at TEXT NOT NULL,
+ FOREIGN KEY (image_id) REFERENCES image(id)
+ )"""
+ )
+ cur.execute(
+ "CREATE INDEX IF NOT EXISTS image_embedding_idx_model_hash ON image_embedding(model, text_hash)"
+ )
+
+ @staticmethod
+ def compute_text_hash(text: str) -> str:
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
+
+ @classmethod
+ def get_by_image_ids(cls, conn: Connection, image_ids: List[int]):
+ if not image_ids:
+ return {}
+ placeholders = ",".join("?" * len(image_ids))
+ query = f"SELECT image_id, model, dim, text_hash, vec, updated_at FROM image_embedding WHERE image_id IN ({placeholders})"
+ with closing(conn.cursor()) as cur:
+ cur.execute(query, image_ids)
+ rows = cur.fetchall()
+ res = {}
+ for row in rows:
+ res[row[0]] = {
+ "image_id": row[0],
+ "model": row[1],
+ "dim": row[2],
+ "text_hash": row[3],
+ "vec": row[4],
+ "updated_at": row[5],
+ }
+ return res
+
+ @classmethod
+ def upsert(
+ cls,
+ conn: Connection,
+ image_id: int,
+ model: str,
+ dim: int,
+ text_hash: str,
+ vec_blob: bytes,
+ updated_at: Optional[str] = None,
+ ):
+ updated_at = updated_at or datetime.now().isoformat()
+ with closing(conn.cursor()) as cur:
+ cur.execute(
+ """INSERT INTO image_embedding (image_id, model, dim, text_hash, vec, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ON CONFLICT(image_id) DO UPDATE SET
+ model = excluded.model,
+ dim = excluded.dim,
+ text_hash = excluded.text_hash,
+ vec = excluded.vec,
+ updated_at = excluded.updated_at
+ """,
+ (image_id, model, dim, text_hash, vec_blob, updated_at),
+ )
+
+
+class TopicTitleCache:
+ """
+ Cache cluster titles/keywords to avoid repeated LLM calls.
+ """
+
+ @classmethod
+ def create_table(cls, conn: Connection):
+ with closing(conn.cursor()) as cur:
+ cur.execute(
+ """CREATE TABLE IF NOT EXISTS topic_title_cache (
+ cluster_hash TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ keywords TEXT NOT NULL,
+ model TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ )"""
+ )
+ cur.execute(
+ "CREATE INDEX IF NOT EXISTS topic_title_cache_idx_model ON topic_title_cache(model)"
+ )
+
+ @classmethod
+ def get(cls, conn: Connection, cluster_hash: str):
+ with closing(conn.cursor()) as cur:
+ cur.execute(
+ "SELECT title, keywords, model, updated_at FROM topic_title_cache WHERE cluster_hash = ?",
+ (cluster_hash,),
+ )
+ row = cur.fetchone()
+ if not row:
+ return None
+ title, keywords, model, updated_at = row
+ try:
+ kw = json.loads(keywords) if isinstance(keywords, str) else []
+ except Exception:
+ kw = []
+ if not isinstance(kw, list):
+ kw = []
+ return {"title": title, "keywords": kw, "model": model, "updated_at": updated_at}
+
+ @classmethod
+ def upsert(
+ cls,
+ conn: Connection,
+ cluster_hash: str,
+ title: str,
+ keywords: List[str],
+ model: str,
+ updated_at: Optional[str] = None,
+ ):
+ updated_at = updated_at or datetime.now().isoformat()
+ kw = json.dumps([str(x) for x in (keywords or [])], ensure_ascii=False)
+ with closing(conn.cursor()) as cur:
+ cur.execute(
+ """INSERT INTO topic_title_cache (cluster_hash, title, keywords, model, updated_at)
+ VALUES (?, ?, ?, ?, ?)
+ ON CONFLICT(cluster_hash) DO UPDATE SET
+ title = excluded.title,
+ keywords = excluded.keywords,
+ model = excluded.model,
+ updated_at = excluded.updated_at
+ """,
+ (cluster_hash, title, kw, model, updated_at),
+ )
+
+
class Tag:
def __init__(self, name: str, score: int, type: str, count=0, color = ""):
self.name = name
diff --git a/scripts/iib/img_cache_gen.py b/scripts/iib/img_cache_gen.py
index be8d5e6..94c584e 100644
--- a/scripts/iib/img_cache_gen.py
+++ b/scripts/iib/img_cache_gen.py
@@ -6,11 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
import time
from PIL import Image
-def generate_image_cache(dirs, size:str, verbose=False):
+def generate_image_cache(dirs: List[str], size:str, verbose=True):
+ dirs = [r"C:\Users\zanllp\Desktop\repo\Z-Image-Turbo\client"]
start_time = time.time()
cache_base_dir = get_cache_dir()
-
+ verbose=True
def process_image(item):
+ if '\\node_modules\\' in item.path:
+ return
if item.is_dir():
verbose and print(f"Processing directory: {item.path}")
for sub_item in os.scandir(item.path):
diff --git a/scripts/iib/topic_cluster.py b/scripts/iib/topic_cluster.py
new file mode 100644
index 0000000..8b2d6b1
--- /dev/null
+++ b/scripts/iib/topic_cluster.py
@@ -0,0 +1,931 @@
+import hashlib
+import json
+import math
+import os
+import re
+from array import array
+from contextlib import closing
+from typing import Dict, List, Optional, Tuple
+
+import requests
+from fastapi import Depends, FastAPI, HTTPException
+from pydantic import BaseModel
+
+from scripts.iib.db.datamodel import DataBase, ImageEmbedding, TopicTitleCache
+from scripts.iib.tool import cwd
+
+
+def _normalize_base_url(base_url: str) -> str:
+ return base_url[:-1] if base_url.endswith("/") else base_url
+
+
+_PROMPT_NORMALIZE_ENABLED = os.getenv("IIB_PROMPT_NORMALIZE", "1").strip().lower() not in ["0", "false", "no", "off"]
+# balanced: keep some discriminative style words (e.g. "科学插图/纪实/胶片") while dropping boilerplate quality/camera terms
+# theme_only: more aggressive removal, closer to "only subject/theme nouns"
+_PROMPT_NORMALIZE_MODE = (os.getenv("IIB_PROMPT_NORMALIZE_MODE", "balanced") or "balanced").strip().lower()
+
+# Remove common SD boilerplate / quality descriptors.
+_DROP_PATTERNS_COMMON = [
+ # SD / A1111 tags
+ r"MFrGXJqNrOUPCPqPrQ|]@`+`2h1lBlZnXp*r;rWrkz9{4{B}x-#c-#y-$;-$l-$y-%Q-%n-(i-(x-)i-/!-3*-5B-9V",wan:"#=$0&o.]0F4@5X5b6*628u9p -+b-+(-(_-(.-&h-#%{@wGuWs}s|rJrDlaWTV}V+NAMvKfIgGKFX9a7c,7&]&+%~",bie:"-/A-/;fGe2`#M'M!$!#I",pao:"-/>-+i-'^~o|2w=hA]$[P?.4J4H3d06.M'^%A!S",geng:"-/7-&A{TzHlrh=ZIOlK4IX=X2p&M",shua:"-//-%j",cuo:"-.y-.p-*5wukWkSh!ZKY&WuV4(o$j$'",kei:"-.woU",la:"-.v-%3-$n~L|8[RXFXEWnUEU2R`MOI6DT:T0['o$A",pou:"-.l-'_-&[{]twtO]+]&Z+YGJS/<",tuan:"-.I~!}~}K}HyPy&f7`>[}XIVmGLE;;.:m8t2[,F%v%p",zuan:"-.)XOTt",keng:"-,x-([|t|kvIZCXlVgBF/C",gao:"-,Z-(I-(>wRlpWjNHGxGwGdG>E~E3Dm,)!y!t",lang:"-,V-&J-$~{Jy[r{llgiSeOIOHO;KRHHG4Cp=[3Y,z*%(s",weng:"-,@-#oyxv{kfU!Pd9o'N'&",tao:"-+m-)E-'+-%DwPwMw*r}i/fl`j[oYBWXL,JkGtE?><=) ${x} MFrGXJqNrOUPCPqPrQ|]@`+`2h1lBlZnXp*r;rWrkz9{4{B}x-#c-#y-$;-$l-$y-%Q-%n-(i-(x-)i-/!-3*-5B-9V",wan:"#=$0&o.]0F4@5X5b6*628u9p -+b-+(-(_-(.-&h-#%{@wGuWs}s|rJrDlaWTV}V+NAMvKfIgGKFX9a7c,7&]&+%~",bie:"-/A-/;fGe2`#M'M!$!#I",pao:"-/>-+i-'^~o|2w=hA]$[P?.4J4H3d06.M'^%A!S",geng:"-/7-&A{TzHlrh=ZIOlK4IX=X2p&M",shua:"-//-%j",cuo:"-.y-.p-*5wukWkSh!ZKY&WuV4(o$j$'",kei:"-.woU",la:"-.v-%3-$n~L|8[RXFXEWnUEU2R`MOI6DT:T0['o$A",pou:"-.l-'_-&[{]twtO]+]&Z+YGJS/<",tuan:"-.I~!}~}K}HyPy&f7`>[}XIVmGLE;;.:m8t2[,F%v%p",zuan:"-.)XOTt",keng:"-,x-([|t|kvIZCXlVgBF/C",gao:"-,Z-(I-(>wRlpWjNHGxGwGdG>E~E3Dm,)!y!t",lang:"-,V-&J-$~{Jy[r{llgiSeOIOHO;KRHHG4Cp=[3Y,z*%(s",weng:"-,@-#oyxv{kfU!Pd9o'N'&",tao:"-+m-)E-'+-%DwPwMw*r}i/fl`j[oYBWXL,JkGtE?><=) ${z} 暂无结果ZYZZ]U_6_9d9fYj6j~lWm)mep)rQrbrctvwkxc{y|U}6~?~C~`~m-!Z-*'-+R-/j-0j-3i-4/-4@-5,-5f-6j-6s-7)-9G-9W-9X",tuo:"%U%V&z0L2J4v?{@$F_H6MUTbT~Y'Yc^QdHdQnVq+r`x1{{|;|<-&d-(.-(z-({-)1-)J-)K-*:-*e-*p-+$-+3-.b-/%-/[-0b-3O-4,-6_-8}-9$-9?",zhe:"#'%+%E'P2f2|
}I-*S-+S-0~-2b-5X-8{",cou:"@ThJiK",chuang:"'_,H,L,q{+{E",piao:"$+).1D7a:;
lMi@i$fDf@b1`Y_4XyW6TMMzJ$I:GOD{=#
{let t=0,n=1;for(let a=e.length;a--;)t+=n*la.indexOf(e.charAt(a)),n*=91;return t},Tt=(e,t)=>{let n,a,i,s,w;for(n in e)if(e.hasOwnProperty(n))for(a=e[n].match(ia),i=0;i
');continue}const K=G[x];b||(b=K.includes("("));const se=["tag"];b&&se.push("has-parentheses"),K.length<32&&se.push("short-tag"),U.push(`${K}`),b&&(b=!K.includes(")"))}return U.join(a.showCommaInInfoPanel?",":" ")}he("load",o=>{const r=o.target;r.className==="ant-image-preview-img"&&(z.value=`${r.naturalWidth} x ${r.naturalHeight}`)},{capture:!0});const te=R(()=>{const o=[{name:A("fileSize"),val:n.file.size}];return z.value&&o.push({name:A("resolution"),val:z.value}),o}),be=()=>{const o="Negative prompt:",r=P.value.includes(o)?P.value.split(o)[0]:W.value[0]??"";de(Ae(r.trim()))},d=()=>document.body.requestFullscreen(),m=o=>{de(typeof o=="object"?JSON.stringify(o,null,4):o)},q=o=>{o.key.startsWith("Arrow")?(o.stopPropagation(),o.preventDefault(),document.dispatchEvent(new KeyboardEvent("keydown",o))):o.key==="Escape"&&document.fullscreenElement&&document.exitFullscreen()};he("dblclick",o=>{var r;((r=o.target)==null?void 0:r.className)==="ant-image-preview-img"&&ke()});const F=R(()=>p.value||I.value.expanded),me=we(Be+"contextShowFullPath",!1),$e=R(()=>me.value?n.file.fullpath:n.file.name),_e=we(Be+"tagA2ZClassify",!1),Ft=R(()=>{var G;const o=(G=a.conf)==null?void 0:G.all_custom_tags.map(U=>{var x,K;return{char:((x=U.display_name)==null?void 0:x[0])||((K=U.name)==null?void 0:K[0]),...U}}).reduce((U,b)=>{var K;let x="#";if(/[a-z]/i.test(b.char))x=b.char.toUpperCase();else if(/[\u4e00-\u9fa5]/.test(b.char))try{x=((K=/^\[?(\w)/.exec(da(b.char)+""))==null?void 0:K[1])??"#"}catch(se){console.log("err",se)}return x=x.toUpperCase(),U[x]||(U[x]=[]),U[x].push(b),U},{});return Object.entries(o??{}).sort((U,b)=>U[0].charCodeAt(0)-b[0].charCodeAt(0))}),Ee=()=>{ke(),t("contextMenuClick",{key:"tiktokView"},n.file,n.idx)};return(o,r)=>{var ot;const G=Mn,U=ge,b=gn,x=pn,K=hn,se=fn,At=ge,nt=Cn,Pt=xn,Dt=vn,at=mn,It=$n;return $(),_("div",{ref_key:"el",ref:s,class:Fe(["full-screen-menu",{"unset-size":!c(I).expanded,lr:c(p),"always-on":c(v),"mouse-in":N.value}]),onWheelCapture:r[13]||(r[13]=dt(()=>{},["stop"])),onKeydownCapture:q},[c(p)?($(),_("div",ga)):B("",!0),O("div",pa,[O("div",ha,[c(p)?B("",!0):($(),_("div",{key:0,ref_key:"dragHandle",ref:h,class:"icon",style:{cursor:"grab"},title:c(A)("dragToMovePanel")},[u(c(Un))],8,fa)),c(p)?B("",!0):($(),_("div",{key:1,class:"icon",style:{cursor:"pointer"},onClick:r[0]||(r[0]=f=>c(I).expanded=!c(I).expanded),title:c(A)("clickToToggleMaximizeMinimize")},[F.value?($(),oe(c(cn),{key:0})):($(),oe(c(dn),{key:1}))],8,va)),O("div",{style:{display:"flex","flex-direction":"column","align-items":"center",cursor:"grab"},class:"icon",title:c(A)("fullscreenview"),onClick:d},[O("img",{src:c(Kn),style:{width:"21px",height:"21px","padding-bottom":"2px"},alt:""},null,8,$a)],8,ma),u(G,{"get-popup-container":j},{overlay:k(()=>[u(_n,{file:o.file,idx:o.idx,"selected-tag":w.value,onContextMenuClick:r[1]||(r[1]=(f,H,ne)=>t("contextMenuClick",f,H,ne))},null,8,["file","idx","selected-tag"])]),default:k(()=>[c(I).expanded?B("",!0):($(),_("div",ya,[u(c(ct))]))]),_:1}),F.value?($(),_("div",wa)):B("",!0),F.value?($(),_("div",ka,[u(G,{trigger:["hover"],"get-popup-container":j},{overlay:k(()=>[u(se,{onClick:r[2]||(r[2]=f=>t("contextMenuClick",f,o.file,o.idx))},{default:k(()=>{var f;return[((f=c(a).conf)==null?void 0:f.launch_mode)!=="server"?($(),_(Q,{key:0},[u(b,{key:"send2txt2img"},{default:k(()=>[C(y(o.$t("sendToTxt2img")),1)]),_:1}),u(b,{key:"send2img2img"},{default:k(()=>[C(y(o.$t("sendToImg2img")),1)]),_:1}),u(b,{key:"send2inpaint"},{default:k(()=>[C(y(o.$t("sendToInpaint")),1)]),_:1}),u(b,{key:"send2extras"},{default:k(()=>[C(y(o.$t("sendToExtraFeatures")),1)]),_:1}),u(x,{key:"sendToThirdPartyExtension",title:o.$t("sendToThirdPartyExtension")},{default:k(()=>[u(b,{key:"send2controlnet-txt2img"},{default:k(()=>[C("ControlNet - "+y(o.$t("t2i")),1)]),_:1}),u(b,{key:"send2controlnet-img2img"},{default:k(()=>[C("ControlNet - "+y(o.$t("i2i")),1)]),_:1}),u(b,{key:"send2outpaint"},{default:k(()=>[C("openOutpaint")]),_:1})]),_:1},8,["title"])],64)):B("",!0),u(b,{key:"send2BatchDownload"},{default:k(()=>[C(y(o.$t("sendToBatchDownload")),1)]),_:1}),u(x,{key:"copy2target",title:o.$t("copyTo")},{default:k(()=>[($(!0),_(Q,null,ue(c(a).quickMovePaths,H=>($(),oe(b,{key:`copy-to-${H.dir}`},{default:k(()=>[C(y(H.zh),1)]),_:2},1024))),128))]),_:1},8,["title"]),u(x,{key:"move2target",title:o.$t("moveTo")},{default:k(()=>[($(!0),_(Q,null,ue(c(a).quickMovePaths,H=>($(),oe(b,{key:`move-to-${H.dir}`},{default:k(()=>[C(y(H.zh),1)]),_:2},1024))),128))]),_:1},8,["title"]),u(K),u(b,{key:"deleteFiles"},{default:k(()=>[C(y(o.$t("deleteSelected")),1)]),_:1}),u(b,{key:"previewInNewWindow"},{default:k(()=>[C(y(o.$t("previewInNewWindow")),1)]),_:1}),u(b,{key:"copyPreviewUrl"},{default:k(()=>[C(y(o.$t("copySourceFilePreviewLink")),1)]),_:1}),u(b,{key:"copyFilePath"},{default:k(()=>[C(y(o.$t("copyFilePath")),1)]),_:1}),u(K),u(b,{key:"tiktokView",onClick:Ee},{default:k(()=>[C(y(o.$t("tiktokView")),1)]),_:1})]}),_:1})]),default:k(()=>[u(U,null,{default:k(()=>[C(y(c(A)("openContextMenu")),1)]),_:1})]),_:1}),u(At,{onClick:r[3]||(r[3]=f=>t("contextMenuClick",{key:"download"},n.file,n.idx))},{default:k(()=>[C(y(o.$t("download")),1)]),_:1}),P.value?($(),oe(U,{key:0,onClick:r[4]||(r[4]=f=>c(de)(P.value))},{default:k(()=>[C(y(o.$t("copyPrompt")),1)]),_:1})):B("",!0),P.value?($(),oe(U,{key:1,onClick:be},{default:k(()=>[C(y(o.$t("copyPositivePrompt")),1)]),_:1})):B("",!0),u(U,{onClick:Ee,onTouchstart:dt(Ee,["prevent"]),type:"default"},{default:k(()=>[C(y(o.$t("tiktokView")),1)]),_:1},8,["onTouchstart"])])):B("",!0)]),F.value?($(),_("div",ba,[O("div",_a,[O("span",Oa,[O("span",za,y(o.$t("fileName")),1),O("span",{class:"value",title:$e.value,onDblclick:r[5]||(r[5]=f=>c(de)($e.value))},y($e.value),41,xa),O("span",{style:{margin:"0 8px",cursor:"pointer"},title:"Click to expand full path",onClick:r[6]||(r[6]=f=>me.value=!c(me))},[u(c(ct))])]),($(!0),_(Q,null,ue(te.value,f=>($(),_("span",{class:"info-tag",key:f.name},[O("span",Ca,y(f.name),1),O("span",{class:"value",title:f.val,onDblclick:H=>c(de)(f.val)},y(f.val),41,Ma)]))),128))]),(ot=c(a).conf)!=null&&ot.all_custom_tags?($(),_("div",La,[O("div",{class:"sort-tag-switch",onClick:r[7]||(r[7]=f=>_e.value=!c(_e))},[c(_e)?($(),oe(c(Tn),{key:1})):($(),oe(c(Gn),{key:0}))]),O("div",{class:"tag",onClick:r[8]||(r[8]=(...f)=>c(Ve)&&c(Ve)(...f)),style:Te({"--tag-color":"var(--zp-luminous)"})},"+ "+y(o.$t("add")),5),c(_e)?($(!0),_(Q,{key:0},ue(Ft.value,([f,H])=>($(),_("div",{key:f,class:"tag-alpha-item"},[O("h4",Sa,y(f)+" : ",1),O("div",null,[($(!0),_(Q,null,ue(H,ne=>($(),_("div",{class:Fe(["tag",{selected:w.value.some(lt=>lt.id===ne.id)}]),onClick:lt=>t("contextMenuClick",{key:`toggle-tag-${ne.id}`},o.file,o.idx),key:ne.id,style:Te({"--tag-color":c(i).getColor(ne)})},y(ne.name),15,Ea))),128))])]))),128)):($(!0),_(Q,{key:1},ue(c(a).conf.all_custom_tags,f=>($(),_("div",{class:Fe(["tag",{selected:w.value.some(H=>H.id===f.id)}]),onClick:H=>t("contextMenuClick",{key:`toggle-tag-${f.id}`},o.file,o.idx),key:f.id,style:Te({"--tag-color":c(i).getColor(f)})},y(f.name),15,Ta))),128))])):B("",!0),O("div",Fa,[O("div",Aa,[C(y(o.$t("experimentalLRLayout"))+": ",1),u(nt,{checked:c(p),"onUpdate:checked":r[9]||(r[9]=f=>ze(p)?p.value=f:null),size:"small"},null,8,["checked"])]),c(p)?($(),_(Q,{key:0},[O("div",Pa,[C(y(o.$t("width"))+": ",1),u(Pt,{value:c(l),"onUpdate:value":r[10]||(r[10]=f=>ze(l)?l.value=f:null),style:{width:"64px"},step:16,min:128,max:1024},null,8,["value"])]),u(Dt,{title:o.$t("alwaysOnTooltipInfo")},{default:k(()=>[O("div",Da,[C(y(o.$t("alwaysOn"))+": ",1),u(nt,{checked:c(v),"onUpdate:checked":r[11]||(r[11]=f=>ze(v)?v.value=f:null),size:"small"},null,8,["checked"])])]),_:1},8,["title"])],64)):B("",!0)]),u(It,{activeKey:c(M),"onUpdate:activeKey":r[12]||(r[12]=f=>ze(M)?M.value=f:null)},{default:k(()=>[u(at,{key:"structedData",tab:o.$t("structuredData")},{default:k(()=>[O("div",null,[T.value.prompt?($(),_(Q,{key:0},[Ia,ja,O("code",{innerHTML:ae(T.value.prompt??"")},null,8,Wa)],64)):B("",!0),T.value.negativePrompt?($(),_(Q,{key:1},[Ua,qa,O("code",{innerHTML:ae(T.value.negativePrompt??"")},null,8,Va)],64)):B("",!0)]),Object.keys(D.value).length?($(),_(Q,{key:0},[Na,Ba,O("table",null,[($(!0),_(Q,null,ue(D.value,(f,H)=>($(),_("tr",{key:H,class:"gen-info-frag"},[O("td",Xa,y(H),1),typeof f=="object"?($(),_("td",{key:0,style:{cursor:"pointer"},onDblclick:ne=>m(f)},[O("code",null,y(f),1)],40,Ha)):($(),_("td",{key:1,style:{cursor:"pointer"},onDblclick:ne=>m(c(Ae)(f))},y(c(Ae)(f)),41,Ja))]))),128))])],64)):B("",!0)]),_:1},8,["tab"]),u(at,{key:"sourceText",tab:o.$t("sourceText")},{default:k(()=>[O("code",null,y(P.value),1)]),_:1},8,["tab"])]),_:1},8,["activeKey"])])):B("",!0)]),c(I).expanded&&!c(p)?($(),_("div",{key:1,class:"mouse-sensor",ref_key:"resizeHandle",ref:g,title:c(A)("dragToResizePanel")},[u(c(Dn))],8,Ya)):B("",!0)],34)}}});const po=St(Za,[["__scopeId","data-v-50c80b83"]]),Ga={key:0,class:"float-panel"},Ka={key:0,class:"select-actions"},Qa={key:1},Ra=Lt({__name:"MultiSelectKeep",props:{show:{type:Boolean}},emits:["selectAll","reverseSelect","clearAllSelected"],setup(e,{emit:t}){const n=Ye(),a=()=>{t("clearAllSelected"),n.keepMultiSelect=!1},i=()=>{n.keepMultiSelect=!0};return(s,w)=>{const z=ge;return s.show?($(),_("div",Ga,[c(n).keepMultiSelect?($(),_("div",Ka,[u(z,{size:"small",onClick:w[0]||(w[0]=S=>t("selectAll"))},{default:k(()=>[C(y(s.$t("select-all")),1)]),_:1}),u(z,{size:"small",onClick:w[1]||(w[1]=S=>t("reverseSelect"))},{default:k(()=>[C(y(s.$t("rerverse-select")),1)]),_:1}),u(z,{size:"small",onClick:w[2]||(w[2]=S=>t("clearAllSelected"))},{default:k(()=>[C(y(s.$t("clear-all-selected")),1)]),_:1}),u(z,{size:"small",onClick:a},{default:k(()=>[C(y(s.$t("exit")),1)]),_:1})])):($(),_("div",Qa,[u(z,{size:"small",type:"primary",onClick:i},{default:k(()=>[C(y(s.$t("keep-multi-selected")),1)]),_:1})]))])):B("",!0)}}});const ho=St(Ra,[["__scopeId","data-v-b04c3508"]]);export{so as L,ho as M,uo as R,co as a,go as b,ro as c,po as f,ea as o,he as u};
+*/let Dt=19968,ra=(40896-Dt)/2,Ke="",Se=",",ca=(()=>{let e=[];for(let t=33;t<127;t++)t!=34&&t!=92&&t!=45&&e.push(String.fromCharCode(t));return e.join(Ke)})(),it={a:{yi:"!]#R$!$q(3(p)[*2*g+6+d.C.q0[0w1L2<717l8B8E9?:8;V;[;e;{<)<+.>4??@~A`BbC:CGC^CiDMDjDkF!H/H;JaL?M.M2MoNCN|OgO|P$P)PBPyQ~R%R.S.T;TZYZZ]U_6_9d9fYj6j~lWm)mep)rQrbrctvwkxc{y|U}6~?~C~`~m-!Z-*'-+R-/j-0j-3i-4/-4@-5,-5f-6j-6s-7)-9G-9W-9X",tuo:"%U%V&z0L2J4v?{@$F_H6MUTbT~Y'Yc^QdHdQnVq+r`x1{{|;|<-&d-(.-(z-({-)1-)J-)K-*:-*e-*p-+$-+3-.b-/%-/[-0b-3O-4,-6_-8}-9$-9?",zhe:"#'%+%E'P2f2|
}I-*S-+S-0~-2b-5X-8{",cou:"@ThJiK",chuang:"'_,H,L,q{+{E",piao:"$+).1D7a:;
lMi@i$fDf@b1`Y_4XyW6TMMzJ$I:GOD{=#
{let t=0,n=1;for(let a=e.length;a--;)t+=n*ca.indexOf(e.charAt(a)),n*=91;return t},Pt=(e,t)=>{let n,a,i,s,w;for(n in e)if(e.hasOwnProperty(n))for(a=e[n].match(da),i=0;i
');continue}const J=H[z];O||(O=J.includes("("));const le=["tag"];O&&le.push("has-parentheses"),J.length<32&&le.push("short-tag"),j.push(`${J}`),O&&(O=!J.includes(")"))}return j.join(a.showCommaInInfoPanel?",":" ")}$e("load",o=>{const r=o.target;r.className==="ant-image-preview-img"&&(M.value=`${r.naturalWidth} x ${r.naturalHeight}`)},{capture:!0});const xe=ee(()=>{const o=[{name:F("fileSize"),val:n.file.size}];return M.value&&o.push({name:F("resolution"),val:M.value}),o}),d=()=>{const o="Negative prompt:",r=I.value.includes(o)?I.value.split(o)[0]:W.value[0]??"";pe(We(r.trim()))},_=()=>document.body.requestFullscreen(),q=o=>{pe(typeof o=="object"?JSON.stringify(o,null,4):o)},P=o=>{o.key.startsWith("Arrow")?(o.stopPropagation(),o.preventDefault(),document.dispatchEvent(new KeyboardEvent("keydown",o))):o.key==="Escape"&&document.fullscreenElement&&document.exitFullscreen()};$e("dblclick",o=>{var r;((r=o.target)==null?void 0:r.className)==="ant-image-preview-img"&&Oe()});const he=ee(()=>A.value||m.value.expanded),_e=be(Ze+"contextShowFullPath",!1),Ie=ee(()=>_e.value?n.file.fullpath:n.file.name),ze=be(Ze+"tagA2ZClassify",!1),jt=ee(()=>{var H;const o=(H=a.conf)==null?void 0:H.all_custom_tags.map(j=>{var z,J;return{char:((z=j.display_name)==null?void 0:z[0])||((J=j.name)==null?void 0:J[0]),...j}}).reduce((j,O)=>{var J;let z="#";if(/[a-z]/i.test(O.char))z=O.char.toUpperCase();else if(/[\u4e00-\u9fa5]/.test(O.char))try{z=((J=/^\[?(\w)/.exec(ma(O.char)+""))==null?void 0:J[1])??"#"}catch(le){console.log("err",le)}return z=z.toUpperCase(),j[z]||(j[z]=[]),j[z].push(O),j},{});return Object.entries(o??{}).sort((j,O)=>j[0].charCodeAt(0)-O[0].charCodeAt(0))}),Fe=()=>{Oe(),t("contextMenuClick",{key:"tiktokView"},n.file,n.idx)},Wt=async()=>{var o,r;if(!L.value.prompt){R.warning("没有找到提示词");return}if(!((r=(o=a.conf)==null?void 0:o.all_custom_tags)!=null&&r.length)){R.warning("没有自定义标签");return}try{const H=L.value.prompt,O=`你是一个专业的AI助手,负责分析Stable Diffusion提示词并将其分类到相应的标签中。
+
+你的任务是:
+1. 分析给定的提示词
+2. 从提供的标签列表中找出所有相关的标签
+3. 只返回匹配的标签名称,用逗号分隔
+4. 如果没有匹配的标签,返回空字符串
+5. 标签匹配应该基于语义相似性和主题相关性
+
+可用的标签:${a.conf.all_custom_tags.map(Y=>Y.name).join(", ")}
+
+请只返回标签名称,不要包含其他内容。`,J=(await mn({messages:[{role:"system",content:O},{role:"user",content:`请分析这个提示词并返回匹配的标签:${H}`}],temperature:.3,max_tokens:200})).choices[0].message.content.trim();if(!J){R.info("AI没有找到匹配的标签");return}const le=J.split(",").map(Y=>Y.trim()).filter(Y=>Y),fe=a.conf.all_custom_tags.filter(Y=>le.some(we=>Y.name.toLowerCase()===we.toLowerCase()||Y.name.toLowerCase().includes(we.toLowerCase())||we.toLowerCase().includes(Y.name.toLowerCase())));if(fe.length===0){R.info("没有找到有效的匹配标签");return}for(const Y of fe)t("contextMenuClick",{key:`toggle-tag-${Y.id}`},n.file,n.idx);R.success(`已添加 ${fe.length} 个标签:${fe.map(Y=>Y.name).join(", ")}`)}catch(H){console.error("AI分析标签失败:",H),R.error("AI分析标签失败,请检查配置")}};return(o,r)=>{var ut,rt,ct;const H=An,j=me,O=vn,z=$n,J=yn,le=_n,fe=me,Y=Tn,we=En,Ut=wn,st=kn,qt=bn;return h(),x("div",{ref_key:"el",ref:s,class:je(["full-screen-menu",{"unset-size":!c(m).expanded,lr:c(A),"always-on":c(y),"mouse-in":X.value}]),onWheelCapture:r[13]||(r[13]=mt(()=>{},["stop"])),onKeydownCapture:P},[c(A)?(h(),x("div",va)):B("",!0),b("div",$a,[b("div",ya,[c(A)?B("",!0):(h(),x("div",{key:0,ref_key:"dragHandle",ref:E,class:"icon",style:{cursor:"grab"},title:c(F)("dragToMovePanel")},[u(c(Xn))],8,_a)),c(A)?B("",!0):(h(),x("div",{key:1,class:"icon",style:{cursor:"pointer"},onClick:r[0]||(r[0]=g=>c(m).expanded=!c(m).expanded),title:c(F)("clickToToggleMaximizeMinimize")},[he.value?(h(),ie(c(hn),{key:0})):(h(),ie(c(fn),{key:1}))],8,wa)),b("div",{style:{display:"flex","flex-direction":"column","align-items":"center",cursor:"grab"},class:"icon",title:c(F)("fullscreenview"),onClick:_},[b("img",{src:c(na),style:{width:"21px",height:"21px","padding-bottom":"2px"},alt:""},null,8,ba)],8,ka),u(H,{"get-popup-container":G},{overlay:k(()=>[u(Mn,{file:o.file,idx:o.idx,"selected-tag":w.value,onContextMenuClick:r[1]||(r[1]=(g,N,oe)=>t("contextMenuClick",g,N,oe))},null,8,["file","idx","selected-tag"])]),default:k(()=>[c(m).expanded?B("",!0):(h(),x("div",Oa,[u(c(ft))]))]),_:1}),he.value?(h(),x("div",xa)):B("",!0),he.value?(h(),x("div",za,[u(H,{trigger:["hover"],"get-popup-container":G},{overlay:k(()=>[u(le,{onClick:r[2]||(r[2]=g=>t("contextMenuClick",g,o.file,o.idx))},{default:k(()=>{var g;return[((g=c(a).conf)==null?void 0:g.launch_mode)!=="server"?(h(),x(Q,{key:0},[u(O,{key:"send2txt2img"},{default:k(()=>[C(v(o.$t("sendToTxt2img")),1)]),_:1}),u(O,{key:"send2img2img"},{default:k(()=>[C(v(o.$t("sendToImg2img")),1)]),_:1}),u(O,{key:"send2inpaint"},{default:k(()=>[C(v(o.$t("sendToInpaint")),1)]),_:1}),u(O,{key:"send2extras"},{default:k(()=>[C(v(o.$t("sendToExtraFeatures")),1)]),_:1}),u(z,{key:"sendToThirdPartyExtension",title:o.$t("sendToThirdPartyExtension")},{default:k(()=>[u(O,{key:"send2controlnet-txt2img"},{default:k(()=>[C("ControlNet - "+v(o.$t("t2i")),1)]),_:1}),u(O,{key:"send2controlnet-img2img"},{default:k(()=>[C("ControlNet - "+v(o.$t("i2i")),1)]),_:1}),u(O,{key:"send2outpaint"},{default:k(()=>[C("openOutpaint")]),_:1})]),_:1},8,["title"])],64)):B("",!0),u(O,{key:"send2BatchDownload"},{default:k(()=>[C(v(o.$t("sendToBatchDownload")),1)]),_:1}),u(z,{key:"copy2target",title:o.$t("copyTo")},{default:k(()=>[(h(!0),x(Q,null,se(c(a).quickMovePaths,N=>(h(),ie(O,{key:`copy-to-${N.dir}`},{default:k(()=>[C(v(N.zh),1)]),_:2},1024))),128))]),_:1},8,["title"]),u(z,{key:"move2target",title:o.$t("moveTo")},{default:k(()=>[(h(!0),x(Q,null,se(c(a).quickMovePaths,N=>(h(),ie(O,{key:`move-to-${N.dir}`},{default:k(()=>[C(v(N.zh),1)]),_:2},1024))),128))]),_:1},8,["title"]),u(J),u(O,{key:"deleteFiles"},{default:k(()=>[C(v(o.$t("deleteSelected")),1)]),_:1}),u(O,{key:"previewInNewWindow"},{default:k(()=>[C(v(o.$t("previewInNewWindow")),1)]),_:1}),u(O,{key:"copyPreviewUrl"},{default:k(()=>[C(v(o.$t("copySourceFilePreviewLink")),1)]),_:1}),u(O,{key:"copyFilePath"},{default:k(()=>[C(v(o.$t("copyFilePath")),1)]),_:1}),u(J),u(O,{key:"tiktokView",onClick:Fe},{default:k(()=>[C(v(o.$t("tiktokView")),1)]),_:1})]}),_:1})]),default:k(()=>[u(j,null,{default:k(()=>[C(v(c(F)("openContextMenu")),1)]),_:1})]),_:1}),u(fe,{onClick:r[3]||(r[3]=g=>t("contextMenuClick",{key:"download"},n.file,n.idx))},{default:k(()=>[C(v(o.$t("download")),1)]),_:1}),I.value?(h(),ie(j,{key:0,onClick:r[4]||(r[4]=g=>c(pe)(I.value))},{default:k(()=>[C(v(o.$t("copyPrompt")),1)]),_:1})):B("",!0),I.value?(h(),ie(j,{key:1,onClick:d},{default:k(()=>[C(v(o.$t("copyPositivePrompt")),1)]),_:1})):B("",!0),I.value&&((rt=(ut=c(a).conf)==null?void 0:ut.all_custom_tags)!=null&&rt.length)?(h(),ie(j,{key:2,onClick:Wt,type:"primary"},{default:k(()=>[C(v(o.$t("aiAnalyzeTags")),1)]),_:1})):B("",!0),u(j,{onClick:Fe,onTouchstart:mt(Fe,["prevent"]),type:"default"},{default:k(()=>[C(v(o.$t("tiktokView")),1)]),_:1},8,["onTouchstart"])])):B("",!0)]),he.value?(h(),x("div",Ca,[b("div",Ma,[b("span",La,[b("span",Sa,v(o.$t("fileName")),1),b("span",{class:"value",title:Ie.value,onDblclick:r[5]||(r[5]=g=>c(pe)(Ie.value))},v(Ie.value),41,Ea),b("span",{style:{margin:"0 8px",cursor:"pointer"},title:"Click to expand full path",onClick:r[6]||(r[6]=g=>_e.value=!c(_e))},[u(c(ft))])]),(h(!0),x(Q,null,se(xe.value,g=>(h(),x("span",{class:"info-tag",key:g.name},[b("span",Ta,v(g.name),1),b("span",{class:"value",title:g.val,onDblclick:N=>c(pe)(g.val)},v(g.val),41,Aa)]))),128))]),(ct=c(a).conf)!=null&&ct.all_custom_tags?(h(),x("div",Ia,[b("div",{class:"sort-tag-switch",onClick:r[7]||(r[7]=g=>ze.value=!c(ze))},[c(ze)?(h(),ie(c(Pn),{key:1})):(h(),ie(c(ta),{key:0}))]),b("div",{class:"tag",onClick:r[8]||(r[8]=(...g)=>c(Je)&&c(Je)(...g)),style:Pe({"--tag-color":"var(--zp-luminous)"})},"+ "+v(o.$t("add")),5),c(ze)?(h(!0),x(Q,{key:0},se(jt.value,([g,N])=>(h(),x("div",{key:g,class:"tag-alpha-item"},[b("h4",Fa,v(g)+" : ",1),b("div",null,[(h(!0),x(Q,null,se(N,oe=>(h(),x("div",{class:je(["tag",{selected:w.value.some(dt=>dt.id===oe.id)}]),onClick:dt=>t("contextMenuClick",{key:`toggle-tag-${oe.id}`},o.file,o.idx),key:oe.id,style:Pe({"--tag-color":c(i).getColor(oe)})},v(oe.name),15,Da))),128))])]))),128)):(h(!0),x(Q,{key:1},se(c(a).conf.all_custom_tags,g=>(h(),x("div",{class:je(["tag",{selected:w.value.some(N=>N.id===g.id)}]),onClick:N=>t("contextMenuClick",{key:`toggle-tag-${g.id}`},o.file,o.idx),key:g.id,style:Pe({"--tag-color":c(i).getColor(g)})},v(g.name),15,Pa))),128))])):B("",!0),b("div",ja,[b("div",Wa,[C(v(o.$t("experimentalLRLayout"))+": ",1),u(Y,{checked:c(A),"onUpdate:checked":r[9]||(r[9]=g=>Me(A)?A.value=g:null),size:"small"},null,8,["checked"])]),c(A)?(h(),x(Q,{key:0},[b("div",Ua,[C(v(o.$t("width"))+": ",1),u(we,{value:c($),"onUpdate:value":r[10]||(r[10]=g=>Me($)?$.value=g:null),style:{width:"64px"},step:16,min:128,max:1024},null,8,["value"])]),u(Ut,{title:o.$t("alwaysOnTooltipInfo")},{default:k(()=>[b("div",qa,[C(v(o.$t("alwaysOn"))+": ",1),u(Y,{checked:c(y),"onUpdate:checked":r[11]||(r[11]=g=>Me(y)?y.value=g:null),size:"small"},null,8,["checked"])])]),_:1},8,["title"])],64)):B("",!0)]),u(qt,{activeKey:c(p),"onUpdate:activeKey":r[12]||(r[12]=g=>Me(p)?p.value=g:null)},{default:k(()=>[u(st,{key:"structedData",tab:o.$t("structuredData")},{default:k(()=>[b("div",null,[L.value.prompt?(h(),x(Q,{key:0},[Va,Na,b("code",{innerHTML:ne(L.value.prompt??"")},null,8,Ba)],64)):B("",!0),L.value.negativePrompt?(h(),x(Q,{key:1},[Xa,Ha,b("code",{innerHTML:ne(L.value.negativePrompt??"")},null,8,Ja)],64)):B("",!0)]),Object.keys(D.value).length?(h(),x(Q,{key:0},[Ya,Za,b("table",null,[(h(!0),x(Q,null,se(D.value,(g,N)=>(h(),x("tr",{key:N,class:"gen-info-frag"},[b("td",Ga,v(N),1),typeof g=="object"?(h(),x("td",{key:0,style:{cursor:"pointer"},onDblclick:oe=>q(g)},[b("code",null,v(g),1)],40,Ka)):(h(),x("td",{key:1,style:{cursor:"pointer"},onDblclick:oe=>q(c(We)(g))},v(c(We)(g)),41,Qa))]))),128))])],64)):B("",!0),S.value&&Object.keys(S.value).length?(h(),x(Q,{key:1},[Ra,eo,b("table",to,[(h(!0),x(Q,null,se(S.value,(g,N)=>(h(),x("tr",{key:N,class:"gen-info-frag"},[b("td",no,v(N),1),b("td",{style:{cursor:"pointer"},onDblclick:oe=>q(g)},[b("code",oo,v(typeof g=="string"?g:JSON.stringify(g,null,2)),1)],40,ao)]))),128))])],64)):B("",!0)]),_:1},8,["tab"]),u(st,{key:"sourceText",tab:o.$t("sourceText")},{default:k(()=>[b("code",null,v(I.value),1)]),_:1},8,["tab"])]),_:1},8,["activeKey"])])):B("",!0)]),c(m).expanded&&!c(A)?(h(),x("div",{key:1,class:"mouse-sensor",ref_key:"resizeHandle",ref:f,title:c(F)("dragToResizePanel")},[u(c(qn))],8,lo)):B("",!0)],34)}}});const Oo=Ft(io,[["__scopeId","data-v-4fda442c"]]),so={key:0,class:"float-panel"},uo={key:0,class:"select-actions"},ro={key:1},co=It({__name:"MultiSelectKeep",props:{show:{type:Boolean}},emits:["selectAll","reverseSelect","clearAllSelected"],setup(e,{emit:t}){const n=Re(),a=()=>{t("clearAllSelected"),n.keepMultiSelect=!1},i=()=>{n.keepMultiSelect=!0};return(s,w)=>{const M=me;return s.show?(h(),x("div",so,[c(n).keepMultiSelect?(h(),x("div",uo,[u(M,{size:"small",onClick:w[0]||(w[0]=T=>t("selectAll"))},{default:k(()=>[C(v(s.$t("select-all")),1)]),_:1}),u(M,{size:"small",onClick:w[1]||(w[1]=T=>t("reverseSelect"))},{default:k(()=>[C(v(s.$t("rerverse-select")),1)]),_:1}),u(M,{size:"small",onClick:w[2]||(w[2]=T=>t("clearAllSelected"))},{default:k(()=>[C(v(s.$t("clear-all-selected")),1)]),_:1}),u(M,{size:"small",onClick:a},{default:k(()=>[C(v(s.$t("exit")),1)]),_:1})])):(h(),x("div",ro,[u(M,{size:"small",type:"primary",onClick:i},{default:k(()=>[C(v(s.$t("keep-multi-selected")),1)]),_:1})]))])):B("",!0)}}});const xo=Ft(co,[["__scopeId","data-v-b6f9a67c"]]);export{yo as L,xo as M,_o as R,ko as a,bo as b,wo as c,Oo as f,la as o,$e as u};
diff --git a/vue/dist/assets/MultiSelectKeep-bf9f4da8.css b/vue/dist/assets/MultiSelectKeep-bf9f4da8.css
new file mode 100644
index 0000000..405e2f8
--- /dev/null
+++ b/vue/dist/assets/MultiSelectKeep-bf9f4da8.css
@@ -0,0 +1 @@
+.full-screen-menu[data-v-4fda442c]{position:fixed;z-index:9999;background:var(--zp-primary-background);padding:8px 16px;box-shadow:0 0 4px var(--zp-secondary);border-radius:4px}.full-screen-menu .tags-container[data-v-4fda442c]{margin:4px 0}.full-screen-menu .tags-container .tag[data-v-4fda442c]{margin-right:4px;margin-bottom:4px;padding:2px 16px;border-radius:4px;display:inline-block;cursor:pointer;font-weight:700;transition:.5s all ease;border:2px solid var(--tag-color);color:var(--tag-color);background:var(--zp-primary-background);user-select:none}.full-screen-menu .tags-container .tag.selected[data-v-4fda442c]{background:var(--tag-color);color:#fff}.full-screen-menu .container[data-v-4fda442c]{height:100%;display:flex;overflow:hidden;flex-direction:column}.full-screen-menu .gen-info[data-v-4fda442c]{flex:1;word-break:break-all;white-space:pre-line;overflow:auto;z-index:1;padding-top:4px;position:relative}.full-screen-menu .gen-info code[data-v-4fda442c]{font-size:.9em;display:block;padding:4px;background:var(--zp-primary-background);border-radius:4px;margin-right:20px;white-space:pre-wrap;word-break:break-word;line-height:1.78em}.full-screen-menu .gen-info code[data-v-4fda442c] .natural-text{margin:.5em 0;line-height:1.6em;text-align:justify;color:var(--zp-primary)}.full-screen-menu .gen-info code[data-v-4fda442c] .short-tag{word-break:break-all;white-space:nowrap}.full-screen-menu .gen-info code[data-v-4fda442c] span.tag{background:var(--zp-secondary-variant-background);color:var(--zp-primary);padding:2px 4px;border-radius:6px;margin-right:6px;margin-top:4px;line-height:1.3em;display:inline-block}.full-screen-menu .gen-info code[data-v-4fda442c] .has-parentheses.tag{background:rgba(255,100,100,.14)}.full-screen-menu .gen-info code[data-v-4fda442c] span.tag:hover{background:rgba(120,0,0,.15)}.full-screen-menu .gen-info table[data-v-4fda442c]{font-size:1em;border-radius:4px;border-collapse:separate;margin-bottom:3em}.full-screen-menu .gen-info table tr td[data-v-4fda442c]:first-child{white-space:nowrap;vertical-align:top}.full-screen-menu .gen-info table.extra-meta-table .extra-meta-value[data-v-4fda442c]{display:block;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:.85em;background:var(--zp-secondary-variant-background);padding:8px;border-radius:4px}.full-screen-menu .gen-info table td[data-v-4fda442c]{padding-right:14px;padding-left:4px;border-bottom:1px solid var(--zp-secondary);border-collapse:collapse}.full-screen-menu .gen-info .info-tags .info-tag[data-v-4fda442c]{display:inline-block;overflow:hidden;border-radius:4px;margin-right:8px;border:2px solid var(--zp-primary)}.full-screen-menu .gen-info .info-tags .name[data-v-4fda442c]{background-color:var(--zp-primary);color:var(--zp-primary-background);padding:4px;border-bottom-right-radius:4px}.full-screen-menu .gen-info .info-tags .value[data-v-4fda442c]{padding:4px}.full-screen-menu.unset-size[data-v-4fda442c]{width:unset!important;height:unset!important}.full-screen-menu .mouse-sensor[data-v-4fda442c]{position:absolute;bottom:0;right:0;transform:rotate(90deg);cursor:se-resize;z-index:1;background:var(--zp-primary-background);border-radius:2px}.full-screen-menu .mouse-sensor>*[data-v-4fda442c]{font-size:18px;padding:4px}.full-screen-menu .action-bar[data-v-4fda442c]{display:flex;align-items:center;user-select:none;gap:4px}.full-screen-menu .action-bar .icon[data-v-4fda442c]{font-size:1.5em;padding:2px 4px;border-radius:4px}.full-screen-menu .action-bar .icon[data-v-4fda442c]:hover{background:var(--zp-secondary-variant-background)}.full-screen-menu .action-bar>*[data-v-4fda442c]{flex-wrap:wrap}.full-screen-menu.lr[data-v-4fda442c]{top:var(--6e46b464)!important;right:0!important;bottom:0!important;left:100vw!important;height:unset!important;width:var(--02641c57)!important;transition:left ease .3s}.full-screen-menu.lr.always-on[data-v-4fda442c],.full-screen-menu.lr.mouse-in[data-v-4fda442c]{left:var(--c403b44a)!important}.tag-alpha-item[data-v-4fda442c]{display:flex;margin-top:4px}.tag-alpha-item h4[data-v-4fda442c]{width:32px;flex-shrink:0}.sort-tag-switch[data-v-4fda442c]{display:inline-block;padding-right:16px;padding-left:8px;cursor:pointer;user-select:none}.sort-tag-switch span[data-v-4fda442c]{transition:all ease .3s;transform:scale(1.2)}.sort-tag-switch:hover span[data-v-4fda442c]{transform:scale(1.3)}.lr-layout-control[data-v-4fda442c]{display:flex;align-items:center;gap:16px;padding:4px 8px;flex-wrap:wrap;border-radius:2px;border-left:3px solid var(--zp-luminous);background-color:var(--zp-secondary-background)}.lr-layout-control .ctrl-item[data-v-4fda442c]{display:flex;align-items:center;gap:4px;flex-wrap:nowrap}.select-actions[data-v-b6f9a67c]>:not(:last-child){margin-right:4px}.float-panel[data-v-b6f9a67c]{position:absolute;bottom:32px;right:32px;background:var(--zp-primary-background);border-radius:4px;z-index:1000;padding:8px;box-shadow:0 0 4px var(--zp-secondary)}
diff --git a/vue/dist/assets/SubstrSearch-30b4727e.js b/vue/dist/assets/SubstrSearch-30b4727e.js
new file mode 100644
index 0000000..17325be
--- /dev/null
+++ b/vue/dist/assets/SubstrSearch-30b4727e.js
@@ -0,0 +1 @@
+import{c as a,A as Fe,d as Ue,c9 as Be,r as w,o as Ee,cd as te,m as He,C as Pe,az as Ge,z as Ke,B as Le,E as ae,ce as je,a1 as qe,U as f,V as U,a3 as t,a4 as e,W as d,X as o,Y as i,a2 as y,$ as k,a5 as B,cp as Ne,ag as O,a6 as le,L as Je,af as We,Z as Qe,T as se,aj as Xe,cq as Ye,ah as Ze,ak as ne,ci as et,ai as tt,aP as at,aQ as lt,cr as st,ck as nt,a0 as it}from"./index-db391c6a.js";import{S as ot}from"./index-6be5f2d5.js";/* empty css *//* empty css */import"./index-e3af27b3.js";import{c as rt,d as dt,F as ut}from"./FileItem-9be5bb5d.js";import{M as ct,o as pt,L as ft,R as vt,f as mt}from"./MultiSelectKeep-cd70772d.js";import{c as gt,u as _t}from"./hook-6746a807.js";import{f as M,H as ie,_ as ht,a as yt}from"./searchHistory-f5718832.js";import"./numInput.vue_vue_type_style_index_0_scoped_bd954eda_lang-28fed536.js";/* empty css */import"./index-4a1bd1ce.js";import"./_isIterateeCall-6ab5736a.js";import"./index-ae90fb7e.js";import"./index-83d83387.js";import"./shortcut-ace377a3.js";import"./Checkbox-be055c11.js";import"./index-30c22d1a.js";import"./useGenInfoDiff-9f58ef19.js";var kt={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"defs",attrs:{},children:[{tag:"style",attrs:{}}]},{tag:"path",attrs:{d:"M952 474H829.8C812.5 327.6 696.4 211.5 550 194.2V72c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v122.2C327.6 211.5 211.5 327.6 194.2 474H72c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h122.2C211.5 696.4 327.6 812.5 474 829.8V952c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V829.8C696.4 812.5 812.5 696.4 829.8 550H952c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zM512 756c-134.8 0-244-109.2-244-244s109.2-244 244-244 244 109.2 244 244-109.2 244-244 244z"}},{tag:"path",attrs:{d:"M512 392c-32.1 0-62.1 12.4-84.8 35.2-22.7 22.7-35.2 52.7-35.2 84.8s12.5 62.1 35.2 84.8C449.9 619.4 480 632 512 632s62.1-12.5 84.8-35.2C619.4 574.1 632 544 632 512s-12.5-62.1-35.2-84.8A118.57 118.57 0 00512 392z"}}]},name:"aim",theme:"outlined"};const wt=kt;function oe(u){for(var c=1;c
+ Extra Meta Info
+
+
+
+
+ {{ key }}
+
+
+ {{ typeof val === 'string' ? val : JSON.stringify(val, null, 2) }}
+ {{ imageGenInfo }}
@@ -608,6 +708,21 @@ const onTiktokViewClick = () => {
tr td:first-child {
white-space: nowrap;
+ vertical-align: top;
+ }
+ }
+
+ table.extra-meta-table {
+ .extra-meta-value {
+ display: block;
+ max-height: 200px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.85em;
+ background: var(--zp-secondary-variant-background);
+ padding: 8px;
+ border-radius: 4px;
}
}
diff --git a/vue/src/page/globalSetting/globalSetting.vue b/vue/src/page/globalSetting/globalSetting.vue
index 63c383f..5e33bb3 100644
--- a/vue/src/page/globalSetting/globalSetting.vue
+++ b/vue/src/page/globalSetting/globalSetting.vue
@@ -19,7 +19,6 @@ import { throttle, debounce } from 'lodash-es'
import { useLocalStorage } from '@vueuse/core'
import { prefix } from '@/util/const'
-
const globalStore = useGlobalStore()
const wsStore = useWorkspeaceSnapshot()
@@ -71,7 +70,7 @@ const defaultInitinalPageOptions = computed(() => {
const shortCutsCountRec = computed(() => {
const rec = globalStore.shortcut
const res = {} as Dict{{ t('shortcutKey') }}