vivekchakraverty Claude Opus 4.8 commited on
Commit
ac00fb3
·
1 Parent(s): 2559985

Add content-goal radio and a dedicated generated-images section

Browse files

- New single-select "Content goal" radio (Informational, Persuasive,
Authoritative, Thought Leadership) threaded through generate() ->
orchestrator.run() -> writer.write_post(); each goal injects a distinct
stance/tone directive into the writing prompt. Included in the cache key.
- UI: move the image gallery into its own full-width "Generated images"
section (3 columns, larger, click-to-preview) so all generated images are
shown prominently in the Space.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +23 -5
  2. pipeline/orchestrator.py +8 -3
  3. pipeline/writer.py +42 -6
app.py CHANGED
@@ -11,7 +11,7 @@ import traceback
11
 
12
  import gradio as gr
13
 
14
- from pipeline import config, orchestrator
15
 
16
  INTRO = """
17
  # 📝 Blog Post Generator
@@ -40,7 +40,8 @@ def _preview(markdown: str, images: list) -> str:
40
  return re.sub(r"\[IMAGE:\s*.+?\]", repl, markdown, flags=re.DOTALL)
41
 
42
 
43
- def generate(topic, primary, secondary, brief, target_wordcount, hf_token, progress=gr.Progress()):
 
44
  log_lines = []
45
  gallery, docx_file, preview = [], None, ""
46
 
@@ -49,7 +50,7 @@ def generate(topic, primary, secondary, brief, target_wordcount, hf_token, progr
49
 
50
  try:
51
  for frac, message, result in orchestrator.run(
52
- hf_token, topic, primary, secondary, brief, target_wordcount
53
  ):
54
  progress(frac, desc=message)
55
  log_lines.append(message)
@@ -93,6 +94,12 @@ def build_ui() -> gr.Blocks:
93
  step=100,
94
  precision=0,
95
  )
 
 
 
 
 
 
96
  hf_token = gr.Textbox(
97
  label="Hugging Face token (billed to you)",
98
  type="password",
@@ -102,12 +109,23 @@ def build_ui() -> gr.Blocks:
102
  with gr.Column(scale=2):
103
  status_box = gr.Markdown(label="Progress")
104
  docx_out = gr.File(label="Download .docx")
105
- gallery = gr.Gallery(label="Generated images", columns=2, height=320)
106
  preview = gr.Markdown(label="Preview")
107
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  run_btn.click(
109
  fn=generate,
110
- inputs=[topic, primary, secondary, brief, target_wc, hf_token],
111
  outputs=[status_box, preview, gallery, docx_out],
112
  )
113
  gr.Markdown(
 
11
 
12
  import gradio as gr
13
 
14
+ from pipeline import config, orchestrator, writer
15
 
16
  INTRO = """
17
  # 📝 Blog Post Generator
 
40
  return re.sub(r"\[IMAGE:\s*.+?\]", repl, markdown, flags=re.DOTALL)
41
 
42
 
43
+ def generate(topic, primary, secondary, brief, target_wordcount, content_goal, hf_token,
44
+ progress=gr.Progress()):
45
  log_lines = []
46
  gallery, docx_file, preview = [], None, ""
47
 
 
50
 
51
  try:
52
  for frac, message, result in orchestrator.run(
53
+ hf_token, topic, primary, secondary, brief, target_wordcount, content_goal
54
  ):
55
  progress(frac, desc=message)
56
  log_lines.append(message)
 
94
  step=100,
95
  precision=0,
96
  )
