BlogWriter / app.py
vivekchakraverty's picture
Fix InvalidPathError: whitelist OUT_DIR for Gradio 5 file serving
f2d4db5
Raw
History Blame Contribute Delete
5.61 kB
"""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)],
)