BlogWriter / pipeline /orchestrator.py
vivekchakraverty's picture
Add content-goal radio and a dedicated generated-images section
ac00fb3
Raw
History Blame Contribute Delete
4.17 kB
"""End-to-end orchestration of the blog-post pipeline.
Kept UI-agnostic: `run()` is a generator that yields (progress_fraction, message,
partial_result) tuples so any front-end can drive a progress bar. The final yield
carries the complete result dict.
"""
from __future__ import annotations
from pathlib import Path
from typing import Iterator, Tuple
from . import (
cache,
captions,
config,
extract,
images,
llm,
openpagerank,
search_terms,
searxng_client,
writer,
)
def run(
hf_token: str,
topic: str,
primary_keyword: str,
secondary_keyword: str,
brief: str,
target_wordcount: int = config.DEFAULT_WORD_COUNT,
content_goal: str = writer.DEFAULT_GOAL,
) -> Iterator[Tuple[float, str, dict]]:
if not topic.strip():
raise ValueError("Please enter a topic.")
client = llm.make_client(hf_token) # raises if token missing → billed to user
try:
target_wordcount = int(target_wordcount)
except (TypeError, ValueError):
target_wordcount = config.DEFAULT_WORD_COUNT
target_wordcount = max(300, min(target_wordcount, 5000))
if content_goal not in writer.GOAL_GUIDANCE:
content_goal = writer.DEFAULT_GOAL
key = cache.run_key(
topic, primary_keyword, secondary_keyword, brief, str(target_wordcount), content_goal
)
run_dir = config.OUT_DIR / key
run_dir.mkdir(parents=True, exist_ok=True)
result: dict = {"key": key}
# 1) search terms
yield 0.05, "Generating search terms…", result
terms = cache.get(key, "terms") or search_terms.generate_search_terms(
client, topic, primary_keyword, secondary_keyword, brief
)
cache.put(key, "terms", terms)
result["terms"] = terms
# 2) SearXNG search
yield 0.15, f"Searching the web for {len(terms)} queries…", result
candidates = cache.get(key, "candidates") or searxng_client.search(terms, config.TOP_N)
if not candidates:
raise RuntimeError(
"No search results from SearXNG. Is the SearXNG service running "
"(check the Space logs)?"
)
cache.put(key, "candidates", candidates)
result["candidates"] = candidates
# 3) OpenPageRank authority ranking
yield 0.30, f"Ranking {len(candidates)} domains by OpenPageRank…", result
top = openpagerank.rank_results(candidates, config.TOP_K)
cache.put(key, "top", top)
result["top"] = top
# 4) extract source material
yield 0.40, f"Extracting content from top {len(top)} pages…", result
sources = extract.extract_sources(top)
if not sources:
raise RuntimeError("Could not extract readable content from the top pages.")
result["sources"] = [{k: v for k, v in s.items() if k != "text"} for s in sources]
# 5) write the post
yield 0.55, f"Writing the ~{target_wordcount}-word {content_goal} blog post…", result
markdown = writer.write_post(
client, topic, primary_keyword, secondary_keyword, brief, sources,
target_wordcount=target_wordcount, content_goal=content_goal,
)
(run_dir / "post.md").write_text(markdown, encoding="utf-8")
result["markdown"] = markdown
# 6) generate images (remote FLUX.1-schnell inference calls, billed to the user)
scenes = writer.parse_image_markers(markdown)
yield 0.70, f"Generating {len(scenes)} images with FLUX.1-schnell…", result
imgs = images.generate_images(client, hf_token, topic, scenes, run_dir)
result["images"] = imgs
n_ok = sum(1 for im in imgs if im.get("path"))
if n_ok == 0 and imgs:
yield 0.72, f"⚠ Image generation failed: {imgs[0].get('error', 'unknown error')}", result
else:
yield 0.72, f"Generated {n_ok}/{len(imgs)} images.", result
# 7) caption images
yield 0.85, "Captioning images…", result
imgs = captions.caption_images(client, imgs)
result["images"] = imgs
# 8) build docx
yield 0.95, "Building the .docx file…", result
from . import docx_builder
docx_path = docx_builder.build_docx(markdown, imgs, run_dir / "blog.docx")
result["docx_path"] = str(docx_path)
yield 1.0, "Done.", result