97
+ content_goal = gr.Radio(
98
+ label="Content goal",
99
+ choices=list(writer.GOAL_GUIDANCE.keys()),
100
+ value=writer.DEFAULT_GOAL,
101
+ info="Shapes the article's stance and tone.",
102
+ )
103
  hf_token = gr.Textbox(
104
  label="Hugging Face token (billed to you)",
105
  type="password",
 
109
  with gr.Column(scale=2):
110
  status_box = gr.Markdown(label="Progress")
111
  docx_out = gr.File(label="Download .docx")
 
112
  preview = gr.Markdown(label="Preview")
113
 
114
+ # Dedicated full-width section showing every generated image with its caption.
115
+ gr.Markdown("## 🖼️ Generated images")
116
+ gallery = gr.Gallery(
117
+ label="Generated images",
118
+ show_label=False,
119
+ columns=3,
120
+ height=560,
121
+ object_fit="contain",
122
+ preview=False,
123
+ allow_preview=True,
124
+ )
125
+
126
  run_btn.click(
127
  fn=generate,
128
+ inputs=[topic, primary, secondary, brief, target_wc, content_goal, hf_token],
129
  outputs=[status_box, preview, gallery, docx_out],
130
  )
131
  gr.Markdown(
pipeline/orchestrator.py CHANGED
@@ -30,6 +30,7 @@ def run(
30
  secondary_keyword: str,
31
  brief: str,
32
  target_wordcount: int = config.DEFAULT_WORD_COUNT,
 
33
  ) -> Iterator[Tuple[float, str, dict]]:
34
  if not topic.strip():
35
  raise ValueError("Please enter a topic.")
@@ -40,8 +41,12 @@ def run(
40
  except (TypeError, ValueError):
41
  target_wordcount = config.DEFAULT_WORD_COUNT
42
  target_wordcount = max(300, min(target_wordcount, 5000))
 
 
43
 
44
- key = cache.run_key(topic, primary_keyword, secondary_keyword, brief, str(target_wordcount))
 
 
45
  run_dir = config.OUT_DIR / key
46
  run_dir.mkdir(parents=True, exist_ok=True)
47
  result: dict = {"key": key}
@@ -79,10 +84,10 @@ def run(
79
  result["sources"] = [{k: v for k, v in s.items() if k != "text"} for s in sources]
80
 
81
  # 5) write the post
82
- yield 0.55, f"Writing the ~{target_wordcount}-word blog post…", result
83
  markdown = writer.write_post(
84
  client, topic, primary_keyword, secondary_keyword, brief, sources,
85
- target_wordcount=target_wordcount,
86
  )
87
  (run_dir / "post.md").write_text(markdown, encoding="utf-8")
88
  result["markdown"] = markdown
 
30
  secondary_keyword: str,
31
  brief: str,
32
  target_wordcount: int = config.DEFAULT_WORD_COUNT,
33
+ content_goal: str = writer.DEFAULT_GOAL,
34
  ) -> Iterator[Tuple[float, str, dict]]:
35
  if not topic.strip():
36
  raise ValueError("Please enter a topic.")
 
41
  except (TypeError, ValueError):
42
  target_wordcount = config.DEFAULT_WORD_COUNT
43
  target_wordcount = max(300, min(target_wordcount, 5000))
44
+ if content_goal not in writer.GOAL_GUIDANCE:
45
+ content_goal = writer.DEFAULT_GOAL
46
 
47
+ key = cache.run_key(
48
+ topic, primary_keyword, secondary_keyword, brief, str(target_wordcount), content_goal
49
+ )
50
  run_dir = config.OUT_DIR / key
51
  run_dir.mkdir(parents=True, exist_ok=True)
52
  result: dict = {"key": key}
 
84
  result["sources"] = [{k: v for k, v in s.items() if k != "text"} for s in sources]
85
 
86
  # 5) write the post
87
+ yield 0.55, f"Writing the ~{target_wordcount}-word {content_goal} blog post…", result
88
  markdown = writer.write_post(
89
  client, topic, primary_keyword, secondary_keyword, brief, sources,
90
+ target_wordcount=target_wordcount, content_goal=content_goal,
91
  )
92
  (run_dir / "post.md").write_text(markdown, encoding="utf-8")
93
  result["markdown"] = markdown
pipeline/writer.py CHANGED
@@ -14,12 +14,45 @@ from huggingface_hub import InferenceClient
14
  from . import config, llm
15
  from .aeo_guidelines import AEO_GUIDELINES
16
 
17
-
18
- def _build_system(target_wordcount: int) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  return (
20
  "You are an expert blog writer and SEO/AEO editor. Using ONLY the supplied source "
21
  "material as factual grounding, write an original, engaging, well-structured blog "
22
  "post. Do not copy sentences verbatim from the sources; synthesize in your own voice.\n\n"
 
23
  "Structural requirements:\n"
24
  "- Start with a single '# ' H1 title that includes the primary keyword.\n"
25
  "- Use the primary keyword naturally in the first 100 words and in at least one '## ' heading.\n"
@@ -46,25 +79,28 @@ def write_post(
46
  brief: str,
47
  sources: List[dict],
48
  target_wordcount: int = config.DEFAULT_WORD_COUNT,
 
49
  ) -> str:
50
  source_block = _format_sources(sources)
51
  user = (
52
  f"Topic: {topic}\n"
53
  f"Primary keyword: {primary_keyword}\n"
54
  f"Secondary keyword: {secondary_keyword}\n"
 
55
  f"Target word count: {target_wordcount}\n"
56
  f"Brief: {brief}\n\n"
57
  f"SOURCE MATERIAL (ranked by domain authority):\n{source_block}\n\n"
58
- f"Write the full ~{target_wordcount}-word blog post in Markdown now, following the "
59
- "AI-citation guidelines (direct answer up top, question-style headings, a comparison/"
60
- "feature table, an FAQ, cited sources, stated limitations)."
 
61
  )
62
  # Allow enough output tokens for the requested length (~1.6 tokens/word + overhead).
63
  max_tokens = max(1500, min(int(target_wordcount * 2) + 600, 8000))
64
  md = llm.chat(
65
  client,
66
  config.MODEL_WRITER,
67
- _build_system(target_wordcount),
68
  user,
69
  max_tokens=max_tokens,
70
  temperature=0.7,
 
14
  from . import config, llm
15
  from .aeo_guidelines import AEO_GUIDELINES
16
 
17
+ # Content-goal directives shape the post's stance and tone. One is selected in the UI.
18
+ GOAL_GUIDANCE = {
19
+ "Informational": (
20
+ "Content goal: INFORMATIONAL. Prioritize clear, objective, comprehensive "
21
+ "explanation. Teach the reader, define terms, stay neutral and factual, and favor "
22
+ "accuracy and completeness over opinion or salesmanship."
23
+ ),
24
+ "Persuasive": (
25
+ "Content goal: PERSUASIVE. Build a convincing case for a clear position or action. "
26
+ "Lead with benefits, address likely objections and counterpoints, support claims "
27
+ "with concrete evidence and examples, and close with a motivating call to action — "
28
+ "without overstating or making unsupported claims."
29
+ ),
30
+ "Authoritative": (
31
+ "Content goal: AUTHORITATIVE. Write as the definitive reference on the topic. Be "
32
+ "precise and thorough, back every key claim with specific evidence, data or primary "
33
+ "sources, cover edge cases and limitations, and use a confident expert tone that "
34
+ "earns citation and trust."
35
+ ),
36
+ "Thought Leadership": (
37
+ "Content goal: THOUGHT LEADERSHIP. Offer an original, forward-looking perspective. "
38
+ "Frame the topic within broader industry trends, share a distinctive point of view "
39
+ "or informed prediction, challenge conventional assumptions where warranted, and "
40
+ "support opinions with clear reasoning and evidence."
41
+ ),
42
+ }
43
+ DEFAULT_GOAL = "Informational"
44
+
45
+
46
+ def _goal_directive(content_goal: str) -> str:
47
+ return GOAL_GUIDANCE.get(content_goal, GOAL_GUIDANCE[DEFAULT_GOAL])
48
+
49
+
50
+ def _build_system(target_wordcount: int, content_goal: str) -> str:
51
  return (
52
  "You are an expert blog writer and SEO/AEO editor. Using ONLY the supplied source "
53
  "material as factual grounding, write an original, engaging, well-structured blog "
54
  "post. Do not copy sentences verbatim from the sources; synthesize in your own voice.\n\n"
55
+ f"{_goal_directive(content_goal)}\n\n"
56
  "Structural requirements:\n"
57
  "- Start with a single '# ' H1 title that includes the primary keyword.\n"
58
  "- Use the primary keyword naturally in the first 100 words and in at least one '## ' heading.\n"
 
79
  brief: str,
80
  sources: List[dict],
81
  target_wordcount: int = config.DEFAULT_WORD_COUNT,
82
+ content_goal: str = DEFAULT_GOAL,
83
  ) -> str:
84
  source_block = _format_sources(sources)
85
  user = (
86
  f"Topic: {topic}\n"
87
  f"Primary keyword: {primary_keyword}\n"
88
  f"Secondary keyword: {secondary_keyword}\n"
89
+ f"Content goal: {content_goal}\n"
90
  f"Target word count: {target_wordcount}\n"
91
  f"Brief: {brief}\n\n"
92
  f"SOURCE MATERIAL (ranked by domain authority):\n{source_block}\n\n"
93
+ f"Write the full ~{target_wordcount}-word blog post in Markdown now, matching the "
94
+ f"'{content_goal}' content goal and following the AI-citation guidelines (direct "
95
+ "answer up top, question-style headings, a comparison/feature table, an FAQ, cited "
96
+ "sources, stated limitations)."
97
  )
98
  # Allow enough output tokens for the requested length (~1.6 tokens/word + overhead).
99
  max_tokens = max(1500, min(int(target_wordcount * 2) + 600, 8000))
100
  md = llm.chat(
101
  client,
102
  config.MODEL_WRITER,
103
+ _build_system(target_wordcount, content_goal),
104
  user,
105
  max_tokens=max_tokens,
106
  temperature=0.7,