import io import os import re import tempfile import textwrap import urllib.request import zipfile import gradio as gr from fpdf import FPDF from google import genai from google.genai import types # --------------------------------------------------------------------------- # Gemini setup # --------------------------------------------------------------------------- GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") # --------------------------------------------------------------------------- # Font helper # --------------------------------------------------------------------------- FONT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fonts") FONT_PATH = os.path.join(FONT_DIR, "DejaVuSans.ttf") FONT_BOLD_PATH = os.path.join(FONT_DIR, "DejaVuSans-Bold.ttf") DEJAVU_ZIP_URL = ( "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/" "version_2_37/dejavu-fonts-ttf-2.37.zip" ) def _ensure_fonts(): if os.path.exists(FONT_PATH) and os.path.exists(FONT_BOLD_PATH): return os.makedirs(FONT_DIR, exist_ok=True) data = urllib.request.urlopen(DEJAVU_ZIP_URL).read() with zipfile.ZipFile(io.BytesIO(data)) as zf: for name in zf.namelist(): basename = os.path.basename(name) if basename == "DejaVuSans.ttf": with open(FONT_PATH, "wb") as f: f.write(zf.read(name)) elif basename == "DejaVuSans-Bold.ttf": with open(FONT_BOLD_PATH, "wb") as f: f.write(zf.read(name)) _ensure_fonts() # --------------------------------------------------------------------------- # Prompt template # --------------------------------------------------------------------------- SYSTEM_PROMPT = textwrap.dedent("""\ You are a world-class project architect, curriculum designer, and technical mentor with 15+ years of experience shipping production software and teaching developers at every level. Your job is to turn a **topic**, a **skill level**, and a **project difficulty** (1-10) into an extremely detailed, actionable project blueprint that leaves ZERO ambiguity. Use the EXACT ## headers below (including the ## markdown prefix). Do NOT add any extra top-level headers or preamble before the first section. ## Project Title A creative, memorable, and descriptive title. Include a short tagline in italics on the next line that summarises the project in one sentence. ## Project Description Write 5-7 sentences covering: - What the project IS and what problem it solves. - Who the target user/audience is. - Why this project matters (real-world relevance, career value). - What the finished product looks like (paint a vivid picture). - One sentence on the key technical challenge that makes it interesting. ## Goals & Learning Outcomes Provide **8-10** concrete, measurable outcomes using action verbs. Group them into: - **Core Outcomes** (must-achieve) — at least 5 - **Stretch Outcomes** (nice-to-have) — at least 3 ## Architecture Overview - **Architecture pattern** and WHY it fits this project. - **Component diagram** — list every major module/service. - **Data flow** — how data moves through the system. - **Folder / file structure** — tree view with comments. ## Step-by-Step Instructions Provide **at least 10** numbered steps. For EACH step include: 1. **Step title** in bold. 2. **Objective** — one sentence. 3. **Sub-tasks** — 3-6 specific actions. 4. **Deliverable** — what the learner should have at the end. 5. **Estimated time** — realistic range. 6. **Checkpoint** — how to verify the step is done correctly. ## Tools & Technologies Needed Bulleted list of every tool, library, framework, language, API, and service. For each: name/version, what it is used for, install command or link. ## Project Timeline | Phase | Steps Covered | Estimated Duration | Key Milestone | At least 4 phases: Setup, Core Build, Polish, Deploy. ## Tips & Common Mistakes At least 8 tips: - **Do's** (best practices) — at least 4 - **Don'ts** (pitfalls and how to avoid) — at least 4 ## Bonus Challenges 5-7 stretch goals ordered by difficulty. Each: one-line description + a hint on how to approach it. ## Resources & References 5-8 high-quality links with a one-line note on what the learner gains. ## Master Prompt Write a **minimum of 1500 words** — a ready-to-use AI prompt the reader can copy-paste into any AI coding assistant to build this project. Include: full context, tech stack, folder structure, sample data, architecture guidance, DB schema, API contracts, coding style rules, implementation order, edge cases, testing strategy, deployment checklist, and a 10+ item verification checklist. Wrap the ENTIRE master prompt inside a fenced code block. """) def build_user_prompt(topic: str, level: str, difficulty: int) -> str: level_context = { "Beginner": ( "The learner is new to this field. Use simple language, explain " "every concept on first use, provide copy-paste-ready code " "snippets, and include extra checkpoints so they never feel lost." ), "Intermediate": ( "The learner has foundational knowledge but needs guidance on " "architecture decisions, best practices, and connecting multiple " "components. Challenge them but don't overwhelm." ), "Advanced": ( "The learner is experienced. Focus on production-grade patterns, " "performance optimization, scalability, security, CI/CD, and " "professional-level engineering decisions. Skip basics." ), } difficulty_context = ( "very simple, minimal features, perfect for learning fundamentals" if difficulty <= 3 else "moderate complexity, multiple integrated features, real-world useful" if difficulty <= 6 else "high complexity, production-grade, ambitious scope with advanced patterns" ) return ( f"**Topic:** {topic}\n" f"**Skill Level:** {level}\n" f"**Level Context:** {level_context.get(level, level_context['Intermediate'])}\n" f"**Project Difficulty:** {difficulty}/10 ({difficulty_context})\n\n" "Generate the full, highly detailed project plan now." ) # --------------------------------------------------------------------------- # Gemini API call # --------------------------------------------------------------------------- def call_gemini(topic: str, level: str, difficulty: int) -> str: if not GEMINI_API_KEY: raise gr.Error( "GEMINI_API_KEY is not set. Add it as a Secret in your " "Hugging Face Space settings." ) client = genai.Client(api_key=GEMINI_API_KEY) try: response = client.models.generate_content( model="gemini-2.0-flash-lite", contents=build_user_prompt(topic, level, difficulty), config=types.GenerateContentConfig( system_instruction=SYSTEM_PROMPT, temperature=0.9, max_output_tokens=8192, ), ) except Exception as e: msg = str(e) if "429" in msg or "RESOURCE_EXHAUSTED" in msg: raise gr.Error( "API quota exceeded. Please wait a few minutes and try again, " "or upgrade your Gemini API plan at https://ai.dev/rate-limit" ) raise gr.Error(f"Gemini API error: {msg}") return response.text # --------------------------------------------------------------------------- # Markdown → section parser # --------------------------------------------------------------------------- SECTION_TITLES = [ "Project Title", "Project Description", "Goals & Learning Outcomes", "Step-by-Step Instructions", "Tools & Technologies Needed", "Tips & Common Mistakes", "Bonus Challenges", "Master Prompt", ] def parse_sections(md_text: str) -> dict: parts = re.split(r"(?m)^##\s+", md_text) sections = {} for part in parts: if not part.strip(): continue first_line, _, body = part.partition("\n") title = first_line.strip().rstrip("#").strip() sections[title] = body.strip() return sections # --------------------------------------------------------------------------- # PDF generator # --------------------------------------------------------------------------- class ProjectPDF(FPDF): DARK = (30, 30, 46) ACCENT = (114, 137, 218) LIGHT = (248, 248, 242) SUBTEXT = (166, 172, 205) def __init__(self): super().__init__() self.add_font("DejaVu", "", FONT_PATH, uni=True) self.add_font("DejaVu", "B", FONT_BOLD_PATH, uni=True) self.set_auto_page_break(auto=True, margin=20) def cover_page(self, title, topic, level, difficulty): self.add_page() self.set_fill_color(*self.DARK) self.rect(0, 0, 210, 297, "F") self.set_fill_color(*self.ACCENT) self.rect(0, 80, 210, 6, "F") self.set_y(100) self.set_font("DejaVu", "B", 26) self.set_text_color(*self.LIGHT) self.multi_cell(0, 14, title, align="C") self.ln(12) self.set_font("DejaVu", "", 13) self.set_text_color(*self.SUBTEXT) self.cell( 0, 10, f"Topic: {topic} | Level: {level} | Difficulty: {difficulty}/10", align="C", new_x="LMARGIN", new_y="NEXT", ) self.ln(6) self.set_font("DejaVu", "", 11) self.cell( 0, 10, "Generated by Prompt2Project — powered by Google Gemini", align="C", new_x="LMARGIN", new_y="NEXT", ) def section_header(self, title): self.ln(6) self.set_fill_color(*self.ACCENT) self.set_text_color(255, 255, 255) self.set_font("DejaVu", "B", 14) self.cell(0, 10, f" {title}", fill=True, new_x="LMARGIN", new_y="NEXT") self.ln(4) def body_text(self, text): self.set_text_color(50, 50, 50) self.set_font("DejaVu", "", 10) for line in text.split("\n"): stripped = line.strip() if not stripped: self.ln(3) continue if stripped.startswith(("- ", "* ", "• ")): bullet_text = stripped.lstrip("-*• ").strip() self.cell(6) self.set_font("DejaVu", "B", 10) self.cell(5, 6, "•") self.set_font("DejaVu", "", 10) self.multi_cell(self.w - self.r_margin - self.get_x(), 6, bullet_text) elif re.match(r"^\d+[\.\)]\s", stripped): num, _, rest = stripped.partition(" ") self.set_font("DejaVu", "B", 10) self.cell(10, 6, num) self.set_font("DejaVu", "", 10) self.multi_cell(self.w - self.r_margin - self.get_x(), 6, rest) elif stripped.startswith("**") and "**" in stripped[2:]: self.set_font("DejaVu", "B", 11) self.multi_cell(0, 7, stripped.replace("**", "")) self.set_font("DejaVu", "", 10) else: self.multi_cell(0, 6, stripped.replace("**", "")) def generate_pdf(sections, topic, level, difficulty): pdf = ProjectPDF() title = sections.get("Project Title", "Untitled Project") pdf.cover_page(title, topic, level, difficulty) pdf.add_page() for sec_title in SECTION_TITLES: if sec_title == "Project Title": continue body = sections.get(sec_title, "") if not body: continue if sec_title == "Master Prompt": pdf.add_page() pdf.section_header(sec_title) pdf.body_text(body) tmp = tempfile.NamedTemporaryFile( delete=False, suffix=".pdf", prefix="prompt2project_" ) pdf.output(tmp.name) return tmp.name # --------------------------------------------------------------------------- # Main handler # --------------------------------------------------------------------------- def generate_project(topic: str, level: str, difficulty: int): if not topic or not topic.strip(): raise gr.Error("Please enter a topic.") md_text = call_gemini(topic.strip(), level, int(difficulty)) sections = parse_sections(md_text) pdf_path = generate_pdf(sections, topic.strip(), level, int(difficulty)) return md_text, pdf_path # --------------------------------------------------------------------------- # CSS # --------------------------------------------------------------------------- CSS = """ body, .gradio-container { background: #0f0f1a !important; } /* ── constrain page width ── */ .gradio-container > .main, .gradio-container .tabs, .gradio-container > div { max-width: 860px !important; margin-left: auto !important; margin-right: auto !important; } #header { text-align: center; padding: 2rem 0 1.5rem; max-width: 860px; margin: 0 auto; } #header h1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 2.2rem; font-weight: 800; margin-bottom: 0.25rem; } #header p { color: #888; font-size: 1rem; margin: 0; } /* ── cards ── */ #config-card, #output-card, #pdf-row { background: #1a1a2e !important; border: 1px solid #2a2a4a !important; border-radius: 16px !important; padding: 1.75rem 2rem !important; margin-bottom: 1.25rem !important; } /* ── inputs ── */ #config-card input, #config-card textarea, #config-card select { background: #0f0f1a !important; border: 1px solid #2a2a4a !important; color: #e0e0f0 !important; border-radius: 8px !important; } #config-card label, #config-card .label-wrap span { color: #aaa !important; } /* ── generate button ── */ #btn-row { display: flex !important; justify-content: center !important; padding: 0.75rem 0 0.25rem !important; } #generate-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; font-weight: 700 !important; font-size: 1rem !important; border: none !important; border-radius: 10px !important; width: auto !important; min-width: 220px !important; max-width: 260px !important; padding: 10px 32px !important; margin: 0 auto 1.5rem auto !important; display: block !important; } #generate-btn:hover { opacity: 0.88 !important; transform: translateY(-1px) !important; } /* ── output card text ── */ #output-card p, #output-card li, #output-card h1, #output-card h2, #output-card h3, #output-card h4, #output-card code { color: #e0e0f0 !important; } #output-card code { background: #0f0f1a !important; border-radius: 4px !important; padding: 2px 5px !important; } #output-card pre { background: #0f0f1a !important; border: 1px solid #2a2a4a !important; border-radius: 8px !important; padding: 1rem !important; } footer { display: none !important; } """ # --------------------------------------------------------------------------- # Gradio UI # NOTE: gr.Blocks() accepts ONLY title= in this Gradio version. # css goes into demo.launch(css=CSS) — same pattern as Intervexa. # --------------------------------------------------------------------------- with gr.Blocks(title="Prompt2Project") as demo: gr.HTML(""" """) with gr.Group(elem_id="config-card"): gr.Markdown("### ⚙️ Configuration") with gr.Row(): topic_input = gr.Textbox( label="📌 Topic", placeholder="e.g. Machine Learning, Web Development, Game Design…", scale=3, lines=1, max_lines=1, ) level_input = gr.Dropdown( label="📊 Skill Level", choices=["Beginner", "Intermediate", "Advanced"], value="Intermediate", scale=1, ) difficulty_input = gr.Slider( label="🎯 Difficulty (1–10)", minimum=1, maximum=10, step=1, value=5, scale=2, ) generate_btn = gr.Button("✨ Generate Project", elem_id="generate-btn") with gr.Group(elem_id="output-card"): gr.Markdown("### 📋 Generated Project Plan") md_output = gr.Markdown( value="*Your project plan will appear here after clicking Generate…*" ) with gr.Group(elem_id="pdf-row"): pdf_output = gr.File(label="📄 Download Project PDF", interactive=False) gr.HTML( "
" "Built with ❤️ using Gradio & ❤️ Google Gemini ❤️" "
" ) generate_btn.click( fn=generate_project, inputs=[topic_input, level_input, difficulty_input], outputs=[md_output, pdf_output], ) if __name__ == "__main__": demo.launch(css=CSS)