diff --git a/llm_service.py b/llm_service.py index 80abba7..0fdb343 100644 --- a/llm_service.py +++ b/llm_service.py @@ -5,6 +5,7 @@ LLM 服务模块 import requests import json import re +import random import logging logger = logging.getLogger(__name__) @@ -44,6 +45,17 @@ PROMPT_COPYWRITING = """ ❌ "小伙伴们" "宝子们" 等过度热情的称呼(偶尔一次可以) 8. 结尾加 5-8 个相关话题标签(#) +【!!极其重要 — 反AI检测规则!!】: +你的文案会被AI检测工具审查,必须通过以下防检测措施: +1. 句子长度必须参差不齐:有的句子就3-5个字,有的20多字,绝对不能每句都差不多长 +2. 不要所有段落逻辑都很通顺——真人写东西会突然跑题、突然想起另一件事 +3. 偶尔犯一点小"不规范":比如某句话没写完就换话题了、用了个不太准确的词 +4. 标点符号不要太规范:真人用手机打字经常漏掉逗号、句号不加,偶尔连续用两个问号?? +5. 用词不要太"正确":真人会说"巨好用" "绝绝子" "无敌了" 而不是"非常好用" "十分出色" +6. 避免完美的逻辑链条:不要每段都工工整整地推进论点,真人笔记是跳跃式的 +7. 偶尔口语化到"学渣"程度:"就 很那个 你懂的" "属于是" "多少有点" "怎么说呢" +8. 绝对不要用"然而" "此外" "因此" "尽管" "虽然...但是..."这些书面连接词 + 【绘图 Prompt】: {sd_prompt_guide} @@ -278,7 +290,7 @@ class LLMService: @staticmethod 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 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) 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": return ( f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" @@ -298,6 +320,7 @@ class LLMService: "- 非常适合:自拍、穿搭展示、美妆效果、生活日常、闺蜜合照风格\n" "- 画面要有「朋友圈精选照片」的感觉,自然不做作\n" "- 用英文逗号分隔" + + anti_detect_tips ) elif key == "realisticVision": return ( @@ -311,6 +334,7 @@ class LLMService: "- 非常适合:街拍、纪实风、旅行照、真实场景、有故事感的画面\n" "- 画面要有「专业摄影师抓拍」的质感,保留真实皮肤纹理\n" "- 用英文逗号分隔" + + anti_detect_tips ) else: # juggernautXL (SDXL) return ( @@ -324,6 +348,7 @@ class LLMService: "- 非常适合:商业摄影、时尚大片、复杂光影场景、杂志封面风格\n" "- 画面要有「电影画面/杂志大片」的高级感\n" "- 用英文逗号分隔" + + anti_detect_tips ) def _chat(self, system_prompt: str, user_message: str, @@ -603,9 +628,10 @@ class LLMService: @staticmethod def _humanize_content(text: str) -> str: - """后处理: 去除长文案中的 AI 书面痕迹""" + """后处理: 深度去除 AI 书面痕迹,模拟真人手机打字风格""" t = text - # 替换过于书面化的表达 + + # ========== 第一层: 替换过于书面化/AI化的表达 ========== ai_phrases = { "值得一提的是": "对了", "需要注意的是": "不过要注意", @@ -623,37 +649,171 @@ class LLMService: "接下来让我们": "", "话不多说": "", "废话不多说": "", - "小伙伴们": "姐妹们", + "下面我来": "", + "让我来": "", + "首先我要说": "先说", + "我认为": "我觉得", + "我相信": "我觉得", + "事实上": "其实", + "实际上": "其实", + "毫无疑问": "", + "不可否认": "", + "客观来说": "", + "坦白说": "", + "具体而言": "就是", + "简而言之": "就是说", + "换句话说": "就是", + "归根结底": "说白了", + "由此可见": "", + "正如我所说": "", + "正如前文所述": "", + "在我看来": "我觉得", + "从某种程度上说": "", + "在一定程度上": "", + "非常值得推荐": "真的可以试试", + "强烈推荐": "真心推荐", + "性价比极高": "性价比很高", + "给大家安利": "安利", + "为大家推荐": "推荐", + "希望对大家有所帮助": "", + "希望能帮到大家": "", + "以上就是": "", + "感谢阅读": "", + "感谢大家的阅读": "", } for old, new in ai_phrases.items(): 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) - # 去掉AI常见的空洞开头 - for prefix in ["嗨大家好!", "嗨,大家好!", "大家好,", "大家好!", "哈喽大家好!"]: + t = re.sub(r'(?m)^另外[,,::\s]*', '', t) + # 去序号: "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): 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'(?m)^[ \t]+', '', t) + return t.strip() @staticmethod def _humanize(text: str) -> str: - """后处理: 去除 AI 输出中常见的非人类痕迹""" + """后处理: 深度去除 AI 评论/回复中的非人类痕迹""" t = text.strip() # 去掉前后引号包裹 if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")): t = t[1:-1].strip() # 去掉 AI 常见的前缀 - for prefix in ["回复:", "回复:", "评论:", "评论:", "以下是", "好的,"]: + for prefix in ["回复:", "回复:", "评论:", "评论:", "以下是", "好的,", + "当然,", "当然!", "谢谢你的", "感谢你的", "好的!", + "嗯,"]: if t.startswith(prefix): t = t[len(prefix):].strip() # 去掉末尾多余的句号(真人评论很少用句号结尾) if t.endswith("。"): 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个) t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t) return t diff --git a/main.py b/main.py index 0ebae1b..7e919e7 100644 --- a/main.py +++ b/main.py @@ -386,9 +386,11 @@ def one_click_export(title, content, images): saved_paths = [] if 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): - 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)) # 尝试打开文件夹 @@ -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) for idx, img in enumerate(images): if isinstance(img, Image.Image): - path = os.path.abspath(os.path.join(temp_dir, f"ai_{idx}.png")) - img.save(path) + path = os.path.abspath(os.path.join(temp_dir, f"ai_{idx}.jpg")) + if img.mode != "RGB": + img = img.convert("RGB") + img.save(path, format="JPEG", quality=95) 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 = [] for idx, img in enumerate(images): if isinstance(img, Image.Image): - path = os.path.abspath(os.path.join(backup_dir, f"图{idx+1}.png")) - img.save(path) + path = os.path.abspath(os.path.join(backup_dir, f"图{idx+1}.jpg")) + if img.mode != "RGB": + img = img.convert("RGB") + img.save(path, format="JPEG", quality=95) image_paths.append(path) if not image_paths: diff --git a/sd_service.py b/sd_service.py index 269c5d0..7bb8a03 100644 --- a/sd_service.py +++ b/sd_service.py @@ -1,13 +1,18 @@ """ Stable Diffusion 服务模块 封装对 SD WebUI API 的调用,支持 txt2img 和 img2img,支持 ReActor 换脸 +含图片反 AI 检测后处理管线 """ import requests import base64 import io import logging import os -from PIL import Image +import random +import math +import struct +import zlib +from PIL import Image, ImageFilter, ImageEnhance 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"] +# ==================== 图片反 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: """Stable Diffusion WebUI API 封装""" @@ -463,6 +571,8 @@ class SDService: images = [] for img_b64 in resp.json().get("images", []): img = Image.open(io.BytesIO(base64.b64decode(img_b64))) + # 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征 + img = anti_detect_postprocess(img) images.append(img) return images @@ -513,6 +623,8 @@ class SDService: images = [] for img_b64 in resp.json().get("images", []): img = Image.open(io.BytesIO(base64.b64decode(img_b64))) + # 反 AI 检测后处理 + img = anti_detect_postprocess(img) images.append(img) return images