xhs_factory/ui/tab_create.py
zhoujie 1ec520b47e feat(content): 新增智能选题引擎、批量创作和图文协同优化
- 新增智能选题引擎 `TopicEngine`,整合热点数据与历史权重,提供多维度评分和创作角度建议
- 新增内容模板系统 `ContentTemplate`,支持从 JSON 文件加载模板并应用于文案生成
- 新增批量创作功能 `batch_generate_copy`,支持串行生成多篇文案并自动入草稿队列
- 升级文案质量流水线:实现 Prompt 分层架构(基础层 + 风格层 + 人设层)、LLM 自检与改写机制、深度去 AI 化后处理
- 优化图文协同:新增封面图策略选择、SD prompt 与文案语义联动、图文匹配度评估
- 集成数据闭环:在文案生成中自动注入 `AnalyticsService` 权重数据,实现发布 → 数据回收 → 优化创作的完整循环
- 更新 UI 组件:新增选题推荐展示区、批量创作折叠面板、封面图策略选择器和图文匹配度评分展示

♻️ refactor(llm): 重构 Prompt 架构并增强去 AI 化处理

- 将 `PROMPT_COPYWRITING` 拆分为分层架构(基础层 + 风格层 + 人设层),提高维护性和灵活性
- 增强 `_humanize_content` 方法:新增语气词注入、标点不规范化、段落节奏打散和 emoji 密度控制
- 新增 `_self_check` 和 `_self_check_rewrite` 方法,实现文案 AI 痕迹自检与自动改写
- 新增 `evaluate_image_text_match` 方法,支持文案与 SD prompt 的语义匹配度评估(可选,失败不阻塞)
- 新增封面图策略配置 `COVER_STRATEGIES` 和情感基调映射 `EMOTION_SD_MAP`

📝 docs(openspec): 归档内容创作优化提案和详细规格

- 新增 `openspec/changes/archive/2026-02-28-optimize-content-creation/` 目录,包含设计文档、提案、规格说明和任务清单
- 新增 `openspec/specs/` 下的批量创作、文案质量流水线、图文协同、服务内容和智能选题引擎规格文档
- 更新 `openspec/specs/services-content/spec.md`,反映新增的批量创作和智能选题入口函数

🔧 chore(config): 更新服务配置和 UI 集成

- 在 `services/content.py` 中集成权重数据自动注入逻辑,实现数据驱动创作
- 在 `ui/app.py` 中新增选题推荐、批量生成和图文匹配度评估的回调函数
- 在 `ui/tab_create.py` 中新增智能选题推荐区、批量创作面板和图文匹配度评估组件
- 修复 `services/sd_service.py` 中的头像文件路径问题,确保目录存在
2026-02-28 21:04:09 +08:00

