- 新增操作历史记录,防止对同一笔记重复评论、点赞、收藏和回复
- 新增每日操作统计与限额管理,包含评论、点赞、收藏、发布和回复的独立上限
- 新增错误冷却机制,连续错误后自动暂停操作一段时间
- 新增运营时段控制,允许设置每日自动运营的开始和结束时间
- 新增收藏功能,支持一键收藏和定时自动收藏
- 新增随机人设池,提供25种预设小红书博主风格人设,支持随机切换
- 扩充主题池、风格池和评论关键词池,增加运营多样性
- 优化自动化调度器,显示下次执行时间和实时统计摘要
- 优化发布功能,增加本地备份机制,失败时保留文案和图片
🐛 fix(llm): 修复绘图提示词中的人物特征要求
- 在绘图提示词模板中明确要求人物必须是东亚面孔的中国人
- 添加具体的人物特征描述,如黑发、深棕色眼睛、精致五官等
- 禁止出现西方人或欧美人特征
- 调整整体画面风格偏向东方审美、清新淡雅和小红书风格
159 lines
5.2 KiB
Python
159 lines
5.2 KiB
Python
"""
|
|
Stable Diffusion 服务模块
|
|
封装对 SD WebUI API 的调用,支持 txt2img 和 img2img
|
|
"""
|
|
import requests
|
|
import base64
|
|
import io
|
|
import logging
|
|
from PIL import Image
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SD_TIMEOUT = 900 # 图片生成可能需要较长时间
|
|
|
|
# 默认反向提示词(针对 JuggernautXL / SDXL 优化,偏向东方审美)
|
|
DEFAULT_NEGATIVE = (
|
|
"nsfw, nudity, lowres, bad anatomy, bad hands, text, error, missing fingers, "
|
|
"extra digit, fewer digits, cropped, worst quality, low quality, normal quality, "
|
|
"jpeg artifacts, signature, watermark, blurry, deformed, mutated, disfigured, "
|
|
"ugly, duplicate, morbid, mutilated, poorly drawn face, poorly drawn hands, "
|
|
"extra limbs, fused fingers, too many fingers, long neck, username, "
|
|
"out of frame, distorted, oversaturated, underexposed, overexposed, "
|
|
"western face, european face, caucasian, deep-set eyes, high nose bridge, "
|
|
"blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair"
|
|
)
|
|
|
|
|
|
class SDService:
|
|
"""Stable Diffusion WebUI API 封装"""
|
|
|
|
def __init__(self, sd_url: str = "http://127.0.0.1:7860"):
|
|
self.sd_url = sd_url.rstrip("/")
|
|
|
|
def check_connection(self) -> tuple[bool, str]:
|
|
"""检查 SD 服务是否可用"""
|
|
try:
|
|
resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
|
|
if resp.status_code == 200:
|
|
count = len(resp.json())
|
|
return True, f"SD 已连接,{count} 个模型可用"
|
|
return False, f"SD 返回异常状态: {resp.status_code}"
|
|
except requests.exceptions.ConnectionError:
|
|
return False, "SD WebUI 未启动或端口错误"
|
|
except Exception as e:
|
|
return False, f"SD 连接失败: {e}"
|
|
|
|
def get_models(self) -> list[str]:
|
|
"""获取 SD 模型列表"""
|
|
resp = requests.get(f"{self.sd_url}/sdapi/v1/sd-models", timeout=5)
|
|
resp.raise_for_status()
|
|
return [m["title"] for m in resp.json()]
|
|
|
|
def switch_model(self, model_name: str):
|
|
"""切换 SD 模型"""
|
|
try:
|
|
requests.post(
|
|
f"{self.sd_url}/sdapi/v1/options",
|
|
json={"sd_model_checkpoint": model_name},
|
|
timeout=60,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("模型切换失败: %s", e)
|
|
|
|
def txt2img(
|
|
self,
|
|
prompt: str,
|
|
negative_prompt: str = DEFAULT_NEGATIVE,
|
|
model: str = None,
|
|
steps: int = 30,
|
|
cfg_scale: float = 5.0,
|
|
width: int = 832,
|
|
height: int = 1216,
|
|
batch_size: int = 2,
|
|
seed: int = -1,
|
|
sampler_name: str = "DPM++ 2M",
|
|
scheduler: str = "Karras",
|
|
) -> list[Image.Image]:
|
|
"""文生图(参数针对 JuggernautXL 优化)"""
|
|
if model:
|
|
self.switch_model(model)
|
|
|
|
payload = {
|
|
"prompt": prompt,
|
|
"negative_prompt": negative_prompt,
|
|
"steps": steps,
|
|
"cfg_scale": cfg_scale,
|
|
"width": width,
|
|
"height": height,
|
|
"batch_size": batch_size,
|
|
"seed": seed,
|
|
"sampler_name": sampler_name,
|
|
"scheduler": scheduler,
|
|
}
|
|
|
|
resp = requests.post(
|
|
f"{self.sd_url}/sdapi/v1/txt2img",
|
|
json=payload,
|
|
timeout=SD_TIMEOUT,
|
|
)
|
|
resp.raise_for_status()
|
|
|
|
images = []
|
|
for img_b64 in resp.json().get("images", []):
|
|
img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
|
|
images.append(img)
|
|
return images
|
|
|
|
def img2img(
|
|
self,
|
|
init_image: Image.Image,
|
|
prompt: str,
|
|
negative_prompt: str = DEFAULT_NEGATIVE,
|
|
denoising_strength: float = 0.5,
|
|
steps: int = 30,
|
|
cfg_scale: float = 5.0,
|
|
sampler_name: str = "DPM++ 2M",
|
|
scheduler: str = "Karras",
|
|
) -> list[Image.Image]:
|
|
"""图生图(参数针对 JuggernautXL 优化)"""
|
|
# 将 PIL Image 转为 base64
|
|
buf = io.BytesIO()
|
|
init_image.save(buf, format="PNG")
|
|
init_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
|
|
payload = {
|
|
"init_images": [init_b64],
|
|
"prompt": prompt,
|
|
"negative_prompt": negative_prompt,
|
|
"denoising_strength": denoising_strength,
|
|
"steps": steps,
|
|
"cfg_scale": cfg_scale,
|
|
"width": init_image.width,
|
|
"height": init_image.height,
|
|
"sampler_name": sampler_name,
|
|
"scheduler": scheduler,
|
|
}
|
|
|
|
resp = requests.post(
|
|
f"{self.sd_url}/sdapi/v1/img2img",
|
|
json=payload,
|
|
timeout=SD_TIMEOUT,
|
|
)
|
|
resp.raise_for_status()
|
|
|
|
images = []
|
|
for img_b64 in resp.json().get("images", []):
|
|
img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
|
|
images.append(img)
|
|
return images
|
|
|
|
def get_lora_models(self) -> list[str]:
|
|
"""获取可用的 LoRA 模型列表"""
|
|
try:
|
|
resp = requests.get(f"{self.sd_url}/sdapi/v1/loras", timeout=5)
|
|
resp.raise_for_status()
|
|
return [lora["name"] for lora in resp.json()]
|
|
except Exception:
|
|
return []
|