rsm-roguchi commited on
Commit
6b7bfdd
·
1 Parent(s): f9e7cf4

update new files

Browse files
app.py CHANGED
@@ -29,7 +29,6 @@ from server import (
29
  ui = ui.page_fluid(
30
  ui.page_navbar(
31
  blog.ui,
32
- #general_blog.ui,
33
  meta.ui,
34
  threads.ui,
35
  twitter.ui,
@@ -45,7 +44,6 @@ ui = ui.page_fluid(
45
 
46
  def server(input, output, session):
47
  blog_srv.server(input, output, session)
48
- #general_blog_srv.server(input, output, session)
49
  meta_srv.server(input, output, session)
50
  threads_srv.server(input, output, session)
51
  twitter_srv.server(input, output, session)
 
29
  ui = ui.page_fluid(
30
  ui.page_navbar(
31
  blog.ui,
 
32
  meta.ui,
33
  threads.ui,
34
  twitter.ui,
 
44
 
45
  def server(input, output, session):
46
  blog_srv.server(input, output, session)
 
47
  meta_srv.server(input, output, session)
48
  threads_srv.server(input, output, session)
49
  twitter_srv.server(input, output, session)
server/blog.py CHANGED
@@ -72,8 +72,7 @@ async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
72
  llm="gemini",
73
  md=False,
74
  temperature=0.6,
75
- max_tokens=100,
76
- model_name="gemini-2.0-flash-lite"
77
  )
78
  print(condensed_topic_raw)
79
 
@@ -89,6 +88,7 @@ async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
89
  condensed_topic = ["trading cards"]
90
 
91
  # === Step 2: Pull suggestions from PyTrends ===
 
92
  all_suggestions = set()
93
  try:
94
  pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
@@ -149,6 +149,7 @@ async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
149
  print(f"[INFO] Fallback keywords used: {filtered_keywords[:top_n]}")
150
 
151
  # === Step 5: Enforce minimum of 30 keywords ===
 
152
  combined_keywords = list(dict.fromkeys(filtered_keywords)) # remove duplicates
153
  if len(combined_keywords) < 30:
154
  needed = 30 - len(combined_keywords)
@@ -203,8 +204,10 @@ def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
203
  }
204
  data = {
205
  "article": {
 
206
  "title": title,
207
- "body_html": html_body
 
208
  }
209
  }
210
 
@@ -228,6 +231,7 @@ def server(input, output, session):
228
  return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
229
 
230
  keywords, scraped = await get_keywords_and_content(url)
 
231
  related_keywords.set(keywords)
232
  keyword_str = ", ".join(keywords)
233
 
@@ -256,6 +260,7 @@ def server(input, output, session):
256
  f"- Use <h1> for the blog title\n"
257
  f"- Use <h2> for section headers\n"
258
  f"- Use <p> for all paragraphs\n"
 
259
  f"- NO Markdown, NO triple backticks, NO code blocks, NO formatting fences\n"
260
  f"- DO NOT include any hyperlinks, URLs, web addresses, or references to any external sites or brands — no exceptions\n"
261
  f"- DO NOT include any <a> tags except for the final line below\n\n"
 
72
  llm="gemini",
73
  md=False,
74
  temperature=0.6,
75
+ max_tokens=100
 
76
  )
77
  print(condensed_topic_raw)
78
 
 
88
  condensed_topic = ["trading cards"]
89
 
90
  # === Step 2: Pull suggestions from PyTrends ===
91
+ time.sleep(3)
92
  all_suggestions = set()
93
  try:
94
  pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
 
149
  print(f"[INFO] Fallback keywords used: {filtered_keywords[:top_n]}")
150
 
151
  # === Step 5: Enforce minimum of 30 keywords ===
152
+ time.sleep(3)
153
  combined_keywords = list(dict.fromkeys(filtered_keywords)) # remove duplicates
154
  if len(combined_keywords) < 30:
155
  needed = 30 - len(combined_keywords)
 
204
  }
205
  data = {
206
  "article": {
207
+ "author": "Ultima Supply Writer: (Bingus)",
208
  "title": title,
209
+ "body_html": html_body,
210
+
211
  }
212
  }
213
 
 
231
  return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
232
 
233
  keywords, scraped = await get_keywords_and_content(url)
234
+ time.sleep(3)
235
  related_keywords.set(keywords)
236
  keyword_str = ", ".join(keywords)
237
 
 
260
  f"- Use <h1> for the blog title\n"
261
  f"- Use <h2> for section headers\n"
262
  f"- Use <p> for all paragraphs\n"
263
+ f"- Avoid using all caps\n"
264
  f"- NO Markdown, NO triple backticks, NO code blocks, NO formatting fences\n"
265
  f"- DO NOT include any hyperlinks, URLs, web addresses, or references to any external sites or brands — no exceptions\n"
266
  f"- DO NOT include any <a> tags except for the final line below\n\n"
server/general_blog.py DELETED
@@ -1,319 +0,0 @@
1
- import os, sys, re, ast, time, requests
2
- from bs4 import BeautifulSoup
3
- from pytrends.request import TrendReq
4
- from shiny import ui, reactive, render
5
- from playwright.async_api import async_playwright
6
- from dotenv import load_dotenv
7
-
8
- # === LLM Connect ===
9
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "code")))
10
- from llm_connect import get_response
11
-
12
- load_dotenv()
13
-
14
- SHOPIFY_STORE = "ultima-supply.myshopify.com"
15
- SHOPIFY_TOKEN = os.getenv("SHOPIFY_TOKEN")
16
- SHOPIFY_API_VERSION = "2024-04"
17
- BLOG_ID = "73667707064"
18
-
19
- # === Static scraper for pokemon.com ===
20
- async def scrape_section_content_from_url(url: str) -> str:
21
- try:
22
- async with async_playwright() as p:
23
- browser = await p.chromium.launch(headless=True)
24
- page = await browser.new_page()
25
- await page.goto(url, timeout=30000)
26
- await page.wait_for_load_state("networkidle")
27
- html = await page.content()
28
- await browser.close()
29
-
30
- soup = BeautifulSoup(html, "html.parser")
31
- print(soup)
32
-
33
- # Match all divs and extract text
34
- content_blocks = soup.find_all("div")
35
- if not content_blocks:
36
- print("[WARN] No <div> elements found.")
37
- return ""
38
-
39
- texts = [div.get_text(separator=" ", strip=True) for div in content_blocks if div.get_text(strip=True)]
40
- print(f"[INFO] Extracted {len(texts)} content blocks.")
41
- return "\n\n".join(texts)
42
-
43
- except Exception as e:
44
- print(f"[ERROR] Scraping failed: {e}")
45
- return ""
46
-
47
-
48
-
49
- # === Keyword generation + scraping ===
50
- async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
51
- scraped_text = scrape_section_content_from_url(url)
52
- if not scraped_text:
53
- print("[ERROR] No scraped content. Cannot proceed.")
54
- return [], ""
55
-
56
- # === Step 1: Extract condensed topic keywords ===
57
- try:
58
- condensed_prompt = (
59
- "Extract exactly 5 to 7 Google search phrases from the content below that reflect real user search intent. "
60
- "Each phrase should describe a specific product, use case, or collector topic — not generic brands or categories.\n\n"
61
- "⚠️ Rules:\n"
62
- "- Each phrase must be 2 to 5 words\n"
63
- "- All phrases must be lowercase and ASCII-only\n"
64
- "- Do NOT include apostrophes, single quotes, or quotation marks — rewrite or skip any phrases that contain them\n"
65
- "- Do NOT include single words or overly broad terms like 'pokemon'\n"
66
- "- Do NOT return line breaks, bullet points, or list formatting\n\n"
67
- "✅ Output format:\n"
68
- "Return a single comma-separated string of keyword phrases, with no brackets, no quotes, and no explanation.\n"
69
- "Example output:\n"
70
- "vintage charizard value, graded card pricing, rare booster packs, psa 10 umbreon, tcg price trends\n\n"
71
- f"Content:\n{scraped_text}"
72
- )
73
-
74
-
75
- condensed_topic_raw = get_response(
76
- input=condensed_prompt,
77
- template=lambda x: x.strip(),
78
- llm="gemini",
79
- md=False,
80
- temperature=0.6,
81
- max_tokens=100,
82
- model_name="gemini-2.0-flash-lite"
83
- )
84
- print(condensed_topic_raw)
85
-
86
- # Parse comma-separated string
87
- condensed_topic = [kw.strip() for kw in condensed_topic_raw.split(",") if kw.strip()]
88
-
89
- if not condensed_topic:
90
- condensed_topic = ["trading cards"]
91
-
92
- print(f"[INFO] Condensed topic keywords: {condensed_topic}")
93
- except Exception as e:
94
- print(f"[WARN] Could not infer topics: {e}")
95
- condensed_topic = ["trading cards"]
96
-
97
- # === Step 2: Pull suggestions from PyTrends ===
98
- all_suggestions = set()
99
- try:
100
- pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
101
- for topic in condensed_topic:
102
- time.sleep(5)
103
- suggestions = pytrends.suggestions(keyword=topic)
104
- if suggestions:
105
- titles = [s["title"] for s in suggestions]
106
- all_suggestions.update(titles)
107
- print(f"[INFO] Suggestions for '{topic}': {titles[:3]}")
108
- except Exception as e:
109
- print(f"[WARN] PyTrends suggestions failed: {e}")
110
-
111
- all_suggestions = list(all_suggestions)
112
-
113
- # === Step 3: Let Gemini filter suggestions for relevance ===
114
- filtered_keywords = []
115
- if all_suggestions:
116
- filter_prompt = (
117
- f"The following article was scraped:\n\n{scraped_text[:1500]}\n\n"
118
- f"Here is a list of keyword suggestions:\n{all_suggestions}\n\n"
119
- "Return only the keywords that are clearly relevant to the article topic. "
120
- "Return a valid Python list of strings only. No explanation, bullets, or formatting."
121
- )
122
-
123
- raw_filtered = get_response(
124
- input=filter_prompt,
125
- template=lambda x: x.strip(),
126
- llm="gemini",
127
- md=False,
128
- temperature=0.3,
129
- max_tokens=200
130
- )
131
-
132
- match = re.search(r"\[.*?\]", raw_filtered)
133
- if match:
134
- try:
135
- filtered_keywords = ast.literal_eval(match.group(0))
136
- except:
137
- filtered_keywords = []
138
-
139
- # === Step 4: Fallback to Gemini keyword generation if needed ===
140
- if not filtered_keywords:
141
- fallback_prompt = (
142
- f"You are an SEO expert. Generate {llm_n} niche-relevant SEO keywords "
143
- f"based on this content:\n\n{scraped_text}\n\n"
144
- "Return a comma-separated list of lowercase 2–5 word search phrases. No formatting."
145
- )
146
- fallback_keywords_raw = get_response(
147
- input=fallback_prompt,
148
- template=lambda x: x.strip(),
149
- llm="gemini",
150
- md=False,
151
- temperature=0.7,
152
- max_tokens=400
153
- )
154
- filtered_keywords = [kw.strip() for kw in fallback_keywords_raw.split(",") if kw.strip()]
155
- print(f"[INFO] Fallback keywords used: {filtered_keywords[:top_n]}")
156
-
157
- # === Step 5: Enforce minimum of 30 keywords ===
158
- combined_keywords = list(dict.fromkeys(filtered_keywords)) # remove duplicates
159
- if len(combined_keywords) < 30:
160
- needed = 30 - len(combined_keywords)
161
- print(f"[INFO] Need {needed} more keywords to reach 30. Using Gemini to pad.")
162
-
163
- pad_prompt = (
164
- f"The following article content is missing SEO keyword coverage:\n\n"
165
- f"{scraped_text}\n\n"
166
- f"Generate exactly {needed} additional SEO keyword phrases.\n"
167
- "Each keyword must:\n"
168
- "- be 2 to 5 words long\n"
169
- "- be lowercase only\n"
170
- "- use ASCII characters only (no symbols or accents)\n"
171
- "- be clearly relevant to the article\n"
172
- "- avoid generic terms like 'pokemon'\n\n"
173
- "Return only the keywords as a single comma-separated string, with no extra formatting or explanation.\n"
174
- "Example output:\n"
175
- "keyword one, keyword two, keyword three"
176
- )
177
-
178
- pad_raw = get_response(
179
- input=pad_prompt,
180
- template=lambda x: x.strip(),
181
- llm="gemini",
182
- md=False,
183
- temperature=0.7,
184
- max_tokens=200
185
- )
186
-
187
- pad_keywords = []
188
- print(pad_raw)
189
-
190
- try:
191
- pad_keywords = [kw.strip() for kw in pad_raw.split(",") if kw.strip()]
192
- except Exception as e:
193
- print(f"[WARN] Keyword parsing failed: {e}")
194
- pad_keywords = []
195
-
196
- combined_keywords = list(dict.fromkeys(combined_keywords + pad_keywords))
197
- print(f"[INFO] Padded {len(pad_keywords)} keywords:", pad_keywords)
198
-
199
- return combined_keywords[:30], scraped_text
200
-
201
-
202
- # === Shopify publisher ===
203
- def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
204
- url = f"https://{SHOPIFY_STORE}/admin/api/{SHOPIFY_API_VERSION}/blogs/{blog_id}/articles.json"
205
- headers = {
206
- "X-Shopify-Access-Token": SHOPIFY_TOKEN,
207
- "Content-Type": "application/json"
208
- }
209
- data = {
210
- "article": {
211
- "title": title,
212
- "body_html": html_body
213
- }
214
- }
215
-
216
- response = requests.post(url, json=data, headers=headers)
217
- if response.status_code == 201:
218
- return True, response.json()
219
- else:
220
- return False, response.text
221
-
222
- # === Shiny Server ===
223
- def server(input, output, session):
224
- related_keywords = reactive.Value([])
225
- generated_blog = reactive.Value(("", "")) # (title, html_content)
226
-
227
- @output
228
- @render.ui
229
- @reactive.event(input.blog_generate_btn)
230
- async def blog_result_gen():
231
- url = input.blog_url()
232
- if not url:
233
- return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
234
-
235
- keywords, scraped = await get_keywords_and_content(url)
236
- related_keywords.set(keywords)
237
- keyword_str = ", ".join(keywords)
238
-
239
- # Title generation from scraped text
240
- infer_topic_prompt = (
241
- f"Based on the following article content:\n\n{scraped[:2000]}\n\n"
242
- f"Return a short, descriptive blog post title (max 70 characters)."
243
- f"Return ONLY the TITLE"
244
- )
245
- seo_title = get_response(
246
- input=infer_topic_prompt,
247
- template=lambda x: x.strip().replace('"', ''),
248
- llm="gemini",
249
- md=False,
250
- temperature=0.5,
251
- max_tokens=20
252
- )
253
-
254
- # Blog generation with injected SEO
255
- prompt = (
256
- f"You are a content writer for a collectibles brand called 'Ultima Supply'.\n"
257
- f"Given the following scraped content:\n\n{scraped}\n\n"
258
- f"Rewrite this in an engaging, original, and heavily detailed SEO-optimized blog post.\n"
259
- f"Naturally and organically integrate the following SEO keywords throughout the content:\n{keyword_str}\n\n"
260
- f"⚠️ STRICT FORMATTING RULES (must be followed exactly):\n"
261
- f"- Use <h1> for the blog title\n"
262
- f"- Use <h2> for section headers\n"
263
- f"- Use <p> for all paragraphs\n"
264
- f"- NO Markdown, NO triple backticks, NO code blocks, NO formatting fences\n"
265
- f"- DO NOT include any hyperlinks, URLs, web addresses, or references to any external sites or brands — no exceptions\n"
266
- f"- DO NOT include any <a> tags except for the final line below\n\n"
267
- f"✅ FINAL LINE ONLY:\n"
268
- f"Add this exact call-to-action at the very end of the post inside its own <p> tag:\n"
269
- f"Visit <a href='https://ultima-supply.myshopify.com'>Ultima Supply</a> to explore more collectibles."
270
- )
271
-
272
- blog_html = get_response(
273
- input=prompt,
274
- template=lambda x: x.strip(),
275
- llm="gemini",
276
- md=False,
277
- temperature=0.9,
278
- max_tokens=5000
279
- )
280
-
281
- blog_html = re.sub(r"```[a-zA-Z]*\n?", "", blog_html).strip()
282
- blog_html = blog_html.replace("```", "").strip()
283
-
284
- generated_blog.set((seo_title, blog_html))
285
-
286
- return ui.HTML(
287
- f"<p><strong>✅ Blog generated with title:</strong> {seo_title}</p>"
288
- f"<p>Click 'Post to Shopify' to publish.</p>{blog_html}"
289
- )
290
-
291
- @output
292
- @render.ui
293
- def keywords_used_gen():
294
- kws = related_keywords()
295
- if not kws:
296
- return ui.HTML("<p><strong>No SEO keywords retrieved yet.</strong></p>")
297
-
298
- return ui.HTML(
299
- f"<p><strong>✅ SEO Keywords Injected ({len(kws)}):</strong></p><ul>"
300
- + "".join(f"<li>{kw}</li>" for kw in kws) +
301
- "</ul>"
302
- )
303
-
304
- @reactive.effect
305
- @reactive.event(input.blog_post_btn)
306
- def post_to_shopify():
307
- seo_title, html = generated_blog()
308
-
309
- if not html:
310
- ui.notification_show("⚠️ No blog generated yet.", type="warning")
311
- return
312
-
313
- success, response = publish_blog_post(title=seo_title, html_body=html)
314
-
315
- if success:
316
- ui.notification_show("✅ Blog posted to Shopify successfully!", type="message")
317
- else:
318
- ui.notification_show(f"❌ Failed to publish: {response}", type="error")
319
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server/meta.py CHANGED
@@ -4,6 +4,7 @@ import requests
4
  from dotenv import load_dotenv
