File size: 4,335 Bytes
31fa536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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

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, 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
        ):
            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…",
                )
                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")
                gallery = gr.Gallery(label="Generated images", columns=2, height=320)
                preview = gr.Markdown(label="Preview")

        run_btn.click(
            fn=generate,
            inputs=[topic, primary, secondary, brief, 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)