mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-07 08:36:49 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
from typing import Protocol, Dict, Any
|
||||
|
||||
|
||||
class BaseProcessor(Protocol):
|
||||
name: str
|
||||
supported_exts: list
|
||||
config_schema: list
|
||||
produces_file: bool
|
||||
|
||||
async def process(self, input_bytes: bytes, path: str, config: Dict[str, Any]) -> bytes:
|
||||
"""处理文件内容并返回处理后的内容"""
|
||||
...
|
||||
|
||||
# 约定:每个处理器需定义
|
||||
# PROCESSOR_TYPE: str
|
||||
# CONFIG_SCHEMA: list
|
||||
# PROCESSOR_FACTORY: Callable[[], BaseProcessor]
|
||||
@@ -0,0 +1,67 @@
|
||||
from .base import BaseProcessor
|
||||
from typing import Dict, Any
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from io import BytesIO
|
||||
from fastapi.responses import Response
|
||||
from services.logging import LogService
|
||||
|
||||
class ImageWatermarkProcessor:
|
||||
name = "图片水印"
|
||||
supported_exts = ["jpg", "jpeg", "png", "bmp"]
|
||||
config_schema = [
|
||||
{"key": "text", "label": "水印文字", "type": "string", "required": True},
|
||||
{
|
||||
"key": "position",
|
||||
"label": "位置",
|
||||
"type": "select",
|
||||
"required": False,
|
||||
"default": "bottom-right",
|
||||
"options": [
|
||||
{"value": "top-left", "label": "左上"},
|
||||
{"value": "center", "label": "居中"},
|
||||
{"value": "bottom-right", "label": "右下"},
|
||||
],
|
||||
},
|
||||
{"key": "font_size", "label": "字体大小", "type": "number", "required": False, "default": 24},
|
||||
]
|
||||
produces_file = True
|
||||
|
||||
async def process(self, input_bytes: bytes,path: str, config: Dict[str, Any]) -> Response:
|
||||
text = config.get("text", "")
|
||||
position = config.get("position", "bottom-right")
|
||||
font_size = int(config.get("font_size", 24))
|
||||
img = Image.open(BytesIO(input_bytes)).convert("RGBA")
|
||||
watermark = Image.new("RGBA", img.size)
|
||||
draw = ImageDraw.Draw(watermark)
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", font_size)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
w, h = img.size
|
||||
try:
|
||||
text_w, text_h = font.getsize(text)
|
||||
except AttributeError:
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
if position == "bottom-right":
|
||||
xy = (w - text_w - 10, h - text_h - 10)
|
||||
elif position == "top-left":
|
||||
xy = (10, 10)
|
||||
else:
|
||||
xy = (w // 2 - text_w // 2, h // 2 - text_h // 2)
|
||||
draw.text(xy, text, font=font, fill=(255, 255, 255, 128))
|
||||
out = Image.alpha_composite(img, watermark)
|
||||
buf = BytesIO()
|
||||
out.convert("RGB").save(buf, format="JPEG")
|
||||
await LogService.info(
|
||||
"processor:image_watermark",
|
||||
f"Watermarked image {path}",
|
||||
details={"path": path, "config": config},
|
||||
)
|
||||
return Response(content=buf.getvalue(), media_type="image/jpeg")
|
||||
|
||||
PROCESSOR_TYPE = "image_watermark"
|
||||
PROCESSOR_NAME = ImageWatermarkProcessor.name
|
||||
SUPPORTED_EXTS = ImageWatermarkProcessor.supported_exts
|
||||
CONFIG_SCHEMA = ImageWatermarkProcessor.config_schema
|
||||
PROCESSOR_FACTORY = lambda: ImageWatermarkProcessor()
|
||||
@@ -0,0 +1,65 @@
|
||||
import pkgutil
|
||||
import inspect
|
||||
from importlib import import_module
|
||||
from typing import Dict, Callable
|
||||
from .base import BaseProcessor
|
||||
|
||||
ProcessorFactory = Callable[[], BaseProcessor]
|
||||
TYPE_MAP: Dict[str, ProcessorFactory] = {}
|
||||
CONFIG_SCHEMAS: Dict[str, dict] = {}
|
||||
|
||||
def discover_processors():
|
||||
import services.processors
|
||||
processors_pkg = services.processors
|
||||
TYPE_MAP.clear()
|
||||
CONFIG_SCHEMAS.clear()
|
||||
for modinfo in pkgutil.iter_modules(processors_pkg.__path__):
|
||||
if modinfo.name.startswith("_"):
|
||||
continue
|
||||
full_name = f"{processors_pkg.__name__}.{modinfo.name}"
|
||||
try:
|
||||
module = import_module(full_name)
|
||||
except Exception:
|
||||
continue
|
||||
processor_type = getattr(module, "PROCESSOR_TYPE", None)
|
||||
processor_name = getattr(module, "PROCESSOR_NAME", None)
|
||||
supported_exts = getattr(module, "SUPPORTED_EXTS", None)
|
||||
schema = getattr(module, "CONFIG_SCHEMA", None)
|
||||
factory = getattr(module, "PROCESSOR_FACTORY", None)
|
||||
if not processor_type:
|
||||
continue
|
||||
if factory is None:
|
||||
for attr in module.__dict__.values():
|
||||
if inspect.isclass(attr) and attr.__name__.endswith("Processor"):
|
||||
def _mk(cls=attr):
|
||||
return lambda: cls()
|
||||
factory = _mk()
|
||||
break
|
||||
if not callable(factory):
|
||||
continue
|
||||
TYPE_MAP[processor_type] = factory
|
||||
produces_file = getattr(module, "produces_file", None)
|
||||
if produces_file is None and hasattr(factory(), "produces_file"):
|
||||
produces_file = getattr(factory(), "produces_file")
|
||||
if isinstance(schema, list):
|
||||
CONFIG_SCHEMAS[processor_type] = {
|
||||
"type": processor_type,
|
||||
"name": processor_name or processor_type,
|
||||
"supported_exts": supported_exts or [],
|
||||
"config_schema": schema,
|
||||
"produces_file": produces_file if produces_file is not None else False
|
||||
}
|
||||
|
||||
def get_config_schemas() -> Dict[str, dict]:
|
||||
return CONFIG_SCHEMAS
|
||||
|
||||
def get_config_schema(processor_type: str):
|
||||
return CONFIG_SCHEMAS.get(processor_type)
|
||||
|
||||
def get(processor_type: str) -> BaseProcessor:
|
||||
factory = TYPE_MAP.get(processor_type)
|
||||
if factory:
|
||||
return factory()
|
||||
return None
|
||||
|
||||
discover_processors()
|
||||
@@ -0,0 +1,90 @@
|
||||
from typing import Dict, Any
|
||||
from fastapi.responses import Response
|
||||
import base64
|
||||
from services.ai import describe_image_base64, get_text_embedding
|
||||
from services.vector_db import VectorDBService
|
||||
from services.logging import LogService
|
||||
|
||||
|
||||
class VectorIndexProcessor:
|
||||
name = "向量索引"
|
||||
supported_exts = ["jpg", "jpeg", "png", "bmp", "txt", "md"]
|
||||
config_schema = [
|
||||
{
|
||||
"key": "action", "label": "操作", "type": "select", "required": True, "default": "create",
|
||||
"options": [
|
||||
{"value": "create", "label": "创建索引"},
|
||||
{"value": "destroy", "label": "销毁索引"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "index_type", "label": "索引类型", "type": "select", "required": True, "default": "vector",
|
||||
"options": [
|
||||
{"value": "vector", "label": "向量索引"},
|
||||
{"value": "simple", "label": "普通索引"},
|
||||
]
|
||||
}
|
||||
]
|
||||
produces_file = False
|
||||
|
||||
async def process(self, input_bytes: bytes, path: str, config: Dict[str, Any]) -> Response:
|
||||
action = config.get("action", "create")
|
||||
index_type = config.get("index_type", "vector")
|
||||
vector_db = VectorDBService()
|
||||
collection_name = "vector_collection"
|
||||
if action == "destroy":
|
||||
vector_db.delete_vector(collection_name, path)
|
||||
await LogService.info(
|
||||
"processor:vector_index",
|
||||
f"Destroyed {index_type} index for {path}",
|
||||
details={"path": path, "action": "destroy", "index_type": index_type},
|
||||
)
|
||||
return Response(content=f"文件 {path} 的 {index_type} 索引已销毁", media_type="text/plain")
|
||||
|
||||
if index_type == 'simple':
|
||||
vector_db.ensure_collection(collection_name, vector=False)
|
||||
vector_db.upsert_vector(collection_name, {'path': path})
|
||||
await LogService.info(
|
||||
"processor:vector_index",
|
||||
f"Created simple index for {path}",
|
||||
details={"path": path, "action": "create", "index_type": "simple"},
|
||||
)
|
||||
return Response(content=f"文件 {path} 的普通索引已创建", media_type="text/plain")
|
||||
|
||||
file_ext = path.split('.')[-1].lower()
|
||||
description = ""
|
||||
embedding = None
|
||||
|
||||
if file_ext in ["jpg", "jpeg", "png", "bmp"]:
|
||||
base64_image = base64.b64encode(input_bytes).decode("utf-8")
|
||||
description = await describe_image_base64(base64_image)
|
||||
embedding = await get_text_embedding(description)
|
||||
log_message = f"Indexed image {path}"
|
||||
response_message = f"图片已索引,描述:{description}"
|
||||
elif file_ext in ["txt", "md"]:
|
||||
text = input_bytes.decode("utf-8")
|
||||
embedding = await get_text_embedding(text)
|
||||
description = text[:100] + "..." if len(text) > 100 else text
|
||||
log_message = f"Indexed text file {path}"
|
||||
response_message = f"文本文件已索引"
|
||||
|
||||
if embedding is None:
|
||||
return Response(content="不支持的文件类型", status_code=400)
|
||||
|
||||
vector_db.ensure_collection(collection_name, vector=True)
|
||||
vector_db.upsert_vector(
|
||||
collection_name, {'path': path, 'embedding': embedding})
|
||||
|
||||
await LogService.info(
|
||||
"processor:vector_index",
|
||||
log_message,
|
||||
details={"path": path, "description": description, "action": "create", "index_type": "vector"},
|
||||
)
|
||||
return Response(content=response_message, media_type="text/plain")
|
||||
|
||||
|
||||
PROCESSOR_TYPE = "vector_index"
|
||||
PROCESSOR_NAME = VectorIndexProcessor.name
|
||||
SUPPORTED_EXTS = VectorIndexProcessor.supported_exts
|
||||
CONFIG_SCHEMA = VectorIndexProcessor.config_schema
|
||||
def PROCESSOR_FACTORY(): return VectorIndexProcessor()
|
||||
Reference in New Issue
Block a user