- 新增智能选题引擎 `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` 中的头像文件路径问题,确保目录存在
345 lines
14 KiB
Python
345 lines
14 KiB
Python
"""
|
||
内容创作 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,
|
||
}
|