Spaces:
Sleeping
Sleeping
| """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) | |