5
  from bs4 import BeautifulSoup
6
  import subprocess
 
7
  from llm_connect import get_response
8
 
9
  load_dotenv()
@@ -67,6 +68,8 @@ def generate_facebook_post(topic: str = "", url: str = "", min_len: int = 500, m
67
 
68
  if len(post) <= max_len:
69
  return post
 
 
70
 
71
  # Step 2: Shorten if needed
72
  shorten_prompt = (
 
4
  from dotenv import load_dotenv
5
  from bs4 import BeautifulSoup
6
  import subprocess
7
+ import time
8
  from llm_connect import get_response
9
 
10
  load_dotenv()
 
68
 
69
  if len(post) <= max_len:
70
  return post
71
+
72
+ time.sleep(5)
73
 
74
  # Step 2: Shorten if needed
75
  shorten_prompt = (
server/threads.py CHANGED
@@ -3,6 +3,7 @@ import os
3
  import requests
4
  from dotenv import load_dotenv
5
  from bs4 import BeautifulSoup
 
6
  from llm_connect import get_response
7
 
8
  # Load env
@@ -74,6 +75,8 @@ def generate_threads_post( # keeping the function name to avoid UI refactor
74
 
75
  if len(post) <= max_len:
76
  return post
 
 
77
 
78
  # Step 2: Shorten if needed
79
  shorten_prompt = (
 
3
  import requests
4
  from dotenv import load_dotenv
5
  from bs4 import BeautifulSoup
6
+ import time
7
  from llm_connect import get_response
8
 
9
  # Load env
 
75
 
76
  if len(post) <= max_len:
77
  return post
78
+
79
+ time.sleep(5)
80
 
81
  # Step 2: Shorten if needed
82
  shorten_prompt = (
server/twitter.py CHANGED
@@ -4,6 +4,7 @@ import requests
4
  from bs4 import BeautifulSoup
5
  from dotenv import load_dotenv, find_dotenv
6
  import tweepy
 
7
  from llm_connect import get_response
8
 
9
  # Load environment variables
@@ -55,6 +56,8 @@ def generate_tweet_from_text(text: str, url: str='https://ultimasupply.com/blogs
55
  if len(tweet) <= max_len:
56
  return tweet
57
  else:
 
 
58
  shorten_prompt = (
59
  f"You are shortening this tweet to {max_len} characters or fewer.\n"
60
  f"YOU MUST KEEP ALL of the original SEO hashtags and the Shopify blog link intact in the final output.\n"
 
4
  from bs4 import BeautifulSoup
5
  from dotenv import load_dotenv, find_dotenv
6
  import tweepy
7
+ import time
8
  from llm_connect import get_response
9
 
10
  # Load environment variables
 
56
  if len(tweet) <= max_len:
57
  return tweet
58
  else:
59
+ time.sleep(5)
60
+
61
  shorten_prompt = (
62
  f"You are shortening this tweet to {max_len} characters or fewer.\n"
63
  f"YOU MUST KEEP ALL of the original SEO hashtags and the Shopify blog link intact in the final output.\n"
threads.ipynb DELETED
File without changes
ui/general_blog.py DELETED
@@ -1,19 +0,0 @@
1
- from shiny import ui
2
-
3
- ui = ui.nav_panel(
4
- "General Blog Writer",
5
-
6
- # Scoped URL input
7
- ui.input_text("blog_url", "Enter Pokémon.com Article URL", placeholder="https://www.pokemon.com/us/..."),
8
-
9
- # Scoped buttons
10
- ui.div(
11
- ui.input_action_button("blog_generate_btn", "Generate Blog from Article"),
12
- ui.input_action_button("blog_post_btn", "Post to Shopify", class_="ms-3"),
13
- class_="mt-3 mb-3"
14
- ),
15
-
16
- # Scoped outputs
17
- ui.output_ui("blog_result_gen"),
18
- ui.output_ui("blog_keywords_used_gen")
19
- )