feat(llm): 增强文案反 AI 检测能力并优化图片后处理

- 在系统提示词中新增【反AI检测规则】章节,包含句子长度、逻辑跳跃、标点符号等8项防检测措施
- 为所有SD模型提示词指南添加通用反AI检测技巧,强调真实手机拍摄风格
- 深度重构 `_humanize_content` 方法,新增8层真人化处理:替换书面表达、打散句子长度、随机添加口语元素、模拟手机打字标点习惯
- 增强 `_humanize` 方法,去除更多AI前缀,随机化标点,限制表情符号堆叠
- 在 `sd_service.py` 新增 `anti_detect_postprocess` 图片后处理管线,包含元数据剥离、随机裁剪、色彩微扰、不均匀噪声、JPEG压缩回环等7步处理
- 所有图片生成后自动经过反检测处理,输出格式统一为JPEG以模拟真实手机照片
- 更新 `main.py` 中的图片保存逻辑,统一使用JPEG格式并确保RGB模式转换
This commit is contained in:
zhoujie 2026-02-10 21:37:03 +08:00
parent 156a18ae0c
commit 0c91c00dcf
3 changed files with 294 additions and 16 deletions

View File

@ -5,6 +5,7 @@ LLM 服务模块
import requests import requests
import json import json
import re import re
import random
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,6 +45,17 @@ PROMPT_COPYWRITING = """
"小伙伴们" "宝子们" 等过度热情的称呼偶尔一次可以 "小伙伴们" "宝子们" 等过度热情的称呼偶尔一次可以
8. 结尾加 5-8 个相关话题标签(#) 8. 结尾加 5-8 个相关话题标签(#)
!!极其重要 反AI检测规则!!
你的文案会被AI检测工具审查必须通过以下防检测措施
1. 句子长度必须参差不齐有的句子就3-5个字有的20多字绝对不能每句都差不多长
2. 不要所有段落逻辑都很通顺真人写东西会突然跑题突然想起另一件事
3. 偶尔犯一点小"不规范"比如某句话没写完就换话题了用了个不太准确的词
4. 标点符号不要太规范真人用手机打字经常漏掉逗号句号不加偶尔连续用两个问号??
5. 用词不要太"正确"真人会说"巨好用" "绝绝子" "无敌了" 而不是"非常好用" "十分出色"
6. 避免完美的逻辑链条不要每段都工工整整地推进论点真人笔记是跳跃式的
7. 偶尔口语化到"学渣"程度"就 很那个 你懂的" "属于是" "多少有点" "怎么说呢"
8. 绝对不要用"然而" "此外" "因此" "尽管" "虽然...但是..."这些书面连接词
绘图 Prompt 绘图 Prompt
{sd_prompt_guide} {sd_prompt_guide}
@ -278,7 +290,7 @@ class LLMService:
@staticmethod @staticmethod
def get_sd_prompt_guide(sd_model_name: str = None) -> str: def get_sd_prompt_guide(sd_model_name: str = None) -> str:
"""根据当前 SD 模型生成 LLM 使用的绘图 Prompt 指南""" """根据当前 SD 模型生成 LLM 使用的绘图 Prompt 指南(含反 AI 检测指导)"""
from sd_service import SD_MODEL_PROFILES, detect_model_profile from sd_service import SD_MODEL_PROFILES, detect_model_profile
key = detect_model_profile(sd_model_name) if sd_model_name else "juggernautXL" key = detect_model_profile(sd_model_name) if sd_model_name else "juggernautXL"
@ -287,6 +299,16 @@ class LLMService:
display = profile.get("display_name", key) display = profile.get("display_name", key)
desc = profile.get("description", "") desc = profile.get("description", "")
# 通用反 AI 检测 prompt 技巧
anti_detect_tips = (
"\n\n【重要 - 反AI检测】生成的图片需要尽量像真实手机拍摄\n"
"- 在 prompt 中加入真实感关键词shot on iPhone, casual snapshot, real life, candid photo\n"
"- 加入微小不完美slight motion blur, natural background, everyday environment\n"
"- 避免过度完美的构图词:不要用 perfect composition, symmetrical, flawless 等\n"
"- 光线自然化:用 natural daylight, indoor ambient light, window light 而非 studio lighting\n"
"- 模拟手机拍照特征phone camera, slightly overexposed, casual angle, not centered\n"
)
if key == "majicmixRealistic": if key == "majicmixRealistic":
return ( return (
f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n"
@ -298,6 +320,7 @@ class LLMService:
"- 非常适合:自拍、穿搭展示、美妆效果、生活日常、闺蜜合照风格\n" "- 非常适合:自拍、穿搭展示、美妆效果、生活日常、闺蜜合照风格\n"
"- 画面要有「朋友圈精选照片」的感觉,自然不做作\n" "- 画面要有「朋友圈精选照片」的感觉,自然不做作\n"
"- 用英文逗号分隔" "- 用英文逗号分隔"
+ anti_detect_tips
) )
elif key == "realisticVision": elif key == "realisticVision":
return ( return (
@ -311,6 +334,7 @@ class LLMService:
"- 非常适合:街拍、纪实风、旅行照、真实场景、有故事感的画面\n" "- 非常适合:街拍、纪实风、旅行照、真实场景、有故事感的画面\n"
"- 画面要有「专业摄影师抓拍」的质感,保留真实皮肤纹理\n" "- 画面要有「专业摄影师抓拍」的质感,保留真实皮肤纹理\n"
"- 用英文逗号分隔" "- 用英文逗号分隔"
+ anti_detect_tips
) )
else: # juggernautXL (SDXL) else: # juggernautXL (SDXL)
return ( return (
@ -324,6 +348,7 @@ class LLMService:
"- 非常适合:商业摄影、时尚大片、复杂光影场景、杂志封面风格\n" "- 非常适合:商业摄影、时尚大片、复杂光影场景、杂志封面风格\n"
"- 画面要有「电影画面/杂志大片」的高级感\n" "- 画面要有「电影画面/杂志大片」的高级感\n"
"- 用英文逗号分隔" "- 用英文逗号分隔"
+ anti_detect_tips
) )
def _chat(self, system_prompt: str, user_message: str, def _chat(self, system_prompt: str, user_message: str,
@ -603,9 +628,10 @@ class LLMService:
@staticmethod @staticmethod
def _humanize_content(text: str) -> str: def _humanize_content(text: str) -> str:
"""后处理: 去除长文案中的 AI 书面痕迹""" """后处理: 深度去除 AI 书面痕迹,模拟真人手机打字风格"""
t = text t = text
# 替换过于书面化的表达
# ========== 第一层: 替换过于书面化/AI化的表达 ==========
ai_phrases = { ai_phrases = {
"值得一提的是": "对了", "值得一提的是": "对了",
"需要注意的是": "不过要注意", "需要注意的是": "不过要注意",
@ -623,37 +649,171 @@ class LLMService:
"接下来让我们": "", "接下来让我们": "",
"话不多说": "", "话不多说": "",
"废话不多说": "", "废话不多说": "",
"小伙伴们": "姐妹们", "下面我来": "",
"让我来": "",
"首先我要说": "先说",
"我认为": "我觉得",
"我相信": "我觉得",
"事实上": "其实",
"实际上": "其实",
"毫无疑问": "",
"不可否认": "",
"客观来说": "",
"坦白说": "",
"具体而言": "就是",
"简而言之": "就是说",
"换句话说": "就是",
"归根结底": "说白了",
"由此可见": "",
"正如我所说": "",
"正如前文所述": "",
"在我看来": "我觉得",
"从某种程度上说": "",
"在一定程度上": "",
"非常值得推荐": "真的可以试试",
"强烈推荐": "真心推荐",
"性价比极高": "性价比很高",
"给大家安利": "安利",
"为大家推荐": "推荐",
"希望对大家有所帮助": "",
"希望能帮到大家": "",
"以上就是": "",
"感谢阅读": "",
"感谢大家的阅读": "",
} }
for old, new in ai_phrases.items(): for old, new in ai_phrases.items():
t = t.replace(old, new) t = t.replace(old, new)
# 去掉 "首先" "其次" "最后" 的分点罗列感
# ========== 第二层: 去掉分点罗列感 ==========
t = re.sub(r'(?m)^首先[,:\s]*', '', t) t = re.sub(r'(?m)^首先[,:\s]*', '', t)
t = re.sub(r'(?m)^其次[,:\s]*', '', t) t = re.sub(r'(?m)^其次[,:\s]*', '', t)
t = re.sub(r'(?m)^最后[,:\s]*', '', t) t = re.sub(r'(?m)^最后[,:\s]*', '', t)
t = re.sub(r'(?m)^再者[,:\s]*', '', t) t = re.sub(r'(?m)^再者[,:\s]*', '', t)
# 去掉AI常见的空洞开头 t = re.sub(r'(?m)^另外[,:\s]*', '', t)
for prefix in ["嗨大家好!", "嗨,大家好!", "大家好,", "大家好!", "哈喽大家好!"]: # 去序号: "1. " "2、" "①" 等
t = re.sub(r'(?m)^[①②③④⑤⑥⑦⑧⑨⑩]\s*', '', t)
t = re.sub(r'(?m)^[1-9][.、))]\s*', '', t)
# ========== 第三层: 去掉AI常见的空洞开头 ==========
for prefix in ["嗨大家好!", "嗨,大家好!", "大家好,", "大家好!",
"哈喽大家好!", "Hello大家好", "嗨~", "hey~",
"各位姐妹大家好!", "各位宝子们好!"]:
if t.startswith(prefix): if t.startswith(prefix):
t = t[len(prefix):].strip() t = t[len(prefix):].strip()
# ========== 第四层: 标点符号真人化 ==========
# AI 特征: 每句话都有完整标点 → 真人经常不加标点或只用逗号
sentences = t.split('\n')
humanized_lines = []
for line in sentences:
if not line.strip():
humanized_lines.append(line)
continue
# 随机去掉句末句号 (真人经常不打句号)
if line.rstrip().endswith('') and random.random() < 0.35:
line = line.rstrip()[:-1]
# 随机把部分逗号替换成空格或什么都不加 (模拟打字不加标点)
if random.random() < 0.15:
# 只替换一个逗号
comma_positions = [m.start() for m in re.finditer(r'[,]', line)]
if comma_positions:
pos = random.choice(comma_positions)
line = line[:pos] + ' ' + line[pos+1:]
humanized_lines.append(line)
t = '\n'.join(humanized_lines)
# ========== 第五层: 随机添加真人口语化元素 ==========
# 在段落开头随机插入口语衔接词
oral_connectors = [
"对了 ", "哦对 ", "话说 ", "然后 ", "就是说 ", "emmm ", "",
"说真的 ", "不是 ", "离谱的是 ", "我发现 ",
]
paragraphs = t.split('\n\n')
if len(paragraphs) > 2:
# 在中间段落随机加1-2个口语衔接词
inject_count = random.randint(1, min(2, len(paragraphs) - 2))
inject_indices = random.sample(range(1, len(paragraphs)), inject_count)
for idx in inject_indices:
if paragraphs[idx].strip() and not any(paragraphs[idx].strip().startswith(c.strip()) for c in oral_connectors):
connector = random.choice(oral_connectors)
paragraphs[idx] = connector + paragraphs[idx].lstrip()
t = '\n\n'.join(paragraphs)
# ========== 第六层: 句子长度打散 ==========
# AI 特征: 句子长度高度均匀 → 真人笔记长短参差不齐
# 随机把一些长句用换行打散
lines = t.split('\n')
final_lines = []
for line in lines:
# 超过60字的行, 随机在一个位置断句
if len(line) > 60 and random.random() < 0.3:
# 找到中间附近的标点位置断句
mid = len(line) // 2
best_pos = -1
for offset in range(0, mid):
for check_pos in [mid + offset, mid - offset]:
if 0 < check_pos < len(line) and line[check_pos] in ',。!?、,':
best_pos = check_pos
break
if best_pos > 0:
break
if best_pos > 0:
final_lines.append(line[:best_pos + 1])
final_lines.append(line[best_pos + 1:].lstrip())
continue
final_lines.append(line)
t = '\n'.join(final_lines)
# ========== 第七层: 随机注入微小不完美 ==========
# 真人打字偶尔有重复字、多余空格等
if random.random() < 0.2:
# 随机在某处加一个波浪号或省略号
insert_chars = ['~', '...', '', '..']
lines = t.split('\n')
if lines:
target = random.randint(0, len(lines) - 1)
if lines[target].rstrip() and not lines[target].rstrip()[-1] in '~.。!?!?':
lines[target] = lines[target].rstrip() + random.choice(insert_chars)
t = '\n'.join(lines)
# ========== 第八层: 清理 ==========
# 去掉连续3个以上的 emoji
t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t)
# 清理多余空行 # 清理多余空行
t = re.sub(r'\n{3,}', '\n\n', t) t = re.sub(r'\n{3,}', '\n\n', t)
# 清理行首多余空格 (手机打字不会缩进)
t = re.sub(r'(?m)^[ \t]+', '', t)
return t.strip() return t.strip()
@staticmethod @staticmethod
def _humanize(text: str) -> str: def _humanize(text: str) -> str:
"""后处理: 去除 AI 输出中常见的非人类痕迹""" """后处理: 深度去除 AI 评论/回复中的非人类痕迹"""
t = text.strip() t = text.strip()
# 去掉前后引号包裹 # 去掉前后引号包裹
if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")): if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")):
t = t[1:-1].strip() t = t[1:-1].strip()
# 去掉 AI 常见的前缀 # 去掉 AI 常见的前缀
for prefix in ["回复:", "回复:", "评论:", "评论:", "以下是", "好的,"]: for prefix in ["回复:", "回复:", "评论:", "评论:", "以下是", "好的,",
"当然,", "当然!", "谢谢你的", "感谢你的", "好的!",
"嗯,"]:
if t.startswith(prefix): if t.startswith(prefix):
t = t[len(prefix):].strip() t = t[len(prefix):].strip()
# 去掉末尾多余的句号(真人评论很少用句号结尾) # 去掉末尾多余的句号(真人评论很少用句号结尾)
if t.endswith(""): if t.endswith(""):
t = t[:-1] t = t[:-1]
# 去掉末尾的"哦" "呢" 堆叠 (AI 常见)
t = re.sub(r'[哦呢呀哈]{2,}$', lambda m: m.group()[0], t)
# 替换过于完整规范的标点为口语化
if random.random() < 0.25 and '' in t:
# 随机去掉一个逗号
comma_pos = [m.start() for m in re.finditer('', t)]
if comma_pos:
pos = random.choice(comma_pos)
t = t[:pos] + ' ' + t[pos+1:]
# 随机去掉末尾感叹号(真人不是每句都加!)
if t.endswith('') and random.random() < 0.3:
t = t[:-1]
# 限制连续 emoji最多2个 # 限制连续 emoji最多2个
t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t) t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t)
return t return t

18
main.py
View File

@ -386,9 +386,11 @@ def one_click_export(title, content, images):
saved_paths = [] saved_paths = []
if images: if images:
for idx, img in enumerate(images): for idx, img in enumerate(images):
path = os.path.join(folder_path, f"{idx+1}.png") path = os.path.join(folder_path, f"{idx+1}.jpg")
if isinstance(img, Image.Image): if isinstance(img, Image.Image):
img.save(path) if img.mode != "RGB":
img = img.convert("RGB")
img.save(path, format="JPEG", quality=95)
saved_paths.append(os.path.abspath(path)) saved_paths.append(os.path.abspath(path))
# 尝试打开文件夹 # 尝试打开文件夹
@ -422,8 +424,10 @@ def publish_to_xhs(title, content, tags_str, images, local_images, mcp_url, sche
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
for idx, img in enumerate(images): for idx, img in enumerate(images):
if isinstance(img, Image.Image): if isinstance(img, Image.Image):
path = os.path.abspath(os.path.join(temp_dir, f"ai_{idx}.png")) path = os.path.abspath(os.path.join(temp_dir, f"ai_{idx}.jpg"))
img.save(path) if img.mode != "RGB":
img = img.convert("RGB")
img.save(path, format="JPEG", quality=95)
image_paths.append(path) image_paths.append(path)
# 添加本地上传的图片 # 添加本地上传的图片
@ -2082,8 +2086,10 @@ def auto_publish_once(topics_str, mcp_url, sd_url_val, sd_model_name, model, per
image_paths = [] image_paths = []
for idx, img in enumerate(images): for idx, img in enumerate(images):
if isinstance(img, Image.Image): if isinstance(img, Image.Image):
path = os.path.abspath(os.path.join(backup_dir, f"{idx+1}.png")) path = os.path.abspath(os.path.join(backup_dir, f"{idx+1}.jpg"))
img.save(path) if img.mode != "RGB":
img = img.convert("RGB")
img.save(path, format="JPEG", quality=95)
image_paths.append(path) image_paths.append(path)
if not image_paths: if not image_paths:

View File

@ -1,13 +1,18 @@
""" """
Stable Diffusion 服务模块 Stable Diffusion 服务模块
封装对 SD WebUI API 的调用支持 txt2img img2img支持 ReActor 换脸 封装对 SD WebUI API 的调用支持 txt2img img2img支持 ReActor 换脸
含图片反 AI 检测后处理管线
""" """
import requests import requests
import base64 import base64
import io import io
import logging import logging
import os import os
from PIL import Image import random
import math
import struct
import zlib
from PIL import Image, ImageFilter, ImageEnhance
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -262,6 +267,109 @@ def get_sd_preset(name: str, model_name: str = None) -> dict:
DEFAULT_NEGATIVE = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["negative_prompt"] DEFAULT_NEGATIVE = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["negative_prompt"]
# ==================== 图片反 AI 检测管线 ====================
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: class SDService:
"""Stable Diffusion WebUI API 封装""" """Stable Diffusion WebUI API 封装"""
@ -463,6 +571,8 @@ class SDService:
images = [] images = []
for img_b64 in resp.json().get("images", []): for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64))) img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
# 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征
img = anti_detect_postprocess(img)
images.append(img) images.append(img)
return images return images
@ -513,6 +623,8 @@ class SDService:
images = [] images = []
for img_b64 in resp.json().get("images", []): for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64))) img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
# 反 AI 检测后处理
img = anti_detect_postprocess(img)
images.append(img) images.append(img)
return images return images