347 lines
10 KiB
Python
347 lines
10 KiB
Python
"""
|
||
图片元数据服务
|
||
|
||
负责管理生成图片的元数据,支持绑定到角色/聊天的特定楼层
|
||
数据持久化到 data/image_metadata 目录
|
||
"""
|
||
import json
|
||
import uuid
|
||
from pathlib import Path
|
||
from typing import List, Dict, Optional, Any
|
||
from datetime import datetime
|
||
|
||
try:
|
||
from backend.models.internal import ImageMetadata
|
||
from backend.core.config import settings
|
||
except ImportError:
|
||
from models.internal import ImageMetadata
|
||
from core.config import settings
|
||
|
||
|
||
class ImageMetadataService:
|
||
"""
|
||
图片元数据服务
|
||
|
||
功能:
|
||
- 记录生成图片的元数据
|
||
- 按角色/聊天/楼层组织
|
||
- 支持 swipe(同一楼层多张图片)
|
||
- 提供画廊查询接口
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.metadata_dir = settings.DATA_PATH / "image_metadata"
|
||
self.metadata_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 图片存储目录
|
||
self.images_dir = settings.DATA_PATH / "images"
|
||
self.images_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
def _get_chat_metadata_file(self, chat_id: str) -> Path:
|
||
"""获取指定聊天的元数据文件路径"""
|
||
# chat_id 格式: role_name/chat_name
|
||
parts = chat_id.split("/")
|
||
if len(parts) == 2:
|
||
role_name, chat_name = parts
|
||
role_dir = self.metadata_dir / role_name
|
||
role_dir.mkdir(parents=True, exist_ok=True)
|
||
return role_dir / f"{chat_name}.json"
|
||
else:
|
||
# fallback
|
||
return self.metadata_dir / f"{chat_id.replace('/', '_')}.json"
|
||
|
||
def _load_chat_metadata(self, chat_id: str) -> List[ImageMetadata]:
|
||
"""加载指定聊天的所有图片元数据"""
|
||
file_path = self._get_chat_metadata_file(chat_id)
|
||
|
||
if not file_path.exists():
|
||
return []
|
||
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
return [ImageMetadata(**item) for item in data]
|
||
except Exception as e:
|
||
print(f"[ImageMetadata] 加载元数据失败: {e}")
|
||
return []
|
||
|
||
def _save_chat_metadata(self, chat_id: str, metadata_list: List[ImageMetadata]):
|
||
"""保存聊天的所有图片元数据"""
|
||
file_path = self._get_chat_metadata_file(chat_id)
|
||
|
||
try:
|
||
data = [m.model_dump() for m in metadata_list]
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
print(f"[ImageMetadata] 保存元数据失败: {e}")
|
||
|
||
async def add_image(
|
||
self,
|
||
chat_id: str,
|
||
role_name: str,
|
||
chat_name: str,
|
||
floor: int,
|
||
filename: str,
|
||
filepath: str,
|
||
prompt: Optional[str] = None,
|
||
negative_prompt: Optional[str] = None,
|
||
seed: Optional[int] = None,
|
||
model: Optional[str] = None,
|
||
workflow_name: Optional[str] = None,
|
||
task_id: Optional[str] = None,
|
||
generation_time: Optional[float] = None,
|
||
width: Optional[int] = None,
|
||
height: Optional[int] = None,
|
||
file_size: Optional[int] = None
|
||
) -> ImageMetadata:
|
||
"""
|
||
添加图片元数据
|
||
|
||
Args:
|
||
chat_id: 聊天ID
|
||
role_name: 角色名称
|
||
chat_name: 聊天名称
|
||
floor: 楼层号
|
||
filename: 文件名
|
||
filepath: 文件相对路径
|
||
prompt: 提示词
|
||
negative_prompt: 负面提示词
|
||
seed: 随机种子
|
||
model: 使用的模型
|
||
workflow_name: 工作流名称
|
||
task_id: 任务ID
|
||
generation_time: 生成耗时
|
||
width: 图片宽度
|
||
height: 图片高度
|
||
file_size: 文件大小
|
||
|
||
Returns:
|
||
ImageMetadata: 创建的元数据
|
||
"""
|
||
metadata_list = self._load_chat_metadata(chat_id)
|
||
|
||
# 计算 swipe_index
|
||
same_floor_images = [m for m in metadata_list if m.floor == floor]
|
||
swipe_index = len(same_floor_images)
|
||
|
||
# 如果这是该楼层的第一张图片,将其他图片的 isCurrentSwipe 设为 False
|
||
if swipe_index == 0:
|
||
for m in metadata_list:
|
||
if m.floor == floor:
|
||
m.isCurrentSwipe = False
|
||
|
||
metadata = ImageMetadata(
|
||
id=str(uuid.uuid4()),
|
||
chatId=chat_id,
|
||
roleName=role_name,
|
||
chatName=chat_name,
|
||
floor=floor,
|
||
filename=filename,
|
||
filepath=filepath,
|
||
prompt=prompt,
|
||
negativePrompt=negative_prompt,
|
||
seed=seed,
|
||
model=model,
|
||
workflowName=workflow_name,
|
||
taskId=task_id,
|
||
generationTime=generation_time,
|
||
width=width,
|
||
height=height,
|
||
fileSize=file_size,
|
||
swipeIndex=swipe_index,
|
||
isCurrentSwipe=True
|
||
)
|
||
|
||
metadata_list.append(metadata)
|
||
self._save_chat_metadata(chat_id, metadata_list)
|
||
|
||
return metadata
|
||
|
||
async def get_images_by_chat(
|
||
self,
|
||
chat_id: str,
|
||
floor: Optional[int] = None
|
||
) -> List[ImageMetadata]:
|
||
"""
|
||
获取指定聊天的图片列表
|
||
|
||
Args:
|
||
chat_id: 聊天ID
|
||
floor: 楼层号(可选,用于过滤)
|
||
|
||
Returns:
|
||
图片元数据列表
|
||
"""
|
||
metadata_list = self._load_chat_metadata(chat_id)
|
||
|
||
if floor is not None:
|
||
metadata_list = [m for m in metadata_list if m.floor == floor]
|
||
|
||
# 按楼层和 swipe_index 排序
|
||
metadata_list.sort(key=lambda m: (m.floor, m.swipeIndex))
|
||
|
||
return metadata_list
|
||
|
||
async def get_images_by_role(self, role_name: str) -> List[ImageMetadata]:
|
||
"""获取指定角色的所有图片"""
|
||
all_images = []
|
||
|
||
role_dir = self.metadata_dir / role_name
|
||
if not role_dir.exists():
|
||
return []
|
||
|
||
for chat_file in role_dir.glob("*.json"):
|
||
chat_name = chat_file.stem
|
||
chat_id = f"{role_name}/{chat_name}"
|
||
images = self._load_chat_metadata(chat_id)
|
||
all_images.extend(images)
|
||
|
||
# 按创建时间排序
|
||
all_images.sort(key=lambda m: m.createdAt, reverse=True)
|
||
|
||
return all_images
|
||
|
||
async def delete_image(self, chat_id: str, image_id: str) -> bool:
|
||
"""
|
||
删除图片元数据(不删除实际文件)
|
||
|
||
Args:
|
||
chat_id: 聊天ID
|
||
image_id: 图片ID
|
||
|
||
Returns:
|
||
bool: 是否成功删除
|
||
"""
|
||
metadata_list = self._load_chat_metadata(chat_id)
|
||
|
||
# 找到要删除的图片
|
||
target_image = None
|
||
for m in metadata_list:
|
||
if m.id == image_id:
|
||
target_image = m
|
||
break
|
||
|
||
if not target_image:
|
||
return False
|
||
|
||
floor = target_image.floor
|
||
swipe_index = target_image.swipeIndex
|
||
|
||
# 删除该图片
|
||
metadata_list = [m for m in metadata_list if m.id != image_id]
|
||
|
||
# 重新调整同一楼层其他图片的 swipe_index
|
||
same_floor_images = [m for m in metadata_list if m.floor == floor]
|
||
same_floor_images.sort(key=lambda m: m.swipeIndex)
|
||
|
||
for idx, m in enumerate(same_floor_images):
|
||
m.swipeIndex = idx
|
||
m.isCurrentSwipe = (idx == 0) # 第一个为当前显示
|
||
|
||
self._save_chat_metadata(chat_id, metadata_list)
|
||
return True
|
||
|
||
async def clear_chat_images(self, chat_id: str) -> int:
|
||
"""
|
||
清空指定聊天的所有图片元数据
|
||
|
||
Args:
|
||
chat_id: 聊天ID
|
||
|
||
Returns:
|
||
int: 删除的图片数量
|
||
"""
|
||
metadata_list = self._load_chat_metadata(chat_id)
|
||
count = len(metadata_list)
|
||
|
||
# 清空元数据文件
|
||
file_path = self._get_chat_metadata_file(chat_id)
|
||
if file_path.exists():
|
||
file_path.unlink()
|
||
|
||
return count
|
||
|
||
async def set_current_swipe(self, chat_id: str, image_id: str) -> bool:
|
||
"""
|
||
设置某张图片为当前显示的 swipe
|
||
|
||
Args:
|
||
chat_id: 聊天ID
|
||
image_id: 图片ID
|
||
|
||
Returns:
|
||
bool: 是否成功设置
|
||
"""
|
||
metadata_list = self._load_chat_metadata(chat_id)
|
||
|
||
target_image = None
|
||
for m in metadata_list:
|
||
if m.id == image_id:
|
||
target_image = m
|
||
break
|
||
|
||
if not target_image:
|
||
return False
|
||
|
||
floor = target_image.floor
|
||
|
||
# 将同一楼层的所有图片设为非当前
|
||
for m in metadata_list:
|
||
if m.floor == floor:
|
||
m.isCurrentSwipe = False
|
||
|
||
# 设置目标图片为当前
|
||
target_image.isCurrentSwipe = True
|
||
|
||
self._save_chat_metadata(chat_id, metadata_list)
|
||
return True
|
||
|
||
async def get_gallery_stats(self) -> Dict[str, Any]:
|
||
"""
|
||
获取画廊统计信息
|
||
|
||
Returns:
|
||
统计信息字典
|
||
"""
|
||
stats = {
|
||
"totalImages": 0,
|
||
"byRole": {},
|
||
"byChat": {}
|
||
}
|
||
|
||
if not self.metadata_dir.exists():
|
||
return stats
|
||
|
||
for role_dir in self.metadata_dir.iterdir():
|
||
if not role_dir.is_dir():
|
||
continue
|
||
|
||
role_name = role_dir.name
|
||
role_count = 0
|
||
|
||
for chat_file in role_dir.glob("*.json"):
|
||
chat_name = chat_file.stem
|
||
chat_id = f"{role_name}/{chat_name}"
|
||
images = self._load_chat_metadata(chat_id)
|
||
|
||
chat_count = len(images)
|
||
role_count += chat_count
|
||
stats["totalImages"] += chat_count
|
||
|
||
if chat_count > 0:
|
||
stats["byChat"][chat_id] = chat_count
|
||
|
||
if role_count > 0:
|
||
stats["byRole"][role_name] = role_count
|
||
|
||
return stats
|
||
|
||
def get_image_full_path(self, filepath: str) -> Path:
|
||
"""获取图片的完整路径"""
|
||
return self.images_dir / filepath
|
||
|
||
|
||
# 全局实例
|
||
image_metadata_service = ImageMetadataService()
|