Spaces:
Sleeping
Sleeping
File size: 5,840 Bytes
f9e7cf4 6b7bfdd f9e7cf4 6b7bfdd f9e7cf4 |
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
from shiny import reactive, render, ui
import os
import requests
from dotenv import load_dotenv
from bs4 import BeautifulSoup
import time
from llm_connect import get_response
# Load env
load_dotenv()
# ===== Threads config =====
THREADS_TOKEN = os.getenv("THREADS_ACCESS_TOKEN") # <-- set this in your env
# Store latest generated post text
generated_threads_post = reactive.Value("") # keeping the same name so your UI doesn't change
# ---- Helpers ----
def scrape_shopify_blog(url: str) -> str:
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, 'html.parser')
# Adjust this selector to match your Shopify theme if needed
section = soup.find('div', class_='w940 align-center size-content')
return section.get_text(strip=True, separator=' ') if section else ""
except Exception as e:
return f"❌ Error scraping blog: {e}"
def generate_threads_post( # keeping the function name to avoid UI refactor
topic: str = "",
url: str = "",
min_len: int = 100,
max_len: int = 450
) -> str:
"""
Generates post text using your existing LLM helper.
(Text is suitable for Threads now; you can tweak tone if you want.)
"""
if url:
scraped = scrape_shopify_blog(url)
if scraped.startswith("❌") or not scraped.strip():
return f"⚠️ Failed to extract blog content from URL: {url}"
prompt = (
"You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
f"Write a detailed, engaging Threads post (min {min_len} chars, max {max_len} chars) summarizing the Shopify blog:\n\n"
f"{scraped}\n\n"
f"The post MUST include this exact URL: {url}\n"
"Use a casual, friendly tone with emojis.\n"
"VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
f"Keep everything under {max_len} characters total."
)
elif topic:
prompt = (
"You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
f"Write a short, engaging Threads post (min {min_len} chars, max {max_len} chars) about: '{topic}'.\n"
"Use a casual, friendly tone with fun emojis.\n"
"VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
f"The entire post must be under {max_len} characters total."
)
else:
return "⚠️ Provide either a topic or a Shopify blog URL."
# Step 1: Generate
post = get_response(
input=prompt,
template=lambda x: x.strip(),
llm="gemini",
md=False,
temperature=0.9,
max_tokens=250
).strip()
if len(post) <= max_len:
return post
time.sleep(5)
# Step 2: Shorten if needed
shorten_prompt = (
f"Shorten this post to {max_len} characters or fewer. Keep the Shopify link ({url}) and all original hashtags:\n\n{post}"
if url else
f"Shorten this post to {max_len} characters or fewer, keeping the tone, emojis, and all original hashtags:\n\n{post}"
)
shortened = get_response(
input=shorten_prompt,
template=lambda x: x.strip(),
llm="gemini",
md=False,
temperature=0.7,
max_tokens=200
).strip()
return shortened[:max_len]
# ---- Threads poster (replaces the proxy) ----
def post_to_threads(message: str) -> str:
"""
One-shot TEXT post to Threads using your long-lived token.
Uses: POST https://graph.threads.net/me/threads with text params
"""
if not THREADS_TOKEN:
return "❌ Missing THREADS_USER_ACCESS_TOKEN environment variable."
try:
r = requests.post(
"https://graph.threads.net/me/threads",
headers={"Authorization": f"Bearer {THREADS_TOKEN}"},
params={
"text": message,
"media_type": "TEXT",
"auto_publish_text": "true", # one-call publish for text
},
timeout=30,
)
# Prefer JSON, fall back to raw text for debugging
data = {}
try:
data = r.json()
except Exception:
data = {"raw": r.text}
if r.ok and isinstance(data, dict) and data.get("id"):
return f"✅ Post successful! Threads Post ID: {data['id']}"
return f"❌ Threads error ({r.status_code}): {data}"
except requests.RequestException as e:
return f"❌ Network error posting to Threads: {e}"
except Exception as e:
return f"❌ Unexpected error: {e}"
# ---- Shiny server wiring (IDs unchanged) ----
def server(input, output, session):
post_status = reactive.Value("")
@output
@render.ui
@reactive.event(input.gen_btn_threads)
def threads_post_draft():
topic = input.threads_topic().strip()
url = input.threads_url().strip()
if not topic and not url:
return ui.HTML("<p><strong>⚠️ Enter a topic or a blog URL.</strong></p>")
post = generate_threads_post(topic=topic, url=url)
if post.startswith(("⚠️", "❌")):
return ui.HTML(f"<p><strong>{post}</strong></p>")
generated_threads_post.set(post)
return ui.HTML(f"<p><strong>Generated Threads Post:</strong><br>{post}</p>")
@output
@render.text
def threads_post_status():
return post_status()
@reactive.effect
@reactive.event(input.post_btn_threads)
def _():
post = generated_threads_post()
if not post:
post_status.set("⚠️ No post generated yet.")
else:
post_status.set(post_to_threads(post))
|