"""Gradio UI for the Blog Post Generator HF Space. Inputs: topic, primary/secondary keyword, brief, and the user's HF token (password). All paid AI calls are billed to that token. Outputs a live status log, a Markdown preview, an image gallery, and a downloadable .docx. """ from __future__ import annotations import re import traceback import gradio as gr from pipeline import config, orchestrator, writer INTRO = """ # 📝 Blog Post Generator Turn a topic + keywords + brief into a fully-written, **illustrated** blog post exported as `.docx`. The pipeline: **search-term reasoning → self-hosted SearXNG web search → OpenPageRank authority ranking → content extraction → LLM writing → FLUX.1-schnell images → vision captioning → DOCX export.** > All paid AI calls run through **Hugging Face Inference Providers using the token you > enter below**, so they are billed to **you**. The token is used only for this run and > is not stored. """ def _preview(markdown: str, images: list) -> str: """Replace [IMAGE:] markers with their caption text for a readable Markdown preview.""" imgs = [im for im in images if im.get("path")] it = iter(imgs) def repl(_m): im = next(it, None) cap = (im or {}).get("caption") or (im or {}).get("scene") or "image" return f"\n> 🖼️ *{cap}*\n" return re.sub(r"\[IMAGE:\s*.+?\]", repl, markdown, flags=re.DOTALL) def generate(topic, primary, secondary, brief, target_wordcount, content_goal, hf_token, progress=gr.Progress()): log_lines = [] gallery, docx_file, preview = [], None, "" def status(): return "\n".join(f"- {ln}" for ln in log_lines) try: for frac, message, result in orchestrator.run( hf_token, topic, primary, secondary, brief, target_wordcount, content_goal ): progress(frac, desc=message) log_lines.append(message) if result.get("images"): gallery = [ (im["path"], im.get("caption") or im.get("scene", "")) for im in result["images"] if im.get("path") ] if result.get("markdown"): preview = _preview(result["markdown"], result.get("images", [])) if result.get("docx_path"): docx_file = result["docx_path"] yield status(), preview, gallery, docx_file except Exception as e: # noqa: BLE001 - show the user a clean error log_lines.append(f"❌ **Error:** {e}") traceback.print_exc() yield status(), preview, gallery, docx_file def build_ui() -> gr.Blocks: with gr.Blocks(title="Blog Post Generator", theme=gr.themes.Soft()) as demo: gr.Markdown(INTRO) with gr.Row(): with gr.Column(scale=1): topic = gr.Textbox(label="Topic", placeholder="e.g. Home composting for beginners") primary = gr.Textbox(label="Primary keyword", placeholder="e.g. home composting") secondary = gr.Textbox(label="Secondary keyword", placeholder="e.g. kitchen scraps") brief = gr.Textbox( label="Brief", lines=5, placeholder="Audience, angle, tone, must-cover points…", ) target_wc = gr.Number( label="Target word count", value=config.DEFAULT_WORD_COUNT, minimum=300, maximum=5000, step=100, precision=0, ) content_goal = gr.Radio( label="Content goal", choices=list(writer.GOAL_GUIDANCE.keys()), value=writer.DEFAULT_GOAL, info="Shapes the article's stance and tone.", ) hf_token = gr.Textbox( label="Hugging Face token (billed to you)", type="password", placeholder="hf_...", ) run_btn = gr.Button("Generate blog post", variant="primary") with gr.Column(scale=2): status_box = gr.Markdown(label="Progress") docx_out = gr.File(label="Download .docx") preview = gr.Markdown(label="Preview") # Dedicated full-width section showing every generated image with its caption. gr.Markdown("## 🖼️ Generated images") gallery = gr.Gallery( label="Generated images", show_label=False, columns=3, height=560, object_fit="contain", preview=False, allow_preview=True, ) run_btn.click( fn=generate, inputs=[topic, primary, secondary, brief, target_wc, content_goal, hf_token], outputs=[status_box, preview, gallery, docx_out], ) gr.Markdown( f"Models: writer `{config.MODEL_WRITER}` · reasoning `{config.MODEL_REASONING}` · " f"images `{config.MODEL_IMAGE}` · vision `{config.MODEL_VISION}`." ) return demo if __name__ == "__main__": build_ui().queue().launch( server_name="0.0.0.0", server_port=7860, show_api=False, # avoid API-schema generation edge cases # Generated images/docx live under config.OUT_DIR (e.g. /data/out on HF # persistent storage), which Gradio 5's file-serving allowlist otherwise rejects. allowed_paths=[str(config.OUT_DIR)], )