xhs_factory/ui/tab_create.py
zhoujie 1889e9a222 🐛 fix(config): 修复 Windows 下配置文件保存时的权限错误
- 在 `ConfigManager.save()` 方法中增加重试逻辑,当 `os.replace` 因目标文件被占用而抛出 `PermissionError` 时,最多重试 3 次,每次间隔 0.1 秒

♻️ refactor(import): 修正模块导入路径

- 在 `LLMService.get_sd_prompt_guide` 方法中,将 `from sd_service import ...` 修正为 `from services.sd_service import ...`,以修复因相对导入路径错误导致的模块未找到问题

🔧 chore(ui): 优化 UI 组件交互性与初始化参数

- 在 `_fn_batch_generate` 函数中,为 `PublishQueue` 实例化明确指定工作空间参数 `"xhs_workspace"`
- 在 `tab_create.py` 的封面图策略 `gr.Radio` 组件中,添加 `interactive=True` 参数以启用用户交互
2026-02-28 21:19:40 +08:00

346 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 构图和尺寸",
interactive=True,
)
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,
}