- 新增完整的自动化运营模块,包含一键评论、一键发布和定时调度功能 - 【一键评论】自动搜索高赞笔记,AI分析内容并生成个性化评论进行引流 - 【一键发布】随机选择主题和风格,AI生成文案,SD生成图片并自动发布到小红书 - 【定时调度】支持随机时间间隔的自动评论和发布,模拟真人操作节奏降低风险 - 新增自动化日志系统,实时记录操作状态和结果 - 在UI中新增“自动运营”标签页,提供完整的配置和操作界面 📝 docs(prompts): 优化SD提示词生成模板 - 更新文案生成提示词,适配JuggernautXL模型并优化质量描述 - 新增详细的质量词、光影、风格、构图和细节要求 - 移除括号权重语法,改为英文逗号分隔的描述方式 - 优化反向提示词,针对SDXL模型进行适配和增强 🔧 chore(config): 更新安全令牌配置 - 更新config.json中的xsec_token为新的安全令牌值 ⚡️ perf(sd): 优化Stable Diffusion服务参数 - 增加SD服务超时时间至900秒,适应高质量图片生成 - 优化文生图和图生图的默认参数,适配JuggernautXL模型 - 新增采样器和调度器参数配置,提升图片生成质量 - 优化默认反向提示词,针对SDXL模型进行专门优化
157 lines
5.0 KiB
Python
157 lines
5.0 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"
|
|
)
|
|
|
|
|
|
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 []
|