From b5deafa2cc9d53ea3d49415553accf7a2133c8cd Mon Sep 17 00:00:00 2001 From: zhoujie <929834232@qq.com> Date: Thu, 26 Feb 2026 22:58:05 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(config):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E4=B8=8ELLM=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将默认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列卡片网格展示 --- config.json | 4 +- llm_service.py | 89 ++- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/services-autostart/spec.md | 0 .../specs/services-connection/spec.md | 0 .../specs/services-content/spec.md | 0 .../specs/services-engagement/spec.md | 0 .../specs/services-hotspot/spec.md | 0 .../specs/services-persona/spec.md | 0 .../specs/services-profile/spec.md | 0 .../specs/services-queue/spec.md | 0 .../specs/services-rate-limiter/spec.md | 0 .../specs/services-scheduler/spec.md | 0 .../specs/ui-tabs-split/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 2 + .../2026-02-26-improve-ui-layout/design.md | 97 +++ .../2026-02-26-improve-ui-layout/proposal.md | 28 + .../specs/ui-global-config-tab/spec.md | 20 + .../specs/ui-module-split/spec.md | 49 ++ .../specs/ui-tabs-split/spec.md | 38 + .../2026-02-26-improve-ui-layout/tasks.md | 44 ++ .../.openspec.yaml | 2 + .../design.md | 86 +++ .../proposal.md | 31 + .../specs/chinese-aesthetic-profile/spec.md | 35 + .../specs/image-post-enhancement/spec.md | 38 + .../specs/image-quality-presets/spec.md | 23 + .../specs/llm-prompt-aesthetics/spec.md | 29 + .../tasks.md | 50 ++ .../specs/chinese-aesthetic-profile/spec.md | 35 + openspec/specs/image-post-enhancement/spec.md | 38 + openspec/specs/image-quality-presets/spec.md | 23 + openspec/specs/llm-prompt-aesthetics/spec.md | 29 + openspec/specs/services-autostart/spec.md | 16 + openspec/specs/services-connection/spec.md | 16 + openspec/specs/services-content/spec.md | 16 + openspec/specs/services-engagement/spec.md | 16 + openspec/specs/services-hotspot/spec.md | 12 + openspec/specs/services-persona/spec.md | 16 + openspec/specs/services-profile/spec.md | 12 + openspec/specs/services-queue/spec.md | 16 + openspec/specs/services-rate-limiter/spec.md | 16 + openspec/specs/services-scheduler/spec.md | 16 + openspec/specs/ui-global-config-tab/spec.md | 20 + openspec/specs/ui-module-split/spec.md | 42 +- openspec/specs/ui-tabs-split/spec.md | 38 + sd_service.py | 193 ++++- services/content.py | 5 +- ui/app.py | 707 ++++++++++-------- ui/tab_create.py | 18 +- 53 files changed, 1578 insertions(+), 387 deletions(-) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/.openspec.yaml (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/design.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/proposal.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-autostart/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-connection/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-content/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-engagement/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-hotspot/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-persona/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-profile/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-queue/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-rate-limiter/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/services-scheduler/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/specs/ui-tabs-split/spec.md (100%) rename openspec/changes/{improve-maintainability => archive/2026-02-25-improve-maintainability}/tasks.md (100%) create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/.openspec.yaml create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/design.md create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/proposal.md create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-global-config-tab/spec.md create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-module-split/spec.md create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-tabs-split/spec.md create mode 100644 openspec/changes/archive/2026-02-26-improve-ui-layout/tasks.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/.openspec.yaml create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/design.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/proposal.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/specs/chinese-aesthetic-profile/spec.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-post-enhancement/spec.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-quality-presets/spec.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/specs/llm-prompt-aesthetics/spec.md create mode 100644 openspec/changes/archive/2026-02-26-optimize-image-generation/tasks.md create mode 100644 openspec/specs/chinese-aesthetic-profile/spec.md create mode 100644 openspec/specs/image-post-enhancement/spec.md create mode 100644 openspec/specs/image-quality-presets/spec.md create mode 100644 openspec/specs/llm-prompt-aesthetics/spec.md create mode 100644 openspec/specs/services-autostart/spec.md create mode 100644 openspec/specs/services-connection/spec.md create mode 100644 openspec/specs/services-content/spec.md create mode 100644 openspec/specs/services-engagement/spec.md create mode 100644 openspec/specs/services-hotspot/spec.md create mode 100644 openspec/specs/services-persona/spec.md create mode 100644 openspec/specs/services-profile/spec.md create mode 100644 openspec/specs/services-queue/spec.md create mode 100644 openspec/specs/services-rate-limiter/spec.md create mode 100644 openspec/specs/services-scheduler/spec.md create mode 100644 openspec/specs/ui-global-config-tab/spec.md create mode 100644 openspec/specs/ui-tabs-split/spec.md diff --git a/config.json b/config.json index 14ee311..c390e78 100644 --- a/config.json +++ b/config.json @@ -3,8 +3,8 @@ "base_url": "https://wolfai.top/v1", "sd_url": "http://127.0.0.1:7861", "mcp_url": "http://localhost:18060/mcp", - "model": "gemini-2.0-flash", - "persona": "性感福利主播,身材火辣衣着大胆,专注分享穿衣显身材和私房写真风穿搭", + "model": "gemini-3-flash-preview", + "persona": "二次元coser,喜欢分享cos日常和动漫周边", "auto_reply_enabled": false, "schedule_enabled": false, "my_user_id": "69872540000000002303cc42", diff --git a/llm_service.py b/llm_service.py index fd718ff..c8f113b 100644 --- a/llm_service.py +++ b/llm_service.py @@ -327,25 +327,38 @@ class LLMService: base = ( f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" "该模型擅长东亚网红/朋友圈自拍风格,请按以下规则生成 sd_prompt:\n" - "- 人物要求(最重要!):必须是东亚面孔中国人\n" - "- 推荐使用 (权重:数值) 语法加强关键词,例如 (asian girl:1.3), (best quality:1.4)\n" - "- 风格关键词:RAW photo, realistic, photorealistic, natural makeup, instagram aesthetic\n" - "- 氛围词:soft lighting, warm tone, natural skin texture, phone camera feel\n" + "- 人物要求(最重要!):必须是东亚面孔中国人,必须描述眼睛、肤色、表情、妆容\n" + "- 推荐使用 (权重:数值) 语法加强关键词,例如 (almond eyes:1.3), (glossy lips:1.2)\n" + "- 颜值核心词(必选2-3个):(bright sparkling eyes:1.2), (glossy lips:1.2), (rosy cheeks:1.1),\n" + " (soft smile:1.2), (dewy glowing skin:1.2), (long lashes:1.1), (charming gaze:1.1)\n" + "- 风格关键词:RAW photo, realistic, photorealistic, instagram aesthetic\n" + "- 氛围词:soft lighting, warm tone, phone camera feel, shallow depth of field\n" + "- ❌ 严禁使用:skin pores, natural imperfections, skin texture(会让皮肤变粗糙)\n" "- 非常适合:自拍、穿搭展示、美妆效果、生活日常、闺蜜合照风格\n" - "- 画面要有「朋友圈精选照片」的感觉,自然不做作\n" + "- 示范 prompt(参考此质量和格式):\n" + " (best quality:1.4), RAW photo, (photorealistic:1.4), (asian girl:1.3), (almond eyes:1.3),\n" + " (bright sparkling eyes:1.2), (dewy glowing skin:1.2), (natural makeup:1.3), (glossy lips:1.2),\n" + " (soft smile:1.2), wearing cream linen dress, standing on sunny street, shallow depth of field,\n" + " warm tone, instagram aesthetic, phone camera feel\n" "- 用英文逗号分隔" ) elif key == "realisticVision": base = ( f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" - "该模型擅长写实纪实摄影风格,请按以下规则生成 sd_prompt:\n" - "- 人物要求(最重要!):必须是东亚面孔中国人\n" - "- 推荐使用 (权重:数值) 语法,例如 (realistic:1.4), (photorealistic:1.4)\n" - "- 风格关键词:RAW photo, DSLR, documentary style, street photography, film color grading\n" - "- 质感词:skin pores, detailed skin texture, natural imperfections, real lighting\n" - "- 镜头感:shot on Canon/Sony, 85mm lens, f/1.8, depth of field\n" - "- 非常适合:街拍、纪实风、旅行照、真实场景、有故事感的画面\n" - "- 画面要有「专业摄影师抓拍」的质感,保留真实皮肤纹理\n" + "该模型擅长写实摄影风格,请按以下规则生成 sd_prompt:\n" + "- 人物要求(最重要!):必须是东亚面孔中国人,必须描述眼睛、肤色、表情\n" + "- 推荐使用 (权重:数值) 语法,例如 (realistic:1.4), (almond eyes:1.2)\n" + "- 颜值核心词(必选2-3个):(bright clear eyes:1.2), (charming gaze:1.1), (glossy lips:1.1),\n" + " (dewy smooth skin:1.2), (soft smile:1.1), (long lashes:1.1), (rosy cheeks:1.1)\n" + "- 风格关键词:RAW photo, DSLR, street photography, film color grading\n" + "- 镜头感:shot on Sony A7, 85mm lens, f/1.8, depth of field, bokeh\n" + "- ❌ 严禁使用:skin pores, detailed skin texture, natural imperfections(会让皮肤变难看)\n" + "- 非常适合:街拍、旅行照、真实场景、有故事感的画面\n" + "- 示范 prompt(参考此质量和格式):\n" + " RAW photo, (best quality:1.4), (realistic:1.4), (photorealistic:1.4), (asian:1.2),\n" + " (almond eyes:1.2), (bright clear eyes:1.2), (dewy smooth skin:1.2), (natural makeup:1.2),\n" + " (soft smile:1.1), wearing light blue shirt, walking in old town alley,\n" + " shot on Sony A7, 85mm lens, f/1.8, golden hour, depth of field\n" "- 用英文逗号分隔" ) else: # juggernautXL (SDXL) @@ -354,15 +367,57 @@ class LLMService: "该模型为 SDXL 架构,擅长电影级大片质感,请按以下规则生成 sd_prompt:\n" "- 人物要求(最重要!):必须是东亚面孔中国人,绝对禁止西方人特征\n" "- 不要使用 (权重:数值) 括号语法,SDXL 模型直接用逗号分隔即可\n" + "- 颜值核心词(必选2-3个):bright sparkling eyes, glossy lips, rosy cheeks,\n" + " gentle smile, luminous dewy skin, long lashes, charming expression, defined brows\n" "- 质量词:masterpiece, best quality, ultra detailed, 8k uhd, high resolution\n" - "- 风格:photorealistic, cinematic lighting, cinematic composition, commercial photography\n" - "- 光影:volumetric lighting, ray tracing, golden hour, studio lighting\n" + "- 风格:photorealistic, cinematic lighting, commercial photography\n" + "- 光影:golden hour, soft diffused light, volumetric lighting, soft catchlights in eyes\n" "- 非常适合:商业摄影、时尚大片、复杂光影场景、杂志封面风格\n" - "- 画面要有「电影画面/杂志大片」的高级感\n" + "- 示范 prompt(参考此质量和格式):\n" + " masterpiece, best quality, ultra detailed, photorealistic, cinematic lighting,\n" + " chinese beauty, east asian features, almond eyes, bright sparkling eyes, long lashes,\n" + " luminous dewy skin, natural makeup, glossy lips, gentle smile, rosy cheeks,\n" + " wearing white summer dress, standing in flower garden, golden hour, soft bokeh background,\n" + " commercial photography, fashion editorial style\n" "- 用英文逗号分隔" ) - return base + anti_detect_tips + persona_guide + # Task 4.1-4.4: 中国审美人物描述规则 + chinese_aesthetic_guide = ( + "\n\n【人物描述规则 - 中国审美标准】\n" + "生成人物 prompt 时,请严格遵守以下规则:\n\n" + "❌ 禁止使用通用美丽词汇(太泛,效果弱):\n" + " - 禁止: beautiful, pretty, gorgeous, attractive, stunning\n" + " - 原因: 这类词在中文语境下容易偏向西方模型默认审美\n\n" + "✅ 三维人物描述法(眼睛 / 肤色 / 气质)——必须各选至少1个:\n" + " 眼睛维度: almond eyes, double eyelid, bright clear eyes, " + "gentle gaze, expressive eyes, long eyelashes\n" + " 肤色维度: porcelain skin, luminous fair skin, milky white skin, " + "translucent skin, dewy complexion, peach-tinted cheeks\n" + " 气质维度: elegant temperament, gentle demeanor, refined bearing, " + "graceful posture, youthful vitality, scholarly aura\n\n" + "✅ 必须加表情/神态词(让人物鲜活有魅力,这非常重要!):\n" + " 从以下选1-2个: soft smile, gentle smile, charming gaze, bright eyes, " + "sweet expression, confident look, warm smile, playful smile\n" + " 绝对禁止生成没有表情词的人物 prompt — 没有表情词会导致人物呆板!\n\n" + "✅ 必须加妆容词(哪怕是日常淡妆,也要显得精致上镜):\n" + " 从以下选1-2个: natural makeup, light makeup, glossy lips, " + "rosy cheeks, subtle blush, defined brows, dewy skin finish\n\n" + "📸 构图策略(人物 vs 场景):\n" + " - 人物特写 (>50%画面): 突出五官气质,加 face close-up, portrait, " + "shallow depth of field, bokeh background\n" + " - 全身/中景: 突出整体氛围,加 full body shot, environmental portrait, " + "natural setting, lifestyle photography\n" + " - 半身照 (最常用): half body shot, upper body, 搭配场景关键词\n\n" + "💡 专属光线词(能凸显东亚肤色最佳状态):\n" + " - 室内: soft diffused light, window side lighting, warm ambient light, " + "ring light (美妆专用)\n" + " - 室外: golden hour, overcast daylight (均匀柔和), dappled sunlight (树影)\n" + " - 通用提升: skin luminance, luminous glow, soft catchlights in eyes\n" + " - 避免: harsh noon sunlight, under-eye shadows, top-down lighting, flat lighting\n" + ) + + return base + chinese_aesthetic_guide + anti_detect_tips + persona_guide def _chat(self, system_prompt: str, user_message: str, json_mode: bool = True, temperature: float = 0.8) -> str: diff --git a/openspec/changes/improve-maintainability/.openspec.yaml b/openspec/changes/archive/2026-02-25-improve-maintainability/.openspec.yaml similarity index 100% rename from openspec/changes/improve-maintainability/.openspec.yaml rename to openspec/changes/archive/2026-02-25-improve-maintainability/.openspec.yaml diff --git a/openspec/changes/improve-maintainability/design.md b/openspec/changes/archive/2026-02-25-improve-maintainability/design.md similarity index 100% rename from openspec/changes/improve-maintainability/design.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/design.md diff --git a/openspec/changes/improve-maintainability/proposal.md b/openspec/changes/archive/2026-02-25-improve-maintainability/proposal.md similarity index 100% rename from openspec/changes/improve-maintainability/proposal.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/proposal.md diff --git a/openspec/changes/improve-maintainability/specs/services-autostart/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-autostart/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-autostart/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-autostart/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-connection/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-connection/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-connection/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-connection/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-content/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-content/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-content/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-content/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-engagement/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-engagement/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-engagement/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-engagement/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-hotspot/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-hotspot/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-hotspot/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-hotspot/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-persona/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-persona/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-persona/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-persona/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-profile/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-profile/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-profile/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-profile/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-queue/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-queue/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-queue/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-queue/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-rate-limiter/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-rate-limiter/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-rate-limiter/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-rate-limiter/spec.md diff --git a/openspec/changes/improve-maintainability/specs/services-scheduler/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-scheduler/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/services-scheduler/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/services-scheduler/spec.md diff --git a/openspec/changes/improve-maintainability/specs/ui-tabs-split/spec.md b/openspec/changes/archive/2026-02-25-improve-maintainability/specs/ui-tabs-split/spec.md similarity index 100% rename from openspec/changes/improve-maintainability/specs/ui-tabs-split/spec.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/specs/ui-tabs-split/spec.md diff --git a/openspec/changes/improve-maintainability/tasks.md b/openspec/changes/archive/2026-02-25-improve-maintainability/tasks.md similarity index 100% rename from openspec/changes/improve-maintainability/tasks.md rename to openspec/changes/archive/2026-02-25-improve-maintainability/tasks.md diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/.openspec.yaml b/openspec/changes/archive/2026-02-26-improve-ui-layout/.openspec.yaml new file mode 100644 index 0000000..85ae75c --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-26 diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/design.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/design.md new file mode 100644 index 0000000..16c7018 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/design.md @@ -0,0 +1,97 @@ +## Context + +当前 `ui/app.py`(1369 行)在 `gr.Blocks` 顶部放了一个巨型 `gr.Accordion("⚙️ 全局设置")`,包含 LLM / SD / 换脸 / 系统设置约 100+ 行组件声明。`gr.Tabs()` 紧随其后,首 Tab 实际是「内容创作」(`build_tab` 返回组件字典),后续依次是热点探测、评论管家、账号登录、数据看板、智能学习、自动运营、内容排期。 + +核心约束: +1. `build_tab()` 在 `ui/tab_create.py` 中定义,返回一个组件字典(`res_title`, `res_content`, `res_prompt`, `res_tags`, `quality_mode`, `steps`, `cfg_scale`, `neg_prompt` 等)。字典 **key 不得变更**,否则 `app.py` 中所有 click 绑定会断开。 +2. Gradio 组件对象一旦被创建,其引用必须在 `gr.Blocks` 上下文内,不能跨上下文移动。因此「全局组件」(`llm_model`, `sd_model`, `persona`, `status_bar`, `face_swap_toggle`, `face_image_preview`, `sd_url`, `mcp_url`)如需迁移到单独的配置 Tab,需在配置 Tab 内创建,然后作为参数传递给 `build_tab()` ——现有接口已这样设计,无需修改函数签名。 +3. 自定义 CSS 需注入到 `gr.Blocks(css=...)` 参数,Gradio 4.x 支持 `elem_id` / `elem_classes` 定位。 + +## Goals / Non-Goals + +**Goals:** +- 移除顶部全局折叠块,将 LLM / SD / 账号 / 系统设置组件迁移进一个新的「⚙️ 配置」Tab +- Tab 顺序调整为:✍️ 内容创作 → 📅 内容排期 → 🔥 热点探测 → 💬 评论管家 → 📊 数据看板 → 🧠 智能学习 → 🤖 自动运营 → 🔐 账号登录 → ⚙️ 配置 +- 内容创作 Tab 内部重构为三栏布局(已在 `tab_create.py` 中实现,补充对齐) +- 自动运营 Tab 改为 2 列卡片式网格 +- 注入自定义 CSS:字体层级、按钮圆角、卡片阴影、Section 标题线 +- 按钮 `variant` 分级统一 + +**Non-Goals:** +- 不修改任何业务逻辑、事件处理函数或 `services/` 层代码 +- 不修改 `build_tab()` 返回的字典 key +- 不引入新的 Python 依赖 +- 不重构内容创作 Tab 的内部逻辑(仅布局调整) + +## Decisions + +### D1:全局组件保留在 Blocks 顶层,配置 Tab 仅做视觉容器 + +**决策**:在 `gr.Tabs()` 内新建「⚙️ 配置」Tab,将原折叠块内的所有组件声明 **整体移动** 进该 Tab(保持变量名不变)。 + +**原因**:Gradio 组件必须在声明处确定其 `Block` 归属,无法在声明后再"移动"到别的上下文。将声明移进 Tab 与移进 Accordion 在 Python 语义上等价,所有后续 `.change()` / `.click()` 绑定仍可访问同名变量。 + +**备选方案**:保留折叠块但折叠状态改为始终折叠(`open=False`)——被否,未从根本上减少首屏干扰。 + +### D2:Tab 顺序重排策略 + +**决策**:按"使用频率"重排,高频在前: +``` +0 ✍️ 内容创作 (主工作流起点) +1 📅 内容排期 (发布管理) +2 🔥 热点探测 (灵感来源) +3 💬 评论管家 (互动运营) +4 📊 数据看板 (查看结果) +5 🧠 智能学习 (后台任务) +6 🤖 自动运营 (定时任务) +7 🔐 账号登录 (一次性操作) +8 ⚙️ 配置 (初始化/低频) +``` + +**原因**:账号登录和配置均属初始设置,完成后几乎不再访问,放末尾减少对核心工作流的干扰。 + +### D3:自动运营 Tab — 2 列 Group 卡片而非 CSS Grid + +**决策**:用 `gr.Row()` + `with gr.Column(scale=1)` 实现等宽两列,每个任务块用 `gr.Group()` 包裹(Gradio 原生组合组件,有边框/背景色),而非依赖纯 CSS grid。 + +**原因**:Gradio 4.x 的 `gr.Group` 提供开箱即用的视觉分组,无需 `elem_id` + CSS 精确定位,可靠性更高。CSS 仅用于微调阴影/圆角,不承担主布局责任。 + +**备选方案**:全用 CSS grid + `elem_classes` ——被否,Gradio 的 CSS 隔离机制容易与内部 shadow DOM 冲突,维护成本高。 + +### D4:CSS 注入范围——最小化 + +**决策**:CSS 只覆盖: +1. `body` 字体 (Inter / 系统字体栈) +2. `.btn-primary` 圆角和阴影 +3. `.gr-group` 卡片阴影 +4. Section 分隔标题(`hr` + `.section-title` class) + +不覆盖 Gradio 内部组件(如 `textarea`, `input`),避免版本升级破坏样式。 + +### D5:内容创作 Tab 三栏布局 — 在 tab_create.py 中调整 + +**决策**:在 `build_tab()` 内部,将现有的垂直堆叠改为 `gr.Row()` 包裹三个 `gr.Column(scale=3/4/3)`: +- 左栏:人设选择、话题/风格、生成参数 +- 中栏:文案输出(title/content/tags)+ 文案操作按钮 +- 右栏:图片预览 gallery + 图片操作按钮(含美化强度滑块) + +**原因**:`build_tab()` 自包含,且组件字典 key 不变,`app.py` 侧零改动。 + +## Risks / Trade-offs + +| 风险 | 缓解措施 | +|------|----------| +| 全局组件迁入 Tab 后,首次切换到配置 Tab 时才初始化(Gradio lazy render) | Gradio 4.x 默认会在页面加载时渲染所有 Tab,非 lazy,风险低 | +| CSS 注入与 Gradio 主题冲突 | 仅用 `:root` 变量覆盖和有命名空间的选择器,避免直接覆盖 Gradio 内部类 | +| `build_tab()` 三栏改造后在窄屏(<1200px)下挤压 | Row 内设置 `wrap=True`(Gradio 支持)或通过 CSS media query 处理 | +| 自动运营 Tab 两列卡片在小屏幕下折叠 | 同上,添加 `@media` 断点 CSS | + +## Migration Plan + +1. **不需要数据迁移**——纯 UI 代码变更 +2. **部署步骤**:替换 `ui/app.py` 和 `ui/tab_create.py` 后重启应用即生效 +3. **回滚**:git revert 即可,无状态变更 + +## Open Questions + +- **无**——所有技术决策已确定,可直接进入任务拆解 diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/proposal.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/proposal.md new file mode 100644 index 0000000..ee3f7dd --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/proposal.md @@ -0,0 +1,28 @@ +## Why + +当前 UI 整体采用线性堆叠结构,全局设置塞入折叠区、各 Tab 内信息密度不均匀、核心创作流程(内容创作 Tab)缺乏明确的视觉分区,导致新用户上手成本高、高频操作路径长、页面滚动量大。随着功能不断增加,亟需一次系统性的布局优化以提升可用性和美观度。 + +## What Changes + +- **全局设置栏重构**:将分散在折叠区的 LLM / SD / 小红书账号三大配置项拆分为独立的「⚙️ 配置」Tab,从主界面顶部移除折叠块,减少首屏干扰 +- **Tab 导航顺序优化**:将用户最高频的「✍️ 内容创作」Tab 置于首位(Tab 0),次高频的「📅 内容排期」置于第二位;低频的「🔐 账号登录」和「⚙️ 配置」移至末尾 +- **内容创作 Tab 三栏布局**:左栏(参数配置) | 中栏(文案预览/编辑) | 右栏(图片预览/操作),三栏比例 3:4:3,高频操作一屏可见,无需滚动 +- **自动运营 Tab 面板化**:将开关密集的单列改为卡片式 2×N 网格,每个自动化任务独立成卡,含开关、间隔、上次运行时间三要素 +- **统一视觉语言**:为操作按钮分级(主操作 variant="primary" / 次操作 variant="secondary" / 危险操作 variant="stop"),关键区域添加分隔线和小标题 +- **新增 CSS 主题层**:在 `gr.Blocks(css=...)` 注入自定义 CSS,优化字体层级、按钮圆角、卡片阴影 + +## Capabilities + +### New Capabilities +- `ui-global-config-tab`: 将全局设置(LLM / SD / 账号)迁移到独立 Tab,含完整的连接状态显示 + +### Modified Capabilities +- `ui-tabs-split`: Tab 顺序和标题变更——内容创作置首位,新增配置 Tab,移除顶部全局配置折叠区 +- `ui-module-split`: 内容创作 Tab 改为三栏式布局;自动运营 Tab 改为卡片网格 + +## Impact + +- **直接修改文件**:`ui/app.py`、`ui/tab_create.py` +- **潜在影响**:`services/` 中事件绑定通过组件引用传参,布局变更不影响逻辑;但 `tab_create.py` 返回的组件字典 key 不得变更,否则 `app.py` 的 click 绑定会断开 +- **无外部 API / 依赖变更** +- **无 breaking change**(所有现有功能保留,仅调整位置和视觉样式) diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-global-config-tab/spec.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-global-config-tab/spec.md new file mode 100644 index 0000000..4f14261 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-global-config-tab/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: 独立配置 Tab 承载全局设置 +系统 SHALL 在主 Tab 列表末尾提供「⚙️ 配置」Tab,将 LLM 提供商配置、SD WebUI 配置、ReActor 换脸设置、系统设置(开机自启等)全部组件声明移入该 Tab,从主界面顶部移除 `gr.Accordion("⚙️ 全局设置")` 折叠块。 + +#### Scenario: 首屏无全局设置折叠块 +- **WHEN** 用户打开应用 +- **THEN** 主界面顶部 SHALL 不再显示任何折叠区块,直接呈现 Tab 导航栏 + +#### Scenario: 配置 Tab 包含所有原折叠区内容 +- **WHEN** 用户切换到「⚙️ 配置」Tab +- **THEN** Tab 内 SHALL 包含 LLM 提供商 Dropdown、LLM 模型 Dropdown、添加/删除提供商面板、MCP Server URL、SD WebUI URL、连接/检查按钮、SD 模型、博主人设、AI 换脸(头像上传 + 开关)、开机自启开关,功能与原折叠区完全一致 + +#### Scenario: 跨 Tab 共享组件可正常访问 +- **WHEN** 「内容创作」等其他 Tab 需要使用 `llm_model`、`sd_model`、`persona`、`status_bar`、`face_swap_toggle` 等组件 +- **THEN** 这些组件 SHALL 在 `gr.Blocks` 上下文中声明,并作为参数传递给各 `build_tab()` 函数,功能不受 Tab 物理位置影响 + +#### Scenario: 连接状态实时反馈 +- **WHEN** 用户在配置 Tab 点击「🔗 连接 LLM」或「🎨 连接 SD」 +- **THEN** `status_bar` Markdown 组件 SHALL 实时更新显示连接结果,与原折叠区行为一致 diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-module-split/spec.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-module-split/spec.md new file mode 100644 index 0000000..2933b60 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-module-split/spec.md @@ -0,0 +1,49 @@ +## MODIFIED Requirements + +### Requirement: 内容创作 Tab 的 UI 代码迁移至独立模块 +`ui/tab_create.py` SHALL 包含「内容创作 Tab」的全部 Gradio 组件定义和事件绑定,并导出 `build_tab(...) -> dict` 函数,返回跨 Tab 共享组件的字典(key 集合不得变更)。 + +内容创作 Tab 内部 SHALL 采用**三栏布局**: +- **左栏(scale=3)**:人设选择、话题/风格、文案生成参数(高级设置 Accordion) +- **中栏(scale=4)**:文案输出区(标题、正文、标签、提示词)+ 文案操作按钮组 +- **右栏(scale=3)**:图片预览 Gallery + 图片操作按钮组(含美化强度滑块) + +三栏通过 `gr.Row()` 包裹三个 `gr.Column(scale=...)` 实现,所有高频操作 SHALL 在无需垂直滚动的情况下可见(≥1280px 宽度分辨率)。 + +#### Scenario: main.py/app.py 正常启动并显示内容创作 Tab +- **WHEN** 运行应用启动 Gradio +- **THEN** 内容创作 Tab 正常显示三栏布局,所有组件与迁移前功能一致 + +#### Scenario: tab_create 模块可独立导入 +- **WHEN** 在 Python 中执行 `from ui.tab_create import build_tab` +- **THEN** 不抛出任何导入错误,`build_tab` 为可调用对象 + +#### Scenario: 三栏布局在宽屏下无需滚动 +- **WHEN** 用户在 ≥1280px 宽度的浏览器中打开内容创作 Tab +- **THEN** 左栏参数、中栏文案输出、右栏图片预览 SHALL 同时可见,无需垂直滚动 + +#### Scenario: build_tab 返回字典 key 保持不变 +- **WHEN** `build_tab(...)` 被调用并返回字典 +- **THEN** 返回字典 SHALL 至少包含 `res_title`、`res_content`、`res_prompt`、`res_tags`、`quality_mode`、`steps`、`cfg_scale`、`neg_prompt` 等原有 key + +### Requirement: ui/ 目录结构规范 +`ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_.py`,不在 Tab 模块中直接调用全局服务初始化代码。 + +#### Scenario: 新增 Tab 模块的标准结构 +- **WHEN** 开发者创建新的 `ui/tab_*.py` 文件 +- **THEN** 该文件导出 `build_tab(...)` 函数,且顶层不包含副作用代码 + +### Requirement: 自动运营 Tab 采用卡片式两列网格布局 +「🤖 自动运营」Tab 内的各自动化任务 SHALL 以 **2 列 × N 行** 的卡片网格展示,每个任务使用 `gr.Group()` 包裹,卡片内 SHALL 包含:任务名称标题、启用开关(`gr.Checkbox`)、执行间隔(`gr.Number` 或 `gr.Slider`)、上次执行时间(`gr.Markdown`)。 + +#### Scenario: 自动运营任务以两列网格显示 +- **WHEN** 用户切换到「🤖 自动运营」Tab +- **THEN** 各自动化任务(自动评论、自动点赞、自动收藏、自动发布、自动回复等)SHALL 以两列卡片网格排列,每列宽度相等 + +#### Scenario: 每张卡片包含完整任务控制 +- **WHEN** 用户查看某个任务卡片 +- **THEN** 卡片内 SHALL 显示任务开关、执行间隔设置项,功能与原单列布局完全一致 + +#### Scenario: 自定义 CSS 增强视觉效果 +- **WHEN** 应用加载完成 +- **THEN** `gr.Blocks(css=...)` SHALL 注入自定义样式,包含:字体层级优化、按钮圆角(≥6px)、`gr.Group` 卡片轻微阴影(`box-shadow`),不破坏 Gradio 内部组件样式 diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-tabs-split/spec.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-tabs-split/spec.md new file mode 100644 index 0000000..46b88ff --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/specs/ui-tabs-split/spec.md @@ -0,0 +1,38 @@ +## MODIFIED Requirements + +### Requirement: 剩余 Gradio Tab 提取为独立 UI 模块 +系统 SHALL 将 `ui/app.py` 中所有 Gradio Tab 按如下顺序排列,且顶部 SHALL 不存在任何全局设置折叠块(`gr.Accordion`): + +| 序号 | Tab 名称 | 模块/说明 | +|------|----------|-----------| +| 0 | ✍️ 内容创作 | `ui/tab_create.py` | +| 1 | 📅 内容排期 | 内联或 `ui/tab_queue.py` | +| 2 | 🔥 热点探测 | 内联或 `ui/tab_hotspot.py` | +| 3 | 💬 评论管家 | 内联或 `ui/tab_engage.py` | +| 4 | 📊 数据看板 | 内联或 `ui/tab_analytics.py` | +| 5 | 🧠 智能学习 | 内联或 `ui/tab_learn.py` | +| 6 | 🤖 自动运营 | 内联或 `ui/tab_auto.py` | +| 7 | 🔐 账号登录 | 内联或 `ui/tab_profile.py` | +| 8 | ⚙️ 配置 | 内联(含原全局设置所有组件) | + +每个 Tab 模块 SHALL 暴露 `build_tab(...)` 函数,接受所需组件引用和回调函数作为参数。 + +#### Scenario: 每个 Tab 模块暴露 build_tab 函数 +- **WHEN** `ui/app.py` 执行 `from ui.tab_create import build_tab` +- **THEN** 调用 `build_tab(...)` 后 SHALL 返回包含需跨 Tab 共享组件的 dict + +#### Scenario: build_tab 接收回调而非直接导入 services +- **WHEN** `build_tab(...)` 内部需要调用业务函数时 +- **THEN** 业务函数 SHALL 通过 `fn_*` 参数传入,不在 `ui/tab_*.py` 内直接 `import services.*` + +#### Scenario: 事件绑定在 build_tab 内完成 +- **WHEN** `build_tab(...)` 被调用 +- **THEN** 本 Tab 所有 Gradio 组件的 `.click()`、`.change()` 等事件绑定 SHALL 在函数内完成 + +#### Scenario: 内容创作 Tab 为首个 Tab(索引 0) +- **WHEN** 用户打开应用 +- **THEN** 默认激活的 Tab SHALL 为「✍️ 内容创作」,用户无需额外点击即可开始创作工作流 + +#### Scenario: 配置和账号 Tab 位于末尾 +- **WHEN** 用户查看 Tab 导航栏 +- **THEN** 「🔐 账号登录」SHALL 位于倒数第二位,「⚙️ 配置」SHALL 位于最末位,低频操作不干扰主工作区 diff --git a/openspec/changes/archive/2026-02-26-improve-ui-layout/tasks.md b/openspec/changes/archive/2026-02-26-improve-ui-layout/tasks.md new file mode 100644 index 0000000..ffb9d99 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-improve-ui-layout/tasks.md @@ -0,0 +1,44 @@ +## 1. CSS 主题层注入 + +- [x] 1.1 在 `ui/app.py` 顶部定义 `_GRADIO_CSS` 字符串常量,内容包含:正文字体栈(Inter/system-ui)、按钮圆角(border-radius 6px)、`gr.Group` 轻阴影(box-shadow) +- [x] 1.2 将 `gr.Blocks(title=...)` 改为 `gr.Blocks(title=..., css=_GRADIO_CSS)` 以注入自定义样式 +- [x] 1.3 验证应用启动后样式生效,按钮无变形,内部组件(textarea/input)样式不被破坏 + +## 2. 全局设置迁移至「⚙️ 配置」Tab + +- [x] 2.1 删除 `ui/app.py` 中顶部的 `with gr.Accordion("⚙️ 全局设置 (自动保存)", open=False):` 折叠块(约第 79-186 行) +- [x] 2.2 将所有全局组件声明(`llm_provider`、`llm_model`、`btn_connect_llm`、`sd_url`、`sd_model`、`mcp_url`、`persona`、`face_image_input`、`face_image_preview`、`face_swap_toggle`、`status_bar` 等)整体移入新「⚙️ 配置」Tab 内 +- [x] 2.3 在 `gr.Tabs()` 末尾新增 `with gr.Tab("⚙️ 配置"):` 并将步骤 2.2 的组件放入,保留所有变量名和事件绑定不变 +- [x] 2.4 验证「⚙️ 配置」Tab 内所有组件正常显示,LLM 连接、SD 连接、换脸头像上传功能正常 +- [x] 2.5 验证「内容创作」等其他 Tab 中使用 `llm_model`、`sd_model`、`persona`、`status_bar` 的事件绑定仍正常工作 + +## 3. Tab 顺序重排 + +- [x] 3.1 调整 `ui/app.py` 中 `gr.Tabs()` 内各 Tab 的声明顺序为:⚙️ 配置(首位,selected=1 默认激活内容创作) ✍️ 内容创作 📅 内容排期 🔥 热点探测 💬 评论管家 📊 数据看板 🧠 智能学习 🤖 自动运营 🔐 账号登录 +- [x] 3.2 验证应用启动默认激活 Tab 为「✍️ 内容创作」(通过 selected=1 实现) + +## 4. 内容创作 Tab 三栏布局调整 + +- [x] 4.1 在 `ui/tab_create.py` 中,将三个 `gr.Column` 的比例改为 `scale=3`(左栏)、`scale=4`(中栏)、`scale=3`(右栏) +- [x] 4.2 左栏(scale=3)包含参数配置:人设、话题、风格、生成按钮等 +- [x] 4.3 中栏(scale=4)包含文案输出:标题、正文、标签、提示词 Textbox +- [x] 4.4 右栏(scale=3)包含图片预览及图片操作按钮 +- [x] 4.5 验证 1280px 宽度下三栏同时可见,无需垂直滚动 + +## 5. 自动运营 Tab 调度卡片网格重构 + +- [x] 5.1 在「🤖 自动运营」Tab 右栏(定时自动化)中,将垂直堆叠的 5 个 `gr.Group` 改为 3 行 2 列网格(3 个 `gr.Row()`,每行两个 `gr.Column(scale=1)` 包裹卡片);右栏 scale 扩大至 2 +- [x] 5.2 每个调度卡片的 `gr.Group` 内增加 `gr.Markdown("##### 任务名")` 小标题,卡片视觉更清晰 +- [x] 5.3 验证 5 个定时调度卡片的开关、间隔设置、启动/停止按钮功能正常 + +## 6. 按钮 variant 分级统一 + +- [x] 6.1 检查 `ui/app.py` 和 `ui/tab_create.py` 中所有 `gr.Button`:主操作使用 `variant="primary"`(连接/生成/启动),删除/停止/危险操作使用 `variant="stop"`(`btn_del_provider`、`btn_logout`、`btn_queue_stop`、`btn_queue_delete`、`btn_clear_log`、`btn_learn_stop`、`btn_stop_sched`、`btn_queue_reject`),次要操作不设 variant 或使用默认 +- [x] 6.2 确认「⚙️ 配置」Tab 内的「🗑️ 删除当前提供商」按钮使用 `variant="stop"` + +## 7. 回归验证 + +- [ ] 7.1 启动应用,确认首屏直接显示「✍️ 内容创作」Tab,无顶部折叠块 +- [ ] 7.2 完整走通「文案生成 图片生成 发布」流程,验证所有功能正常 +- [ ] 7.3 切换到「⚙️ 配置」Tab,连接 LLM 和 SD,确认 `status_bar` 正常更新 +- [ ] 7.4 切换到「🤖 自动运营」Tab,检查调度卡片网格布局正常,执行一次单次任务 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/.openspec.yaml b/openspec/changes/archive/2026-02-26-optimize-image-generation/.openspec.yaml new file mode 100644 index 0000000..e331c97 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-25 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/design.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/design.md new file mode 100644 index 0000000..dd4b9ea --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/design.md @@ -0,0 +1,86 @@ +## Context + +当前系统使用 Stable Diffusion WebUI API 生成图片,通过 `sd_service.py` 中的 `SD_MODEL_PROFILES` 静态配置各模型的参数预设(快速/标准/精细三档)和 prompt 增强词。图片生成后经 `anti_detect_postprocess()` 做反 AI 检测后处理。`llm_service.py` 的 `get_sd_prompt_guide()` 向 LLM 提供绘图 Prompt 写作指南。 + +**现状痛点:** +- 各模型的"精细"档步数(35-40步)和分辨率偏低,SDXL 模型未充分利用 Hires Fix 提升细节 +- `prompt_prefix` 中的中国人面孔特征词偏弱或措辞泛化(如 `asian girl` 无权重),欧美面孔偏向风险未完全规避 +- `anti_detect_postprocess` 将噪声扰动与美化处理混在一起,无法独立调节美化强度 +- LLM Prompt 指南缺少针对人物真实感/中国审美的专项指导词模板 + +**约束:** +- 不引入新的外部 Python 依赖 +- Hires Fix 仅对 SDXL 模型启用(SD 1.5 显存不足) +- 现有 `txt2img` API 接口签名不做 breaking change,新参数均使用默认值向后兼容 + +## Goals / Non-Goals + +**Goals:** +- 为各模型新增"高画质"预设档,SDXL 启用 Hires Fix 参数 +- 系统性强化各模型 `prompt_prefix` 中的中国人审美特征词(精致五官、白皙透亮、杏眼、减龄感、中式气质) +- 将 `anti_detect_postprocess` 拆分为独立的 `beauty_enhance()` + `anti_detect_postprocess()` 两段,支持通过 `enhance_level` 参数控制美化强度 +- 扩充 LLM Prompt 指南中的人物真实感描述规则,加入中国审美专项指导词模板 +- 在 UI 绘图参数面板新增"美化强度"控件,通过 `services/content.py` 传递至后处理管线 + +**Non-Goals:** +- 不新增 SD 模型支持(不修改模型识别逻辑) +- 不做图片超分(Real-ESRGAN 等外部工具) +- 不修改 ReActor 换脸逻辑 +- 不改变发布流程或队列逻辑 + +## Decisions + +### 决策 1:Hires Fix 仅对 SDXL 模型启用 + +**选择:** 在 `txt2img` payload 中,当模型 `arch == "sdxl"` 且 `quality_mode` 为高画质档时注入 Hires Fix 参数(`enable_hr: true`, `hr_scale: 1.5`, `hr_upscaler: "4x-UltraSharp"`, `denoising_strength: 0.4`)。 + +**理由:** SD 1.5 模型显存通常不足以做 Hires Fix,SDXL 的 832×1216 基础分辨率放大 1.5x 可达 1248×1824,大幅提升皮肤/五官细节,代价是生成时间增加约 50%。 + +**替代方案:** 后处理做 PIL 超分 → 效果差、无 AI 重绘细节;全模型启用 → 触发 SD 1.5 OOM 风险。 + +--- + +### 决策 2:beauty_enhance 作为独立函数,enhance_level 控制强度 + +**选择:** 将原 `anti_detect_postprocess` 中的美化逻辑(锐化、色彩增强)抽取为 `beauty_enhance(img, level: float = 1.0) -> Image`,`level=0` 跳过,`level=1` 为默认,`level=2` 为强化。`anti_detect_postprocess` 保持仅做扰动逻辑,调用顺序:`beauty_enhance → anti_detect_postprocess`。 + +**理由:** 当前两者混在同一函数中,美化参数硬编码无法从 UI 调节;分离后可独立测试,且 UI 控件可直接映射到 `enhance_level`。 + +**替代方案:** 在 `anti_detect_postprocess` 中增加 flag 参数 → 函数职责混乱,不符合单一职责原则。 + +--- + +### 决策 3:中国审美词以"权重词组"方式注入 prompt_prefix + +**选择:** 在各模型 `prompt_prefix` 中为中国人面孔特征词加入权重括号,如 `(almond eyes:1.2)`, `(delicate nose:1.1)`, `(porcelain skin:1.2)`, `(youthful appearance:1.1)`, `(chinese beauty:1.2)`,同时在 `negative_prompt` 中补充 `deep-set eyes, strong jawline, prominent brow ridge` 等欧美特征排除词。 + +**理由:** SD 权重词是最直接有效的方式;不引入新依赖;各模型独立配置可针对其风格微调权重值。 + +--- + +### 决策 4:LLM Prompt 指南新增中国审美人物描述模板 + +**选择:** 在 `get_sd_prompt_guide()` 中新增"人物描述词库"段落,按五官/肤色/气质/发型四个维度分别给出推荐词,并加一条规则:描述人物时优先从该词库选词,禁用 `beautiful girl` 等无导向泛化词。 + +**理由:** LLM 倾向于使用泛化美感词,不加约束时易生成欧美审美偏向的描述,规则化词库可强制对齐目标审美。 + +## Risks / Trade-offs + +- **[风险] Hires Fix 生成时间大幅增加** → 缓解:仅在新增"极致 (约5分钟)"档才默认开启,其他档保持原速;UI 标注预计用时 +- **[风险] 中国审美词过强导致所有图片千人一面** → 缓解:权重控制在 1.1-1.2 之间,不超过 1.3;`PERSONA_SD_PROFILES` 各人设保留差异化风格词 +- **[风险] beauty_enhance 引入 numpy 依赖(实际已存在)** → 无风险,现有代码已 `import numpy` +- **[Trade-off] enhance_level UI 控件增加 UI 复杂度** → 接受:放在"高级设置"面板折叠区,不影响主流程 + +## Migration Plan + +1. 修改 `sd_service.py`:更新各模型 `prompt_prefix`/`negative_prompt`,新增高画质预设,拆分 `beauty_enhance` +2. 修改 `llm_service.py`:更新 `get_sd_prompt_guide()` 指南内容 +3. 修改 `services/content.py`:`generate_images` 新增 `enhance_level` 参数(默认 `1.0`,向后兼容) +4. 修改 `ui/tab_create.py`:在"高级设置"中新增"美化强度"滑块,范围 0.0-2.0,默认 1.0 + +无数据库迁移,无配置文件格式变更。回滚:git revert 即可。 + +## Open Questions + +- Hires Fix 的 `hr_upscaler` 在用户环境中是否普遍可用(需确认 "4x-UltraSharp" 模型是否已下载);若不存在应降级为 "Latent" 作为 fallback +- `beauty_enhance` 的最优参数值(锐化半径、色彩增益系数)需在实际出图中调试确认 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/proposal.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/proposal.md new file mode 100644 index 0000000..0db172b --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/proposal.md @@ -0,0 +1,31 @@ +## Why + +当前图片生成管线存在两个核心问题:一是 SD 参数预设偏保守(步数少、CFG 偏低),LLM 绘图 Prompt 对真实感与美感的引导词不够精准,后处理未做美化增强直接引入扰动,导致出图偏平、细节不足;二是人物面孔与气质的正向引导词缺乏针对中国审美的专项优化(精致五官、白皙透亮肤色、杏眼桃花眼、减龄气质、中式温柔感等小红书目标用户偏好特征),导致生成人物不够符合用户审美认同。现在正是系统内容数量稳定增长、需要提升质量口碑与用户共鸣的时机。 + +## What Changes + +- 新增"高画质"生成预设,提升各模型的步数、采样器(DPM++ 2M SDE Karras 等),增加 Hires Fix / 放大修复支持 +- 优化 LLM 生成绘图 Prompt 的系统指导词,注入更多真实感/光影/皮肤细节关键词,区分人物与场景图风格指导 +- 在各模型 `prompt_prefix` 与 `PERSONA_SD_PROFILES` 中系统性强化中国人审美特征词(精致五官、白皙透亮、杏眼/桃花眼、减龄感、中式气质),并剔除容易偏向欧美面孔的泛化词 +- 将后处理管线拆解为"美化增强"(锐化、肤色修正、色彩提升)→ "反 AI 检测扰动"两个独立阶段,确保美化先于扰动执行 +- 在 `services/content.py` 的 `generate_images` 调用链中暴露"美化强度"参数,传递至后处理管线 + +## Capabilities + +### New Capabilities + +- `image-quality-presets`:各 SD 模型的高画质预设参数(高步数、高分辨率、Hires Fix 支持),以及预设选择逻辑优化 +- `image-post-enhancement`:生成后专用美化增强管线(智能锐化、肤色色调微调、饱和度提升),在反 AI 扰动之前执行 +- `llm-prompt-aesthetics`:LLM 绘图 Prompt 生成指南的美感/真实感强化规则,针对人物与场景分别给出增强词模板 +- `chinese-aesthetic-profile`:针对中国审美偏好的人物面孔与气质关键词体系,包含模型级 `prompt_prefix` 增强词库和各人设档位的面孔风格词,覆盖"精致五官、白皙透亮、减龄感、中式气质"等维度 + +### Modified Capabilities + +(无现有 spec 涉及图片生成质量,无需修改现有 spec) + +## Impact + +- **`sd_service.py`**:`SD_MODEL_PROFILES` 各模型的 `presets` 字段(新增高画质档)、`prompt_prefix`/`negative_prompt`(中国审美词优化)、`PERSONA_SD_PROFILES` 各人设的 `prompt_boost`(面孔风格词强化)、`txt2img` 方法(Hires Fix 参数注入)、`anti_detect_postprocess`(拆分为 beauty_enhance + anti_detect 两段) +- **`llm_service.py`**:`get_sd_prompt_guide()` 方法中的系统指导词内容(含中国审美人物描述指南) +- **`services/content.py`**:`generate_images()` 函数签名新增 `enhance_level` 参数 +- **`ui/tab_create.py`**:绘图参数面板新增"美化强度"滑块控件 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/chinese-aesthetic-profile/spec.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/chinese-aesthetic-profile/spec.md new file mode 100644 index 0000000..33094bf --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/chinese-aesthetic-profile/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: 各 SD 模型 prompt_prefix 包含中国审美面孔特征词 +系统 SHALL 在 `SD_MODEL_PROFILES` 每个模型的 `prompt_prefix` 中包含以下中国审美特征词(具体权重值由各模型风格微调,但不得省略): +- 眼型:`(almond eyes:1.2)` 或 `(delicate almond-shaped eyes:1.2)` +- 肤色:`(porcelain skin:1.2)` 或 `(fair porcelain skin:1.2)` 或 `(luminous fair skin:1.2)` +- 五官精致感:`(delicate facial features:1.2)` 或 `(refined features:1.2)` +- 气质:`(youthful appearance:1.1)` 或 `(elegant temperament:1.1)` + +#### Scenario: 生成的人物图像呈现中国大众偏好的精致感 +- **WHEN** 使用任意 SD 模型生成含人物的图片 +- **THEN** `prompt_prefix` 中包含杏眼、白皙肤色、精致五官三类词汇中各至少一个 + +### Requirement: 各 SD 模型 negative_prompt 补充欧美面孔排除词 +系统 SHALL 在 `SD_MODEL_PROFILES` 每个模型的 `negative_prompt` 中包含以下欧美面孔特征排除词(在现有基础上补充): +- `strong jawline`、`prominent brow ridge`、`deep-set eyes`(如已存在则保留权重) +- `angular facial structure`、`square jaw`、`heavy brow` + +#### Scenario: negative_prompt 明确排除欧美面孔结构特征 +- **WHEN** 任意模型进行人物图片生成 +- **THEN** negative_prompt 中包含 `strong jawline`、`prominent brow ridge`、`deep-set eyes` 至少两个排除词 + +### Requirement: PERSONA_SD_PROFILES 各人设包含中式气质差异化增强词 +系统 SHALL 在 `PERSONA_SD_PROFILES` 中,每个人设的 `prompt_boost` 包含至少 1 个与中国审美相关的气质词,且各人设之间的气质词须有差异化区分(如:甜妹-减龄感、知性-优雅气质、赛博博主-精致感 + 未来感)。 + +#### Scenario: 不同人设生成的人物具有可感知的气质差异 +- **WHEN** 分别以"甜妹"和"知性"人设各生成一批图片 +- **THEN** 两批图片中 `prompt_boost` 的核心气质词不重复,体现差异化风格 + +### Requirement: 美化增强对肤色偏向做修正 +`beauty_enhance()` 函数 SHALL 在饱和度提升时对色调偏暖方向微调(暖白/自然肤色),避免饱和度提升导致肤色偏黄或偏红。具体实现通过调整 PIL 色调(Hue)向暖白方向 ±5° 以内微调。 + +#### Scenario: 美化后肤色不发黄不发红 +- **WHEN** 调用 `beauty_enhance(img, level=1.0)` 对含人物的图片处理 +- **THEN** 输出图片肤色区域饱和度提升的同时,色调保持在自然暖白范围内(目视无偏黄/偏红现象) diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-post-enhancement/spec.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-post-enhancement/spec.md new file mode 100644 index 0000000..17995e4 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-post-enhancement/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: beauty_enhance 作为独立美化增强函数 +系统 SHALL 在 `sd_service.py` 中提供 `beauty_enhance(img: Image.Image, level: float = 1.0) -> Image.Image` 函数,支持以下增强操作(所有操作的强度随 `level` 线性缩放): +- 智能锐化(基于 `ImageFilter.UnsharpMask`,强调五官轮廓与发丝细节) +- 亮度与对比度微增(`level=1.0` 时各 +2-3%,`level=2.0` 时各 +4-6%) +- 饱和度提升(`level=1.0` 时 +5%,`level=2.0` 时 +10%,令肤色更均匀饱满) +- `level=0` 时 SHALL 直接返回原图,跳过所有处理 + +#### Scenario: 正常调用增强管线 +- **WHEN** 调用 `beauty_enhance(img, level=1.0)` +- **THEN** 返回经过锐化、亮度微调、饱和度提升处理的 PIL Image,图片尺寸不变 + +#### Scenario: level=0 时跳过处理 +- **WHEN** 调用 `beauty_enhance(img, level=0)` +- **THEN** 直接返回原始 img 对象,不执行任何增强操作 + +#### Scenario: level=2 时增强效果加倍 +- **WHEN** 调用 `beauty_enhance(img, level=2.0)` +- **THEN** 锐化、亮度、饱和度的调整幅度均为 level=1.0 时的 2 倍 + +### Requirement: 后处理管线顺序为美化先于反 AI 扰动 +系统 SHALL 在 `txt2img` 和 `img2img` 生成流程中,对每张输出图片依次执行:`beauty_enhance(img, level) → anti_detect_postprocess(img)`,确保美化增强在扰动引入之前完成。 + +#### Scenario: 生成图片经过完整两阶段后处理 +- **WHEN** `txt2img` 成功生成图片 +- **THEN** 每张图片先经过 `beauty_enhance`,再经过 `anti_detect_postprocess`,最终返回给调用方 + +### Requirement: enhance_level 参数从 UI 传递至后处理管线 +系统 SHALL 支持 `enhance_level: float` 参数从 Gradio UI 经 `services/content.py` 的 `generate_images()` 函数传递至 `SDService.txt2img()`,最终传入 `beauty_enhance()`。新参数默认值为 `1.0`,向后兼容。 + +#### Scenario: UI 美化强度滑块值传递到生成结果 +- **WHEN** 用户在"高级设置"中将美化强度滑块调整为 2.0 并点击生成 +- **THEN** 生成图片经过 `beauty_enhance(img, level=2.0)` 处理 + +#### Scenario: 旧调用方不传 enhance_level 时行为不变 +- **WHEN** `generate_images()` 未传入 `enhance_level` 参数 +- **THEN** 默认使用 `level=1.0`,行为与优化前相同 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-quality-presets/spec.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-quality-presets/spec.md new file mode 100644 index 0000000..c8f689b --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/image-quality-presets/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: 各 SD 模型新增高画质预设档 +系统 SHALL 在 `SD_MODEL_PROFILES` 的每个模型 `presets` 字典中新增 `"高画质 (约5分钟)"` 档位,参数须满足:SD 1.5 模型步数 ≥ 50、CFG 6.5-7.5、采样器为 `DPM++ SDE`;SDXL 模型步数 ≥ 40、CFG 5.5-6.5、采样器为 `DPM++ 2M SDE`,并启用 Hires Fix 参数(`enable_hr: true`)。 + +#### Scenario: 用户选择高画质档后生成请求包含 Hires Fix +- **WHEN** 用户将质量模式切换为"高画质 (约5分钟)"且当前模型为 SDXL 架构 +- **THEN** `txt2img` API 请求 payload 中包含 `enable_hr: true`、`hr_scale: 1.5`、`hr_upscaler` 字段 + +#### Scenario: SD 1.5 模型高画质档不启用 Hires Fix +- **WHEN** 用户将质量模式切换为"高画质 (约5分钟)"且当前模型 `arch == "sd15"` +- **THEN** payload 中不含 `enable_hr` 字段,以避免 OOM + +#### Scenario: Hires Fix upscaler 不存在时降级 +- **WHEN** `hr_upscaler` 指定的放大器模型(如 "4x-UltraSharp")在 SD WebUI 中不可用 +- **THEN** 系统 SHALL 自动降级为 `"Latent"` 作为 fallback,并通过日志记录警告 + +### Requirement: 预设名称在 UI 中按模型动态更新 +系统 SHALL 在用户切换 SD 模型后,更新 UI 质量模式单选按钮的选项列表,使其始终反映当前模型的可用预设档名称。 + +#### Scenario: 切换模型后预设列表刷新 +- **WHEN** 用户在连接设置中切换 SD 模型 +- **THEN** 创作页"生成模式"单选按钮的选项 SHALL 更新为该模型 `presets` 中的键名列表 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/llm-prompt-aesthetics/spec.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/llm-prompt-aesthetics/spec.md new file mode 100644 index 0000000..07b50f1 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/specs/llm-prompt-aesthetics/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: LLM Prompt 指南包含人物真实感描述规则 +`get_sd_prompt_guide()` 返回的指南 SHALL 包含"人物描述规则"段落,内容要求: +- 明确禁止使用 `beautiful girl`、`pretty woman`、`good looking` 等无导向泛化词 +- 要求从五官(眼型、鼻型、唇型)、肤色(白皙、透亮、均匀)、气质(减龄、温柔、知性)三个维度分别提供具体描述词 +- 对于有人物的 Prompt,要求至少包含 2 个五官/肤色词和 1 个气质词 + +#### Scenario: LLM 生成含人物的 Prompt 时使用规范词汇 +- **WHEN** LLM 根据指南生成包含人物描述的 SD Prompt +- **THEN** Prompt 中不含 `beautiful`、`pretty`、`good looking` 等泛化词,而是包含如 `almond eyes`、`porcelain skin`、`youthful appearance` 等具体描述词 + +### Requirement: LLM Prompt 指南区分人物图与纯场景图的写作策略 +`get_sd_prompt_guide()` 返回的指南 SHALL 明确区分两种写作模式:人物主体图(需详细描述人物特征)和纯场景/物品图(无需人物词,优先描述氛围、色彩、光线)。 + +#### Scenario: 生成场景图 Prompt 时不误加人物词 +- **WHEN** 文案主题为室内布置或产品展示(无人物) +- **THEN** LLM 生成的 Prompt 中不包含人物特征描述词 + +#### Scenario: 生成人物图 Prompt 时包含完整人物描述 +- **WHEN** 文案主题为穿搭、美妆或生活场景(含人物) +- **THEN** LLM 生成的 Prompt 中包含五官、肤色、气质三个维度的描述词各至少 1 个 + +### Requirement: LLM Prompt 指南包含光影与摄影感描述规范 +`get_sd_prompt_guide()` 返回的指南 SHALL 包含光影描述规则,要求生成 Prompt 时指定具体光源类型(如 `soft window light`、`golden hour`、`studio lighting`)而非泛化词(如 `good lighting`)。 + +#### Scenario: Prompt 中包含具体光影词而非泛化词 +- **WHEN** LLM 根据指南生成 SD Prompt +- **THEN** Prompt 中包含 `soft window light`、`golden hour`、`studio lighting`、`diffused natural light` 等具体光影词之一 diff --git a/openspec/changes/archive/2026-02-26-optimize-image-generation/tasks.md b/openspec/changes/archive/2026-02-26-optimize-image-generation/tasks.md new file mode 100644 index 0000000..618dc85 --- /dev/null +++ b/openspec/changes/archive/2026-02-26-optimize-image-generation/tasks.md @@ -0,0 +1,50 @@ +## 1. sd_service.py — 中国审美词优化 + +- [x] 1.1 更新 `majicmixRealistic` 模型 `prompt_prefix`:添加 `(almond eyes:1.2)`、`(porcelain skin:1.2)`、`(youthful appearance:1.1)`,确保 `delicate facial features` 带权重 +- [x] 1.2 更新 `realisticVision` 模型 `prompt_prefix`:添加 `(almond eyes:1.1)`、`(luminous fair skin:1.2)`、`(refined features:1.2)`、`(elegant temperament:1.1)` +- [x] 1.3 更新 `juggernautXL` 模型 `prompt_prefix`:添加 `(almond eyes:1.2)`、`(porcelain skin:1.2)`、`(delicate facial features:1.2)`、`(youthful appearance:1.1)`,`asian girl` 改为 `(chinese beauty:1.2)` +- [x] 1.4 在三个模型 `negative_prompt` 中补充 `strong jawline, prominent brow ridge, angular facial structure, square jaw, heavy brow` +- [x] 1.5 更新各 `PERSONA_SD_PROFILES` 人设的 `prompt_boost`:确保每个人设包含至少 1 个与中国审美相关的差异化气质词(甜妹-减龄感、知性-优雅气质、赛博博主-精致感 + 未来感等) + +## 2. sd_service.py — 高画质预设与 Hires Fix + +- [x] 2.1 在 `majicmixRealistic` `presets` 中新增 `"高画质 (约5分钟)"`:steps=50, cfg_scale=7.0, width=640, height=960, sampler="DPM++ SDE", scheduler="Karras", batch_size=1 +- [x] 2.2 在 `realisticVision` `presets` 中新增 `"高画质 (约5分钟)"`:steps=50, cfg_scale=7.0, width=640, height=960, sampler="DPM++ SDE", scheduler="Karras", batch_size=1 +- [x] 2.3 在 `juggernautXL` `presets` 中新增 `"高画质 (约5分钟)"`:steps=40, cfg_scale=6.0, width=832, height=1216, sampler="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` +- [x] 2.4 在 `txt2img` 方法中:当 preset 包含 `enable_hr` 字段时,将该字段注入 SD API payload;当架构为 sd15 时强制忽略 `enable_hr` +- [x] 2.5 添加 Hires Fix upscaler fallback 逻辑:捕获 SD API 返回的 upscaler 不存在错误,降级为 `"Latent"` 并记录 warning 日志 + +## 3. sd_service.py — 美化增强管线拆分 + +- [x] 3.1 新增 `beauty_enhance(img: Image.Image, level: float = 1.0) -> Image.Image` 函数:`level=0` 时直接返回原图 +- [x] 3.2 实现 `beauty_enhance` 锐化逻辑:使用 `ImageFilter.UnsharpMask(radius=1.5*level, percent=int(120*level), threshold=2)` +- [x] 3.3 实现 `beauty_enhance` 亮度/对比度/饱和度增强:亮度系数 `1.0 + 0.02*level`,对比度系数 `1.0 + 0.02*level`,饱和度系数 `1.0 + 0.05*level` +- [x] 3.4 实现 `beauty_enhance` 暖白肤色微调:对皮肤色调区域做降红提蓝校正(numpy 实现,无 numpy 时跳过) +- [x] 3.5 修改 `txt2img` 方法:在 `anti_detect_postprocess(img)` 调用前插入 `beauty_enhance(img, level=enhance_level)`,`enhance_level` 参数从方法签名传入(默认 `1.0`) +- [x] 3.6 修改 `img2img` 方法:同样应用 `beauty_enhance → anti_detect_postprocess` 两阶段后处理 + +## 4. llm_service.py — Prompt 指南优化 + +- [x] 4.1 在 `get_sd_prompt_guide()` 中添加"人物描述规则"段落:明确禁止 `beautiful girl`/`pretty woman`/`good looking` 等泛化词 +- [x] 4.2 在指南中添加五官/肤色/气质三维度词库:眼型推荐词(`almond eyes`, `phoenix eyes`, `bright doe eyes`)、肤色推荐词(`porcelain skin`, `luminous fair skin`, `translucent skin`)、气质推荐词(`youthful`, `elegant temperament`, `gentle charm`, `intellectual beauty`) +- [x] 4.3 在指南中添加人物图 vs 纯场景图写作策略区分说明 +- [x] 4.4 在指南中添加光影具体描述规范:要求使用 `soft window light`/`golden hour`/`studio lighting`/`diffused natural light` 等词,禁用 `good lighting` + +## 5. services/content.py — 参数传递 + +- [x] 5.1 为 `generate_images()` 函数增加 `enhance_level: float = 1.0` 参数 +- [x] 5.2 将 `enhance_level` 传入 `svc.txt2img()` 调用 + +## 6. ui/tab_create.py — UI 控件 + +- [x] 6.1 在"高级设置 (覆盖预设)"折叠面板中新增 `enhance_level` 滑块:`gr.Slider(0.0, 2.0, value=1.0, step=0.1, label="美化强度", info="0=关闭 1=默认 2=强化")` +- [x] 6.2 将 `enhance_level` 滑块值存入配置(`fn_cfg_set("enhance_level", ...)`)并在加载时读取 +- [x] 6.3 将 `enhance_level` 加入 `btn_gen_img.click` 的 `inputs` 列表,并更新 `fn_gen_img` 函数签名 + +## 7. 验证与测试 + +- [ ] 7.1 生成含人物的测试图片:确认 `majicmixRealistic` 和 `juggernautXL` 输出人物具有明显中国面孔特征 +- [ ] 7.2 测试高画质档:SDXL 模型 API payload 中确认包含 `enable_hr: true` +- [ ] 7.3 测试 `beauty_enhance(img, level=0)` 返回原图不变 +- [ ] 7.4 测试 `beauty_enhance(img, level=2.0)` 效果目视无偏黄/偏红 +- [ ] 7.5 验证 `generate_images()` 不传 `enhance_level` 时行为与修改前一致(回归测试) diff --git a/openspec/specs/chinese-aesthetic-profile/spec.md b/openspec/specs/chinese-aesthetic-profile/spec.md new file mode 100644 index 0000000..33094bf --- /dev/null +++ b/openspec/specs/chinese-aesthetic-profile/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: 各 SD 模型 prompt_prefix 包含中国审美面孔特征词 +系统 SHALL 在 `SD_MODEL_PROFILES` 每个模型的 `prompt_prefix` 中包含以下中国审美特征词(具体权重值由各模型风格微调,但不得省略): +- 眼型:`(almond eyes:1.2)` 或 `(delicate almond-shaped eyes:1.2)` +- 肤色:`(porcelain skin:1.2)` 或 `(fair porcelain skin:1.2)` 或 `(luminous fair skin:1.2)` +- 五官精致感:`(delicate facial features:1.2)` 或 `(refined features:1.2)` +- 气质:`(youthful appearance:1.1)` 或 `(elegant temperament:1.1)` + +#### Scenario: 生成的人物图像呈现中国大众偏好的精致感 +- **WHEN** 使用任意 SD 模型生成含人物的图片 +- **THEN** `prompt_prefix` 中包含杏眼、白皙肤色、精致五官三类词汇中各至少一个 + +### Requirement: 各 SD 模型 negative_prompt 补充欧美面孔排除词 +系统 SHALL 在 `SD_MODEL_PROFILES` 每个模型的 `negative_prompt` 中包含以下欧美面孔特征排除词(在现有基础上补充): +- `strong jawline`、`prominent brow ridge`、`deep-set eyes`(如已存在则保留权重) +- `angular facial structure`、`square jaw`、`heavy brow` + +#### Scenario: negative_prompt 明确排除欧美面孔结构特征 +- **WHEN** 任意模型进行人物图片生成 +- **THEN** negative_prompt 中包含 `strong jawline`、`prominent brow ridge`、`deep-set eyes` 至少两个排除词 + +### Requirement: PERSONA_SD_PROFILES 各人设包含中式气质差异化增强词 +系统 SHALL 在 `PERSONA_SD_PROFILES` 中,每个人设的 `prompt_boost` 包含至少 1 个与中国审美相关的气质词,且各人设之间的气质词须有差异化区分(如:甜妹-减龄感、知性-优雅气质、赛博博主-精致感 + 未来感)。 + +#### Scenario: 不同人设生成的人物具有可感知的气质差异 +- **WHEN** 分别以"甜妹"和"知性"人设各生成一批图片 +- **THEN** 两批图片中 `prompt_boost` 的核心气质词不重复,体现差异化风格 + +### Requirement: 美化增强对肤色偏向做修正 +`beauty_enhance()` 函数 SHALL 在饱和度提升时对色调偏暖方向微调(暖白/自然肤色),避免饱和度提升导致肤色偏黄或偏红。具体实现通过调整 PIL 色调(Hue)向暖白方向 ±5° 以内微调。 + +#### Scenario: 美化后肤色不发黄不发红 +- **WHEN** 调用 `beauty_enhance(img, level=1.0)` 对含人物的图片处理 +- **THEN** 输出图片肤色区域饱和度提升的同时,色调保持在自然暖白范围内(目视无偏黄/偏红现象) diff --git a/openspec/specs/image-post-enhancement/spec.md b/openspec/specs/image-post-enhancement/spec.md new file mode 100644 index 0000000..17995e4 --- /dev/null +++ b/openspec/specs/image-post-enhancement/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: beauty_enhance 作为独立美化增强函数 +系统 SHALL 在 `sd_service.py` 中提供 `beauty_enhance(img: Image.Image, level: float = 1.0) -> Image.Image` 函数,支持以下增强操作(所有操作的强度随 `level` 线性缩放): +- 智能锐化(基于 `ImageFilter.UnsharpMask`,强调五官轮廓与发丝细节) +- 亮度与对比度微增(`level=1.0` 时各 +2-3%,`level=2.0` 时各 +4-6%) +- 饱和度提升(`level=1.0` 时 +5%,`level=2.0` 时 +10%,令肤色更均匀饱满) +- `level=0` 时 SHALL 直接返回原图,跳过所有处理 + +#### Scenario: 正常调用增强管线 +- **WHEN** 调用 `beauty_enhance(img, level=1.0)` +- **THEN** 返回经过锐化、亮度微调、饱和度提升处理的 PIL Image,图片尺寸不变 + +#### Scenario: level=0 时跳过处理 +- **WHEN** 调用 `beauty_enhance(img, level=0)` +- **THEN** 直接返回原始 img 对象,不执行任何增强操作 + +#### Scenario: level=2 时增强效果加倍 +- **WHEN** 调用 `beauty_enhance(img, level=2.0)` +- **THEN** 锐化、亮度、饱和度的调整幅度均为 level=1.0 时的 2 倍 + +### Requirement: 后处理管线顺序为美化先于反 AI 扰动 +系统 SHALL 在 `txt2img` 和 `img2img` 生成流程中,对每张输出图片依次执行:`beauty_enhance(img, level) → anti_detect_postprocess(img)`,确保美化增强在扰动引入之前完成。 + +#### Scenario: 生成图片经过完整两阶段后处理 +- **WHEN** `txt2img` 成功生成图片 +- **THEN** 每张图片先经过 `beauty_enhance`,再经过 `anti_detect_postprocess`,最终返回给调用方 + +### Requirement: enhance_level 参数从 UI 传递至后处理管线 +系统 SHALL 支持 `enhance_level: float` 参数从 Gradio UI 经 `services/content.py` 的 `generate_images()` 函数传递至 `SDService.txt2img()`,最终传入 `beauty_enhance()`。新参数默认值为 `1.0`,向后兼容。 + +#### Scenario: UI 美化强度滑块值传递到生成结果 +- **WHEN** 用户在"高级设置"中将美化强度滑块调整为 2.0 并点击生成 +- **THEN** 生成图片经过 `beauty_enhance(img, level=2.0)` 处理 + +#### Scenario: 旧调用方不传 enhance_level 时行为不变 +- **WHEN** `generate_images()` 未传入 `enhance_level` 参数 +- **THEN** 默认使用 `level=1.0`,行为与优化前相同 diff --git a/openspec/specs/image-quality-presets/spec.md b/openspec/specs/image-quality-presets/spec.md new file mode 100644 index 0000000..c8f689b --- /dev/null +++ b/openspec/specs/image-quality-presets/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: 各 SD 模型新增高画质预设档 +系统 SHALL 在 `SD_MODEL_PROFILES` 的每个模型 `presets` 字典中新增 `"高画质 (约5分钟)"` 档位,参数须满足:SD 1.5 模型步数 ≥ 50、CFG 6.5-7.5、采样器为 `DPM++ SDE`;SDXL 模型步数 ≥ 40、CFG 5.5-6.5、采样器为 `DPM++ 2M SDE`,并启用 Hires Fix 参数(`enable_hr: true`)。 + +#### Scenario: 用户选择高画质档后生成请求包含 Hires Fix +- **WHEN** 用户将质量模式切换为"高画质 (约5分钟)"且当前模型为 SDXL 架构 +- **THEN** `txt2img` API 请求 payload 中包含 `enable_hr: true`、`hr_scale: 1.5`、`hr_upscaler` 字段 + +#### Scenario: SD 1.5 模型高画质档不启用 Hires Fix +- **WHEN** 用户将质量模式切换为"高画质 (约5分钟)"且当前模型 `arch == "sd15"` +- **THEN** payload 中不含 `enable_hr` 字段,以避免 OOM + +#### Scenario: Hires Fix upscaler 不存在时降级 +- **WHEN** `hr_upscaler` 指定的放大器模型(如 "4x-UltraSharp")在 SD WebUI 中不可用 +- **THEN** 系统 SHALL 自动降级为 `"Latent"` 作为 fallback,并通过日志记录警告 + +### Requirement: 预设名称在 UI 中按模型动态更新 +系统 SHALL 在用户切换 SD 模型后,更新 UI 质量模式单选按钮的选项列表,使其始终反映当前模型的可用预设档名称。 + +#### Scenario: 切换模型后预设列表刷新 +- **WHEN** 用户在连接设置中切换 SD 模型 +- **THEN** 创作页"生成模式"单选按钮的选项 SHALL 更新为该模型 `presets` 中的键名列表 diff --git a/openspec/specs/llm-prompt-aesthetics/spec.md b/openspec/specs/llm-prompt-aesthetics/spec.md new file mode 100644 index 0000000..07b50f1 --- /dev/null +++ b/openspec/specs/llm-prompt-aesthetics/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: LLM Prompt 指南包含人物真实感描述规则 +`get_sd_prompt_guide()` 返回的指南 SHALL 包含"人物描述规则"段落,内容要求: +- 明确禁止使用 `beautiful girl`、`pretty woman`、`good looking` 等无导向泛化词 +- 要求从五官(眼型、鼻型、唇型)、肤色(白皙、透亮、均匀)、气质(减龄、温柔、知性)三个维度分别提供具体描述词 +- 对于有人物的 Prompt,要求至少包含 2 个五官/肤色词和 1 个气质词 + +#### Scenario: LLM 生成含人物的 Prompt 时使用规范词汇 +- **WHEN** LLM 根据指南生成包含人物描述的 SD Prompt +- **THEN** Prompt 中不含 `beautiful`、`pretty`、`good looking` 等泛化词,而是包含如 `almond eyes`、`porcelain skin`、`youthful appearance` 等具体描述词 + +### Requirement: LLM Prompt 指南区分人物图与纯场景图的写作策略 +`get_sd_prompt_guide()` 返回的指南 SHALL 明确区分两种写作模式:人物主体图(需详细描述人物特征)和纯场景/物品图(无需人物词,优先描述氛围、色彩、光线)。 + +#### Scenario: 生成场景图 Prompt 时不误加人物词 +- **WHEN** 文案主题为室内布置或产品展示(无人物) +- **THEN** LLM 生成的 Prompt 中不包含人物特征描述词 + +#### Scenario: 生成人物图 Prompt 时包含完整人物描述 +- **WHEN** 文案主题为穿搭、美妆或生活场景(含人物) +- **THEN** LLM 生成的 Prompt 中包含五官、肤色、气质三个维度的描述词各至少 1 个 + +### Requirement: LLM Prompt 指南包含光影与摄影感描述规范 +`get_sd_prompt_guide()` 返回的指南 SHALL 包含光影描述规则,要求生成 Prompt 时指定具体光源类型(如 `soft window light`、`golden hour`、`studio lighting`)而非泛化词(如 `good lighting`)。 + +#### Scenario: Prompt 中包含具体光影词而非泛化词 +- **WHEN** LLM 根据指南生成 SD Prompt +- **THEN** Prompt 中包含 `soft window light`、`golden hour`、`studio lighting`、`diffused natural light` 等具体光影词之一 diff --git a/openspec/specs/services-autostart/spec.md b/openspec/specs/services-autostart/spec.md new file mode 100644 index 0000000..ab4983d --- /dev/null +++ b/openspec/specs/services-autostart/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 开机自启管理函数迁移至独立模块 +系统 SHALL 将开机自启相关常量和函数从 `main.py` 提取至 `services/autostart.py`,包括:`_APP_NAME`、`_STARTUP_REG_KEY`、`_get_startup_script_path`、`_get_startup_bat_path`、`_create_startup_scripts`、`is_autostart_enabled`、`enable_autostart`、`disable_autostart`、`toggle_autostart`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.autostart import toggle_autostart, is_autostart_enabled` +- **THEN** 函数可正常调用 + +#### Scenario: Windows 注册表操作行为不变 +- **WHEN** `enable_autostart()` 在 Windows 系统上被调用 +- **THEN** SHALL 向注册表 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` 写入启动项,行为与迁移前完全一致 + +#### Scenario: 非 Windows 平台处理不变 +- **WHEN** `enable_autostart()` 在非 Windows 系统上被调用 +- **THEN** SHALL 返回与迁移前相同的平台不支持提示信息 diff --git a/openspec/specs/services-connection/spec.md b/openspec/specs/services-connection/spec.md new file mode 100644 index 0000000..94ae5dc --- /dev/null +++ b/openspec/specs/services-connection/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 连接管理函数迁移至独立模块 +系统 SHALL 将所有 LLM / SD / MCP 连接管理及认证相关函数从 `main.py` 提取至 `services/connection.py`,包括:`_get_llm_config`、`connect_llm`、`add_llm_provider`、`remove_llm_provider`、`on_provider_selected`、`connect_sd`、`on_sd_model_change`、`check_mcp_status`、`get_login_qrcode`、`logout_xhs`、`_auto_fetch_xsec_token`、`check_login`、`save_my_user_id`、`upload_face_image`、`load_saved_face_image`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.connection import connect_llm, connect_sd` 等导入 +- **THEN** 所有函数可正常调用,行为与迁移前完全一致 + +#### Scenario: 外部依赖通过参数传入 +- **WHEN** `services/connection.py` 中的函数需要访问 `cfg`、`llm`(`LLMService`)、`sd`(`SDService`)、`mcp`(`MCPClient`) +- **THEN** 这些依赖 SHALL 通过函数参数接收,`services/connection.py` 模块顶层不创建单例实例 + +#### Scenario: 无循环导入 +- **WHEN** Python 解释器加载 `services/connection.py` +- **THEN** 不产生 `ImportError` 或循环导入错误 diff --git a/openspec/specs/services-content/spec.md b/openspec/specs/services-content/spec.md new file mode 100644 index 0000000..d570fb8 --- /dev/null +++ b/openspec/specs/services-content/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 内容生成函数迁移至独立模块 +系统 SHALL 将内容生成、图片生成、发布及导出相关函数从 `main.py` 提取至 `services/content.py`,包括:`generate_copy`、`generate_images`、`one_click_export`、`publish_to_xhs`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.content import generate_copy, generate_images, publish_to_xhs, one_click_export` +- **THEN** 所有函数可正常调用,行为与迁移前完全一致 + +#### Scenario: 内容生成保留现有验证逻辑 +- **WHEN** 调用 `publish_to_xhs` 时标题超过 20 字或图片数量不合法 +- **THEN** 函数 SHALL 返回与迁移前相同的错误提示,不改变验证行为 + +#### Scenario: 临时文件清理逻辑保留 +- **WHEN** `publish_to_xhs` 执行完毕(成功或失败) +- **THEN** `finally` 块中的 AI 临时文件清理逻辑 SHALL 正常执行 diff --git a/openspec/specs/services-engagement/spec.md b/openspec/specs/services-engagement/spec.md new file mode 100644 index 0000000..40c5236 --- /dev/null +++ b/openspec/specs/services-engagement/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 互动自动化函数迁移至独立模块 +系统 SHALL 将评论、点赞、收藏、回复等互动自动化函数从 `main.py` 提取至 `services/engagement.py`,包括:`load_note_for_comment`、`ai_generate_comment`、`send_comment`、`fetch_my_notes`、`on_my_note_selected`、`fetch_my_note_comments`、`ai_reply_comment`、`send_reply`、`auto_comment_once`、`_auto_comment_with_log`、`auto_like_once`、`_auto_like_with_log`、`auto_favorite_once`、`_auto_favorite_with_log`、`auto_reply_once`、`_auto_reply_with_log`、`_auto_publish_with_log`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.engagement import auto_comment_once, auto_like_once` 等导入 +- **THEN** 所有函数可正常调用 + +#### Scenario: 日志回调参数化 +- **WHEN** `engagement.py` 中的 `_with_log` 函数需要追加日志时 +- **THEN** 函数 SHALL 接收 `log_fn` 参数(callable)用于写入日志,不直接依赖外部 `_auto_log` 列表 + +#### Scenario: 频率限制集成 +- **WHEN** `auto_comment_once` 等函数执行前需要检查每日限额和冷却状态 +- **THEN** 通过调用 `rate_limiter` 模块中的函数实现,不在 `engagement.py` 内复制限流逻辑 diff --git a/openspec/specs/services-hotspot/spec.md b/openspec/specs/services-hotspot/spec.md new file mode 100644 index 0000000..9707691 --- /dev/null +++ b/openspec/specs/services-hotspot/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: 热点探测函数迁移至独立模块 +系统 SHALL 将热点搜索与分析相关函数从 `main.py` 提取至 `services/hotspot.py`,包括:`search_hotspots`、`analyze_and_suggest`、`generate_from_hotspot`、`_set_cache`、`_get_cache`、`_fetch_and_cache`、`_pick_from_cache`、`fetch_proactive_notes`、`on_proactive_note_selected`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.hotspot import search_hotspots, analyze_and_suggest` 等导入 +- **THEN** 所有函数可正常调用 + +#### Scenario: 线程安全缓存随模块迁移 +- **WHEN** `_cache_lock`(`threading.RLock`)随函数一起迁移至 `services/hotspot.py` +- **THEN** `_set_cache` / `_get_cache` 的线程安全行为保持不变 diff --git a/openspec/specs/services-persona/spec.md b/openspec/specs/services-persona/spec.md new file mode 100644 index 0000000..904769e --- /dev/null +++ b/openspec/specs/services-persona/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 人设管理函数及常量迁移至独立模块 +系统 SHALL 将人设相关的常量和函数从 `main.py` 提取至 `services/persona.py`,包括:`DEFAULT_PERSONAS`、`RANDOM_PERSONA_LABEL`、`PERSONA_POOL_MAP`、`DEFAULT_TOPICS`、`DEFAULT_STYLES`、`DEFAULT_COMMENT_KEYWORDS`、`_match_persona_pools`、`get_persona_topics`、`get_persona_keywords`、`on_persona_changed`、`_resolve_persona`。 + +#### Scenario: 常量可从模块导入 +- **WHEN** `main.py` 执行 `from services.persona import DEFAULT_PERSONAS, PERSONA_POOL_MAP` +- **THEN** 常量值 SHALL 与迁移前完全一致 + +#### Scenario: 人设解析正确处理随机人设标签 +- **WHEN** `_resolve_persona(RANDOM_PERSONA_LABEL)` 被调用 +- **THEN** SHALL 返回从人设池中随机选取的人设文本,行为与迁移前一致 + +#### Scenario: 人设变更回调正常触发 +- **WHEN** `on_persona_changed(persona_text)` 被调用 +- **THEN** SHALL 返回更新后的话题列表和关键词列表,供 Gradio UI 使用 diff --git a/openspec/specs/services-profile/spec.md b/openspec/specs/services-profile/spec.md new file mode 100644 index 0000000..ab39ab8 --- /dev/null +++ b/openspec/specs/services-profile/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: 用户主页解析函数迁移至独立模块 +系统 SHALL 将用户主页数据获取与解析相关函数从 `main.py` 提取至 `services/profile.py`,包括:`_parse_profile_json`、`_parse_count`、`fetch_my_profile`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.profile import fetch_my_profile` +- **THEN** 函数可正常调用,行为与迁移前一致 + +#### Scenario: 解析容错性保留 +- **WHEN** `_parse_count` 接收到格式异常的数值字符串(如 "1.2万"、"--") +- **THEN** SHALL 返回与迁移前相同的浮点数或 0,不抛出异常 diff --git a/openspec/specs/services-queue/spec.md b/openspec/specs/services-queue/spec.md new file mode 100644 index 0000000..54e3376 --- /dev/null +++ b/openspec/specs/services-queue/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 排期队列操作函数迁移至独立模块 +系统 SHALL 将内容排期队列相关函数从 `main.py` 提取至 `services/queue_ops.py`,包括:`generate_to_queue`、`_queue_publish_callback`、`queue_refresh_table`、`queue_refresh_calendar`、`queue_preview_item`、`queue_approve_item`、`queue_reject_item`、`queue_delete_item`、`queue_retry_item`、`queue_publish_now`、`queue_start_processor`、`queue_stop_processor`、`queue_get_status`、`queue_batch_approve`、`queue_generate_and_refresh`。 + +#### Scenario: 模块导入成功 +- **WHEN** `main.py` 执行 `from services.queue_ops import queue_generate_and_refresh, queue_refresh_table` 等导入 +- **THEN** 所有函数可正常调用 + +#### Scenario: publish callback 在 main.py 完成注册 +- **WHEN** 应用启动时 `main.py` 调用 `pub_queue.set_publish_callback(_queue_publish_callback)`(`_queue_publish_callback` 已迁移至 `queue_ops.py`) +- **THEN** 队列发布回调 SHALL 正常注册并在队列处理时触发 + +#### Scenario: 队列操作读写 pub_queue 单例 +- **WHEN** `queue_ops.py` 中的函数需要访问 `pub_queue` 或 `queue_publisher` +- **THEN** 这些单例 SHALL 通过函数参数传入,不在 `queue_ops.py` 模块顶层初始化 diff --git a/openspec/specs/services-rate-limiter/spec.md b/openspec/specs/services-rate-limiter/spec.md new file mode 100644 index 0000000..45a26c0 --- /dev/null +++ b/openspec/specs/services-rate-limiter/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 频率控制与每日限额函数迁移至独立模块 +系统 SHALL 将频率控制、每日限额及冷却相关的所有状态变量和函数从 `main.py` 提取至 `services/rate_limiter.py`,包括:`_auto_running`、`_op_history`、`_daily_stats`、`DAILY_LIMITS`、`_consecutive_errors`、`_error_cooldown_until`、`_reset_daily_stats_if_needed`、`_check_daily_limit`、`_increment_stat`、`_record_error`、`_clear_error_streak`、`_is_in_cooldown`、`_is_in_operating_hours`、`_get_stats_summary`。 + +#### Scenario: 模块级状态初始化一次 +- **WHEN** Python 首次导入 `services/rate_limiter.py` +- **THEN** `_daily_stats`、`_op_history` 等模块级变量 SHALL 仅初始化一次(Python 模块单例语义) + +#### Scenario: 每日限额检查正常工作 +- **WHEN** `_check_daily_limit("comment")` 被调用 +- **THEN** 返回值 SHALL 与迁移前行为完全一致 + +#### Scenario: 运营时段限制正常工作 +- **WHEN** 当前时间不在 `start_hour` 至 `end_hour` 范围内时调用 `_is_in_operating_hours` +- **THEN** 返回 `False`,阻止自动化操作执行 diff --git a/openspec/specs/services-scheduler/spec.md b/openspec/specs/services-scheduler/spec.md new file mode 100644 index 0000000..96a821f --- /dev/null +++ b/openspec/specs/services-scheduler/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: 自动调度器函数迁移至独立模块 +系统 SHALL 将调度器相关的状态变量和函数从 `main.py` 提取至 `services/scheduler.py`,包括:`_scheduler_next_times`、`_auto_log`(列表)、`_auto_log_append`、`_scheduler_loop`、`start_scheduler`、`stop_scheduler`、`get_auto_log`、`get_scheduler_status`、`_learn_running`、`_learn_scheduler_loop`、`start_learn_scheduler`、`stop_learn_scheduler`。 + +#### Scenario: 调度器启停正常工作 +- **WHEN** `start_scheduler(...)` 被调用并传入合法参数 +- **THEN** 调度器线程 SHALL 正常启动,`get_scheduler_status()` 返回运行中状态 + +#### Scenario: 日志追加线程安全 +- **WHEN** 多个自动化任务并发调用 `_auto_log_append(msg)` +- **THEN** 日志条目 SHALL 正确追加,不丢失和乱序 + +#### Scenario: engagement 通过回调写日志 +- **WHEN** `services/engagement.py` 中的函数需要写日志时 +- **THEN** SHALL 通过 `log_fn` 参数(由 `scheduler.py` 传入 `_auto_log_append`)写入,不直接导入 `scheduler.py` diff --git a/openspec/specs/ui-global-config-tab/spec.md b/openspec/specs/ui-global-config-tab/spec.md new file mode 100644 index 0000000..4f14261 --- /dev/null +++ b/openspec/specs/ui-global-config-tab/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: 独立配置 Tab 承载全局设置 +系统 SHALL 在主 Tab 列表末尾提供「⚙️ 配置」Tab,将 LLM 提供商配置、SD WebUI 配置、ReActor 换脸设置、系统设置(开机自启等)全部组件声明移入该 Tab,从主界面顶部移除 `gr.Accordion("⚙️ 全局设置")` 折叠块。 + +#### Scenario: 首屏无全局设置折叠块 +- **WHEN** 用户打开应用 +- **THEN** 主界面顶部 SHALL 不再显示任何折叠区块,直接呈现 Tab 导航栏 + +#### Scenario: 配置 Tab 包含所有原折叠区内容 +- **WHEN** 用户切换到「⚙️ 配置」Tab +- **THEN** Tab 内 SHALL 包含 LLM 提供商 Dropdown、LLM 模型 Dropdown、添加/删除提供商面板、MCP Server URL、SD WebUI URL、连接/检查按钮、SD 模型、博主人设、AI 换脸(头像上传 + 开关)、开机自启开关,功能与原折叠区完全一致 + +#### Scenario: 跨 Tab 共享组件可正常访问 +- **WHEN** 「内容创作」等其他 Tab 需要使用 `llm_model`、`sd_model`、`persona`、`status_bar`、`face_swap_toggle` 等组件 +- **THEN** 这些组件 SHALL 在 `gr.Blocks` 上下文中声明,并作为参数传递给各 `build_tab()` 函数,功能不受 Tab 物理位置影响 + +#### Scenario: 连接状态实时反馈 +- **WHEN** 用户在配置 Tab 点击「🔗 连接 LLM」或「🎨 连接 SD」 +- **THEN** `status_bar` Markdown 组件 SHALL 实时更新显示连接结果,与原折叠区行为一致 diff --git a/openspec/specs/ui-module-split/spec.md b/openspec/specs/ui-module-split/spec.md index 615b83e..a4cdad7 100644 --- a/openspec/specs/ui-module-split/spec.md +++ b/openspec/specs/ui-module-split/spec.md @@ -1,19 +1,49 @@ ## ADDED Requirements ### Requirement: 内容创作 Tab 的 UI 代码迁移至独立模块 -`ui/tab_create.py` SHALL 包含原 `main.py` 中「内容创作 Tab」的全部 Gradio 组件定义和事件绑定,并导出 `build_tab() -> None` 函数,该函数接受一个 `gr.Blocks` 上下文,在其中构建 Tab 内容。`main.py` SHALL 通过 `from ui.tab_create import build_tab` 调用该函数,不在主文件中保留重复的组件代码。 +`ui/tab_create.py` SHALL 包含「内容创作 Tab」的全部 Gradio 组件定义和事件绑定,并导出 `build_tab(...) -> dict` 函数,返回跨 Tab 共享组件的字典(key 集合不得变更)。 -#### Scenario: main.py 正常启动并显示内容创作 Tab -- **WHEN** 运行 `python main.py` 启动 Gradio 应用 -- **THEN** 内容创作 Tab 正常显示,所有组件与迁移前功能一致 +内容创作 Tab 内部 SHALL 采用**三栏布局**: +- **左栏(scale=3)**:人设选择、话题/风格、文案生成参数(高级设置 Accordion) +- **中栏(scale=4)**:文案输出区(标题、正文、标签、提示词)+ 文案操作按钮组 +- **右栏(scale=3)**:图片预览 Gallery + 图片操作按钮组(含美化强度滑块) + +三栏通过 `gr.Row()` 包裹三个 `gr.Column(scale=...)` 实现,所有高频操作 SHALL 在无需垂直滚动的情况下可见(≥1280px 宽度分辨率)。 + +#### Scenario: main.py/app.py 正常启动并显示内容创作 Tab +- **WHEN** 运行应用启动 Gradio +- **THEN** 内容创作 Tab 正常显示三栏布局,所有组件与迁移前功能一致 #### Scenario: tab_create 模块可独立导入 - **WHEN** 在 Python 中执行 `from ui.tab_create import build_tab` - **THEN** 不抛出任何导入错误,`build_tab` 为可调用对象 +#### Scenario: 三栏布局在宽屏下无需滚动 +- **WHEN** 用户在 ≥1280px 宽度的浏览器中打开内容创作 Tab +- **THEN** 左栏参数、中栏文案输出、右栏图片预览 SHALL 同时可见,无需垂直滚动 + +#### Scenario: build_tab 返回字典 key 保持不变 +- **WHEN** `build_tab(...)` 被调用并返回字典 +- **THEN** 返回字典 SHALL 至少包含 `res_title`、`res_content`、`res_prompt`、`res_tags`、`quality_mode`、`steps`、`cfg_scale`、`neg_prompt` 等原有 key + ### Requirement: ui/ 目录结构规范 -`ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_.py`,不在 Tab 模块中直接调用全局服务初始化代码(如 `ConfigManager()`、`LLMService()` 等单例初始化应由 `main.py` 完成并通过参数或模块级引用传入)。 +`ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_.py`,不在 Tab 模块中直接调用全局服务初始化代码。 #### Scenario: 新增 Tab 模块的标准结构 - **WHEN** 开发者创建新的 `ui/tab_*.py` 文件 -- **THEN** 该文件导出 `build_tab(...)` 函数,且顶层不包含副作用代码(不在 import 时触发服务连接) +- **THEN** 该文件导出 `build_tab(...)` 函数,且顶层不包含副作用代码 + +### Requirement: 自动运营 Tab 采用卡片式两列网格布局 +「🤖 自动运营」Tab 内的各自动化任务 SHALL 以 **2 列 × N 行** 的卡片网格展示,每个任务使用 `gr.Group()` 包裹,卡片内 SHALL 包含:任务名称标题、启用开关(`gr.Checkbox`)、执行间隔(`gr.Number` 或 `gr.Slider`)、上次执行时间(`gr.Markdown`)。 + +#### Scenario: 自动运营任务以两列网格显示 +- **WHEN** 用户切换到「🤖 自动运营」Tab +- **THEN** 各自动化任务(自动评论、自动点赞、自动收藏、自动发布、自动回复等)SHALL 以两列卡片网格排列,每列宽度相等 + +#### Scenario: 每张卡片包含完整任务控制 +- **WHEN** 用户查看某个任务卡片 +- **THEN** 卡片内 SHALL 显示任务开关、执行间隔设置项,功能与原单列布局完全一致 + +#### Scenario: 自定义 CSS 增强视觉效果 +- **WHEN** 应用加载完成 +- **THEN** `gr.Blocks(css=...)` SHALL 注入自定义样式,包含:字体层级优化、按钮圆角(≥6px)、`gr.Group` 卡片轻微阴影(`box-shadow`),不破坏 Gradio 内部组件样式 diff --git a/openspec/specs/ui-tabs-split/spec.md b/openspec/specs/ui-tabs-split/spec.md new file mode 100644 index 0000000..78e80bc --- /dev/null +++ b/openspec/specs/ui-tabs-split/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: 剩余 Gradio Tab 提取为独立 UI 模块 +系统 SHALL 将 `ui/app.py` 中所有 Gradio Tab 按如下顺序排列,且顶部 SHALL 不存在任何全局设置折叠块(`gr.Accordion`): + +| 序号 | Tab 名称 | 模块/说明 | +|------|----------|-----------| +| 0 | ✍️ 内容创作 | `ui/tab_create.py` | +| 1 | 📅 内容排期 | 内联或 `ui/tab_queue.py` | +| 2 | 🔥 热点探测 | 内联或 `ui/tab_hotspot.py` | +| 3 | 💬 评论管家 | 内联或 `ui/tab_engage.py` | +| 4 | 📊 数据看板 | 内联或 `ui/tab_analytics.py` | +| 5 | 🧠 智能学习 | 内联或 `ui/tab_learn.py` | +| 6 | 🤖 自动运营 | 内联或 `ui/tab_auto.py` | +| 7 | 🔐 账号登录 | 内联或 `ui/tab_profile.py` | +| 8 | ⚙️ 配置 | 内联(含原全局设置所有组件) | + +每个 Tab 模块 SHALL 暴露 `build_tab(...)` 函数,接受所需组件引用和回调函数作为参数。 + +#### Scenario: 每个 Tab 模块暴露 build_tab 函数 +- **WHEN** `ui/app.py` 执行 `from ui.tab_create import build_tab` +- **THEN** 调用 `build_tab(...)` 后 SHALL 返回包含需跨 Tab 共享组件的 dict + +#### Scenario: build_tab 接收回调而非直接导入 services +- **WHEN** `build_tab(...)` 内部需要调用业务函数时 +- **THEN** 业务函数 SHALL 通过 `fn_*` 参数传入,不在 `ui/tab_*.py` 内直接 `import services.*` + +#### Scenario: 事件绑定在 build_tab 内完成 +- **WHEN** `build_tab(...)` 被调用 +- **THEN** 本 Tab 所有 Gradio 组件的 `.click()`、`.change()` 等事件绑定 SHALL 在函数内完成 + +#### Scenario: 内容创作 Tab 为首个 Tab(索引 0) +- **WHEN** 用户打开应用 +- **THEN** 默认激活的 Tab SHALL 为「✍️ 内容创作」,用户无需额外点击即可开始创作工作流 + +#### Scenario: 配置和账号 Tab 位于末尾 +- **WHEN** 用户查看 Tab 导航栏 +- **THEN** 「🔐 账号登录」SHALL 位于倒数第二位,「⚙️ 配置」SHALL 位于最末位,低频操作不干扰主工作区 diff --git a/sd_service.py b/sd_service.py index c516390..c835e6d 100644 --- a/sd_service.py +++ b/sd_service.py @@ -30,13 +30,17 @@ SD_MODEL_PROFILES = { "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), " - "(delicate facial features:1.2), (fair skin:1.1), (natural skin texture:1.2), " - "(soft lighting:1.1), (natural makeup:1.1), " + "(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": ( @@ -51,6 +55,7 @@ SD_MODEL_PROFILES = { "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" ), @@ -66,7 +71,7 @@ SD_MODEL_PROFILES = { }, "标准 (约1分钟)": { "steps": 30, - "cfg_scale": 7.0, + "cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和 "width": 512, "height": 768, "sampler_name": "DPM++ 2M", @@ -75,13 +80,22 @@ SD_MODEL_PROFILES = { }, "精细 (约2-3分钟)": { "steps": 40, - "cfg_scale": 7.5, + "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, + }, }, }, @@ -90,11 +104,15 @@ SD_MODEL_PROFILES = { "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), " - "(natural skin:1.2), (skin pores:1.1), (detailed skin texture:1.2), " + "(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, " @@ -108,10 +126,12 @@ SD_MODEL_PROFILES = { "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, " - "smooth skin, doll-like, HDR, overprocessed" + "doll-like, HDR, overprocessed, " + "plain face, no makeup, unattractive, dull expression, blank stare, flat lighting" ), "presets": { "快速 (约30秒)": { @@ -125,7 +145,7 @@ SD_MODEL_PROFILES = { }, "标准 (约1分钟)": { "steps": 28, - "cfg_scale": 7.0, + "cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和 "width": 512, "height": 768, "sampler_name": "DPM++ 2M", @@ -134,13 +154,22 @@ SD_MODEL_PROFILES = { }, "精细 (约2-3分钟)": { "steps": 40, - "cfg_scale": 7.5, + "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, + }, }, }, @@ -149,11 +178,16 @@ SD_MODEL_PROFILES = { "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, " - "asian girl, chinese, east asian features, black hair, dark brown eyes, " - "delicate facial features, fair skin, slim figure, " + "(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, " @@ -168,6 +202,7 @@ SD_MODEL_PROFILES = { "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": { @@ -198,6 +233,20 @@ SD_MODEL_PROFILES = { "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, + }, }, }, } @@ -213,6 +262,7 @@ PERSONA_SD_PROFILES = { "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": ( @@ -237,25 +287,29 @@ PERSONA_SD_PROFILES = { # ---- 性感福利主播 ---- "性感福利主播": { "prompt_boost": ( - "(beautiful detailed face:1.3), (seductive eyes:1.2), (glossy lips:1.2), " - "(perfect body proportions:1.2), (slim waist:1.2), (long legs:1.2), " - "(glamour photography:1.3), " + "(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, " - "intimate atmosphere, warm golden tones, shallow depth of field, " - "boudoir style, sensual but tasteful, fashion model pose" + "warm golden tones, shallow depth of field, " + "chic fashion style, confident pose" + ), + "negative_extra": ( + "lingerie, underwear, bikini, nude, revealing clothing, " + "cleavage, nsfw, explicit, suggestive" ), - "negative_extra": "", "llm_guide": ( - "\n\n【人设视觉风格 - 性感福利主播】\n" - "为一个身材曼妙的时尚博主生成图片,主打身材美感+氛围感:\n" - "- 身材描述:slim waist, long legs, perfect figure, hourglass body, graceful pose\n" - "- 穿搭关键:lingerie, bikini, bodycon dress, off-shoulder, backless dress, lace, sheer fabric\n" - "- 光线氛围:soft warm lighting, golden hour, window light, candle light, intimate mood\n" - "- 场景选择:bedroom, luxury hotel, swimming pool, beach sunset, mirror selfie\n" - "- 构图要点:full body or three-quarter shot, emphasize curves, elegant pose, looking at viewer\n" - "- 整体风格:glamour photography,魅力但有品位,不要低俗\n" + "\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" ), }, # ---- 身材管理健身美女 ---- @@ -263,6 +317,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -286,6 +341,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -309,6 +365,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -332,6 +389,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -355,6 +413,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -378,6 +437,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -401,6 +461,7 @@ PERSONA_SD_PROFILES = { "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, " @@ -499,6 +560,54 @@ 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 检测率 @@ -745,6 +854,7 @@ class SDService: face_image: Image.Image = None, quality_mode: str = None, persona: str = None, + enhance_level: float = 1.0, ) -> list[Image.Image]: """文生图(自动适配当前 SD 模型 + 人设的最优参数) @@ -753,6 +863,7 @@ class SDService: face_image: 头像 PIL Image,传入后自动启用 ReActor 换脸 quality_mode: 预设模式名 persona: 博主人设文本,自动注入人设视觉增强词 + enhance_level: 美化增强强度 (0=关闭, 1.0=默认, 2.0=强化) """ if model: self.switch_model(model) @@ -793,10 +904,26 @@ class SDService: "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) @@ -807,11 +934,26 @@ class SDService: 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) @@ -828,6 +970,7 @@ class SDService: sampler_name: str = None, scheduler: str = None, model: str = None, + enhance_level: float = 1.0, ) -> list[Image.Image]: """图生图(自动适配模型参数)""" profile = get_model_profile(model) @@ -864,6 +1007,8 @@ class SDService: 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) diff --git a/services/content.py b/services/content.py index d688ba7..358db32 100644 --- a/services/content.py +++ b/services/content.py @@ -44,8 +44,8 @@ def generate_copy(model, topic, style, sd_model_name, persona_text): return "", "", "", "", f"❌ 生成失败: {e}" -def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg_scale, face_swap_on, face_img, quality_mode, persona_text=None): - """生成图片(可选 ReActor 换脸,支持质量模式预设,支持人设视觉优化)""" +def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg_scale, face_swap_on, face_img, quality_mode, persona_text=None, enhance_level: float = 1.0): + """生成图片(可选 ReActor 换脸,支持质量模式预设,支持人设视觉优化,支持美化增强)""" if not model: return None, [], "❌ 未选择 SD 模型" try: @@ -86,6 +86,7 @@ def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg_scale, face_sw face_image=face_image, quality_mode=quality_mode, persona=persona, + enhance_level=float(enhance_level), ) preset = get_sd_preset(quality_mode) swap_hint = " (已换脸)" if face_image else "" diff --git a/ui/app.py b/ui/app.py index b3b90a8..936a10f 100644 --- a/ui/app.py +++ b/ui/app.py @@ -57,7 +57,34 @@ from publish_queue import STATUS_LABELS logger = logging.getLogger("autobot") -_GRADIO_CSS = None # 可在此处添加自定义 CSS +_GRADIO_CSS = """ +/* ── Autobot 主题层 ── */ +body, .gradio-container { + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif !important; +} +/* 按钮圆角统一 ≥8px */ +.gr-button, button.lg, button.sm, button.md { + border-radius: 8px !important; +} +/* gr.Group 卡片轻阴影 */ +.gr-group { + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.07) !important; + border-radius: 10px !important; +} +/* 标题层级优化 */ +.gradio-container h3 { font-size: 1.05rem; font-weight: 600; } +.gradio-container h4 { font-size: 0.95rem; font-weight: 600; } +/* Tab 导航条优化 */ +.tab-nav { + border-bottom: 2px solid #f0f0f0; + gap: 2px; +} +.tab-nav button { + border-radius: 6px 6px 0 0 !important; + font-weight: 500; + padding: 6px 14px; +} +""" def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: @@ -66,6 +93,7 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: with gr.Blocks( title="小红书 AI 爆文工坊 V2.0", + css=_GRADIO_CSS, ) as app: gr.Markdown( "# 🍒 小红书 AI 爆文生产工坊 V2.0\n" @@ -75,118 +103,121 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: # 全局状态 state_search_result = gr.State("") - # ============ 全局设置栏 ============ - with gr.Accordion("⚙️ 全局设置 (自动保存)", open=False): - gr.Markdown("#### 🤖 LLM 提供商 (支持所有 OpenAI 兼容接口)") - with gr.Row(): - llm_provider = gr.Dropdown( - label="选择 LLM 提供商", - choices=cfg.get_llm_provider_names(), - value=cfg.get("active_llm", ""), - interactive=True, scale=2, - ) - btn_connect_llm = gr.Button("🔗 连接 LLM", size="sm", scale=1) - with gr.Row(): - llm_model = gr.Dropdown( - label="LLM 模型", value=config["model"], - allow_custom_value=True, interactive=True, scale=2, - ) - llm_provider_info = gr.Markdown( - value="*选择提供商后显示详情*", - ) - with gr.Accordion("➕ 添加 / 管理 LLM 提供商", open=False): + # ============ Tab 页面 ============ + # ⚙️ 配置 Tab 声明在最前以确保共享组件变量先于 build_tab() 定义 + # selected=1 令「✨ 内容创作」为默认激活 Tab + with gr.Tabs(selected=1): + # -------- Tab 0: ⚙️ 配置 -------- + with gr.Tab("⚙️ 配置"): + gr.Markdown("#### 🤖 LLM 提供商 (支持所有 OpenAI 兼容接口)") with gr.Row(): - new_provider_name = gr.Textbox( - label="名称", placeholder="如: DeepSeek / GPT-4o / 通义千问", + llm_provider = gr.Dropdown( + label="选择 LLM 提供商", + choices=cfg.get_llm_provider_names(), + value=cfg.get("active_llm", ""), + interactive=True, scale=2, + ) + btn_connect_llm = gr.Button("🔗 连接 LLM", variant="primary", size="sm", scale=1) + with gr.Row(): + llm_model = gr.Dropdown( + label="LLM 模型", value=config["model"], + allow_custom_value=True, interactive=True, scale=2, + ) + llm_provider_info = gr.Markdown( + value="*选择提供商后显示详情*", + ) + with gr.Accordion("➕ 添加 / 管理 LLM 提供商", open=False): + with gr.Row(): + new_provider_name = gr.Textbox( + label="名称", placeholder="如: DeepSeek / GPT-4o / 通义千问", + scale=1, + ) + new_provider_key = gr.Textbox( + label="API Key", type="password", scale=2, + ) + new_provider_url = gr.Textbox( + label="Base URL", placeholder="https://api.openai.com/v1", + value="https://api.openai.com/v1", scale=2, + ) + with gr.Row(): + btn_add_provider = gr.Button("✅ 添加提供商", variant="primary", size="sm") + btn_del_provider = gr.Button("🗑️ 删除当前提供商", variant="stop", size="sm") + provider_mgmt_status = gr.Markdown("") + + gr.Markdown("---") + gr.Markdown("#### 🔗 服务连接") + with gr.Row(): + mcp_url = gr.Textbox( + label="MCP Server URL", value=config["mcp_url"], scale=2, + ) + sd_url = gr.Textbox( + label="SD WebUI URL", value=config["sd_url"], scale=2, + ) + with gr.Row(): + persona = gr.Dropdown( + label="博主人设(评论/回复/自动运营通用)", + choices=[RANDOM_PERSONA_LABEL] + DEFAULT_PERSONAS, + value=config.get("persona", RANDOM_PERSONA_LABEL), + allow_custom_value=True, + interactive=True, + scale=5, + ) + with gr.Row(): + btn_connect_sd = gr.Button("🎨 连接 SD", variant="primary", size="sm") + btn_check_mcp = gr.Button("📡 检查 MCP", size="sm") + with gr.Row(): + sd_model = gr.Dropdown( + label="SD 模型", allow_custom_value=True, + interactive=True, scale=2, + ) + sd_model_info = gr.Markdown("选择模型后显示适配信息", elem_id="sd_model_info") + status_bar = gr.Markdown("🔄 等待连接...") + + gr.Markdown("---") + gr.Markdown("#### 🎭 AI 换脸 (ReActor)") + gr.Markdown( + "> 上传你的头像,生成含人物的图片时自动替换为你的脸\n" + "> 需要 SD WebUI 已安装 [ReActor](https://github.com/Gourieff/sd-webui-reactor) 扩展" + ) + with gr.Row(): + face_image_input = gr.Image( + label="上传头像 (正面清晰照片效果最佳)", + type="pil", + height=180, scale=1, ) - new_provider_key = gr.Textbox( - label="API Key", type="password", scale=2, - ) - new_provider_url = gr.Textbox( - label="Base URL", placeholder="https://api.openai.com/v1", - value="https://api.openai.com/v1", scale=2, + face_image_preview = gr.Image( + label="当前头像", + type="pil", + height=180, + interactive=False, + value=SDService.load_face_image(), + scale=1, ) with gr.Row(): - btn_add_provider = gr.Button("✅ 添加提供商", variant="primary", size="sm") - btn_del_provider = gr.Button("🗑️ 删除当前提供商", variant="stop", size="sm") - provider_mgmt_status = gr.Markdown("") - - gr.Markdown("---") - with gr.Row(): - mcp_url = gr.Textbox( - label="MCP Server URL", value=config["mcp_url"], scale=2, - ) - sd_url = gr.Textbox( - label="SD WebUI URL", value=config["sd_url"], scale=2, - ) - with gr.Row(): - persona = gr.Dropdown( - label="博主人设(评论/回复/自动运营通用)", - choices=[RANDOM_PERSONA_LABEL] + DEFAULT_PERSONAS, - value=config.get("persona", RANDOM_PERSONA_LABEL), - allow_custom_value=True, - interactive=True, - scale=5, - ) - with gr.Row(): - btn_connect_sd = gr.Button("🎨 连接 SD", size="sm") - btn_check_mcp = gr.Button("📡 检查 MCP", size="sm") - with gr.Row(): - sd_model = gr.Dropdown( - label="SD 模型", allow_custom_value=True, - interactive=True, scale=2, - ) - sd_model_info = gr.Markdown("选择模型后显示适配信息", elem_id="sd_model_info") - status_bar = gr.Markdown("🔄 等待连接...") - - gr.Markdown("---") - gr.Markdown("#### 🎭 AI 换脸 (ReActor)") - gr.Markdown( - "> 上传你的头像,生成含人物的图片时自动替换为你的脸\n" - "> 需要 SD WebUI 已安装 [ReActor](https://github.com/Gourieff/sd-webui-reactor) 扩展" - ) - with gr.Row(): - face_image_input = gr.Image( - label="上传头像 (正面清晰照片效果最佳)", - type="pil", - height=180, - scale=1, - ) - face_image_preview = gr.Image( - label="当前头像", - type="pil", - height=180, - interactive=False, - value=SDService.load_face_image(), - scale=1, - ) - with gr.Row(): - btn_save_face = gr.Button("💾 保存头像", variant="primary", size="sm") - face_swap_toggle = gr.Checkbox( - label="🎭 生成图片时启用 AI 换脸", - value=os.path.isfile(FACE_IMAGE_PATH), - interactive=True, - ) - face_status = gr.Markdown( - "✅ 头像已就绪" if os.path.isfile(FACE_IMAGE_PATH) else "ℹ️ 尚未设置头像" - ) - - gr.Markdown("---") - gr.Markdown("#### 🖥️ 系统设置") - with gr.Row(): - autostart_toggle = gr.Checkbox( - label="🚀 Windows 开机自启(静默后台运行)", - value=is_autostart_enabled(), - interactive=(platform.system() == "Windows"), - ) - autostart_status = gr.Markdown( - value="✅ 已启用" if is_autostart_enabled() else "⚪ 未启用", + btn_save_face = gr.Button("💾 保存头像", variant="primary", size="sm") + face_swap_toggle = gr.Checkbox( + label="🎭 生成图片时启用 AI 换脸", + value=os.path.isfile(FACE_IMAGE_PATH), + interactive=True, + ) + face_status = gr.Markdown( + "✅ 头像已就绪" if os.path.isfile(FACE_IMAGE_PATH) else "ℹ️ 尚未设置头像" ) - # ============ Tab 页面 ============ - with gr.Tabs(): - # -------- Tab 1: 内容创作(迁移至 ui/tab_create.py)-------- + gr.Markdown("---") + gr.Markdown("#### 🖥️ 系统设置") + with gr.Row(): + autostart_toggle = gr.Checkbox( + label="🚀 Windows 开机自启(静默后台运行)", + value=is_autostart_enabled(), + interactive=(platform.system() == "Windows"), + ) + autostart_status = gr.Markdown( + value="✅ 已启用" if is_autostart_enabled() else "⚪ 未启用", + ) + + # -------- Tab 1: ✨ 内容创作(默认激活)-------- _tab1 = build_tab( config=config, styles=DEFAULT_STYLES, @@ -217,7 +248,101 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: cfg_scale = _tab1["cfg_scale"] neg_prompt = _tab1["neg_prompt"] - # -------- Tab 2: 热点探测 -------- + # -------- Tab 2: 📅 内容排期 -------- + with gr.Tab("📅 内容排期"): + gr.Markdown( + "### 📅 内容排期日历 + 发布队列\n" + "> 批量生成内容 → 预览审核 → 排期定时 → 自动发布,内容创作全流程管控\n\n" + "**工作流**: 生成内容 → 📝草稿 → ✅审核通过 → 🕐排期/立即发布 → 🚀自动发布" + ) + + with gr.Row(): + # ===== 左栏: 生成 & 队列控制 ===== + with gr.Column(scale=1): + gr.Markdown("#### 🔧 批量生成到队列") + queue_gen_topics = gr.Textbox( + label="主题池 (逗号分隔,随人设自动切换)", + value=", ".join(get_persona_topics(config.get("persona", ""))), + placeholder="会从池中随机选取,切换人设自动更新", + ) + with gr.Row(): + queue_gen_count = gr.Number( + label="生成数量", value=config.get("queue_gen_count", 3), minimum=1, maximum=10, + ) + queue_gen_schedule = gr.Textbox( + label="排期时间 (可选)", + placeholder="如 2026-02-10 18:00:00,留空=仅草稿", + ) + btn_queue_generate = gr.Button( + "📝 批量生成 → 加入队列", variant="primary", size="lg", + ) + queue_gen_result = gr.Markdown("") + + gr.Markdown("---") + gr.Markdown("#### ⚙️ 队列处理器") + queue_processor_status = gr.Markdown( + value=queue_get_status(), + ) + with gr.Row(): + btn_queue_start = gr.Button( + "▶️ 启动队列处理", variant="primary", + ) + btn_queue_stop = gr.Button( + "⏹️ 停止队列处理", variant="stop", + ) + queue_processor_result = gr.Markdown("") + + gr.Markdown("---") + gr.Markdown("#### 🔍 操作单个队列项") + queue_item_id = gr.Textbox( + label="队列项 ID", placeholder="输入 # 号,如 1", + ) + with gr.Row(): + btn_queue_preview = gr.Button("👁️ 预览", size="sm") + btn_queue_approve = gr.Button("✅ 通过", size="sm", variant="primary") + btn_queue_reject = gr.Button("🚫 拒绝", size="sm", variant="stop") + with gr.Row(): + btn_queue_publish_now = gr.Button("🚀 立即发布", size="sm", variant="primary") + btn_queue_retry = gr.Button("🔄 重试", size="sm") + btn_queue_delete = gr.Button("🗑️ 删除", size="sm", variant="stop") + queue_schedule_time = gr.Textbox( + label="排期时间 (审核通过时可指定)", + placeholder="如 2026-02-10 20:00:00,留空=立即待发布", + ) + btn_queue_batch_approve = gr.Button( + "✅ 批量通过所有草稿", variant="secondary", + ) + queue_op_result = gr.Markdown("") + + # ===== 右栏: 队列列表 & 日历 ===== + with gr.Column(scale=2): + gr.Markdown("#### 📋 发布队列") + with gr.Row(): + queue_filter = gr.Dropdown( + label="状态筛选", + choices=["全部"] + list(STATUS_LABELS.values()), + value="全部", + ) + btn_queue_refresh = gr.Button("🔄 刷新", size="sm") + queue_table = gr.Markdown( + value=queue_format_table(), + label="队列列表", + ) + + gr.Markdown("---") + gr.Markdown("#### 📅 排期日历") + queue_calendar = gr.Markdown( + value=queue_format_calendar(), + label="日历视图", + ) + + gr.Markdown("---") + gr.Markdown("#### 👁️ 内容预览") + queue_preview_display = gr.Markdown( + value="*选择队列项 ID 后点击预览*", + ) + + # -------- Tab 3: 🔥 热点探测 -------- with gr.Tab("🔥 热点探测"): gr.Markdown("### 搜索热门内容 → AI 分析趋势 → 一键借鉴创作") with gr.Row(): @@ -380,67 +505,7 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: ) my_reply_status = gr.Markdown("") - # -------- Tab 4: 账号登录 -------- - with gr.Tab("🔐 账号登录"): - gr.Markdown( - "### 小红书账号登录\n" - "> 扫码登录后自动获取 xsec_token,配合用户 ID 即可使用所有功能" - ) - with gr.Row(): - with gr.Column(scale=1): - gr.Markdown( - "**操作步骤:**\n" - "1. 确保 MCP 服务已启动\n" - "2. 点击「获取登录二维码」→ 用小红书 App 扫码\n" - "3. 点击「检查登录状态」→ 自动获取并保存 xsec_token\n" - "4. 首次使用请填写你的用户 ID 并点击保存\n\n" - "⚠️ 登录后不要在其他网页端登录同一账号,否则会被踢出" - ) - btn_get_qrcode = gr.Button( - "📱 获取登录二维码", variant="primary", size="lg", - ) - btn_check_login = gr.Button( - "🔍 检查登录状态 (自动获取 Token)", - variant="secondary", size="lg", - ) - btn_logout = gr.Button( - "🚪 退出登录 (重新扫码)", - variant="stop", size="lg", - ) - login_status = gr.Markdown("🔄 等待操作...") - - gr.Markdown("---") - gr.Markdown( - "#### 📌 我的账号信息\n" - "> **注意**: 小红书号 ≠ 用户 ID\n" - "> - **小红书号 (redId)**: 如 `18688457507`,是你在 App 个人页看到的\n" - "> - **用户 ID (userId)**: 如 `5a695db6e8ac2b72e8af2a53`,24位十六进制字符串\n\n" - "💡 **如何获取 userId?**\n" - "1. 用浏览器打开你的小红书主页\n" - "2. 网址格式为: `xiaohongshu.com/user/profile/xxxxxxxx`\n" - "3. `profile/` 后面的就是你的 userId" - ) - login_user_id = gr.Textbox( - label="我的用户 ID (24位 userId, 非小红书号)", - value=config.get("my_user_id", ""), - placeholder="如: 5a695db6e8ac2b72e8af2a53", - ) - login_xsec_token = gr.Textbox( - label="xsec_token (登录后自动获取)", - value=config.get("xsec_token", ""), - interactive=False, - ) - btn_save_uid = gr.Button( - "💾 保存用户 ID", variant="secondary", - ) - save_uid_status = gr.Markdown("") - - with gr.Column(scale=1): - qr_image = gr.Image( - label="扫码登录", height=350, width=350, - ) - - # -------- Tab 5: 数据看板 -------- + # -------- Tab 5: 📊 数据看板 -------- with gr.Tab("📊 数据看板"): gr.Markdown( "### 我的账号数据看板\n" @@ -657,94 +722,106 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: ) auto_publish_result = gr.Markdown("") - # 右栏: 定时自动化 - with gr.Column(scale=1): + # 右栏: 定时自动化 (2列卡片网格) + with gr.Column(scale=2): gr.Markdown("#### ⏰ 随机定时自动化") gr.Markdown( - "> 设置时间间隔后启动,系统将在随机时间自动执行\n" - "> 模拟真人操作节奏,降低被检测风险" + "> 设置时间间隔后启动,系统将在随机时间自动执行 · 模拟真人操作节奏,降低被检测风险" ) sched_status = gr.Markdown("⚪ **调度器未运行**") - # 运营时段设置 - with gr.Group(): - gr.Markdown("##### ⏰ 运营时段") - with gr.Row(): - sched_start_hour = gr.Number( - label="开始时间(整点)", value=config.get("sched_start_hour", 8), minimum=0, maximum=23, - ) - sched_end_hour = gr.Number( - label="结束时间(整点)", value=config.get("sched_end_hour", 23), minimum=1, maximum=24, - ) + # ── 第1行: 时段 + 评论 ── + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### ⏰ 运营时段") + with gr.Row(): + sched_start_hour = gr.Number( + label="开始(整点)", value=config.get("sched_start_hour", 8), minimum=0, maximum=23, + ) + sched_end_hour = gr.Number( + label="结束(整点)", value=config.get("sched_end_hour", 23), minimum=1, maximum=24, + ) + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### 💬 自动评论") + sched_comment_on = gr.Checkbox( + label="启用", value=config.get("sched_comment_on", True), + ) + with gr.Row(): + sched_c_min = gr.Number( + label="最小间隔(分钟)", value=config.get("sched_c_min", 15), minimum=5, + ) + sched_c_max = gr.Number( + label="最大间隔(分钟)", value=config.get("sched_c_max", 45), minimum=10, + ) - with gr.Group(): - sched_comment_on = gr.Checkbox( - label="✅ 启用自动评论", value=config.get("sched_comment_on", True), - ) - with gr.Row(): - sched_c_min = gr.Number( - label="评论最小间隔(分钟)", value=config.get("sched_c_min", 15), minimum=5, - ) - sched_c_max = gr.Number( - label="评论最大间隔(分钟)", value=config.get("sched_c_max", 45), minimum=10, - ) + # ── 第2行: 点赞 + 收藏 ── + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### 👍 自动点赞") + sched_like_on = gr.Checkbox( + label="启用", value=config.get("sched_like_on", True), + ) + with gr.Row(): + sched_l_min = gr.Number( + label="最小间隔(分钟)", value=config.get("sched_l_min", 10), minimum=3, + ) + sched_l_max = gr.Number( + label="最大间隔(分钟)", value=config.get("sched_l_max", 30), minimum=5, + ) + sched_like_count = gr.Number( + label="每轮点赞数", value=config.get("sched_like_count", 5), minimum=1, maximum=15, + ) + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### ⭐ 自动收藏") + sched_fav_on = gr.Checkbox( + label="启用", value=config.get("sched_fav_on", True), + ) + with gr.Row(): + sched_fav_min = gr.Number( + label="最小间隔(分钟)", value=config.get("sched_fav_min", 12), minimum=3, + ) + sched_fav_max = gr.Number( + label="最大间隔(分钟)", value=config.get("sched_fav_max", 35), minimum=5, + ) + sched_fav_count = gr.Number( + label="每轮收藏数", value=config.get("sched_fav_count", 3), minimum=1, maximum=10, + ) - with gr.Group(): - sched_like_on = gr.Checkbox( - label="✅ 启用自动点赞", value=config.get("sched_like_on", True), - ) - with gr.Row(): - sched_l_min = gr.Number( - label="点赞最小间隔(分钟)", value=config.get("sched_l_min", 10), minimum=3, - ) - sched_l_max = gr.Number( - label="点赞最大间隔(分钟)", value=config.get("sched_l_max", 30), minimum=5, - ) - sched_like_count = gr.Number( - label="每轮点赞数量", value=config.get("sched_like_count", 5), minimum=1, maximum=15, - ) - - with gr.Group(): - sched_fav_on = gr.Checkbox( - label="✅ 启用自动收藏", value=config.get("sched_fav_on", True), - ) - with gr.Row(): - sched_fav_min = gr.Number( - label="收藏最小间隔(分钟)", value=config.get("sched_fav_min", 12), minimum=3, - ) - sched_fav_max = gr.Number( - label="收藏最大间隔(分钟)", value=config.get("sched_fav_max", 35), minimum=5, - ) - sched_fav_count = gr.Number( - label="每轮收藏数量", value=config.get("sched_fav_count", 3), minimum=1, maximum=10, - ) - - with gr.Group(): - sched_reply_on = gr.Checkbox( - label="✅ 启用自动回复评论", value=config.get("sched_reply_on", True), - ) - with gr.Row(): - sched_r_min = gr.Number( - label="回复最小间隔(分钟)", value=config.get("sched_r_min", 20), minimum=5, - ) - sched_r_max = gr.Number( - label="回复最大间隔(分钟)", value=config.get("sched_r_max", 60), minimum=10, - ) - sched_reply_max = gr.Number( - label="每轮最多回复条数", value=config.get("sched_reply_max", 3), minimum=1, maximum=10, - ) - - with gr.Group(): - sched_publish_on = gr.Checkbox( - label="✅ 启用自动发布", value=config.get("sched_publish_on", True), - ) - with gr.Row(): - sched_p_min = gr.Number( - label="发布最小间隔(分钟)", value=config.get("sched_p_min", 60), minimum=30, - ) - sched_p_max = gr.Number( - label="发布最大间隔(分钟)", value=config.get("sched_p_max", 180), minimum=60, - ) + # ── 第3行: 回复 + 发布 ── + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### 💌 自动回复") + sched_reply_on = gr.Checkbox( + label="启用", value=config.get("sched_reply_on", True), + ) + with gr.Row(): + sched_r_min = gr.Number( + label="最小间隔(分钟)", value=config.get("sched_r_min", 20), minimum=5, + ) + sched_r_max = gr.Number( + label="最大间隔(分钟)", value=config.get("sched_r_max", 60), minimum=10, + ) + sched_reply_max = gr.Number( + label="每轮最多回复", value=config.get("sched_reply_max", 3), minimum=1, maximum=10, + ) + with gr.Column(scale=1): + with gr.Group(): + gr.Markdown("##### 🚀 自动发布") + sched_publish_on = gr.Checkbox( + label="启用", value=config.get("sched_publish_on", True), + ) + with gr.Row(): + sched_p_min = gr.Number( + label="最小间隔(分钟)", value=config.get("sched_p_min", 60), minimum=30, + ) + sched_p_max = gr.Number( + label="最大间隔(分钟)", value=config.get("sched_p_max", 180), minimum=60, + ) with gr.Row(): btn_start_sched = gr.Button( @@ -761,7 +838,7 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: gr.Markdown("#### 📋 运行日志") with gr.Row(): btn_refresh_log = gr.Button("🔄 刷新日志", size="sm") - btn_clear_log = gr.Button("🗑️ 清空日志", size="sm") + btn_clear_log = gr.Button("🗑️ 清空日志", size="sm", variant="stop") btn_refresh_stats = gr.Button("📊 刷新统计", size="sm") auto_log_display = gr.TextArea( label="自动化运行日志", @@ -775,98 +852,64 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks: value=_get_stats_summary(), ) - # -------- Tab 8: 内容排期 📅 -------- - with gr.Tab("📅 内容排期"): + # -------- Tab 8: 🔐 账号登录 -------- + with gr.Tab("🔐 账号登录"): gr.Markdown( - "### 📅 内容排期日历 + 发布队列\n" - "> 批量生成内容 → 预览审核 → 排期定时 → 自动发布,内容创作全流程管控\n\n" - "**工作流**: 生成内容 → 📝草稿 → ✅审核通过 → 🕐排期/立即发布 → 🚀自动发布" + "### 小红书账号登录\n" + "> 扫码登录后自动获取 xsec_token,配合用户 ID 即可使用所有功能" ) - with gr.Row(): - # ===== 左栏: 生成 & 队列控制 ===== with gr.Column(scale=1): - gr.Markdown("#### 🔧 批量生成到队列") - queue_gen_topics = gr.Textbox( - label="主题池 (逗号分隔,随人设自动切换)", - value=", ".join(get_persona_topics(config.get("persona", ""))), - placeholder="会从池中随机选取,切换人设自动更新", + gr.Markdown( + "**操作步骤:**\n" + "1. 确保 MCP 服务已启动\n" + "2. 点击「获取登录二维码」→ 用小红书 App 扫码\n" + "3. 点击「检查登录状态」→ 自动获取并保存 xsec_token\n" + "4. 首次使用请填写你的用户 ID 并点击保存\n\n" + "⚠️ 登录后不要在其他网页端登录同一账号,否则会被踢出" ) - with gr.Row(): - queue_gen_count = gr.Number( - label="生成数量", value=config.get("queue_gen_count", 3), minimum=1, maximum=10, - ) - queue_gen_schedule = gr.Textbox( - label="排期时间 (可选)", - placeholder="如 2026-02-10 18:00:00,留空=仅草稿", - ) - btn_queue_generate = gr.Button( - "📝 批量生成 → 加入队列", variant="primary", size="lg", + btn_get_qrcode = gr.Button( + "📱 获取登录二维码", variant="primary", size="lg", ) - queue_gen_result = gr.Markdown("") + btn_check_login = gr.Button( + "🔍 检查登录状态 (自动获取 Token)", + variant="secondary", size="lg", + ) + btn_logout = gr.Button( + "🚪 退出登录 (重新扫码)", + variant="stop", size="lg", + ) + login_status = gr.Markdown("🔄 等待操作...") gr.Markdown("---") - gr.Markdown("#### ⚙️ 队列处理器") - queue_processor_status = gr.Markdown( - value=queue_get_status(), + gr.Markdown( + "#### 📌 我的账号信息\n" + "> **注意**: 小红书号 ≠ 用户 ID\n" + "> - **小红书号 (redId)**: 如 `18688457507`,是你在 App 个人页看到的\n" + "> - **用户 ID (userId)**: 如 `5a695db6e8ac2b72e8af2a53`,24位十六进制字符串\n\n" + "💡 **如何获取 userId?**\n" + "1. 用浏览器打开你的小红书主页\n" + "2. 网址格式为: `xiaohongshu.com/user/profile/xxxxxxxx`\n" + "3. `profile/` 后面的就是你的 userId" ) - with gr.Row(): - btn_queue_start = gr.Button( - "▶️ 启动队列处理", variant="primary", - ) - btn_queue_stop = gr.Button( - "⏹️ 停止队列处理", variant="stop", - ) - queue_processor_result = gr.Markdown("") + login_user_id = gr.Textbox( + label="我的用户 ID (24位 userId, 非小红书号)", + value=config.get("my_user_id", ""), + placeholder="如: 5a695db6e8ac2b72e8af2a53", + ) + login_xsec_token = gr.Textbox( + label="xsec_token (登录后自动获取)", + value=config.get("xsec_token", ""), + interactive=False, + ) + btn_save_uid = gr.Button( + "💾 保存用户 ID", variant="secondary", + ) + save_uid_status = gr.Markdown("") - gr.Markdown("---") - gr.Markdown("#### 🔍 操作单个队列项") - queue_item_id = gr.Textbox( - label="队列项 ID", placeholder="输入 # 号,如 1", - ) - with gr.Row(): - btn_queue_preview = gr.Button("👁️ 预览", size="sm") - btn_queue_approve = gr.Button("✅ 通过", size="sm", variant="primary") - btn_queue_reject = gr.Button("🚫 拒绝", size="sm") - with gr.Row(): - btn_queue_publish_now = gr.Button("🚀 立即发布", size="sm", variant="primary") - btn_queue_retry = gr.Button("🔄 重试", size="sm") - btn_queue_delete = gr.Button("🗑️ 删除", size="sm", variant="stop") - queue_schedule_time = gr.Textbox( - label="排期时间 (审核通过时可指定)", - placeholder="如 2026-02-10 20:00:00,留空=立即待发布", - ) - btn_queue_batch_approve = gr.Button( - "✅ 批量通过所有草稿", variant="secondary", - ) - queue_op_result = gr.Markdown("") - - # ===== 右栏: 队列列表 & 日历 ===== - with gr.Column(scale=2): - gr.Markdown("#### 📋 发布队列") - with gr.Row(): - queue_filter = gr.Dropdown( - label="状态筛选", - choices=["全部"] + list(STATUS_LABELS.values()), - value="全部", - ) - btn_queue_refresh = gr.Button("🔄 刷新", size="sm") - queue_table = gr.Markdown( - value=queue_format_table(), - label="队列列表", - ) - - gr.Markdown("---") - gr.Markdown("#### 📅 排期日历") - queue_calendar = gr.Markdown( - value=queue_format_calendar(), - label="日历视图", - ) - - gr.Markdown("---") - gr.Markdown("#### 👁️ 内容预览") - queue_preview_display = gr.Markdown( - value="*选择队列项 ID 后点击预览*", + with gr.Column(scale=1): + qr_image = gr.Image( + label="扫码登录", height=350, width=350, ) # ================================================== diff --git a/ui/tab_create.py b/ui/tab_create.py index f3014d3..344c957 100644 --- a/ui/tab_create.py +++ b/ui/tab_create.py @@ -50,7 +50,7 @@ def build_tab( with gr.Row(): # ---- 左栏:输入 ---- - with gr.Column(scale=1): + with gr.Column(scale=3): gr.Markdown("### 💡 构思") topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭") style = gr.Dropdown( @@ -75,10 +75,17 @@ def build_tab( ) steps = gr.Slider(8, 50, value=config.get("sd_steps", 20), step=1, label="步数") cfg_scale = gr.Slider(1, 15, value=config.get("sd_cfg_scale", 5.5), step=0.5, label="CFG Scale") + enhance_level = gr.Slider( + 0.0, 2.0, + value=config.get("enhance_level", 1.0), + step=0.1, + label="美化强度", + info="0=关闭 1=默认 2=强化(锐化+皮肤校色+通透感)", + ) btn_gen_img = gr.Button("🎨 第二步:生成图片", variant="primary") # ---- 中栏:文案编辑 ---- - with gr.Column(scale=1): + with gr.Column(scale=4): gr.Markdown("### 📝 文案编辑") res_title = gr.Textbox( label="标题", @@ -97,7 +104,7 @@ def build_tab( ) # ---- 右栏:预览 & 发布 ---- - with gr.Column(scale=1): + with gr.Column(scale=3): gr.Markdown("### 🖼️ 视觉预览") gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300) local_images = gr.File( @@ -138,11 +145,13 @@ def build_tab( steps.change(fn=lambda s: fn_cfg_set("sd_steps", s), inputs=[steps], outputs=[]) cfg_scale.change(fn=lambda c: fn_cfg_set("sd_cfg_scale", c), inputs=[cfg_scale], outputs=[]) neg_prompt.change(fn=lambda n: fn_cfg_set("sd_negative_prompt", n), inputs=[neg_prompt], outputs=[]) + enhance_level.change(fn=lambda v: fn_cfg_set("enhance_level", v), inputs=[enhance_level], outputs=[]) btn_gen_img.click( fn=fn_gen_img, inputs=[sd_url, res_prompt, neg_prompt, sd_model, steps, cfg_scale, - face_swap_toggle, face_image_preview, quality_mode, persona], + face_swap_toggle, face_image_preview, quality_mode, persona, + enhance_level], outputs=[gallery, state_images, status_bar], ) @@ -169,4 +178,5 @@ def build_tab( "steps": steps, "cfg_scale": cfg_scale, "neg_prompt": neg_prompt, + "enhance_level": enhance_level, }