Spaces:
Sleeping
Sleeping
File size: 5,614 Bytes
31fa536 ac00fb3 31fa536 ac00fb3 31fa536 ac00fb3 31fa536 2559985 ac00fb3 31fa536 ac00fb3 31fa536 ac00fb3 31fa536 2ab4f11 f2d4db5 2ab4f11 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """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)],
)
|