Spaces:
Runtime error
Runtime error
File size: 15,200 Bytes
94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 ce83e73 94da7e5 | 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | import gradio as gr
import google.generativeai as genai
import json
import os
import requests
from datetime import datetime
# Configure Gemini
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
AIRTABLE_API_KEY = os.environ.get("AIRTABLE_API_KEY", "")
AIRTABLE_BASE_ID = os.environ.get("AIRTABLE_BASE_ID", "app7WFijpDJ46Cib3")
AIRTABLE_TABLE = "Pipeline Queue"
if GEMINI_API_KEY:
genai.configure(api_key=GEMINI_API_KEY)
NICHES = [
"passive income", "dropshipping Australia", "lawn care business",
"pet care business", "AI tools for small business", "freelancing",
"print on demand", "affiliate marketing", "ecommerce Australia",
"side hustle ideas", "digital products", "self publishing",
"social media marketing", "home based business", "trading for beginners",
"lawn mowing business Australia", "WarmPaws pet products"
]
# ββ Global state to hold last results ββββββββββββββββββββββββββββββββββββββββββ
last_ideas = []
def research_ebook_ideas(niche: str, count: int, api_key: str):
global last_ideas
key = api_key.strip() or GEMINI_API_KEY
if not key:
return "β Please enter a Gemini API key.", "{}", "[]", gr.update(visible=False)
try:
genai.configure(api_key=key)
model = genai.GenerativeModel(
model_name="gemini-2.0-flash",
system_instruction=(
"You are an expert SEO strategist and digital publishing consultant "
"for Brett Apps, an Australian self-publishing business. "
"Return ONLY valid JSON β no markdown, no backticks, no explanation."
)
)
# Step 1: SEO Keyword Research
kw_prompt = f"""Generate SEO keyword research for niche: "{niche}" targeting Australian audiences.
Return ONLY valid JSON:
{{
"niche": "{niche}",
"primary_keywords": ["kw1","kw2","kw3","kw4","kw5"],
"long_tail_keywords": ["lt1","lt2","lt3","lt4","lt5"],
"buyer_intent_keywords": ["bi1","bi2","bi3"],
"australian_keywords": ["au1","au2","au3"],
"trending_topics": ["t1","t2","t3"],
"search_volume_estimate": "high",
"competition_level": "medium",
"monetisation_potential": "high"
}}"""
kw_response = model.generate_content(kw_prompt)
kw_data = json.loads(kw_response.text.strip())
# Step 2: Generate eBook Ideas
all_keywords = (
kw_data.get("primary_keywords", []) +
kw_data.get("long_tail_keywords", []) +
kw_data.get("australian_keywords", [])
)
ideas_prompt = f"""Create {count} high-converting eBook ideas for niche "{niche}".
Use these SEO keywords naturally in titles/subtitles: {", ".join(all_keywords[:10])}
Trending topics to reference: {", ".join(kw_data.get("trending_topics", []))}
Rules:
- Titles must include 1-2 primary keywords
- Subtitles should include long-tail or buyer intent keywords
- Target Australian audiences
- Price range $7-$27 AUD
- Word count 8,000-15,000 words
Return ONLY valid JSON:
{{
"ebook_ideas": [
{{
"rank": 1,
"title": "SEO-optimised title with keyword",
"subtitle": "Long-tail keyword subtitle for Australian audience",
"primary_keyword": "main keyword",
"secondary_keywords": ["k1","k2","k3"],
"target_audience": "specific audience",
"pain_point": "core problem solved",
"unique_angle": "differentiator from competitors",
"chapter_count": 8,
"estimated_words": 10000,
"recommended_price_aud": 17,
"competition_score": "low",
"demand_score": "high",
"seo_score": "high",
"estimated_monthly_searches": "500-1000",
"pipeline_ready_topic": "exact topic for pipeline"
}}
]
}}"""
ideas_response = model.generate_content(ideas_prompt)
ideas_data = json.loads(ideas_response.text.strip())
ideas = ideas_data.get("ebook_ideas", [])
last_ideas = ideas
# Format output
output_lines = []
output_lines.append(f"## π eBook Market Research β {niche}\n")
output_lines.append(
f"**Search Volume:** {kw_data.get('search_volume_estimate','?').upper()} | "
f"**Competition:** {kw_data.get('competition_level','?').upper()} | "
f"**Monetisation:** {kw_data.get('monetisation_potential','?').upper()}\n"
)
output_lines.append(f"**π Top Keywords:** {' β’ '.join(kw_data.get('primary_keywords',[])[:5])}\n")
output_lines.append(f"**π Trending:** {' β’ '.join(kw_data.get('trending_topics',[]))}\n")
output_lines.append("---\n")
for idea in ideas:
se = {"high": "π’", "medium": "π‘", "low": "π΄"}
output_lines.append(f"### #{idea.get('rank','?')} {idea.get('title','')}")
output_lines.append(f"*{idea.get('subtitle','')}*\n")
output_lines.append(f"**π Primary Keyword:** `{idea.get('primary_keyword','')}`")
output_lines.append(f"**π Monthly Searches:** ~{idea.get('estimated_monthly_searches','?')}")
output_lines.append(
f"**π° Price:** ${idea.get('recommended_price_aud','?')} AUD | "
f"**π Words:** ~{idea.get('estimated_words',0):,} | "
f"**Chapters:** {idea.get('chapter_count','?')}"
)
output_lines.append(
f"**Demand:** {se.get(idea.get('demand_score',''),'β')} {idea.get('demand_score','?').upper()} | "
f"**SEO:** {se.get(idea.get('seo_score',''),'β')} {idea.get('seo_score','?').upper()} | "
f"**Competition:** {se.get(idea.get('competition_score',''),'β')} {idea.get('competition_score','?').upper()}"
)
output_lines.append(f"**π― Unique Angle:** {idea.get('unique_angle','')}")
output_lines.append(f"**π₯ Audience:** {idea.get('target_audience','')}")
output_lines.append(f"**π© Pain Point:** {idea.get('pain_point','')}")
output_lines.append(f"**π Pipeline Topic:** `{idea.get('pipeline_ready_topic','')}`")
output_lines.append("\n---\n")
return (
"\n".join(output_lines),
json.dumps(kw_data, indent=2),
json.dumps(ideas, indent=2),
gr.update(visible=True)
)
except json.JSONDecodeError as e:
return f"β JSON parse error: {e}\n\nTry again β Gemini occasionally returns malformed JSON.", "{}", "[]", gr.update(visible=False)
except Exception as e:
msg = str(e)
if "429" in msg:
return "β οΈ Gemini quota exceeded. Wait a minute and try again, or use a different API key.", "{}", "[]", gr.update(visible=False)
return f"β Error: {msg}", "{}", "[]", gr.update(visible=False)
def send_to_pipeline(selected_rank: int, at_key: str):
"""Send the selected eBook idea to the Airtable Pipeline Queue."""
global last_ideas
key = at_key.strip() or AIRTABLE_API_KEY
if not key:
return "β Please enter your Airtable API key."
if not last_ideas:
return "β No ideas found. Generate ideas first."
# Find the selected idea (rank is 1-based)
idx = max(0, selected_rank - 1)
if idx >= len(last_ideas):
idx = 0
idea = last_ideas[idx]
topic = idea.get("pipeline_ready_topic") or f"{idea.get('title','')} β {idea.get('subtitle','')}"
payload = {
"fields": {
"Topic": topic,
"Title": idea.get("title", ""),
"Subtitle": idea.get("subtitle", ""),
"Primary Keyword": idea.get("primary_keyword", ""),
"Target Audience": idea.get("target_audience", ""),
"Pain Point": idea.get("pain_point", ""),
"Unique Angle": idea.get("unique_angle", ""),
"Recommended Price (AUD)": idea.get("recommended_price_aud", 17),
"Estimated Words": idea.get("estimated_words", 10000),
"Chapter Count": idea.get("chapter_count", 8),
"Demand Score": idea.get("demand_score", ""),
"SEO Score": idea.get("seo_score", ""),
"Competition Score": idea.get("competition_score", ""),
"Monthly Searches": idea.get("estimated_monthly_searches", ""),
"Status": "Queued",
"Queued At": datetime.utcnow().isoformat() + "Z",
"Source": "EbookAgent HF Space",
}
}
try:
url = f"https://api.airtable.com/v0/{AIRTABLE_BASE_ID}/{requests.utils.quote(AIRTABLE_TABLE)}"
response = requests.post(
url,
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
},
json=payload,
timeout=15,
)
if response.status_code in (200, 201):
data = response.json()
record_id = data.get("id", "unknown")
return (
f"β
**Sent to Pipeline Queue!**\n\n"
f"**Topic:** {topic}\n"
f"**Airtable Record:** `{record_id}`\n"
f"**Status:** Queued\n\n"
f"Your eBook pipeline will pick this up on the next run. π"
)
else:
err = response.json()
# If table doesn't exist, provide setup instructions
if "NOT_FOUND" in str(err) or response.status_code == 404:
return (
f"β οΈ **Pipeline Queue table not found in Airtable.**\n\n"
f"Create a table called **'Pipeline Queue'** in base `{AIRTABLE_BASE_ID}` with these fields:\n"
f"- Topic (Single line text)\n"
f"- Title, Subtitle, Primary Keyword, Target Audience (Single line text)\n"
f"- Pain Point, Unique Angle (Long text)\n"
f"- Recommended Price (AUD), Estimated Words, Chapter Count (Number)\n"
f"- Demand Score, SEO Score, Competition Score, Monthly Searches (Single line text)\n"
f"- Status (Single select: Queued, In Progress, Done)\n"
f"- Queued At (Date), Source (Single line text)\n\n"
f"Then click **Send to Pipeline** again."
)
return f"β Airtable error ({response.status_code}): {json.dumps(err, indent=2)}"
except Exception as e:
return f"β Request failed: {str(e)}"
# ββ Gradio UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Blocks(
title="Brett Apps β eBook Market Research Agent",
theme=gr.themes.Base(
primary_hue="purple",
secondary_hue="indigo",
neutral_hue="slate",
font=gr.themes.GoogleFont("Inter"),
),
css="""
.header { text-align: center; padding: 2rem 0 1rem; }
.header h1 { font-size: 2rem; font-weight: 800; color: #a855f7; }
.header p { color: #94a3b8; font-size: 0.95rem; }
.send-box { border: 1px solid #7c3aed; border-radius: 12px; padding: 1rem; background: #1e1b2e; }
footer { display: none !important; }
"""
) as demo:
with gr.Column(elem_classes="header"):
gr.HTML("""
<h1>π eBook Market Research Agent</h1>
<p>Brett Apps Β· SEO Keyword-Injected eBook Idea Generator Β· brett@brettapps.com</p>
""")
with gr.Row():
# ββ Left panel βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=1):
api_key_input = gr.Textbox(
label="π Gemini API Key",
placeholder="AIza... (or set GEMINI_API_KEY secret)",
type="password",
info="Get your key at aistudio.google.com"
)
niche_dropdown = gr.Dropdown(
choices=NICHES,
value="passive income",
label="π Select Niche",
allow_custom_value=True,
info="Choose a niche or type your own"
)
count_slider = gr.Slider(
minimum=1, maximum=10, value=5, step=1,
label="π‘ Number of eBook Ideas"
)
run_btn = gr.Button("π Generate eBook Ideas", variant="primary", size="lg")
gr.Markdown("---")
# ββ Send to Pipeline panel βββββββββββββββββββββββββββββββββββββββββ
with gr.Column(visible=False, elem_classes="send-box") as pipeline_panel:
gr.Markdown("### π Send to Pipeline")
at_key_input = gr.Textbox(
label="Airtable API Key",
placeholder="patXXX... (or set AIRTABLE_API_KEY secret)",
type="password"
)
idea_rank = gr.Slider(
minimum=1, maximum=10, value=1, step=1,
label="Which idea to send? (by rank #)"
)
send_btn = gr.Button("π€ Send to Airtable Pipeline Queue", variant="secondary")
pipeline_status = gr.Markdown("")
gr.Markdown("""
---
**Brett Apps eBook Pipeline**
`Market Research β Outline β Writer β Design β Publish β Sales`
""")
# ββ Right panel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.Column(scale=2):
output_md = gr.Markdown(
value="*Results will appear here after generation...*"
)
with gr.Accordion("π¦ Raw JSON Output", open=False):
with gr.Row():
kw_json = gr.Code(label="Keyword Data", language="json", lines=15)
ideas_json = gr.Code(label="eBook Ideas", language="json", lines=15)
# ββ Event handlers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
run_btn.click(
fn=research_ebook_ideas,
inputs=[niche_dropdown, count_slider, api_key_input],
outputs=[output_md, kw_json, ideas_json, pipeline_panel],
show_progress=True
)
send_btn.click(
fn=send_to_pipeline,
inputs=[idea_rank, at_key_input],
outputs=[pipeline_status]
)
gr.Examples(
examples=[
["passive income", 5, ""],
["lawn care business", 3, ""],
["dropshipping Australia", 5, ""],
["AI tools for small business", 5, ""],
],
inputs=[niche_dropdown, count_slider, api_key_input],
label="Quick Examples"
)
if __name__ == "__main__":
demo.launch()
|