xhs_factory/sd_service.py
zhoujie b5deafa2cc feat(config): 更新模型配置与LLM提示词指南
- 将默认LLM模型从gemini-2.0-flash升级为gemini-3-flash-preview
- 将博主人设从"性感福利主播"更改为"二次元coser"
- 优化LLM生成SD提示词的指南,新增中国审美人物描述规则
- 为各SD模型添加颜值核心词、示范prompt和禁止使用的关键词
- 新增三维人物描述法(眼睛/肤色/气质)和专属光线词指导

📦 build(openspec): 归档旧规范并创建新规范

- 将improve-maintainability规范归档至2026-02-25目录
- 新增2026-02-26-improve-ui-layout规范,包含UI布局优化设计
- 新增2026-02-26-optimize-image-generation规范,包含图片生成优化设计
- 在根目录openspec/specs下新增图片质量、后处理、中国审美和LLM提示词规范

♻️ refactor(sd_service): 优化SD模型配置和图片后处理

- 为各SD模型添加中国审美特征词和欧美面孔排除词
- 新增高画质预设档,SDXL模型启用Hires Fix参数
- 将后处理拆分为beauty_enhance和anti_detect_postprocess两个独立函数
- 新增美化增强功能,支持通过enhance_level参数控制强度

♻️ refactor(services): 更新内容生成服务以支持美化增强

- 在generate_images函数中新增enhance_level参数
- 将美化强度参数传递至SDService.txt2img调用

♻️ refactor(ui): 优化UI布局和添加美化强度控件

- 注入自定义CSS主题层,优化字体、按钮和卡片样式
- 将全局设置迁移至独立的"⚙️ 配置"Tab,优化Tab顺序
- 在内容创作Tab的高级设置中添加美化强度滑块控件
- 优化自动运营Tab布局,改为2列卡片网格展示
2026-02-26 22:58:05 +08:00

