"""Gradio UI for the AI Digital Marketing Plan Generator. All LLM calls are billed to the user's own Hugging Face token, entered in a password-style box below and used in-memory only for the duration of a request — never logged, stored, or persisted. """ from __future__ import annotations import json import re import tempfile from pathlib import Path import gradio as gr from modules import ads, composer, keywords, llm, rag, seo, social AUTO_MODEL_LABEL = "Auto (recommended model per task)" # Load the RAG index (downloading it from RAG_DATASET_ID if not present locally) # at container startup rather than on the first user request — trades a # slower cold start for no first-request latency spike, and surfaces a # download/credentials failure immediately in the startup logs instead of # silently during someone's first plan generation. print("[startup] Loading RAG index...") if rag.is_available(): print(f"[startup] RAG index loaded: {rag.chunk_count()} chunks.") else: print("[startup] RAG index NOT available — plans will be generated without RAG grounding.") INDUSTRY_LABELS = { "ecommerce_retail": "Ecommerce / Retail", "apparel_fashion": "Apparel / Fashion", "b2b_saas": "B2B SaaS", "technology_electronics": "Technology / Electronics", "education": "Education", "finance_insurance": "Finance / Insurance", "health_medical": "Health / Medical", "home_improvement": "Home Improvement", "legal": "Legal", "real_estate": "Real Estate", "travel_hospitality": "Travel / Hospitality", "automotive": "Automotive", "beauty_personal_care": "Beauty / Personal Care", "restaurants_food": "Restaurants / Food", "fitness_wellness": "Fitness / Wellness", "nonprofit": "Nonprofit", "professional_services": "Professional Services", "furniture_home_goods": "Furniture / Home Goods", "industrial_manufacturing": "Industrial / Manufacturing", "consumer_services": "Consumer Services", } INDUSTRY_CHOICES = [(label, key) for key, label in INDUSTRY_LABELS.items()] EXAMPLES = [ [ "Handmade full-grain leather laptop bags and backpacks, sold direct-to-consumer online.", 2000, "2 people: 1 generalist marketer (full-time), 1 designer (10 hrs/week)", "apparel_fashion", "US", ], [ "A B2B SaaS tool that automates expense report approvals for mid-size companies.", 8000, "3 people: 1 growth marketer, 1 content writer, 1 part-time designer", "b2b_saas", "US", ], ] def _derive_seed_keywords(hf_token: str, model: str, product_description: str) -> list[str]: prompt = f"""Given this product/service description, list 8-12 seed keywords a potential customer might search for. Respond ONLY with a JSON array of strings, no other text. Product/service: {product_description} """ raw = llm.chat( hf_token=hf_token, model=model, messages=[{"role": "user", "content": prompt}], # The answer itself is ~100 tokens, but reasoning models (the default # GLM-5.2 included) think before answering and that counts against # max_tokens — 400 left no room and made content come back empty. max_tokens=2000, temperature=0.3, ) match = re.search(r"\[.*\]", raw, re.DOTALL) if not match: raise llm.LLMError("Could not parse seed keywords from the model's response.") return json.loads(match.group(0)) def _keyword_sources_note(keyword_data: list[keywords.KeywordData]) -> str: sources = sorted({kd.source for kd in keyword_data}) labels = { "google_ads_api": "Google Ads API (official)", "keyword_surfer": "live Keyword Surfer scrape", "autocomplete_trends": "Google Autocomplete + Trends (estimated)", "llm_estimate": "LLM estimate (no live data available)", } return ", ".join(labels.get(s, s) for s in sources) if sources else "no keyword data available" def generate_plan( product_description: str, budget_usd_per_month: float, manpower_summary: str, industry_key: str, geo: str, hf_token: str, model: str, ): # "Auto" lets each module use its own recommended model (SEO/planning, # social/creative-writing, ads/quantitative each favor a different model — # see RECOMMENDED_MODEL in seo.py / social.py / ads.py). Picking a specific # model here overrides all tasks with that one model instead. selected_model = None if model == AUTO_MODEL_LABEL else model status = "" seo_md, social_md, ads_md, full_md = "", "", "", "" download_path = None def state(): return status, seo_md, social_md, ads_md, full_md, download_path if not product_description or not product_description.strip(): status = "Please describe your product or service." yield state() return if not hf_token or not hf_token.strip(): status = "Please enter your Hugging Face access token." yield state() return utility_model = selected_model or llm.DEFAULT_MODEL try: status = "Deriving seed keywords from your product description..." yield state() seed_keywords = _derive_seed_keywords(hf_token, utility_model, product_description) status = f"Researching {len(seed_keywords)} keywords (this may take a minute)..." yield state() keyword_data = keywords.research_keywords(seed_keywords, hf_token, utility_model, geo=geo) keyword_source_note = _keyword_sources_note(keyword_data) status = f"Building SEO plan (keyword data: {keyword_source_note})..." yield state() seo_md = seo.build_seo_plan( hf_token, product_description, manpower_summary, keyword_data, model=selected_model ) yield state() status = "Building organic social media plan..." yield state() social_md = social.build_social_plan( hf_token, product_description, manpower_summary, INDUSTRY_LABELS.get(industry_key, industry_key), geo, industry_key=industry_key, model=selected_model, ) yield state() status = "Building paid advertising plan..." yield state() ads_md = ads.build_ads_plan( hf_token, product_description, float(budget_usd_per_month or 0), manpower_summary, industry_key, geo, keyword_data=keyword_data, model=selected_model, ) yield state() status = "Composing the final plan (retrieving grounding context)..." yield state() full_md = composer.compose_plan( hf_token, product_description, float(budget_usd_per_month or 0), manpower_summary, INDUSTRY_LABELS.get(industry_key, industry_key), geo, seo_md, ads_md, social_md, model=selected_model, ) tmp_dir = Path(tempfile.mkdtemp(prefix="dmplan_")) download_path = str(tmp_dir / "digital_marketing_plan.md") Path(download_path).write_text(full_md, encoding="utf-8") status = f"Done. Keyword data source: {keyword_source_note}." yield state() except llm.LLMError as exc: status = f"Error: {exc}" yield state() except Exception as exc: # keep the UI alive on unexpected failures status = f"Unexpected error: {exc}" yield state() with gr.Blocks(title="AI Digital Marketing Plan Generator") as demo: gr.Markdown( "# AI Digital Marketing Plan Generator\n" "Free tool — **LLM calls are billed to your own Hugging Face token**, entered below. " "Your token is used in-memory only and never stored.\n\n" "Get a token with Inference Providers billing enabled at " "[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)." ) with gr.Row(): with gr.Column(scale=1): product_description = gr.Textbox( label="Product / service description", lines=4, placeholder="e.g. Handmade full-grain leather laptop bags, sold direct-to-consumer online.", ) budget = gr.Number(label="Monthly marketing budget (USD)", value=2000, minimum=0) manpower = gr.Textbox( label="Available manpower", placeholder="e.g. 2 people: 1 generalist marketer full-time, 1 designer 10 hrs/week", ) industry = gr.Dropdown( label="Industry", choices=INDUSTRY_CHOICES, value="ecommerce_retail" ) geo = gr.Textbox(label="Geography (country code, optional)", placeholder="e.g. US") hf_token = gr.Textbox( label="Hugging Face access token", type="password", placeholder="hf_..." ) model = gr.Dropdown( label="Model", choices=[AUTO_MODEL_LABEL] + llm.AVAILABLE_MODELS, value=AUTO_MODEL_LABEL, info="Auto picks a different best-fit model per task (SEO/social/ads each favor a different one) — override to force one model for everything.", ) generate_btn = gr.Button("Generate Plan", variant="primary") status = gr.Markdown() with gr.Column(scale=2): with gr.Tabs(): with gr.Tab("Full Plan"): full_plan_out = gr.Markdown() download_btn = gr.File(label="Download plan (.md)") with gr.Tab("SEO Plan"): seo_out = gr.Markdown() with gr.Tab("Social Plan"): social_out = gr.Markdown() with gr.Tab("Ads Plan"): ads_out = gr.Markdown() gr.Examples( examples=EXAMPLES, inputs=[product_description, budget, manpower, industry, geo], ) generate_btn.click( fn=generate_plan, inputs=[product_description, budget, manpower, industry, geo, hf_token, model], outputs=[status, seo_out, social_out, ads_out, full_plan_out, download_btn], ) if __name__ == "__main__": demo.queue().launch(server_name="0.0.0.0", server_port=7860)