345 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
内容创作 Tab UI 模块
包含 Tab 1「✨ 内容创作」的所有 Gradio 组件定义和事件绑定
"""
import logging
import gradio as gr
logger = logging.getLogger("autobot")
def build_tab(
config: dict,
styles: list,
sd_preset_names: list,
default_negative: str,
# 共享的 Gradio 组件(由 main.py 创建并传入)
llm_model,
sd_model,
sd_url,
persona,
status_bar,
face_swap_toggle,
face_image_preview,
mcp_url,
# 业务处理函数(由 main.py 传入,避免循环导入)
fn_gen_copy,
fn_gen_img,
fn_export,
fn_publish,
fn_get_sd_preset,
fn_cfg_set,
fn_cfg_update,
# 新增: 批量创作 & 选题推荐回调
fn_batch_generate=None,
fn_topic_recommend=None,
fn_evaluate_match=None,
):
"""
构建「✨ 内容创作」Tab注册所有事件绑定。
Parameters
----------
config : 当前配置字典(用于读取初始值)
styles : 文案风格列表
sd_preset_names : SD 生成模式名称列表
default_negative: 默认反向提示词
llm_model / sd_model / ... : 由全局设置栏创建的共享组件引用
fn_* : 业务逻辑回调函数
Returns
-------
None —— 所有事件绑定均在此函数内完成,无需外部再次绑定。
"""
with gr.Tab("✨ 内容创作"):
# 本 Tab 私有的图片状态(不被其他 Tab 使用)
state_images = gr.State([])
with gr.Row():
# ---- 左栏:输入 ----
with gr.Column(scale=3):
gr.Markdown("### 💡 构思")
# === 智能选题推荐 ===
with gr.Accordion("🧠 智能选题推荐", open=False):
btn_recommend = gr.Button("🔍 获取推荐选题", variant="secondary", size="sm")
topic_recommendations = gr.Markdown(
value="点击上方按钮获取推荐选题",
label="推荐选题",
)
topic = gr.Textbox(label="笔记主题", placeholder="例如:优衣库早春穿搭")
style = gr.Dropdown(
styles,
label="风格", value="好物种草",
)
btn_gen_copy = gr.Button("✨ 第一步:生成文案", variant="primary")
gr.Markdown("---")
gr.Markdown("### 🎨 绘图参数")
# 封面图策略选择
cover_strategy = gr.Radio(
["人物特写", "场景展示", "对比图", "文字卡片"],
label="封面图策略",
value="人物特写",
info="影响 SD 构图和尺寸",
)
quality_mode = gr.Radio(
sd_preset_names,
label="生成模式",
value=config.get("quality_mode", "标准 (约1分钟)"),
info="快速≈30s 标准≈1min 精细≈2-3min (SDXL)",
)
with gr.Accordion("高级设置 (覆盖预设)", open=False):
neg_prompt = gr.Textbox(
label="反向提示词",
value=config.get("sd_negative_prompt", default_negative),
lines=2,
)
steps = gr.Slider(8, 50, value=config.get("sd_steps", 20), step=1, label="步数")
cfg_scale = gr.Slider(1, 15, value=config.get("sd_cfg_scale", 5.5), step=0.5, label="CFG Scale")
enhance_level = gr.Slider(
0.0, 2.0,
value=config.get("enhance_level", 1.0),
step=0.1,
label="美化强度",
info="0=关闭 1=默认 2=强化(锐化+皮肤校色+通透感)",
)
btn_gen_img = gr.Button("🎨 第二步:生成图片", variant="primary")
# ---- 中栏:文案编辑 ----
with gr.Column(scale=4):
gr.Markdown("### 📝 文案编辑")
res_title = gr.Textbox(
label="标题",
interactive=True,
info="小红书限制 ≤20 字,超出将无法发布",
)
res_content = gr.TextArea(
label="正文 (可手动修改)", lines=12, interactive=True,
)
res_prompt = gr.TextArea(
label="绘图提示词", lines=3, interactive=True,
)
res_tags = gr.Textbox(
label="话题标签 (逗号分隔)", interactive=True,
placeholder="穿搭, 春季, 好物种草",
)
# ---- 右栏:预览 & 发布 ----
with gr.Column(scale=3):
gr.Markdown("### 🖼️ 视觉预览")
gallery = gr.Gallery(label="AI 生成图片", columns=2, height=300)
local_images = gr.File(
label="📁 上传本地图片(可混排)",
file_count="multiple",
file_types=["image"],
)
gr.Markdown("### 🚀 发布")
schedule_time = gr.Textbox(
label="定时发布 (可选, ISO8601格式)",
placeholder="如 2026-02-08T18:00:00+08:00留空=立即发布",
)
with gr.Row():
btn_export = gr.Button("📂 导出本地", variant="secondary")
btn_publish = gr.Button("🚀 发布到小红书", variant="primary")
publish_msg = gr.Markdown("")
# === 图文匹配度评分 ===
with gr.Accordion("📊 图文匹配度", open=False):
btn_eval_match = gr.Button("评估匹配度", variant="secondary", size="sm")
match_score_display = gr.Markdown("点击按钮评估文案与图片的匹配度")
# === 批量创作面板 ===
with gr.Accordion("📦 批量创作", open=False):
with gr.Row():
with gr.Column(scale=2):
batch_topics = gr.TextArea(
label="批量主题 (每行一个最多10个)",
placeholder="优衣库早春穿搭\n百元床品测评\n新手养宠攻略",
lines=5,
)
with gr.Column(scale=1):
batch_template = gr.Dropdown(
choices=["(不使用模板)", "好物种草", "日常分享", "攻略教程"],
value="(不使用模板)",
label="内容模板",
)
btn_batch_gen = gr.Button("🚀 批量生成", variant="primary")
btn_smart_gen = gr.Button("🧠 智能选题+生成", variant="secondary")
batch_result = gr.Markdown("")
# ---- 事件绑定 ----
btn_gen_copy.click(
fn=fn_gen_copy,
inputs=[llm_model, topic, style, sd_model, persona],
outputs=[res_title, res_content, res_prompt, res_tags, status_bar],
)
def save_quality_mode(mode):
fn_cfg_set("quality_mode", mode)
p = fn_get_sd_preset(mode)
return p["steps"], p["cfg_scale"]
quality_mode.change(
fn=save_quality_mode,
inputs=[quality_mode],
outputs=[steps, cfg_scale],
)
steps.change(fn=lambda s: fn_cfg_set("sd_steps", s), inputs=[steps], outputs=[])
cfg_scale.change(fn=lambda c: fn_cfg_set("sd_cfg_scale", c), inputs=[cfg_scale], outputs=[])
neg_prompt.change(fn=lambda n: fn_cfg_set("sd_negative_prompt", n), inputs=[neg_prompt], outputs=[])
enhance_level.change(fn=lambda v: fn_cfg_set("enhance_level", v), inputs=[enhance_level], outputs=[])
btn_gen_img.click(
fn=fn_gen_img,
inputs=[sd_url, res_prompt, neg_prompt, sd_model, steps, cfg_scale,
face_swap_toggle, face_image_preview, quality_mode, persona,
enhance_level],
outputs=[gallery, state_images, status_bar],
)
btn_export.click(
fn=fn_export,
inputs=[res_title, res_content, state_images],
outputs=[publish_msg],
)
btn_publish.click(
fn=fn_publish,
inputs=[res_title, res_content, res_tags, state_images,
local_images, mcp_url, schedule_time],
outputs=[publish_msg],
)
# ---- 新增事件绑定 ----
# 智能选题推荐
def _on_recommend(model_name):
if not fn_topic_recommend:
return "⚠️ 选题推荐功能未连接"
try:
recommendations = fn_topic_recommend(model_name)
if not recommendations:
return "暂无推荐选题,请先搜索热点或积累数据"
lines = []
for i, r in enumerate(recommendations, 1):
angles_str = "".join(r.get("angles", [])[:2])
lines.append(
f"**{i}. {r['topic']}** (评分: {r['score']})\n"
f" {r.get('reason', '')}\n"
f" 💡 角度: {angles_str}"
)
return "\n\n".join(lines)
except Exception as e:
logger.error("选题推荐失败: %s", e)
return f"❌ 推荐失败: {e}"
btn_recommend.click(
fn=_on_recommend,
inputs=[llm_model],
outputs=[topic_recommendations],
)
# 图文匹配度评估
def _on_eval_match(model_name, content, sd_prompt):
if not fn_evaluate_match:
return "⚠️ 图文匹配度评估功能未连接"
if not content or not sd_prompt:
return "请先生成文案和图片后再评估"
try:
result = fn_evaluate_match(model_name, content, sd_prompt)
if result.get("skipped"):
return "⚠️ 评估超时或失败,已跳过"
score = result.get("match_score", 0)
suggestions = result.get("suggestions", [])
icon = "🟢" if score >= 80 else ("🟡" if score >= 50 else "🔴")
text = f"{icon} 匹配度: **{score}/100**"
if suggestions:
text += "\n\n改进建议:\n" + "\n".join(f"- {s}" for s in suggestions)
if score < 50:
text += "\n\n⚠️ 匹配度较低,建议重新生成图片提示词"
return text
except Exception as e:
return f"评估失败: {e}"
btn_eval_match.click(
fn=_on_eval_match,
inputs=[llm_model, res_content, res_prompt],
outputs=[match_score_display],
)
# 批量生成
def _on_batch_generate(model_name, topics_text, style_val, sd_model_name, persona_text, template):
if not fn_batch_generate:
return "⚠️ 批量创作功能未连接"
topics = [t.strip() for t in topics_text.strip().split("\n") if t.strip()]
if not topics:
return "❌ 请输入至少一个主题(每行一个)"
template_name = template if template != "(不使用模板)" else ""
try:
results, status = fn_batch_generate(
model_name, topics, style_val, sd_model_name, persona_text, template_name
)
lines = [f"### {status}\n"]
for r in results:
if "error" in r:
lines.append(f"❌ **{r.get('topic', '未知')}**: {r['error']}")
else:
lines.append(f"✅ **{r.get('title', '无标题')}** — {r.get('topic', '')}")
return "\n\n".join(lines)
except Exception as e:
return f"❌ 批量生成失败: {e}"
btn_batch_gen.click(
fn=_on_batch_generate,
inputs=[llm_model, batch_topics, style, sd_model, persona, batch_template],
outputs=[batch_result],
)
# 智能选题+生成
def _on_smart_generate(model_name, style_val, sd_model_name, persona_text):
if not fn_topic_recommend or not fn_batch_generate:
return "⚠️ 智能选题功能未连接"
try:
recommendations = fn_topic_recommend(model_name)
if not recommendations:
return "❌ 选题引擎未找到推荐主题"
# 取前 3 个推荐
topics = [r["topic"] for r in recommendations[:3]]
results, status = fn_batch_generate(
model_name, topics, style_val, sd_model_name, persona_text, ""
)
lines = [f"### {status}\n", "**使用推荐选题:**"]
for r in results:
if "error" in r:
lines.append(f"❌ **{r.get('topic', '未知')}**: {r['error']}")
else:
lines.append(f"✅ **{r.get('title', '无标题')}**")
return "\n\n".join(lines)
except Exception as e:
return f"❌ 智能生成失败: {e}"
btn_smart_gen.click(
fn=_on_smart_generate,
inputs=[llm_model, style, sd_model, persona],
outputs=[batch_result],
)
# 返回可能被其他 Tab 引用的组件
return {
"res_title": res_title,
"res_content": res_content,
"res_prompt": res_prompt,
"res_tags": res_tags,
"quality_mode": quality_mode,
"steps": steps,
"cfg_scale": cfg_scale,
"neg_prompt": neg_prompt,
"enhance_level": enhance_level,
"cover_strategy": cover_strategy,
}