Spaces:
Running
Running
| """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)], | |
| ) | |