1025 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Stable Diffusion 服务模块
封装对 SD WebUI API 的调用,支持 txt2img 和 img2img支持 ReActor 换脸
含图片反 AI 检测后处理管线
"""
import requests
import base64
import io
import logging
import os
import random
import math
import struct
import zlib
from PIL import Image, ImageFilter, ImageEnhance
logger = logging.getLogger(__name__)
SD_TIMEOUT = 1800 # 图片生成可能需要较长时间
# 头像文件默认保存路径
FACE_IMAGE_PATH = os.path.join(os.path.dirname(__file__), "my_face.png")
# ==================== 多模型配置系统 ====================
# 每个模型的最优参数、prompt 增强词、负面提示词、三档预设
SD_MODEL_PROFILES = {
# ---- majicmixRealistic: 东亚网红感,朋友圈自拍/美妆/穿搭 (SD 1.5) ----
"majicmixRealistic": {
"display_name": "majicmixRealistic ⭐⭐⭐⭐⭐",
"description": "东亚网红感 | 朋友圈自拍、美妆、穿搭",
"arch": "sd15", # SD 1.5 架构
"clip_skip": 2, # majicmix 系模型在 clip_skip=2 时人脸颜值更高
# 自动追加到 prompt 前面的增强词
"prompt_prefix": (
"(best quality:1.4), (masterpiece:1.4), (ultra detailed:1.3), "
"(photorealistic:1.4), (realistic:1.3), raw photo, "
"(asian girl:1.3), (chinese:1.2), (east asian features:1.2), "
"(almond eyes:1.3), (bright sparkling eyes:1.2), (long lashes:1.1), "
"(porcelain skin:1.2), (luminous fair skin:1.2), (dewy glowing skin:1.2), "
"(delicate facial features:1.3), (youthful appearance:1.1), "
"(natural makeup:1.3), (glossy lips:1.2), (rosy cheeks:1.1), "
"(soft smile:1.2), (charming expression:1.1), (soft lighting:1.1), "
),
# 自动追加到 prompt 后面的补充词
"prompt_suffix": (
", film grain, shallow depth of field, "
"instagram aesthetic, xiaohongshu style, phone camera feel"
),
"negative_prompt": (
"(nsfw:1.5), (nudity:1.5), (worst quality:2), (low quality:2), (normal quality:2), "
"lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, "
"blurry, deformed, mutated, disfigured, ugly, duplicate, "
"poorly drawn face, poorly drawn hands, extra limbs, fused fingers, "
"too many fingers, long neck, out of frame, "
"western face, european face, caucasian, deep-set eyes, high nose bridge, "
"strong jawline, prominent brow ridge, angular facial structure, square jaw, heavy brow, "
"blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair, "
"painting, cartoon, anime, sketch, illustration, 3d render"
),
"presets": {
"快速 (约30秒)": {
"steps": 20,
"cfg_scale": 7.0,
"width": 512,
"height": 768,
"sampler_name": "Euler a",
"scheduler": "Normal",
"batch_size": 2,
},
"标准 (约1分钟)": {
"steps": 30,
"cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和
"width": 512,
"height": 768,
"sampler_name": "DPM++ 2M",
"scheduler": "Karras",
"batch_size": 2,
},
"精细 (约2-3分钟)": {
"steps": 40,
"cfg_scale": 6.5,
"width": 576,
"height": 864,
"sampler_name": "DPM++ SDE",
"scheduler": "Karras",
"batch_size": 2,
},
"高画质 (约5分钟)": {
"steps": 50,
"cfg_scale": 6.5,
"width": 640,
"height": 960,
"sampler_name": "DPM++ SDE",
"scheduler": "Karras",
"batch_size": 1,
},
},
},
# ---- Realistic Vision: 写实摄影感,纪实摄影/街拍/真实质感 (SD 1.5) ----
"realisticVision": {
"display_name": "Realistic Vision ⭐⭐⭐⭐",
"description": "写实摄影感 | 纪实摄影、街拍、真实质感",
"arch": "sd15",
"clip_skip": 2, # RV 系列在 clip_skip=2 时人脸细节更自然
"prompt_prefix": (
"RAW photo, (best quality:1.4), (masterpiece:1.3), (realistic:1.4), "
"(photorealistic:1.4), 8k uhd, DSLR, high quality, "
"(asian:1.2), (chinese girl:1.2), (east asian features:1.1), "
"(almond eyes:1.2), (bright clear eyes:1.2), (long lashes:1.1), "
"(luminous fair skin:1.2), (dewy smooth skin:1.2), (refined features:1.2), "
"(elegant temperament:1.1), (natural makeup:1.2), (glossy lips:1.1), "
"(soft smile:1.1), (charming gaze:1.1), "
),
"prompt_suffix": (
", shot on Canon EOS R5, 85mm lens, f/1.8, "
"natural lighting, documentary style, street photography, "
"film color grading, depth of field"
),
"negative_prompt": (
"(nsfw:1.5), (nudity:1.5), (worst quality:2), (low quality:2), (normal quality:2), "
"lowres, bad anatomy, bad hands, text, error, missing fingers, "
"extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, "
"blurry, deformed, mutated, disfigured, ugly, duplicate, "
"poorly drawn face, extra limbs, fused fingers, long neck, "
"western face, european face, caucasian, deep-set eyes, "
"strong jawline, prominent brow ridge, angular facial structure, square jaw, heavy brow, "
"blonde hair, blue eyes, green eyes, freckles, "
"painting, cartoon, anime, sketch, illustration, 3d render, "
"over-sharpened, over-saturated, plastic skin, airbrushed, "
"doll-like, HDR, overprocessed, "
"plain face, no makeup, unattractive, dull expression, blank stare, flat lighting"
),
"presets": {
"快速 (约30秒)": {
"steps": 20,
"cfg_scale": 7.0,
"width": 512,
"height": 768,
"sampler_name": "Euler a",
"scheduler": "Normal",
"batch_size": 2,
},
"标准 (约1分钟)": {
"steps": 28,
"cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和
"width": 512,
"height": 768,
"sampler_name": "DPM++ 2M",
"scheduler": "Karras",
"batch_size": 2,
},
"精细 (约2-3分钟)": {
"steps": 40,
"cfg_scale": 6.5,
"width": 576,
"height": 864,
"sampler_name": "DPM++ SDE",
"scheduler": "Karras",
"batch_size": 2,
},
"高画质 (约5分钟)": {
"steps": 50,
"cfg_scale": 6.5,
"width": 640,
"height": 960,
"sampler_name": "DPM++ SDE",
"scheduler": "Karras",
"batch_size": 1,
},
},
},
# ---- Juggernaut XL: 电影大片感,高画质/商业摄影/复杂背景 (SDXL) ----
"juggernautXL": {
"display_name": "Juggernaut XL ⭐⭐⭐⭐",
"description": "电影大片感 | 高画质、商业摄影、复杂背景",
"arch": "sdxl", # SDXL 架构
"clip_skip": 1, # SDXL 使用完整 CLIP 编码
"prompt_prefix": (
"masterpiece, best quality, ultra detailed, 8k uhd, high resolution, "
"photorealistic, cinematic lighting, cinematic composition, "
"(chinese beauty:1.3), east asian features, black hair, dark brown eyes, "
"(almond eyes:1.3), (bright sparkling eyes:1.2), (long lashes:1.1), "
"(porcelain skin:1.2), (luminous dewy skin:1.2), (delicate facial features:1.3), "
"(youthful appearance:1.1), slim figure, "
"natural makeup, (glossy lips:1.1), (rosy cheeks:1.1), "
"(gentle smile:1.2), (charming expression:1.1), "
),
"prompt_suffix": (
", cinematic color grading, anamorphic lens, bokeh, "
"volumetric lighting, ray tracing, global illumination, "
"commercial photography, editorial style, vogue aesthetic"
),
"negative_prompt": (
"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, "
"strong jawline, prominent brow ridge, angular facial structure, square jaw, heavy brow, "
"blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair"
),
"presets": {
"快速 (约30秒)": {
"steps": 12,
"cfg_scale": 5.0,
"width": 768,
"height": 1024,
"sampler_name": "Euler a",
"scheduler": "Normal",
"batch_size": 2,
},
"标准 (约1分钟)": {
"steps": 20,
"cfg_scale": 5.5,
"width": 832,
"height": 1216,
"sampler_name": "DPM++ 2M",
"scheduler": "Karras",
"batch_size": 2,
},
"精细 (约2-3分钟)": {
"steps": 35,
"cfg_scale": 6.0,
"width": 832,
"height": 1216,
"sampler_name": "DPM++ 2M SDE",
"scheduler": "Karras",
"batch_size": 2,
},
"高画质 (约5分钟)": {
"steps": 40,
"cfg_scale": 6.0,
"width": 832,
"height": 1216,
"sampler_name": "DPM++ 2M SDE",
"scheduler": "Karras",
"batch_size": 1,
"enable_hr": True,
"hr_scale": 1.5,
"hr_upscaler": "4x-UltraSharp",
"hr_second_pass_steps": 20,
"denoising_strength": 0.4,
},
},
},
}
# ==================== 人设 SD 视觉配置 ====================
# 每个人设对应一组 SD prompt 增强词 + 视觉风格 + LLM 绘图指导
# key 匹配规则: persona 文本中包含 key 即命中
PERSONA_SD_PROFILES = {
# ---- 赛博AI虚拟博主 ----
"赛博AI虚拟博主": {
# 追加到 SD prompt 前面的人设特有增强词
"prompt_boost": (
"(perfect face:1.3), (extremely detailed face:1.3), (beautiful detailed eyes:1.3), "
"(flawless skin:1.2), (ultra high resolution face:1.2), (glossy lips:1.1), "
"(futuristic chinese beauty:1.2), "
),
# 追加到 SD prompt 后面的风格词
"prompt_style": (
", cinematic portrait, fashion editorial, vibrant colors, "
"dramatic lighting, creative background, fantasy environment, "
"instagram influencer, trending on artstation"
),
# 额外追加到负面提示词
"negative_extra": "",
# LLM 绘图指导 (追加到 sd_prompt_guide)
"llm_guide": (
"\n\n【人设视觉风格 - 赛博AI虚拟博主】\n"
"你在为一个大方承认自己是AI的虚拟博主生成图片主打高颜值+场景奇观:\n"
"- 五官极致精致:重点描述 detailed eyes, long eyelashes, glossy lips, perfect skin\n"
"- 场景大胆奇幻:巴黎埃菲尔铁塔前、东京霓虹街头、赛博朋克城市、外太空空间站、水下宫殿、樱花隧道等\n"
"- 光效华丽neon lights, cyberpunk glow, holographic, lens flare, volumetric fog\n"
"- 穿搭多变:可大胆使用 futuristic outfit, holographic dress, cyberpunk jacket, maid outfit, school uniform, wedding dress 等\n"
"- 构图要有视觉冲击力close-up portrait, dynamic angle, full body shot, looking at viewer\n"
"- 整体风格:超现实+高颜值,不需要追求真实感,要追求视觉震撼\n"
),
},
# ---- 性感福利主播 ----
"性感福利主播": {
"prompt_boost": (
"(beautiful detailed face:1.3), (charming eyes:1.2), (glossy lips:1.2), "
"(elegant posture:1.2), (slender figure:1.1), "
"(glamour photography:1.2), (delicate asian features:1.1), "
),
"prompt_style": (
", soft glamour lighting, beauty retouching, "
"warm golden tones, shallow depth of field, "
"chic fashion style, confident pose"
),
"negative_extra": (
"lingerie, underwear, bikini, nude, revealing clothing, "
"cleavage, nsfw, explicit, suggestive"
),
"llm_guide": (
"\n\n【人设视觉风格 - 时尚魅力博主】\n"
"为一个时尚魅力博主生成图片,主打精致感+时尚感,严禁擦边内容:\n"
"- 穿搭关键elegant dress, chic outfit, stylish fashion, trendy clothes, "
"off-shoulder top (不露胸), midi skirt, fitted blazer\n"
"- 光线氛围soft warm lighting, golden hour, window light, beauty lighting\n"
"- 场景选择city street, cafe, rooftop, shopping district, fashion studio\n"
"- 构图要点half body or full body shot, elegant pose, confident expression, "
"fashion editorial style\n"
"- 整体风格:高端时尚杂志风,精致大方,绝对禁止 bikini/lingerie/revealing/nsfw 等词\n"
),
},
# ---- 身材管理健身美女 ----
"身材管理健身美女": {
"prompt_boost": (
"(fit body:1.3), (athletic physique:1.2), (toned muscles:1.2), "
"(healthy glow:1.2), (confident pose:1.2), "
"(healthy luminous skin:1.2), (delicate east asian face:1.1), "
),
"prompt_style": (
", fitness photography, gym environment, athletic wear, "
"dynamic pose, energetic mood, healthy lifestyle, "
"motivational, natural sweat, workout aesthetic"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 身材管理健身美女】\n"
"为一个健身达人生成图片,主打健康美+运动感:\n"
"- 身材描述fit body, toned abs, lean muscles, athletic build, healthy skin\n"
"- 穿搭关键sports bra, yoga pants, running outfit, gym wear, crop top\n"
"- 场景选择gym, yoga studio, outdoor running, home workout, mirror selfie at gym\n"
"- 动作姿势workout pose, stretching, running, yoga pose, flexing, plank\n"
"- 光线gym lighting, natural daylight, morning sun, energetic bright tones\n"
"- 整体风格:充满活力的运动健身风,展现自律和力量美\n"
),
},
# ---- 温柔知性的时尚博主 ----
"温柔知性时尚博主": {
"prompt_boost": (
"(elegant:1.2), (gentle expression:1.2), (sophisticated:1.2), "
"(fashion forward:1.2), (graceful:1.1), "
"(chinese elegance:1.2), (refined temperament:1.2), (porcelain skin:1.1), "
),
"prompt_style": (
", fashion editorial, street style photography, "
"chic outfit, elegant pose, soft natural tones, "
"magazine cover quality, lifestyle photography"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 温柔知性时尚博主】\n"
"为一个知性优雅的时尚博主生成图片,主打高级感穿搭:\n"
"- 气质描述elegant, gentle smile, sophisticated, poised, graceful\n"
"- 穿搭关键french style, minimalist outfit, trench coat, silk blouse, midi skirt, neutral colors\n"
"- 场景选择cafe, art gallery, tree-lined street, bookstore, european architecture\n"
"- 构图three-quarter shot, walking pose, looking away candidly, holding coffee\n"
"- 色调warm neutral tones, soft creamy, muted colors, film aesthetic\n"
"- 整体风格:法式优雅+知性温柔,像时尚杂志的生活方式大片\n"
),
},
# ---- 文艺青年摄影师 ----
"文艺青年摄影师": {
"prompt_boost": (
"(artistic:1.2), (moody atmosphere:1.2), (film grain:1.2), "
"(vintage tones:1.1), "
"(delicate features:1.1), (gentle east asian face:1.1), "
),
"prompt_style": (
", film photography, 35mm film, kodak portra 400, "
"vintage color grading, indie aesthetic, dreamy atmosphere, "
"golden hour, nostalgic mood"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 文艺青年摄影师】\n"
"为文艺摄影师生成图片,主打胶片感+故事性:\n"
"- 风格关键film grain, vintage tones, kodak portra, dreamy, nostalgic\n"
"- 场景选择old streets, abandoned places, cafe corner, train station, seaside, flower field\n"
"- 光线golden hour, window light, overcast soft light, dappled sunlight\n"
"- 构图off-center composition, back view, silhouette, reflection\n"
"- 色调warm vintage, faded colors, low contrast, film color grading\n"
"- 整体风格:独立电影画面感,有故事性的文艺氛围\n"
),
},
# ---- 二次元coser ----
"二次元coser": {
"prompt_boost": (
"(cosplay:1.3), (detailed costume:1.2), (colorful:1.2), "
"(anime inspired:1.1), (vibrant:1.2), "
"(bright clear skin:1.1), (big bright eyes:1.2), (youthful:1.1), "
),
"prompt_style": (
", cosplay photography, anime convention, colorful costume, "
"dynamic pose, vibrant colors, fantasy setting, "
"dramatic lighting, character portrayal"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 二次元coser】\n"
"为cos博主生成图片主打二次元还原+视觉冲击:\n"
"- 风格关键cosplay, detailed costume, colorful wig, contact lenses, anime style\n"
"- 场景选择anime convention, fantasy landscape, school rooftop, cherry blossoms, studio backdrop\n"
"- 动作姿势character pose, dynamic action, cute pose, peace sign, holding prop\n"
"- 光效colored lighting, rim light, sparkle effects, dramatic shadows\n"
"- 色调vibrant saturated, anime color palette, high contrast\n"
"- 整体风格真人cos感兼具二次元的鲜艳感和真实摄影的质感\n"
),
},
# ---- 汉服爱好者 ----
"汉服爱好者": {
"prompt_boost": (
"(traditional chinese dress:1.3), (hanfu:1.3), (chinese aesthetic:1.2), "
"(elegant traditional:1.2), (delicate accessories:1.1), "
"(classical chinese beauty:1.3), (ethereal temperament:1.2), "
),
"prompt_style": (
", traditional chinese photography, han dynasty style, "
"ink painting aesthetic, bamboo forest, ancient architecture, "
"flowing silk fabric, classical beauty, ethereal atmosphere"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 汉服爱好者】\n"
"为国风汉服博主生成图片,主打古典美+中国风:\n"
"- 服饰描述hanfu, flowing silk robes, wide sleeves, hair accessories, jade earrings, fan\n"
"- 场景选择bamboo forest, ancient temple, moon gate, lotus pond, plum blossom, mountain mist\n"
"- 光线soft diffused light, misty atmosphere, morning fog, moonlight, lantern glow\n"
"- 构图full body flowing fabric, profile view, looking down gently, holding umbrella\n"
"- 色调muted earth tones, ink wash style, red and white contrast, jade green\n"
"- 整体风格:仙气飘飘的古风摄影,有水墨画的意境\n"
),
},
# ---- 独居女孩 ----
"独居女孩": {
"prompt_boost": (
"(cozy atmosphere:1.3), (warm lighting:1.2), (homey:1.2), "
"(casual style:1.1), (relaxed:1.1), "
"(fresh natural beauty:1.1), (natural bare face:1.1), "
),
"prompt_style": (
", cozy home photography, warm ambient light, "
"casual indoor style, hygge aesthetic, "
"soft blanket, candle light, peaceful morning"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 独居女孩】\n"
"为独居生活博主生成图片,主打温馨氛围感+仪式感:\n"
"- 氛围关键cozy, warm, hygge, peaceful, intimate, ambient candlelight\n"
"- 场景选择small apartment, kitchen cooking, bathtub, reading by window, balcony garden\n"
"- 穿搭关键oversized sweater, pajamas, casual homewear, messy bun\n"
"- 光线warm lamp light, candle glow, morning window light, fairy lights\n"
"- 道具coffee mug, book, cat, houseplant, scented candle, blanket\n"
"- 整体风格:温暖治愈的独居日常,有仪式感的精致生活\n"
),
},
# ---- 资深美妆博主 ----
"资深美妆博主": {
"prompt_boost": (
"(flawless makeup:1.3), (detailed eye makeup:1.3), (beauty close-up:1.2), "
"(perfect skin:1.2), (beauty lighting:1.2), "
"(luminous porcelain skin:1.2), (delicate east asian features:1.1), "
),
"prompt_style": (
", beauty photography, ring light, macro lens, "
"studio beauty lighting, makeup tutorial style, "
"dewy skin, perfect complexion, vibrant lip color"
),
"negative_extra": "",
"llm_guide": (
"\n\n【人设视觉风格 - 资深美妆博主】\n"
"为美妆博主生成图片,主打妆容特写+产品展示:\n"
"- 妆容描述detailed eye shadow, winged eyeliner, glossy lips, dewy foundation, blush\n"
"- 构图要点face close-up, half face, eye close-up, lip close-up, before and after\n"
"- 场景选择vanity desk, bathroom mirror, ring light studio, flat lay of products\n"
"- 光线ring light, beauty dish, soft diffused studio light, bright even lighting\n"
"- 色调clean bright, pink tones, neutral with pops of color\n"
"- 整体风格:专业美妆教程感,妆容细节清晰可见\n"
),
},
}
def get_persona_sd_profile(persona_text: str) -> dict | None:
"""根据人设文本匹配 SD 视觉配置,返回 profile dict 或 None"""
if not persona_text:
return None
for key, profile in PERSONA_SD_PROFILES.items():
if key in persona_text:
return profile
return None
# 默认配置 profile key
DEFAULT_MODEL_PROFILE = "juggernautXL"
def detect_model_profile(model_name: str) -> str:
"""根据 SD 模型名称自动识别对应的 profile key"""
name_lower = model_name.lower() if model_name else ""
if "majicmix" in name_lower or "majic" in name_lower:
return "majicmixRealistic"
elif "realistic" in name_lower and "vision" in name_lower:
return "realisticVision"
elif "rv" in name_lower and ("v5" in name_lower or "v6" in name_lower or "v4" in name_lower):
return "realisticVision" # RV v5.1 等简写
elif "juggernaut" in name_lower or "jugger" in name_lower:
return "juggernautXL"
# 根据架构猜测
elif "xl" in name_lower or "sdxl" in name_lower:
return "juggernautXL" # SDXL 架构默认用 Juggernaut 参数
else:
return DEFAULT_MODEL_PROFILE # 无法识别时默认
def get_model_profile(model_name: str = None) -> dict:
"""获取模型配置 profile"""
key = detect_model_profile(model_name) if model_name else DEFAULT_MODEL_PROFILE
return SD_MODEL_PROFILES.get(key, SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE])
def get_model_profile_info(model_name: str = None) -> str:
"""获取当前模型的显示信息 (Markdown 格式)"""
profile = get_model_profile(model_name)
key = detect_model_profile(model_name) if model_name else DEFAULT_MODEL_PROFILE
is_default = key == DEFAULT_MODEL_PROFILE and model_name and detect_model_profile(model_name) == DEFAULT_MODEL_PROFILE
# 如果检测结果是默认回退的, 说明是未知模型
actual_key = detect_model_profile(model_name) if model_name else None
presets = profile["presets"]
first_preset = list(presets.values())[0]
res = f"{first_preset.get('width', '?')}×{first_preset.get('height', '?')}"
lines = [
f"**🎨 {profile['display_name']}** | `{profile['arch'].upper()}` | {res}",
f"> {profile['description']}",
]
if model_name and not any(k in (model_name or "").lower() for k in ["majicmix", "realistic", "juggernaut"]):
lines.append(f"> ⚠️ 未识别的模型,使用默认档案 ({profile['display_name']})")
return "\n".join(lines)
# ==================== 兼容旧接口 ====================
# 默认预设和反向提示词 (使用 Juggernaut XL 作为默认)
SD_PRESETS = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["presets"]
SD_PRESET_NAMES = list(SD_PRESETS.keys())
def get_sd_preset(name: str, model_name: str = None) -> dict:
"""获取生成预设参数,自动适配模型"""
profile = get_model_profile(model_name)
presets = profile.get("presets", SD_PRESETS)
return presets.get(name, presets.get("标准 (约1分钟)", list(presets.values())[0]))
# 默认反向提示词Juggernaut XL
DEFAULT_NEGATIVE = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["negative_prompt"]
# ==================== 图片反 AI 检测管线 ====================
def beauty_enhance(img: Image.Image, level: float = 1.0) -> Image.Image:
"""对生成图片进行美化增强,强调中国审美的细腻肤色与精致感
处理流程:
1. 锐化 (USM 算法,使五官更清晰)
2. 亮度 / 对比度 / 饱和度微增 (提升通透感)
3. 暖白皮肤校色 (适度降红提蓝,模拟东亚偏白瓷感)
Args:
img: PIL Image (RGB)
level: 增强强度0=关闭1.0=默认2.0=强化
Returns:
处理后的 PIL Image
"""
if level <= 0:
return img
if img.mode != "RGB":
img = img.convert("RGB")
# Step 1: 非锐化蒙版(让五官、眼睛更清晰)
radius = max(0.1, 1.5 * level)
percent = int(120 * level)
img = img.filter(ImageFilter.UnsharpMask(radius=radius, percent=percent, threshold=2))
# Step 2: 亮度 / 对比度 / 饱和度微增(提升通透感)
img = ImageEnhance.Brightness(img).enhance(1.0 + 0.02 * level)
img = ImageEnhance.Contrast(img).enhance(1.0 + 0.02 * level)
img = ImageEnhance.Color(img).enhance(1.0 + 0.05 * level)
# Step 3: 暖白皮肤校色(适度降红提蓝,模拟东亚白瓷感)
try:
import numpy as np
arr = np.array(img, dtype=np.float32)
r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]
# 皮肤色调区域:偏暖肤色 (R>G>B, 亮度适中)
skin_mask = (r > 140) & (g > 90) & (b > 70) & (r > g) & (g > b)
correction = 0.015 * level
arr[:, :, 0] = np.where(skin_mask, np.clip(r - r * correction, 0, 255), r)
arr[:, :, 2] = np.where(skin_mask, np.clip(b + b * correction * 0.5, 0, 255), b)
img = Image.fromarray(arr.astype(np.uint8))
except ImportError:
logger.debug("numpy 未安装,跳过皮肤校色步骤")
logger.debug("✨ beauty_enhance 完成 (level=%.1f)", level)
return img
def anti_detect_postprocess(img: Image.Image) -> Image.Image:
"""对 AI 生成的图片进行后处理,模拟手机拍摄/加工特征,降低 AI 检测率
处理流程:
1. 剥离所有元数据 (EXIF/SD参数/PNG chunks)
2. 微小随机裁剪 (模拟手机截图不完美)
3. 微小旋转+校正 (破坏像素完美对齐)
4. 色彩微扰 (模拟手机屏幕色差)
5. 不均匀高斯噪声 (模拟传感器噪声)
6. 微小锐化 (模拟手机锐化算法)
7. JPEG 压缩回环 (最关键: 引入真实压缩伪影)
"""
if img.mode != "RGB":
img = img.convert("RGB")
w, h = img.size
# Step 1: 微小随机裁剪 (1-3 像素, 破坏边界对齐)
crop_l = random.randint(0, 3)
crop_t = random.randint(0, 3)
crop_r = random.randint(0, 3)
crop_b = random.randint(0, 3)
if crop_l + crop_r < w and crop_t + crop_b < h:
img = img.crop((crop_l, crop_t, w - crop_r, h - crop_b))
# Step 2: 极微旋转 (0.1°-0.5°, 破坏完美像素排列)
if random.random() < 0.6:
angle = random.uniform(-0.5, 0.5)
img = img.rotate(angle, resample=Image.BICUBIC, expand=False,
fillcolor=(
random.randint(240, 255),
random.randint(240, 255),
random.randint(240, 255),
))
# Step 3: 色彩微扰 (模拟手机屏幕/相机色差)
# 亮度微调
brightness_factor = random.uniform(0.97, 1.03)
img = ImageEnhance.Brightness(img).enhance(brightness_factor)
# 对比度微调
contrast_factor = random.uniform(0.97, 1.03)
img = ImageEnhance.Contrast(img).enhance(contrast_factor)
# 饱和度微调
saturation_factor = random.uniform(0.96, 1.04)
img = ImageEnhance.Color(img).enhance(saturation_factor)
# Step 4: 不均匀传感器噪声 (比均匀噪声更像真实相机)
try:
import numpy as np
arr = np.array(img, dtype=np.float32)
# 生成不均匀噪声: 中心弱边缘强 (模拟暗角)
h_arr, w_arr = arr.shape[:2]
y_grid, x_grid = np.mgrid[0:h_arr, 0:w_arr]
center_y, center_x = h_arr / 2, w_arr / 2
dist = np.sqrt((y_grid - center_y) ** 2 + (x_grid - center_x) ** 2)
max_dist = np.sqrt(center_y ** 2 + center_x ** 2)
# 噪声强度: 中心 1.0, 边缘 2.5
noise_strength = 1.0 + 1.5 * (dist / max_dist)
noise_strength = noise_strength[:, :, np.newaxis]
# 高斯噪声
noise = np.random.normal(0, random.uniform(1.5, 3.0), arr.shape) * noise_strength
arr = np.clip(arr + noise, 0, 255).astype(np.uint8)
img = Image.fromarray(arr)
except ImportError:
# numpy 不可用时用 PIL 的简单模糊代替
pass
# Step 5: 轻微锐化 (模拟手机后处理)
if random.random() < 0.5:
img = img.filter(ImageFilter.SHARPEN)
# 再做一次轻微模糊中和, 避免过度锐化
img = img.filter(ImageFilter.GaussianBlur(radius=0.3))
# Step 6: JPEG 压缩回环 (最关键! 引入真实压缩伪影)
# 模拟: 手机保存 → 社交平台压缩 → 重新上传
quality = random.randint(85, 93) # 质量略低于完美, 像手机存储
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality, subsampling=0)
buf.seek(0)
img = Image.open(buf).copy() # 重新加载, 已包含 JPEG 伪影
# Step 7: resize 回原始尺寸附近 (模拟平台缩放)
# 微小缩放 ±2%
scale = random.uniform(0.98, 1.02)
new_w = int(img.width * scale)
new_h = int(img.height * scale)
if new_w > 100 and new_h > 100:
img = img.resize((new_w, new_h), Image.LANCZOS)
logger.info("🛡️ 图片反检测后处理完成: crop=%dx%d%dx%d, jpeg_q=%d, scale=%.2f",
w, h, img.width, img.height, quality, scale)
return img
def strip_metadata(img: Image.Image) -> Image.Image:
"""彻底剥离图片所有元数据 (EXIF, SD参数, PNG text chunks)"""
clean = Image.new(img.mode, img.size)
clean.putdata(list(img.getdata()))
return clean
class SDService:
"""Stable Diffusion WebUI API 封装"""
def __init__(self, sd_url: str = "http://127.0.0.1:7860"):
self.sd_url = sd_url.rstrip("/")
# ---------- 工具方法 ----------
@staticmethod
def _image_to_base64(img: Image.Image) -> str:
"""PIL Image → base64 字符串"""
buf = io.BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
@staticmethod
def load_face_image(path: str = None) -> Image.Image | None:
"""加载头像图片,不存在则返回 None"""
path = path or FACE_IMAGE_PATH
if path and os.path.isfile(path):
try:
return Image.open(path).convert("RGB")
except Exception as e:
logger.warning("头像加载失败: %s", e)
return None
@staticmethod
def save_face_image(img: Image.Image, path: str = None) -> str:
"""保存头像图片,返回保存路径"""
path = path or FACE_IMAGE_PATH
img = img.convert("RGB")
img.save(path, format="PNG")
logger.info("头像已保存: %s", path)
return path
def _build_reactor_args(self, face_image: Image.Image) -> dict:
"""构建 ReActor 换脸参数alwayson_scripts 格式)
参数索引对照 (reactor script-info):
0: source_image (base64) 1: enable 2: source_faces
3: target_faces 4: model 5: restore_face
6: restore_visibility 7: restore_first 8: upscaler
9: scale 10: upscaler_vis 11: swap_in_source
12: swap_in_generated 13: log_level 14: gender_source
15: gender_target 16: save_original 17: codeformer_weight
18: source_hash_check 19: target_hash_check 20: exec_provider
21: face_mask_correction 22: select_source 23: face_model
24: source_folder 25: multiple_sources 26: random_image
27: force_upscale 28: threshold 29: max_faces
30: tab_single
"""
face_b64 = self._image_to_base64(face_image)
return {
"reactor": {
"args": [
face_b64, # 0: source image (base64)
True, # 1: enable ReActor
"0", # 2: source face index
"0", # 3: target face index
"inswapper_128.onnx", # 4: swap model
"CodeFormer", # 5: restore face method
1, # 6: restore face visibility
True, # 7: restore face first, then upscale
"None", # 8: upscaler
1, # 9: scale
1, # 10: upscaler visibility
False, # 11: swap in source
True, # 12: swap in generated
1, # 13: console log level (0=min, 1=med, 2=max)
0, # 14: gender detection source (0=No)
0, # 15: gender detection target (0=No)
False, # 16: save original
0.8, # 17: CodeFormer weight (0=max effect, 1=min)
False, # 18: source hash check
False, # 19: target hash check
"CUDA", # 20: execution provider
True, # 21: face mask correction
0, # 22: select source (0=Image, 1=FaceModel, 2=Folder)
"", # 23: face model filename (when #22=1)
"", # 24: source folder path (when #22=2)
None, # 25: skip for API
False, # 26: random image
False, # 27: force upscale
0.6, # 28: face detection threshold
2, # 29: max faces to detect (0=unlimited)
],
}
}
def has_reactor(self) -> bool:
"""检查 SD WebUI 是否安装了 ReActor 扩展"""
try:
resp = requests.get(f"{self.sd_url}/sdapi/v1/scripts", timeout=5)
scripts = resp.json()
all_scripts = scripts.get("txt2img", []) + scripts.get("img2img", [])
return any("reactor" in s.lower() for s in all_scripts)
except Exception:
return False
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 = None,
model: str = None,
steps: int = None,
cfg_scale: float = None,
width: int = None,
height: int = None,
batch_size: int = None,
seed: int = -1,
sampler_name: str = None,
scheduler: str = None,
face_image: Image.Image = None,
quality_mode: str = None,
persona: str = None,
enhance_level: float = 1.0,
) -> list[Image.Image]:
"""文生图(自动适配当前 SD 模型 + 人设的最优参数)
Args:
model: SD 模型名,自动识别并应用对应配置
face_image: 头像 PIL Image传入后自动启用 ReActor 换脸
quality_mode: 预设模式名
persona: 博主人设文本,自动注入人设视觉增强词
enhance_level: 美化增强强度 (0=关闭, 1.0=默认, 2.0=强化)
"""
if model:
self.switch_model(model)
# 自动识别模型配置
profile = get_model_profile(model)
profile_key = detect_model_profile(model)
logger.info("🎯 SD 模型识别: %s%s (%s)",
model or "默认", profile_key, profile["description"])
# 加载模型专属预设参数
preset = get_sd_preset(quality_mode, model) if quality_mode else get_sd_preset("标准 (约1分钟)", model)
# 自动增强 prompt: 人设增强 + 模型前缀 + 原始 prompt + 模型后缀 + 人设风格
persona_sd = get_persona_sd_profile(persona)
persona_boost = persona_sd.get("prompt_boost", "") if persona_sd else ""
persona_style = persona_sd.get("prompt_style", "") if persona_sd else ""
enhanced_prompt = persona_boost + profile.get("prompt_prefix", "") + prompt + profile.get("prompt_suffix", "") + persona_style
if persona_sd:
logger.info("🎭 人设视觉增强已注入: +%d boost词 +%d style词",
len(persona_boost), len(persona_style))
# 使用模型专属反向提示词 + 人设额外负面词
final_negative = negative_prompt if negative_prompt is not None else profile.get("negative_prompt", DEFAULT_NEGATIVE)
if persona_sd and persona_sd.get("negative_extra"):
final_negative = final_negative + ", " + persona_sd["negative_extra"]
payload = {
"prompt": enhanced_prompt,
"negative_prompt": final_negative,
"steps": steps if steps is not None else preset["steps"],
"cfg_scale": cfg_scale if cfg_scale is not None else preset["cfg_scale"],
"width": width if width is not None else preset["width"],
"height": height if height is not None else preset["height"],
"batch_size": batch_size if batch_size is not None else preset["batch_size"],
"seed": seed,
"sampler_name": sampler_name if sampler_name is not None else preset["sampler_name"],
"scheduler": scheduler if scheduler is not None else preset["scheduler"],
}
# CLIP Skip 注入:通过 override_settings 设置,对人脸颜值有显著影响
clip_skip = profile.get("clip_skip", 1)
payload["override_settings"] = {"CLIP_stop_at_last_layers": clip_skip}
payload["override_settings_restore_afterwards"] = True
if clip_skip != 1:
logger.info("📎 CLIP Skip 已设置: %d(模型 %s 专属)", clip_skip, profile_key)
logger.info("SD 生成参数 [%s]: steps=%s, cfg=%.1f, %dx%d, sampler=%s",
profile_key, payload['steps'], payload['cfg_scale'],
payload['width'], payload['height'], payload['sampler_name'])
# Task 2.4: Hires Fix 参数注入(仅 SDXL 架构,且预设中包含 enable_hr
if preset.get("enable_hr") and profile.get("arch") != "sd15":
payload["enable_hr"] = True
payload["hr_scale"] = preset.get("hr_scale", 1.5)
payload["hr_upscaler"] = preset.get("hr_upscaler", "4x-UltraSharp")
payload["hr_second_pass_steps"] = preset.get("hr_second_pass_steps", 20)
payload["denoising_strength"] = preset.get("denoising_strength", 0.4)
logger.info("🔍 Hires Fix 已启用: scale=%.1f, upscaler=%s, second_steps=%d",
payload["hr_scale"], payload["hr_upscaler"], payload["hr_second_pass_steps"])
# 如果提供了头像,通过 ReActor 换脸
if face_image is not None:
payload["alwayson_scripts"] = self._build_reactor_args(face_image)
logger.info("🎭 ReActor 换脸已启用")
resp = requests.post(
f"{self.sd_url}/sdapi/v1/txt2img",
json=payload,
timeout=SD_TIMEOUT,
)
# Task 2.5: Hires Fix upscaler 降级回退
if not resp.ok and payload.get("enable_hr"):
fallback_upscaler = "Latent"
logger.warning(
"⚠️ Hires Fix 失败 (upscaler=%s),回退至 %s 重试",
payload.get("hr_upscaler"), fallback_upscaler,
)
payload["hr_upscaler"] = fallback_upscaler
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)))
# Task 3.5: 美化增强(精致感 + 中国审美皮肤校色)
img = beauty_enhance(img, level=enhance_level)
# 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征
img = anti_detect_postprocess(img)
images.append(img)
return images
def img2img(
self,
init_image: Image.Image,
prompt: str,
negative_prompt: str = None,
denoising_strength: float = 0.5,
steps: int = 30,
cfg_scale: float = None,
sampler_name: str = None,
scheduler: str = None,
model: str = None,
enhance_level: float = 1.0,
) -> list[Image.Image]:
"""图生图(自动适配模型参数)"""
profile = get_model_profile(model)
preset = get_sd_preset("标准 (约1分钟)", model)
# 将 PIL Image 转为 base64
buf = io.BytesIO()
init_image.save(buf, format="PNG")
init_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
enhanced_prompt = profile.get("prompt_prefix", "") + prompt + profile.get("prompt_suffix", "")
final_negative = negative_prompt if negative_prompt is not None else profile.get("negative_prompt", DEFAULT_NEGATIVE)
payload = {
"init_images": [init_b64],
"prompt": enhanced_prompt,
"negative_prompt": final_negative,
"denoising_strength": denoising_strength,
"steps": steps,
"cfg_scale": cfg_scale if cfg_scale is not None else preset["cfg_scale"],
"width": init_image.width,
"height": init_image.height,
"sampler_name": sampler_name if sampler_name is not None else preset["sampler_name"],
"scheduler": scheduler if scheduler is not None else preset["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)))
# Task 3.6: 美化增强
img = beauty_enhance(img, level=enhance_level)
# 反 AI 检测后处理
img = anti_detect_postprocess(img)
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 []