""" 内容创作 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, }