feat(config): 更新模型配置与LLM提示词指南

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

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

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

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

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

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

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

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

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

View File

@ -3,8 +3,8 @@
"base_url": "https://wolfai.top/v1", "base_url": "https://wolfai.top/v1",
"sd_url": "http://127.0.0.1:7861", "sd_url": "http://127.0.0.1:7861",
"mcp_url": "http://localhost:18060/mcp", "mcp_url": "http://localhost:18060/mcp",
"model": "gemini-2.0-flash", "model": "gemini-3-flash-preview",
"persona": "性感福利主播,身材火辣衣着大胆,专注分享穿衣显身材和私房写真风穿搭", "persona": "二次元coser喜欢分享cos日常和动漫周边",
"auto_reply_enabled": false, "auto_reply_enabled": false,
"schedule_enabled": false, "schedule_enabled": false,
"my_user_id": "69872540000000002303cc42", "my_user_id": "69872540000000002303cc42",

View File

@ -327,25 +327,38 @@ class LLMService:
base = ( base = (
f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n"
"该模型擅长东亚网红/朋友圈自拍风格,请按以下规则生成 sd_prompt\n" "该模型擅长东亚网红/朋友圈自拍风格,请按以下规则生成 sd_prompt\n"
"- 人物要求(最重要!):必须是东亚面孔中国人\n" "- 人物要求(最重要!):必须是东亚面孔中国人,必须描述眼睛、肤色、表情、妆容\n"
"- 推荐使用 (权重:数值) 语法加强关键词,例如 (asian girl:1.3), (best quality:1.4)\n" "- 推荐使用 (权重:数值) 语法加强关键词,例如 (almond eyes:1.3), (glossy lips:1.2)\n"
"- 风格关键词RAW photo, realistic, photorealistic, natural makeup, instagram aesthetic\n" "- 颜值核心词必选2-3个(bright sparkling eyes:1.2), (glossy lips:1.2), (rosy cheeks:1.1),\n"
"- 氛围词soft lighting, warm tone, natural skin texture, phone camera feel\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"
"- 画面要有「朋友圈精选照片」的感觉,自然不做作\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": elif key == "realisticVision":
base = ( base = (
f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n" f"生成 Stable Diffusion 英文提示词,当前使用模型: {display} ({desc})\n"
"该模型擅长写实纪实摄影风格,请按以下规则生成 sd_prompt\n" "该模型擅长写实摄影风格,请按以下规则生成 sd_prompt\n"
"- 人物要求(最重要!):必须是东亚面孔中国人\n" "- 人物要求(最重要!):必须是东亚面孔中国人,必须描述眼睛、肤色、表情\n"
"- 推荐使用 (权重:数值) 语法,例如 (realistic:1.4), (photorealistic:1.4)\n" "- 推荐使用 (权重:数值) 语法,例如 (realistic:1.4), (almond eyes:1.2)\n"
"- 风格关键词RAW photo, DSLR, documentary style, street photography, film color grading\n" "- 颜值核心词必选2-3个(bright clear eyes:1.2), (charming gaze:1.1), (glossy lips:1.1),\n"
"- 质感词skin pores, detailed skin texture, natural imperfections, real lighting\n" " (dewy smooth skin:1.2), (soft smile:1.1), (long lashes:1.1), (rosy cheeks:1.1)\n"
"- 镜头感shot on Canon/Sony, 85mm lens, f/1.8, depth of field\n" "- 风格关键词RAW photo, DSLR, street photography, film color grading\n"
"- 非常适合:街拍、纪实风、旅行照、真实场景、有故事感的画面\n" "- 镜头感shot on Sony A7, 85mm lens, f/1.8, depth of field, bokeh\n"
"- 画面要有「专业摄影师抓拍」的质感,保留真实皮肤纹理\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) else: # juggernautXL (SDXL)
@ -354,15 +367,57 @@ class LLMService:
"该模型为 SDXL 架构,擅长电影级大片质感,请按以下规则生成 sd_prompt\n" "该模型为 SDXL 架构,擅长电影级大片质感,请按以下规则生成 sd_prompt\n"
"- 人物要求(最重要!):必须是东亚面孔中国人,绝对禁止西方人特征\n" "- 人物要求(最重要!):必须是东亚面孔中国人,绝对禁止西方人特征\n"
"- 不要使用 (权重:数值) 括号语法SDXL 模型直接用逗号分隔即可\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" "- 质量词masterpiece, best quality, ultra detailed, 8k uhd, high resolution\n"
"- 风格photorealistic, cinematic lighting, cinematic composition, commercial photography\n" "- 风格photorealistic, cinematic lighting, commercial photography\n"
"- 光影:volumetric lighting, ray tracing, golden hour, studio lighting\n" "- 光影:golden hour, soft diffused light, volumetric lighting, soft catchlights in eyes\n"
"- 非常适合:商业摄影、时尚大片、复杂光影场景、杂志封面风格\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, def _chat(self, system_prompt: str, user_message: str,
json_mode: bool = True, temperature: float = 0.8) -> str: json_mode: bool = True, temperature: float = 0.8) -> str:

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-26

View File

@ -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`)——被否,未从根本上减少首屏干扰。
### D2Tab 顺序重排策略
**决策**:按"使用频率"重排,高频在前:
```
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 冲突,维护成本高。
### D4CSS 注入范围——最小化
**决策**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
- **无**——所有技术决策已确定,可直接进入任务拆解

View File

@ -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**(所有现有功能保留,仅调整位置和视觉样式)

View File

@ -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 实时更新显示连接结果,与原折叠区行为一致

View File

@ -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_<name>.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 内部组件样式

View File

@ -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 位于最末位,低频操作不干扰主工作区

View File

@ -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检查调度卡片网格布局正常执行一次单次任务

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-25

View File

@ -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
### 决策 1Hires 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 FixSDXL 的 832×1216 基础分辨率放大 1.5x 可达 1248×1824大幅提升皮肤/五官细节,代价是生成时间增加约 50%。
**替代方案:** 后处理做 PIL 超分 → 效果差、无 AI 重绘细节;全模型启用 → 触发 SD 1.5 OOM 风险。
---
### 决策 2beauty_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 权重词是最直接有效的方式;不引入新依赖;各模型独立配置可针对其风格微调权重值。
---
### 决策 4LLM 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` 的最优参数值(锐化半径、色彩增益系数)需在实际出图中调试确认

View File

@ -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`**:绘图参数面板新增"美化强度"滑块控件

View File

@ -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** 输出图片肤色区域饱和度提升的同时,色调保持在自然暖白范围内(目视无偏黄/偏红现象)

View File

@ -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`,行为与优化前相同

View File

@ -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` 中的键名列表

View File

@ -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` 等具体光影词之一

View File

@ -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` 时行为与修改前一致(回归测试)

View File

@ -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** 输出图片肤色区域饱和度提升的同时,色调保持在自然暖白范围内(目视无偏黄/偏红现象)

View File

@ -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`,行为与优化前相同

View File

@ -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` 中的键名列表

View File

@ -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` 等具体光影词之一

View File

@ -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 返回与迁移前相同的平台不支持提示信息

View File

@ -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` 或循环导入错误

View File

@ -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 正常执行

View File

@ -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` 内复制限流逻辑

View File

@ -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` 的线程安全行为保持不变

View File

@ -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 使用

View File

@ -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不抛出异常

View File

@ -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` 模块顶层初始化

View File

@ -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`,阻止自动化操作执行

View File

@ -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`

View File

@ -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 实时更新显示连接结果,与原折叠区行为一致

View File

@ -1,19 +1,49 @@
## ADDED Requirements ## ADDED Requirements
### Requirement: 内容创作 Tab 的 UI 代码迁移至独立模块 ### 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 内容创作 Tab 内部 SHALL 采用**三栏布局**
- **WHEN** 运行 `python main.py` 启动 Gradio 应用 - **左栏scale=3**:人设选择、话题/风格、文案生成参数(高级设置 Accordion
- **THEN** 内容创作 Tab 正常显示,所有组件与迁移前功能一致 - **中栏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 模块可独立导入 #### Scenario: tab_create 模块可独立导入
- **WHEN** 在 Python 中执行 `from ui.tab_create import build_tab` - **WHEN** 在 Python 中执行 `from ui.tab_create import build_tab`
- **THEN** 不抛出任何导入错误,`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/ 目录结构规范 ### Requirement: ui/ 目录结构规范
`ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_<name>.py`,不在 Tab 模块中直接调用全局服务初始化代码(如 `ConfigManager()``LLMService()` 等单例初始化应由 `main.py` 完成并通过参数或模块级引用传入)。 `ui/` 目录 SHALL 包含 `__init__.py`,每个 Tab 模块文件命名约定为 `tab_<name>.py`,不在 Tab 模块中直接调用全局服务初始化代码。
#### Scenario: 新增 Tab 模块的标准结构 #### Scenario: 新增 Tab 模块的标准结构
- **WHEN** 开发者创建新的 `ui/tab_*.py` 文件 - **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 内部组件样式

View File

@ -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 位于最末位,低频操作不干扰主工作区

View File

@ -30,13 +30,17 @@ SD_MODEL_PROFILES = {
"display_name": "majicmixRealistic ⭐⭐⭐⭐⭐", "display_name": "majicmixRealistic ⭐⭐⭐⭐⭐",
"description": "东亚网红感 | 朋友圈自拍、美妆、穿搭", "description": "东亚网红感 | 朋友圈自拍、美妆、穿搭",
"arch": "sd15", # SD 1.5 架构 "arch": "sd15", # SD 1.5 架构
"clip_skip": 2, # majicmix 系模型在 clip_skip=2 时人脸颜值更高
# 自动追加到 prompt 前面的增强词 # 自动追加到 prompt 前面的增强词
"prompt_prefix": ( "prompt_prefix": (
"(best quality:1.4), (masterpiece:1.4), (ultra detailed:1.3), " "(best quality:1.4), (masterpiece:1.4), (ultra detailed:1.3), "
"(photorealistic:1.4), (realistic:1.3), raw photo, " "(photorealistic:1.4), (realistic:1.3), raw photo, "
"(asian girl:1.3), (chinese:1.2), (east asian features:1.2), " "(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), " "(almond eyes:1.3), (bright sparkling eyes:1.2), (long lashes:1.1), "
"(soft lighting:1.1), (natural makeup: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 后面的补充词
"prompt_suffix": ( "prompt_suffix": (
@ -51,6 +55,7 @@ SD_MODEL_PROFILES = {
"poorly drawn face, poorly drawn hands, extra limbs, fused fingers, " "poorly drawn face, poorly drawn hands, extra limbs, fused fingers, "
"too many fingers, long neck, out of frame, " "too many fingers, long neck, out of frame, "
"western face, european face, caucasian, deep-set eyes, high nose bridge, " "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, " "blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair, "
"painting, cartoon, anime, sketch, illustration, 3d render" "painting, cartoon, anime, sketch, illustration, 3d render"
), ),
@ -66,7 +71,7 @@ SD_MODEL_PROFILES = {
}, },
"标准 (约1分钟)": { "标准 (约1分钟)": {
"steps": 30, "steps": 30,
"cfg_scale": 7.0, "cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和
"width": 512, "width": 512,
"height": 768, "height": 768,
"sampler_name": "DPM++ 2M", "sampler_name": "DPM++ 2M",
@ -75,13 +80,22 @@ SD_MODEL_PROFILES = {
}, },
"精细 (约2-3分钟)": { "精细 (约2-3分钟)": {
"steps": 40, "steps": 40,
"cfg_scale": 7.5, "cfg_scale": 6.5,
"width": 576, "width": 576,
"height": 864, "height": 864,
"sampler_name": "DPM++ SDE", "sampler_name": "DPM++ SDE",
"scheduler": "Karras", "scheduler": "Karras",
"batch_size": 2, "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 ⭐⭐⭐⭐", "display_name": "Realistic Vision ⭐⭐⭐⭐",
"description": "写实摄影感 | 纪实摄影、街拍、真实质感", "description": "写实摄影感 | 纪实摄影、街拍、真实质感",
"arch": "sd15", "arch": "sd15",
"clip_skip": 2, # RV 系列在 clip_skip=2 时人脸细节更自然
"prompt_prefix": ( "prompt_prefix": (
"RAW photo, (best quality:1.4), (masterpiece:1.3), (realistic:1.4), " "RAW photo, (best quality:1.4), (masterpiece:1.3), (realistic:1.4), "
"(photorealistic:1.4), 8k uhd, DSLR, high quality, " "(photorealistic:1.4), 8k uhd, DSLR, high quality, "
"(asian:1.2), (chinese girl:1.2), (east asian features:1.1), " "(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": ( "prompt_suffix": (
", shot on Canon EOS R5, 85mm lens, f/1.8, " ", shot on Canon EOS R5, 85mm lens, f/1.8, "
@ -108,10 +126,12 @@ SD_MODEL_PROFILES = {
"blurry, deformed, mutated, disfigured, ugly, duplicate, " "blurry, deformed, mutated, disfigured, ugly, duplicate, "
"poorly drawn face, extra limbs, fused fingers, long neck, " "poorly drawn face, extra limbs, fused fingers, long neck, "
"western face, european face, caucasian, deep-set eyes, " "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, " "blonde hair, blue eyes, green eyes, freckles, "
"painting, cartoon, anime, sketch, illustration, 3d render, " "painting, cartoon, anime, sketch, illustration, 3d render, "
"over-sharpened, over-saturated, plastic skin, airbrushed, " "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": { "presets": {
"快速 (约30秒)": { "快速 (约30秒)": {
@ -125,7 +145,7 @@ SD_MODEL_PROFILES = {
}, },
"标准 (约1分钟)": { "标准 (约1分钟)": {
"steps": 28, "steps": 28,
"cfg_scale": 7.0, "cfg_scale": 6.0, # 降低 cfg 减少人脸过饱和
"width": 512, "width": 512,
"height": 768, "height": 768,
"sampler_name": "DPM++ 2M", "sampler_name": "DPM++ 2M",
@ -134,13 +154,22 @@ SD_MODEL_PROFILES = {
}, },
"精细 (约2-3分钟)": { "精细 (约2-3分钟)": {
"steps": 40, "steps": 40,
"cfg_scale": 7.5, "cfg_scale": 6.5,
"width": 576, "width": 576,
"height": 864, "height": 864,
"sampler_name": "DPM++ SDE", "sampler_name": "DPM++ SDE",
"scheduler": "Karras", "scheduler": "Karras",
"batch_size": 2, "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 ⭐⭐⭐⭐", "display_name": "Juggernaut XL ⭐⭐⭐⭐",
"description": "电影大片感 | 高画质、商业摄影、复杂背景", "description": "电影大片感 | 高画质、商业摄影、复杂背景",
"arch": "sdxl", # SDXL 架构 "arch": "sdxl", # SDXL 架构
"clip_skip": 1, # SDXL 使用完整 CLIP 编码
"prompt_prefix": ( "prompt_prefix": (
"masterpiece, best quality, ultra detailed, 8k uhd, high resolution, " "masterpiece, best quality, ultra detailed, 8k uhd, high resolution, "
"photorealistic, cinematic lighting, cinematic composition, " "photorealistic, cinematic lighting, cinematic composition, "
"asian girl, chinese, east asian features, black hair, dark brown eyes, " "(chinese beauty:1.3), east asian features, black hair, dark brown eyes, "
"delicate facial features, fair skin, slim figure, " "(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": ( "prompt_suffix": (
", cinematic color grading, anamorphic lens, bokeh, " ", cinematic color grading, anamorphic lens, bokeh, "
@ -168,6 +202,7 @@ SD_MODEL_PROFILES = {
"extra limbs, fused fingers, too many fingers, long neck, username, " "extra limbs, fused fingers, too many fingers, long neck, username, "
"out of frame, distorted, oversaturated, underexposed, overexposed, " "out of frame, distorted, oversaturated, underexposed, overexposed, "
"western face, european face, caucasian, deep-set eyes, high nose bridge, " "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" "blonde hair, red hair, blue eyes, green eyes, freckles, thick body hair"
), ),
"presets": { "presets": {
@ -198,6 +233,20 @@ SD_MODEL_PROFILES = {
"scheduler": "Karras", "scheduler": "Karras",
"batch_size": 2, "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": ( "prompt_boost": (
"(perfect face:1.3), (extremely detailed face:1.3), (beautiful detailed eyes:1.3), " "(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), " "(flawless skin:1.2), (ultra high resolution face:1.2), (glossy lips:1.1), "
"(futuristic chinese beauty:1.2), "
), ),
# 追加到 SD prompt 后面的风格词 # 追加到 SD prompt 后面的风格词
"prompt_style": ( "prompt_style": (
@ -237,25 +287,29 @@ PERSONA_SD_PROFILES = {
# ---- 性感福利主播 ---- # ---- 性感福利主播 ----
"性感福利主播": { "性感福利主播": {
"prompt_boost": ( "prompt_boost": (
"(beautiful detailed face:1.3), (seductive eyes:1.2), (glossy lips:1.2), " "(beautiful detailed face:1.3), (charming eyes:1.2), (glossy lips:1.2), "
"(perfect body proportions:1.2), (slim waist:1.2), (long legs:1.2), " "(elegant posture:1.2), (slender figure:1.1), "
"(glamour photography:1.3), " "(glamour photography:1.2), (delicate asian features:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", soft glamour lighting, beauty retouching, " ", soft glamour lighting, beauty retouching, "
"intimate atmosphere, warm golden tones, shallow depth of field, " "warm golden tones, shallow depth of field, "
"boudoir style, sensual but tasteful, fashion model pose" "chic fashion style, confident pose"
),
"negative_extra": (
"lingerie, underwear, bikini, nude, revealing clothing, "
"cleavage, nsfw, explicit, suggestive"
), ),
"negative_extra": "",
"llm_guide": ( "llm_guide": (
"\n\n【人设视觉风格 - 性感福利主播】\n" "\n\n【人设视觉风格 - 时尚魅力博主】\n"
"为一个身材曼妙的时尚博主生成图片,主打身材美感+氛围感:\n" "为一个时尚魅力博主生成图片,主打精致感+时尚感,严禁擦边内容:\n"
"- 身材描述slim waist, long legs, perfect figure, hourglass body, graceful pose\n" "- 穿搭关键elegant dress, chic outfit, stylish fashion, trendy clothes, "
"- 穿搭关键lingerie, bikini, bodycon dress, off-shoulder, backless dress, lace, sheer fabric\n" "off-shoulder top (不露胸), midi skirt, fitted blazer\n"
"- 光线氛围soft warm lighting, golden hour, window light, candle light, intimate mood\n" "- 光线氛围soft warm lighting, golden hour, window light, beauty lighting\n"
"- 场景选择bedroom, luxury hotel, swimming pool, beach sunset, mirror selfie\n" "- 场景选择city street, cafe, rooftop, shopping district, fashion studio\n"
"- 构图要点full body or three-quarter shot, emphasize curves, elegant pose, looking at viewer\n" "- 构图要点half body or full body shot, elegant pose, confident expression, "
"- 整体风格glamour photography魅力但有品位不要低俗\n" "fashion editorial style\n"
"- 整体风格:高端时尚杂志风,精致大方,绝对禁止 bikini/lingerie/revealing/nsfw 等词\n"
), ),
}, },
# ---- 身材管理健身美女 ---- # ---- 身材管理健身美女 ----
@ -263,6 +317,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(fit body:1.3), (athletic physique:1.2), (toned muscles:1.2), " "(fit body:1.3), (athletic physique:1.2), (toned muscles:1.2), "
"(healthy glow:1.2), (confident pose:1.2), " "(healthy glow:1.2), (confident pose:1.2), "
"(healthy luminous skin:1.2), (delicate east asian face:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", fitness photography, gym environment, athletic wear, " ", fitness photography, gym environment, athletic wear, "
@ -286,6 +341,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(elegant:1.2), (gentle expression:1.2), (sophisticated:1.2), " "(elegant:1.2), (gentle expression:1.2), (sophisticated:1.2), "
"(fashion forward:1.2), (graceful:1.1), " "(fashion forward:1.2), (graceful:1.1), "
"(chinese elegance:1.2), (refined temperament:1.2), (porcelain skin:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", fashion editorial, street style photography, " ", fashion editorial, street style photography, "
@ -309,6 +365,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(artistic:1.2), (moody atmosphere:1.2), (film grain:1.2), " "(artistic:1.2), (moody atmosphere:1.2), (film grain:1.2), "
"(vintage tones:1.1), " "(vintage tones:1.1), "
"(delicate features:1.1), (gentle east asian face:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", film photography, 35mm film, kodak portra 400, " ", film photography, 35mm film, kodak portra 400, "
@ -332,6 +389,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(cosplay:1.3), (detailed costume:1.2), (colorful:1.2), " "(cosplay:1.3), (detailed costume:1.2), (colorful:1.2), "
"(anime inspired:1.1), (vibrant:1.2), " "(anime inspired:1.1), (vibrant:1.2), "
"(bright clear skin:1.1), (big bright eyes:1.2), (youthful:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", cosplay photography, anime convention, colorful costume, " ", cosplay photography, anime convention, colorful costume, "
@ -355,6 +413,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(traditional chinese dress:1.3), (hanfu:1.3), (chinese aesthetic:1.2), " "(traditional chinese dress:1.3), (hanfu:1.3), (chinese aesthetic:1.2), "
"(elegant traditional:1.2), (delicate accessories:1.1), " "(elegant traditional:1.2), (delicate accessories:1.1), "
"(classical chinese beauty:1.3), (ethereal temperament:1.2), "
), ),
"prompt_style": ( "prompt_style": (
", traditional chinese photography, han dynasty style, " ", traditional chinese photography, han dynasty style, "
@ -378,6 +437,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(cozy atmosphere:1.3), (warm lighting:1.2), (homey:1.2), " "(cozy atmosphere:1.3), (warm lighting:1.2), (homey:1.2), "
"(casual style:1.1), (relaxed:1.1), " "(casual style:1.1), (relaxed:1.1), "
"(fresh natural beauty:1.1), (natural bare face:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", cozy home photography, warm ambient light, " ", cozy home photography, warm ambient light, "
@ -401,6 +461,7 @@ PERSONA_SD_PROFILES = {
"prompt_boost": ( "prompt_boost": (
"(flawless makeup:1.3), (detailed eye makeup:1.3), (beauty close-up:1.2), " "(flawless makeup:1.3), (detailed eye makeup:1.3), (beauty close-up:1.2), "
"(perfect skin:1.2), (beauty lighting:1.2), " "(perfect skin:1.2), (beauty lighting:1.2), "
"(luminous porcelain skin:1.2), (delicate east asian features:1.1), "
), ),
"prompt_style": ( "prompt_style": (
", beauty photography, ring light, macro lens, " ", beauty photography, ring light, macro lens, "
@ -499,6 +560,54 @@ DEFAULT_NEGATIVE = SD_MODEL_PROFILES[DEFAULT_MODEL_PROFILE]["negative_prompt"]
# ==================== 图片反 AI 检测管线 ==================== # ==================== 图片反 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: def anti_detect_postprocess(img: Image.Image) -> Image.Image:
"""对 AI 生成的图片进行后处理,模拟手机拍摄/加工特征,降低 AI 检测率 """对 AI 生成的图片进行后处理,模拟手机拍摄/加工特征,降低 AI 检测率
@ -745,6 +854,7 @@ class SDService:
face_image: Image.Image = None, face_image: Image.Image = None,
quality_mode: str = None, quality_mode: str = None,
persona: str = None, persona: str = None,
enhance_level: float = 1.0,
) -> list[Image.Image]: ) -> list[Image.Image]:
"""文生图(自动适配当前 SD 模型 + 人设的最优参数) """文生图(自动适配当前 SD 模型 + 人设的最优参数)
@ -753,6 +863,7 @@ class SDService:
face_image: 头像 PIL Image传入后自动启用 ReActor 换脸 face_image: 头像 PIL Image传入后自动启用 ReActor 换脸
quality_mode: 预设模式名 quality_mode: 预设模式名
persona: 博主人设文本自动注入人设视觉增强词 persona: 博主人设文本自动注入人设视觉增强词
enhance_level: 美化增强强度 (0=关闭, 1.0=默认, 2.0=强化)
""" """
if model: if model:
self.switch_model(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"], "sampler_name": sampler_name if sampler_name is not None else preset["sampler_name"],
"scheduler": scheduler if scheduler is not None else preset["scheduler"], "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", logger.info("SD 生成参数 [%s]: steps=%s, cfg=%.1f, %dx%d, sampler=%s",
profile_key, payload['steps'], payload['cfg_scale'], profile_key, payload['steps'], payload['cfg_scale'],
payload['width'], payload['height'], payload['sampler_name']) 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 换脸 # 如果提供了头像,通过 ReActor 换脸
if face_image is not None: if face_image is not None:
payload["alwayson_scripts"] = self._build_reactor_args(face_image) payload["alwayson_scripts"] = self._build_reactor_args(face_image)
@ -807,11 +934,26 @@ class SDService:
json=payload, json=payload,
timeout=SD_TIMEOUT, 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() resp.raise_for_status()
images = [] images = []
for img_b64 in resp.json().get("images", []): for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64))) img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
# Task 3.5: 美化增强(精致感 + 中国审美皮肤校色)
img = beauty_enhance(img, level=enhance_level)
# 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征 # 反 AI 检测后处理: 剥离元数据 + 模拟手机拍摄特征
img = anti_detect_postprocess(img) img = anti_detect_postprocess(img)
images.append(img) images.append(img)
@ -828,6 +970,7 @@ class SDService:
sampler_name: str = None, sampler_name: str = None,
scheduler: str = None, scheduler: str = None,
model: str = None, model: str = None,
enhance_level: float = 1.0,
) -> list[Image.Image]: ) -> list[Image.Image]:
"""图生图(自动适配模型参数)""" """图生图(自动适配模型参数)"""
profile = get_model_profile(model) profile = get_model_profile(model)
@ -864,6 +1007,8 @@ class SDService:
images = [] images = []
for img_b64 in resp.json().get("images", []): for img_b64 in resp.json().get("images", []):
img = Image.open(io.BytesIO(base64.b64decode(img_b64))) img = Image.open(io.BytesIO(base64.b64decode(img_b64)))
# Task 3.6: 美化增强
img = beauty_enhance(img, level=enhance_level)
# 反 AI 检测后处理 # 反 AI 检测后处理
img = anti_detect_postprocess(img) img = anti_detect_postprocess(img)
images.append(img) images.append(img)

View File

@ -44,8 +44,8 @@ def generate_copy(model, topic, style, sd_model_name, persona_text):
return "", "", "", "", f"❌ 生成失败: {e}" 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): 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 换脸,支持质量模式预设,支持人设视觉优化""" """生成图片(可选 ReActor 换脸,支持质量模式预设,支持人设视觉优化,支持美化增强"""
if not model: if not model:
return None, [], "❌ 未选择 SD 模型" return None, [], "❌ 未选择 SD 模型"
try: try:
@ -86,6 +86,7 @@ def generate_images(sd_url, prompt, neg_prompt, model, steps, cfg_scale, face_sw
face_image=face_image, face_image=face_image,
quality_mode=quality_mode, quality_mode=quality_mode,
persona=persona, persona=persona,
enhance_level=float(enhance_level),
) )
preset = get_sd_preset(quality_mode) preset = get_sd_preset(quality_mode)
swap_hint = " (已换脸)" if face_image else "" swap_hint = " (已换脸)" if face_image else ""

707
ui/app.py
View File

@ -57,7 +57,34 @@ from publish_queue import STATUS_LABELS
logger = logging.getLogger("autobot") 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: 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( with gr.Blocks(
title="小红书 AI 爆文工坊 V2.0", title="小红书 AI 爆文工坊 V2.0",
css=_GRADIO_CSS,
) as app: ) as app:
gr.Markdown( gr.Markdown(
"# 🍒 小红书 AI 爆文生产工坊 V2.0\n" "# 🍒 小红书 AI 爆文生产工坊 V2.0\n"
@ -75,118 +103,121 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
# 全局状态 # 全局状态
state_search_result = gr.State("") state_search_result = gr.State("")
# ============ 全局设置栏 ============ # ============ Tab 页面 ============
with gr.Accordion("⚙️ 全局设置 (自动保存)", open=False): # ⚙️ 配置 Tab 声明在最前以确保共享组件变量先于 build_tab() 定义
gr.Markdown("#### 🤖 LLM 提供商 (支持所有 OpenAI 兼容接口)") # selected=1 令「✨ 内容创作」为默认激活 Tab
with gr.Row(): with gr.Tabs(selected=1):
llm_provider = gr.Dropdown( # -------- Tab 0: ⚙️ 配置 --------
label="选择 LLM 提供商", with gr.Tab("⚙️ 配置"):
choices=cfg.get_llm_provider_names(), gr.Markdown("#### 🤖 LLM 提供商 (支持所有 OpenAI 兼容接口)")
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):
with gr.Row(): with gr.Row():
new_provider_name = gr.Textbox( llm_provider = gr.Dropdown(
label="名称", placeholder="如: DeepSeek / GPT-4o / 通义千问", 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, scale=1,
) )
new_provider_key = gr.Textbox( face_image_preview = gr.Image(
label="API Key", type="password", scale=2, label="当前头像",
) type="pil",
new_provider_url = gr.Textbox( height=180,
label="Base URL", placeholder="https://api.openai.com/v1", interactive=False,
value="https://api.openai.com/v1", scale=2, value=SDService.load_face_image(),
scale=1,
) )
with gr.Row(): with gr.Row():
btn_add_provider = gr.Button("✅ 添加提供商", variant="primary", size="sm") btn_save_face = gr.Button("💾 保存头像", variant="primary", size="sm")
btn_del_provider = gr.Button("🗑️ 删除当前提供商", variant="stop", size="sm") face_swap_toggle = gr.Checkbox(
provider_mgmt_status = gr.Markdown("") label="🎭 生成图片时启用 AI 换脸",
value=os.path.isfile(FACE_IMAGE_PATH),
gr.Markdown("---") interactive=True,
with gr.Row(): )
mcp_url = gr.Textbox( face_status = gr.Markdown(
label="MCP Server URL", value=config["mcp_url"], scale=2, "✅ 头像已就绪" if os.path.isfile(FACE_IMAGE_PATH) else " 尚未设置头像"
)
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 "⚪ 未启用",
) )
# ============ Tab 页面 ============ gr.Markdown("---")
with gr.Tabs(): gr.Markdown("#### 🖥️ 系统设置")
# -------- Tab 1: 内容创作(迁移至 ui/tab_create.py-------- 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( _tab1 = build_tab(
config=config, config=config,
styles=DEFAULT_STYLES, styles=DEFAULT_STYLES,
@ -217,7 +248,101 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
cfg_scale = _tab1["cfg_scale"] cfg_scale = _tab1["cfg_scale"]
neg_prompt = _tab1["neg_prompt"] 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("🔥 热点探测"): with gr.Tab("🔥 热点探测"):
gr.Markdown("### 搜索热门内容 → AI 分析趋势 → 一键借鉴创作") gr.Markdown("### 搜索热门内容 → AI 分析趋势 → 一键借鉴创作")
with gr.Row(): with gr.Row():
@ -380,67 +505,7 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
) )
my_reply_status = gr.Markdown("") my_reply_status = gr.Markdown("")
# -------- Tab 4: 账号登录 -------- # -------- Tab 5: 📊 数据看板 --------
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: 数据看板 --------
with gr.Tab("📊 数据看板"): with gr.Tab("📊 数据看板"):
gr.Markdown( gr.Markdown(
"### 我的账号数据看板\n" "### 我的账号数据看板\n"
@ -657,94 +722,106 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
) )
auto_publish_result = gr.Markdown("") auto_publish_result = gr.Markdown("")
# 右栏: 定时自动化 # 右栏: 定时自动化 (2列卡片网格)
with gr.Column(scale=1): with gr.Column(scale=2):
gr.Markdown("#### ⏰ 随机定时自动化") gr.Markdown("#### ⏰ 随机定时自动化")
gr.Markdown( gr.Markdown(
"> 设置时间间隔后启动,系统将在随机时间自动执行\n" "> 设置时间间隔后启动,系统将在随机时间自动执行 · 模拟真人操作节奏,降低被检测风险"
"> 模拟真人操作节奏,降低被检测风险"
) )
sched_status = gr.Markdown("⚪ **调度器未运行**") sched_status = gr.Markdown("⚪ **调度器未运行**")
# 运营时段设置 # ── 第1行: 时段 + 评论 ──
with gr.Group(): with gr.Row():
gr.Markdown("##### ⏰ 运营时段") with gr.Column(scale=1):
with gr.Row(): with gr.Group():
sched_start_hour = gr.Number( gr.Markdown("##### ⏰ 运营时段")
label="开始时间(整点)", value=config.get("sched_start_hour", 8), minimum=0, maximum=23, with gr.Row():
) sched_start_hour = gr.Number(
sched_end_hour = gr.Number( label="开始(整点)", value=config.get("sched_start_hour", 8), minimum=0, maximum=23,
label="结束时间(整点)", value=config.get("sched_end_hour", 23), minimum=1, maximum=24, )
) 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(): # ── 第2行: 点赞 + 收藏 ──
sched_comment_on = gr.Checkbox( with gr.Row():
label="✅ 启用自动评论", value=config.get("sched_comment_on", True), with gr.Column(scale=1):
) with gr.Group():
with gr.Row(): gr.Markdown("##### 👍 自动点赞")
sched_c_min = gr.Number( sched_like_on = gr.Checkbox(
label="评论最小间隔(分钟)", value=config.get("sched_c_min", 15), minimum=5, label="启用", value=config.get("sched_like_on", True),
) )
sched_c_max = gr.Number( with gr.Row():
label="评论最大间隔(分钟)", value=config.get("sched_c_max", 45), minimum=10, 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(): # ── 第3行: 回复 + 发布 ──
sched_like_on = gr.Checkbox( with gr.Row():
label="✅ 启用自动点赞", value=config.get("sched_like_on", True), with gr.Column(scale=1):
) with gr.Group():
with gr.Row(): gr.Markdown("##### 💌 自动回复")
sched_l_min = gr.Number( sched_reply_on = gr.Checkbox(
label="点赞最小间隔(分钟)", value=config.get("sched_l_min", 10), minimum=3, label="启用", value=config.get("sched_reply_on", True),
) )
sched_l_max = gr.Number( with gr.Row():
label="点赞最大间隔(分钟)", value=config.get("sched_l_max", 30), minimum=5, sched_r_min = gr.Number(
) label="最小间隔(分钟)", value=config.get("sched_r_min", 20), minimum=5,
sched_like_count = gr.Number( )
label="每轮点赞数量", value=config.get("sched_like_count", 5), minimum=1, maximum=15, sched_r_max = gr.Number(
) label="最大间隔(分钟)", value=config.get("sched_r_max", 60), minimum=10,
)
with gr.Group(): sched_reply_max = gr.Number(
sched_fav_on = gr.Checkbox( label="每轮最多回复", value=config.get("sched_reply_max", 3), minimum=1, maximum=10,
label="✅ 启用自动收藏", value=config.get("sched_fav_on", True), )
) with gr.Column(scale=1):
with gr.Row(): with gr.Group():
sched_fav_min = gr.Number( gr.Markdown("##### 🚀 自动发布")
label="收藏最小间隔(分钟)", value=config.get("sched_fav_min", 12), minimum=3, sched_publish_on = gr.Checkbox(
) label="启用", value=config.get("sched_publish_on", True),
sched_fav_max = gr.Number( )
label="收藏最大间隔(分钟)", value=config.get("sched_fav_max", 35), minimum=5, with gr.Row():
) sched_p_min = gr.Number(
sched_fav_count = gr.Number( label="最小间隔(分钟)", value=config.get("sched_p_min", 60), minimum=30,
label="每轮收藏数量", value=config.get("sched_fav_count", 3), minimum=1, maximum=10, )
) sched_p_max = gr.Number(
label="最大间隔(分钟)", value=config.get("sched_p_max", 180), minimum=60,
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,
)
with gr.Row(): with gr.Row():
btn_start_sched = gr.Button( btn_start_sched = gr.Button(
@ -761,7 +838,7 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
gr.Markdown("#### 📋 运行日志") gr.Markdown("#### 📋 运行日志")
with gr.Row(): with gr.Row():
btn_refresh_log = gr.Button("🔄 刷新日志", size="sm") 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") btn_refresh_stats = gr.Button("📊 刷新统计", size="sm")
auto_log_display = gr.TextArea( auto_log_display = gr.TextArea(
label="自动化运行日志", label="自动化运行日志",
@ -775,98 +852,64 @@ def build_app(cfg: "ConfigManager", analytics: "AnalyticsService") -> gr.Blocks:
value=_get_stats_summary(), value=_get_stats_summary(),
) )
# -------- Tab 8: 内容排期 📅 -------- # -------- Tab 8: 🔐 账号登录 --------
with gr.Tab("📅 内容排期"): with gr.Tab("🔐 账号登录"):
gr.Markdown( gr.Markdown(
"### 📅 内容排期日历 + 发布队列\n" "### 小红书账号登录\n"
"> 批量生成内容 → 预览审核 → 排期定时 → 自动发布,内容创作全流程管控\n\n" "> 扫码登录后自动获取 xsec_token配合用户 ID 即可使用所有功能"
"**工作流**: 生成内容 → 📝草稿 → ✅审核通过 → 🕐排期/立即发布 → 🚀自动发布"
) )
with gr.Row(): with gr.Row():
# ===== 左栏: 生成 & 队列控制 =====
with gr.Column(scale=1): with gr.Column(scale=1):
gr.Markdown("#### 🔧 批量生成到队列") gr.Markdown(
queue_gen_topics = gr.Textbox( "**操作步骤:**\n"
label="主题池 (逗号分隔,随人设自动切换)", "1. 确保 MCP 服务已启动\n"
value=", ".join(get_persona_topics(config.get("persona", ""))), "2. 点击「获取登录二维码」→ 用小红书 App 扫码\n"
placeholder="会从池中随机选取,切换人设自动更新", "3. 点击「检查登录状态」→ 自动获取并保存 xsec_token\n"
"4. 首次使用请填写你的用户 ID 并点击保存\n\n"
"⚠️ 登录后不要在其他网页端登录同一账号,否则会被踢出"
) )
with gr.Row(): btn_get_qrcode = gr.Button(
queue_gen_count = gr.Number( "📱 获取登录二维码", variant="primary", size="lg",
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("") 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("---")
gr.Markdown("#### ⚙️ 队列处理器") gr.Markdown(
queue_processor_status = gr.Markdown( "#### 📌 我的账号信息\n"
value=queue_get_status(), "> **注意**: 小红书号 ≠ 用户 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(): login_user_id = gr.Textbox(
btn_queue_start = gr.Button( label="我的用户 ID (24位 userId, 非小红书号)",
"▶️ 启动队列处理", variant="primary", value=config.get("my_user_id", ""),
) placeholder="如: 5a695db6e8ac2b72e8af2a53",
btn_queue_stop = gr.Button( )
"⏹️ 停止队列处理", variant="stop", login_xsec_token = gr.Textbox(
) label="xsec_token (登录后自动获取)",
queue_processor_result = gr.Markdown("") value=config.get("xsec_token", ""),
interactive=False,
)
btn_save_uid = gr.Button(
"💾 保存用户 ID", variant="secondary",
)
save_uid_status = gr.Markdown("")
gr.Markdown("---") with gr.Column(scale=1):
gr.Markdown("#### 🔍 操作单个队列项") qr_image = gr.Image(
queue_item_id = gr.Textbox( label="扫码登录", height=350, width=350,
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 后点击预览*",
) )
# ================================================== # ==================================================

View File

@ -50,7 +50,7 @@ def build_tab(
with gr.Row(): with gr.Row():
# ---- 左栏:输入 ---- # ---- 左栏:输入 ----
with gr.Column(scale=1): with gr.Column(scale=3):
gr.Markdown("### 💡 构思") gr.Markdown("### 💡 构思")
topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭") topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭")
style = gr.Dropdown( style = gr.Dropdown(
@ -75,10 +75,17 @@ def build_tab(
) )
steps = gr.Slider(8, 50, value=config.get("sd_steps", 20), step=1, label="步数") 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") 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") btn_gen_img = gr.Button("🎨 第二步:生成图片", variant="primary")
# ---- 中栏:文案编辑 ---- # ---- 中栏:文案编辑 ----
with gr.Column(scale=1): with gr.Column(scale=4):
gr.Markdown("### 📝 文案编辑") gr.Markdown("### 📝 文案编辑")
res_title = gr.Textbox( res_title = gr.Textbox(
label="标题", label="标题",
@ -97,7 +104,7 @@ def build_tab(
) )
# ---- 右栏:预览 & 发布 ---- # ---- 右栏:预览 & 发布 ----
with gr.Column(scale=1): with gr.Column(scale=3):
gr.Markdown("### 🖼️ 视觉预览") gr.Markdown("### 🖼️ 视觉预览")
gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300) gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300)
local_images = gr.File( 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=[]) 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=[]) 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=[]) 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( btn_gen_img.click(
fn=fn_gen_img, fn=fn_gen_img,
inputs=[sd_url, res_prompt, neg_prompt, sd_model, steps, cfg_scale, 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], outputs=[gallery, state_images, status_bar],
) )
@ -169,4 +178,5 @@ def build_tab(
"steps": steps, "steps": steps,
"cfg_scale": cfg_scale, "cfg_scale": cfg_scale,
"neg_prompt": neg_prompt, "neg_prompt": neg_prompt,
"enhance_level": enhance_level,
} }