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)],
    )